Skip to content

fix: [alt-276] 사장님(MANAGER) 구인구직 API 결함 수정 - #98

Open
ysw789 wants to merge 22 commits into
devfrom
fix/alt-276
Open

fix: [alt-276] 사장님(MANAGER) 구인구직 API 결함 수정#98
ysw789 wants to merge 22 commits into
devfrom
fix/alt-276

Conversation

@ysw789

@ysw789 ysw789 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

변경사항

https://app.notion.com/p/3ac86553162881b59052c82350965088

  • 공고 도메인 Command 타입 추가
  • 매니저 스코프 업장 조회 메서드 추가
  • 공고 등록·수정 UseCase를 Command 입력으로 전환 (업장 소유권 검증, 삭제 공고 수정 차단, 필수값 검증 보강 포함)
  • 삭제된 근무일정을 공고 응답에서 제외
  • 삭제된 근무일정에 지원할 수 없도록 조회 조건 추가
  • 공고 조회 쿼리의 삭제 상태 누락 보완 (목록 근무시간 필터, 매니저 상세)
  • 매니저 공고 목록 응답에 공고 상태 추가
  • 공고 API 스웨거 예시에서 키워드 잔재 제거
  • 공고 등록·수정 및 근무일정 필터링 테스트 추가

리뷰 반영 (2차)

  • 활성 근무일정이 하나도 남지 않으면 공고를 모집 완료(CLOSED)로 전환
  • 삭제된 공고 변경 차단을 Posting 엔티티로 이동하고 UseCase 중복 가드 제거
  • 요청 본문 역직렬화 실패를 프로젝트 오류 포맷으로 응답
  • 공고 수정 요청의 deleteScheduleIds 정규화
  • 공고 지원 API 스펙에 누락된 응답 케이스 추가
  • 채팅 마이그레이션 버전 번호 중복 해소 (아래 별도 항목)

⚠️ 배포 차단 해소 — 마이그레이션 버전 중복

V4·V5·V6가 각각 두 개씩 존재했습니다. #97(업종)이 먼저 병합돼 배포된 뒤 #95(채팅)가 같은 번호를 다시 쓰면서, Flyway가 중복 버전을 만나 기동 단계에서 실패했습니다. #99 배포 실패도 이 충돌을 물려받은 것입니다.

채팅 계열 네 개를 뒤 번호로 옮겼습니다 (내용 변경 없이 파일명만).

변경 전 변경 후
V4__unify_chat_schema.sql V11__unify_chat_schema.sql
V5__chat_message_content_nullable.sql V12__chat_message_content_nullable.sql
V6__backfill_workspace_group_chat.sql V13__backfill_workspace_group_chat.sql
V7__unique_workspace_group_chat_room.sql V14__unique_workspace_group_chat_room.sql

네 파일은 스키마 생성 → 백필 → 인덱스 순서로 서로 의존하므로 상대 순서를 유지한 채 함께 옮겼습니다. 이미 적용된 업종 계열(V4·V5·V6)은 그대로 뒀습니다.

배포 전 확인 — 채팅 계열을 V4~V7로 이미 적용한 환경이 있으면 "적용된 마이그레이션이 로컬에 없음"으로 검증에 실패합니다. 배포가 계속 실패해 온 정황상 적용된 환경은 없을 것으로 보이나, 병합 전 확인 부탁드립니다.

SELECT version, description FROM flyway_schema_history
WHERE description IN ('unify chat schema', 'chat message content nullable',
                      'backfill workspace group chat', 'unique workspace group chat room');

📢 앱 팀 확인 필요 — 클라이언트 영향

1. 새로 400을 반환하는 필드 (공고 등록 POST /manager/postings)

javax.validationjakarta.validation 교체로 그동안 동작하지 않던 검증이 살아납니다.

필드 변경 기존 동작
workspaceId @NotNull 정상 동작 누락해도 통과
paymentType @NotNull 정상 동작 누락해도 통과
schedules @NotEmpty 신규 빈 배열·누락 통과
payAmount @NotNull@Positive 0·음수 통과 (primitive int@NotNull은 항상 통과)

응답 예시:

{"code": "B001", "message": "잘못된 요청입니다.",
 "data": [{"field": "workspaceId", "message": "널이어서는 안됩니다"}]}

2. 잘못된 요일·시각 값의 응답 본문 형식 (공고 수정 PUT /manager/postings/{id})

workingDays·startTime·endTime을 타입 바인딩으로 바꾸면서 역직렬화 단계에서 걸립니다. 기존에는 상태만 400이고 본문이 Spring 기본 형식이라 오류 파싱이 깨졌습니다.

{"code": "B001", "message": "요청 본문을 해석할 수 없습니다."}

"monday"(소문자), "25:00" 등이 해당합니다. 모든 API의 잘못된 JSON 응답이 함께 이 형식으로 통일됩니다.

3. 근무일정을 전부 삭제하면 공고가 자동으로 마감됩니다

수정을 마친 뒤 남은 활성 근무일정이 0개면 공고 상태가 CLOSED로 바뀝니다.

  • 일반 공고 목록·지도 목록에서 사라집니다 (목록 쿼리가 status = OPEN 기준)
  • 해당 공고에 지원할 수 없습니다 → {"code": "B001", "message": "모집이 종료된 공고입니다."}
  • 같은 요청에서 전부 삭제하고 새 일정을 추가하면 OPEN을 유지합니다
  • 다시 열려면 상태 변경 API(PATCH /manager/postings/{id}/status)로 명시적으로 OPEN 처리해야 합니다

자동 마감으로 "모집이 종료된 공고입니다" 오류의 발생 빈도가 늘어납니다. 오류 코드는 기존과 같은 B001입니다.

4. 삭제된 공고 변경 시 409

내용 수정·상태 변경 모두 409 B020입니다. 기존 동작과 동일하며 검증 위치만 엔티티로 옮겼습니다.


테스트

./gradlew clean build 전체 통과.

단위 테스트 — PostingTests(자동 마감 3케이스, 삭제 공고 가드 2케이스), GlobalExceptionHandlerTests(신규, 역직렬화 실패 2케이스), ManagerUpdatePostingTests·CreatePostingTests.

e2e — 로컬 서버 기동 후 #98·#99 변경사항 28건을 curl로 검증, 전부 통과.

영역 확인 내용
공고 등록 정상 200 / 남의 업장 B008 / 필수값 4종 각각 400
타입 바인딩 "monday"·"25:00" → 400 B001
삭제 일정 수정·재삭제 404 B019
자동 마감 마지막 일정 삭제 → CLOSED + 목록에서 제외, 전부 삭제+추가는 OPEN 유지
DELETED 공고 수정·상태 변경 409 B020
시간 필터 삭제 일정 시간대로는 미검색, 활성 일정 시간대로는 검색됨
매니저 목록 ?status=OPEN 하위 호환, 다중 상태 필터 동작, DELETED 항상 제외
applicantCount 지원 목록 건수·DB 집계와 일치
중복 지원 같은 일정·다른 일정 모두 차단, CANCELLED 이력만 있으면 허용

리뷰 코멘트 중 코드 변경 없이 답변드린 항목

  • 스웨거 400 블록의 C001ErrorCode.INTERNAL_SERVER_ERROR가 400으로 선언돼 있어(ErrorCode.java) 현재 스펙이 enum과 일치합니다. enum 상태값 자체는 별도 이슈로 봅니다.
  • CLAUDE.md 인자 전달 규칙CLAUDE.md.gitignore 대상이라 레포에 없고, 규칙은 이미 Command/Result 기준으로 갱신돼 있습니다.
  • 삭제 필터링 세 방식 혼재, 테스트 @Nested 그룹화 — 후속으로 분리합니다.

Summary by CodeRabbit

  • 새로운 기능

    • 공고 등록·수정 시 입력값 변환과 유효성 검사가 강화되었습니다.
    • 관리자가 소유한 활성 워크스페이스에서만 공고를 등록할 수 있습니다.
    • 공고 목록 응답에 상태 정보가 추가되었습니다.
    • 기존 채팅이 그룹 채팅과 이미지 전용 메시지를 지원하도록 개선되었습니다.
  • 버그 수정

    • 삭제된 공고와 일정이 조회 결과에서 제외됩니다.
    • 공고에 활성 일정이 없으면 자동으로 모집 종료 상태로 전환됩니다.
    • 잘못된 요청 본문에 대해 명확한 오류가 반환됩니다.
  • 테스트

    • 공고 및 일정 처리 시나리오 검증이 강화되었습니다.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ysw789, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 59593648-891a-4d67-978f-637e9bde23f0

📥 Commits

Reviewing files that changed from the base of the PR and between a370007 and 5020087.

📒 Files selected for processing (1)
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
📝 Walkthrough

Walkthrough

공고 생성·수정 요청을 도메인 커맨드로 변환합니다. 관리자 소유 워크스페이스를 검증합니다. 삭제된 공고와 일정은 조회에서 제외합니다. 응답에는 활성 일정과 공고 상태를 반영합니다. 채팅 스키마와 기존 데이터 백필 마이그레이션을 추가합니다.

Changes

공고 생성·수정 및 상태 처리

Layer / File(s) Summary
커맨드 변환과 유스케이스 계약
src/main/java/com/dreamteam/alter/adapter/inbound/.../posting/dto/*, src/main/java/com/dreamteam/alter/domain/posting/command/*, src/main/java/com/dreamteam/alter/domain/posting/port/inbound/*, src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/*
생성·수정 DTO가 도메인 커맨드로 변환됩니다. 유스케이스와 컨트롤러는 커맨드와 ManagerActor를 전달합니다.
관리자 소유 워크스페이스 기반 생성
src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java, src/main/java/com/dreamteam/alter/domain/workspace/..., src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java, src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java
공고 생성은 관리자 소유이며 활성화된 워크스페이스만 조회합니다. 커맨드로 공고와 일정을 생성하고 저장합니다.
공고 수정과 일정 생명주기
src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java, src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java, src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java, src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java
삭제·수정·추가 일정 목록으로 공고를 갱신합니다. 삭제 공고의 수정을 차단하고 활성 일정이 없으면 공고를 CLOSED로 변경합니다.
활성 일정과 삭제 상태 조회
src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/*, src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/*, src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java
삭제된 공고·일정을 조회에서 제외합니다. 응답 일정은 활성 일정으로 구성합니다. 매니저 목록 응답에 공고 상태를 추가합니다.
요청 오류와 API 계약
src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java, src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java, src/main/java/com/dreamteam/alter/adapter/inbound/**/controller/*Spec.java
요청 본문 역직렬화 오류를 ILLEGAL_ARGUMENT로 처리합니다. 공고 생성·수정 및 지원 API의 오류 응답 문서를 갱신합니다.
채팅 스키마 통합과 백필
src/main/resources/db/migration/V11__unify_chat_schema.sql, src/main/resources/db/migration/V12__chat_message_content_nullable.sql, src/main/resources/db/migration/V13__backfill_workspace_group_chat.sql, src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql
채팅방 유형과 그룹 멤버 테이블을 추가합니다. 기존 직접 채팅과 워크스페이스 그룹 채팅 데이터를 백필합니다. 메시지 콘텐츠를 nullable로 변경하고 그룹 채팅방 중복을 제한합니다.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ManagerPostingController
  participant CreatePosting
  participant WorkspaceQueryRepository
  participant Posting
  participant PostingRepository
  ManagerPostingController->>CreatePosting: execute(command, actor)
  CreatePosting->>WorkspaceQueryRepository: findByIdAndManagerUser(workspaceId, managerUser)
  WorkspaceQueryRepository-->>CreatePosting: activated workspace
  CreatePosting->>Posting: create(command, workspace)
  CreatePosting->>PostingRepository: save(posting)
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.07% 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
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 매니저 구인구직 API 결함 수정이라는 제목이 공고 도메인 변경과 권한·삭제 상태·검증 보강을 포함한 주요 변경을 정확히 요약합니다.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/alt-276

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.

@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: 2

🤖 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/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java`:
- Around line 46-51: Update UpdatePostingScheduleDto to validate each
workingDays value against the accepted DayOfWeek enum names and validate
startTime/endTime using the required ISO time format before toCommand() performs
DayOfWeek.valueOf and LocalTime.parse. Ensure invalid, null, or empty values are
reported as field validation errors through the existing `@Valid` path rather than
runtime exceptions.

In
`@src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java`:
- Around line 29-31: Group the 조회 실패, 삭제 공고, and 정상 수정 scenarios in
ManagerUpdatePostingTests into separate `@Nested` classes, each with a descriptive
`@DisplayName`. Also group the 활성 일정 조회 scenarios in PostingTests into an
appropriately named `@Nested` class with `@DisplayName`; update both test files
while preserving their existing test behavior.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 482a201f-fc82-4663-b1b7-2374be08918e

📥 Commits

Reviewing files that changed from the base of the PR and between 17f7452 and 94ecfee.

📒 Files selected for processing (27)
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingScheduleRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingScheduleQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/CreatePostingCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/PostingScheduleCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingScheduleCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/CreatePostingUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/ManagerUpdatePostingUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java
  • src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java
  • src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java
  • src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java

Comment on lines +29 to +31
@ExtendWith(MockitoExtension.class)
@DisplayName("ManagerUpdatePosting 테스트")
class ManagerUpdatePostingTests {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

관련 테스트를 @Nested 클래스로 그룹화하세요.

  • src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java#L29-L31: 조회 실패·삭제 공고·정상 수정 시나리오를 @DisplayName이 있는 @Nested 그룹으로 구성하세요.
  • src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java#L14-L15: 활성 일정 조회 시나리오를 @DisplayName이 있는 @Nested 그룹으로 구성하세요.
📍 Affects 2 files
  • src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java#L29-L31 (this comment)
  • src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java#L14-L15
🤖 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/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java`
around lines 29 - 31, Group the 조회 실패, 삭제 공고, and 정상 수정 scenarios in
ManagerUpdatePostingTests into separate `@Nested` classes, each with a descriptive
`@DisplayName`. Also group the 활성 일정 조회 scenarios in PostingTests into an
appropriately named `@Nested` class with `@DisplayName`; update both test files
while preserving their existing test behavior.

Source: Path instructions

@hodoon hodoon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

전체적으로 방향 좋습니다. 특히 도메인 엔티티에서 adapter.inbound DTO 의존을 걷어낸 건 CLAUDE.md 헥사고날 규칙에 정확히 맞고, javax.validationjakarta 교체는 그동안 @NotNull이 아예 동작하지 않던 실제 결함을 잡은 수정입니다. Posting.create에서 .toList()new ArrayList<>()로 바꾼 것도, 기존에는 스케줄 없이 생성하면 schedules가 null이고 있으면 불변 리스트라 이후 addSchedules 호출 시 NPE/UnsupportedOperationException이 나는 잠재 결함이었는데 같이 해소됐습니다.

./gradlew compileJava compileTestJava test --tests "*Posting*" 통과 확인했습니다.

확인했고 문제없는 부분:

  • .on(status.ne(DELETED))가 count·list 쿼리 양쪽에 동일하게 들어가 커서 페이지네이션 totalCount가 어긋나지 않습니다. 두 번째 엔티티 fetch 쿼리에는 조건을 걸지 않은 것도 맞는 판단입니다(fetchJoin에 필터를 걸면 영속 컬렉션이 깨짐 — 대신 getActiveSchedules()로 메모리에서 거르는 구조).
  • 지도 목록(PostingMapListResponseDto)도 PostingListResponse를 재사용하므로 활성 일정 필터가 자동으로 적용됩니다.
  • ManagerUpdatePosting의 DELETED 가드는 기존 ManagerUpdatePostingStatus와 동일한 패턴(CONFLICT)이고 새 ErrorCode를 만들지 않은 것도 맞습니다. 지금은 2곳이라 공통 추출까진 불필요해 보입니다.

아래 라인 코멘트 중 1·2번(중) 만 정리되면 머지해도 될 것 같고, 3~5번은 후속으로 빼도 무방합니다.

for (UpdatePostingScheduleCommand updateCommand : updateSchedules) {
PostingSchedule existingSchedule = this.schedules.stream()
.filter(schedule -> schedule.getId().equals(updateDto.getId()))
.filter(schedule -> schedule.getId().equals(updateCommand.id()))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[중] updateSchedules/deleteSchedules가 DELETED 스케줄까지 대상으로 잡습니다.

이 PR의 취지가 "삭제된 근무일정은 노출되지 않고 지원도 불가"인데, 정작 수정/삭제 경로는 this.schedules 전체를 스캔합니다.

  • 이미 DELETED인 스케줄 ID로 updateSchedules를 보내면 → 내용은 바뀌고 상태는 DELETED로 남는 유령 수정이 됩니다. 게다가 PostingSchedule.update()positionsAvailable까지 재계산해서 삭제된 일정의 잔여 인원이 되살아납니다.
  • deleteScheduleIds에 이미 삭제된 ID를 넣어도 조용히 200이 나갑니다(같은 문제, 아래 deleteSchedules에도 동일).

두 메서드 모두 getActiveSchedules() 기준으로 찾도록 바꾸면 한 곳에서 정리됩니다.

PostingSchedule existingSchedule = getActiveSchedules().stream()
    .filter(schedule -> schedule.getId().equals(updateCommand.id()))
    .findFirst()
    .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND, "수정할 스케줄을 찾을 수 없습니다."));

return new UpdatePostingScheduleCommand(
id,
workingDays.stream().map(DayOfWeek::valueOf).toList(),
LocalTime.parse(startTime),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[중] 시각 파싱 실패가 400이 아니라 500으로 나갑니다.

LocalTime.parse()가 던지는 DateTimeParseExceptionIllegalArgumentException이 아니라 DateTimeException 계열이라 GlobalExceptionHandler의 어떤 핸들러에도 걸리지 않습니다 → 500.
반면 바로 위 DayOfWeek.valueOf()IllegalArgumentException이라 400(ILLEGAL_ARGUMENT)으로 나갑니다. 같은 DTO 안에서 동작이 갈립니다.

@Valid는 바인딩 시점에 끝나고 toCommand()는 컨트롤러 본문에서 호출되므로 검증으로도 못 막습니다.

한 쌍인 CreatePostingScheduleRequestDto는 이미 List<DayOfWeek>/LocalTime 타입이라 Jackson이 역직렬화 단계에서 400을 내줍니다. 이쪽 DTO도 같은 타입으로 맞추면 동작이 통일되고 toCommand()의 파싱 코드도 통째로 사라집니다.

private List<DayOfWeek> workingDays;
private LocalTime startTime;
private LocalTime endTime;

.selectFrom(qWorkspace)
.where(
qWorkspace.id.eq(id),
qWorkspace.managerUser.eq(managerUser)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[하] 업장 상태 조건이 없습니다.

소유권만 검증하고 status는 보지 않아서, 승인 대기(PENDING)·승인 취소(REVOKED)·폐업(CLOSED)·삭제(DELETED) 업장에도 공고를 등록할 수 있습니다.

소유권 검증을 넣는 김에 qWorkspace.status.eq(WorkspaceStatus.ACTIVATED)도 같이 걸지 판단이 필요해 보입니다. 정책상 승인 전 업장의 공고 등록을 허용하는 게 의도라면 그대로 두면 됩니다.

assertThatThrownBy(() -> managerUpdatePosting.execute(1L, command(), actor))
.isInstanceOf(CustomException.class)
.satisfies(ex -> assertThat(((CustomException) ex).getErrorCode()).isEqualTo(ErrorCode.CONFLICT));
then(posting).should(never()).updateContent(command());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[하] 이 never() 검증은 사실상 무의미합니다.

인자 매처가 command()가 만든 특정 값이라, updateContent다른 인자로 호출되더라도 "그 인자로는 호출된 적 없음"이 되어 통과합니다. 실제로 호출 자체가 없었는지를 잡으려면 매처를 넓혀야 합니다.

then(posting).should(never()).updateContent(any());

@@ -167,14 +171,6 @@ ResponseEntity<CommonApiResponse<Void>> updatePostingStatus(
name = "존재하지 않는 공고",
value = "{\"code\" : \"B007\"}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[하] 이번에 추가된 409 케이스가 스펙에 빠졌습니다.

공고 등록 쪽에는 B008 예시를 추가하셨는데, 이 PR에서 새로 생긴 "삭제된 공고 수정 차단"(ErrorCode.CONFLICT / B020 / 409)은 수정 엔드포인트 문서에 반영되지 않았습니다. 409 @ApiResponse 블록을 하나 추가해주시면 좋겠습니다.

@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/java/com/dreamteam/alter/domain/posting/entity/Posting.java (1)

105-109: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

삭제 공고 검증을 Posting.updateContent()에도 추가하세요.

ManagerUpdatePostingPostingStatus.DELETED를 검사하더라도, Line [105]-[109]의 엔티티 메서드는 상태를 검사하지 않고 공고 내용을 변경합니다. 다른 유스케이스가 이 public 메서드를 호출하면 삭제 공고 수정 규칙을 우회할 수 있습니다.

메서드 시작 부분에서 삭제 상태를 거부하는 도메인 검증을 추가하세요. ManagerUpdatePosting의 검사는 권한과 입력 경계 검증으로 유지하세요.

As per path instructions, 엔티티는 비즈니스 규칙과 불변식(상태 전이)을 캡슐화해야 합니다.

권장 수정
 public void updateContent(UpdatePostingCommand command) {
+    if (PostingStatus.DELETED.equals(status)) {
+        throw new CustomException(ErrorCode.CONFLICT, "삭제 상태의 공고는 내용 수정이 불가합니다.");
+    }
+
     this.title = command.title();
🤖 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/java/com/dreamteam/alter/domain/posting/entity/Posting.java` around
lines 105 - 109, Update Posting.updateContent to reject updates when the posting
status is PostingStatus.DELETED before applying any command fields. Keep
ManagerUpdatePosting’s existing check for authorization and input-boundary
validation, while enforcing the deleted-posting state invariant within the
public entity method.

Source: Path instructions

🤖 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/java/com/dreamteam/alter/domain/posting/entity/Posting.java`:
- Around line 105-109: Update Posting.updateContent to reject updates when the
posting status is PostingStatus.DELETED before applying any command fields. Keep
ManagerUpdatePosting’s existing check for authorization and input-boundary
validation, while enforcing the deleted-posting state invariant within the
public entity method.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e1e0675d-e902-4b73-948e-e4aaaa593d28

📥 Commits

Reviewing files that changed from the base of the PR and between 94ecfee and e25e297.

📒 Files selected for processing (6)
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java
  • src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java
  • src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java

@hodoon hodoon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Code review

라인별로 코멘트 남겼습니다. 전반적으로 방향이 좋은 PR입니다. 특히 아래 두 가지가 실질적인 수확이라고 봅니다.

  • CreatePostingRequestDtojavax.validationjakarta.validation import 교체. 그동안 workspaceId · paymentType@NotNull아예 동작하지 않고 있었습니다. 이 PR 에서 가장 값진 수정입니다.
  • Posting 엔티티에서 어댑터 DTO 의존을 걷어내고 command 패키지를 도입한 것. 헥사고날 의존성 방향 위반을 실제로 해소했습니다.

블로커는 없습니다. 확인 부탁드리는 항목은 다음과 같습니다.

  1. 활성 근무일정이 0개인 공고가 여전히 목록에 노출됩니다 (.on() + leftJoin 조합).
  2. 검증이 실질적으로 강화되면서 기존 클라이언트가 400 을 받게 되는 케이스가 생깁니다 — 릴리스 노트 필요.
  3. CLAUDE.md 의 "DTO 그대로 전달" 규칙과 새 Command 패턴이 어긋납니다 — 문서 갱신 필요.

머지 순서: #99PostingQueryRepositoryImpleqPostingStatusinPostingStatusOrDefault 로 바꾸고 ManagerPostingListResponse 의 status 필드를 전제로 하고 있습니다. 이 PR 을 먼저 머지하는 편이 충돌이 적습니다.

.select(qPosting.countDistinct())
.from(qPosting)
.leftJoin(qPosting.schedules, qPostingSchedule)
.on(qPostingSchedule.status.ne(PostingStatus.DELETED))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[중간] 활성 근무일정이 0개인 공고가 목록에 남습니다

.on() 으로 DELETED 일정을 조인에서 걸러낸 방향은 정확합니다. 이전에는 삭제된 일정의 시간대로도 공고가 검색됐으니 실제 결함 수정입니다.

다만 leftJoin 이라 매칭되는 활성 일정이 하나도 없으면 NULL 행이 남습니다. 그리고 아래 WHERE 의 시간 필터는

gteStartTime(qPostingSchedule, filter.getStartTime()),
lteEndTime(qPostingSchedule, filter.getEndTime())

필터가 null 이면 조건 자체를 만들지 않으므로 NULL 행이 걸러지지 않습니다. 결과적으로 시간 필터 없이 목록을 조회하면, 근무일정이 전부 삭제된 OPEN 공고가 schedules: [] 로 응답에 포함됩니다.

"삭제된 근무일정을 공고 응답에서 제외" 라는 이 PR 의 목적을 생각하면 이런 공고는 목록에서 빠지는 게 자연스러워 보입니다. leftJoinjoin (inner) 으로 바꾸거나 WHERE 에 활성 일정 exists 조건을 추가하는 방향을 검토해 주세요.

의도적으로 남기신 거라면 그대로 두셔도 됩니다. 다만 98번 라인의 getPostingsWithCursor 도 동일 패턴이라 count 와 list 는 서로 일관되게 동작합니다.

"}" +
"]")
@Valid
@NotEmpty

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[중간] 실질적으로 파괴적인 검증 변경 — 릴리스 노트에 남겨주세요

이 PR 에서 javax.validation.constraints.NotNull import 를 jakarta.validation.constraints.NotNull 로 교체하셨는데, 이게 생각보다 영향이 큽니다.

Jakarta EE 9+ 검증기는 javax.validation 어노테이션을 인식하지 않습니다. 즉 지금까지 workspaceIdpaymentType@NotNull 은 아무 일도 하지 않았고, workspaceId: null 로 요청하면 그대로 통과해 NPE 나 엉뚱한 조회로 이어졌을 겁니다. 좋은 수정입니다.

다만 결과적으로 아래 세 가지가 동시에 클라이언트에 400 을 새로 발생시킵니다.

  • workspaceId 누락 (기존엔 통과)
  • paymentType 누락 (기존엔 통과)
  • schedules 빈 배열 · 누락 (@NotEmpty 신규)

PR 설명의 "필수값 검증 보강" 만으로는 앱 팀이 영향 범위를 알기 어렵습니다. 어떤 필드가 새로 400 을 내는지 목록으로 적어두시면 좋겠습니다.

Comment on lines +26 to +32
@NotEmpty
@Schema(description = "근무일", example = "[\"MONDAY\", \"WEDNESDAY\"]")
private List<String> workingDays;
private List<DayOfWeek> workingDays;

@NotNull
@Schema(description = "시작 시간", example = "09:00")
private String startTime;
private LocalTime startTime;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

타입 정리 좋습니다 — 응답 코드 변화만 확인 부탁드립니다

List<String>List<DayOfWeek>, StringLocalTime 전환으로 Posting.updateSchedules 안에 있던 DayOfWeek.valueOf · LocalTime.parse 파싱이 사라졌습니다. 도메인이 파싱 책임을 지지 않게 된 점, CreatePostingScheduleRequestDto 와 타입이 통일된 점 모두 좋습니다.

와이어 포맷("09:00", "MONDAY")은 Jackson JavaTimeModule 이 동일하게 처리하므로 정상 요청은 호환됩니다.

다만 잘못된 값이 올 때 응답이 달라집니다. 기존에는 도메인까지 들어와 LocalTime.parseDateTimeParseException 을 던졌고, 이제는 역직렬화 단계에서 HttpMessageNotReadableException 이 납니다. 프로젝트의 GlobalExceptionHandler 가 이걸 어떤 ErrorCode 로 내보내는지 한 번 확인해 주세요.

참고로 @NotNull@Positive (positionsNeeded) 도 함께 고치셨는데, primitive int@NotNull 은 항상 통과하므로 이것도 그동안 무의미했던 검증입니다. 좋은 수정입니다.

Comment on lines +29 to +30
workspaceQueryRepository.findByIdAndManagerUser(command.workspaceId(), actor.getManagerUser())
.orElseThrow(() -> new CustomException(ErrorCode.WORKSPACE_NOT_FOUND));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

권한 검증 추가 — 핵심 수정입니다

기존 findById 는 업장 소유권을 전혀 확인하지 않아서, 매니저가 남의 업장 ID 로 공고를 등록할 수 있었습니다. findByIdAndManagerUser 로 바꾸면서 소유권과 ACTIVATED 상태까지 함께 거른 건 정확한 처리입니다.

WORKSPACE_NOT_FOUND(400 / B008) 를 재사용해 "없음" 과 "권한 없음" 을 구분하지 않은 것도 정보 노출 관점에서 맞는 선택입니다. 스웨거 예시 문구도 그에 맞게 적어두셨네요.

Comment on lines +33 to +34
if (PostingStatus.DELETED.equals(posting.getStatus())) {
throw new CustomException(ErrorCode.CONFLICT);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[낮음] 동일한 가드가 두 UseCase 에 중복됩니다

ManagerUpdatePostingStatus:29-32 에 글자 그대로 같은 블록이 이미 있습니다. 기존 패턴을 따르신 거라 이 PR 에서 이렇게 쓴 건 맞습니다.

다만 findByManagerAndId 를 쓰는 모든 호출부가 같은 가드를 각자 들고 있어야 하는 구조라, 앞으로 이 리포지터리 메서드를 쓰는 UseCase 가 하나 더 생기면 빠뜨리기 쉽습니다. PostingvalidateModifiable() 같은 도메인 메서드로 접거나, 리포지터리에서 DELETED 를 아예 제외하는 방향을 다음 기회에 검토해 보시면 좋겠습니다.

ErrorCode.CONFLICT 가 409 / B020 이고 스웨거에도 409 블록으로 정확히 추가하셨습니다.

Comment on lines +63 to +69
toCommands(createSchedules, CreatePostingScheduleRequestDto::toCommand),
toCommands(updateSchedules, UpdatePostingScheduleDto::toCommand),
deleteScheduleIds
);
}

private static <T, R> List<R> toCommands(List<T> source, Function<T, R> mapper) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[낮음] deleteScheduleIds 만 정규화에서 빠졌습니다

createSchedules · updateSchedulestoCommandsnullList.of() 정규화가 되는데, deleteScheduleIds 는 raw 로 넘어가 Command 안에서 null 일 수 있습니다.

updateContentObjectUtils.isNotEmpty 로 방어하고 있어 지금 당장 깨지지는 않습니다. 다만 Command 를 받는 쪽에서 "세 리스트 중 두 개만 non-null 보장" 이라는 규칙을 알아야 하는 건 불필요한 부담입니다.

ObjectUtils.isEmpty(deleteScheduleIds) ? List.of() : deleteScheduleIds

한 줄이면 셋 다 동일해집니다.

posting
))
.toList();
posting.schedules = new ArrayList<>();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

잠재 버그 하나가 같이 사라졌습니다

기존 코드는 posting.schedulesstream().toList() 결과로 채웠는데, 이건 불변 리스트라서 그 인스턴스에 addSchedules 를 호출하면 UnsupportedOperationException 이 났을 겁니다. new ArrayList<>() 로 바꾸면서 해소됐네요.

덧붙여 ObjectUtils.isNotEmpty(command.schedules()) 조건은 이제 없어도 됩니다. CreatePostingRequestDto.schedules@NotEmpty 가 붙어 컨트롤러 단에서 걸러지고, 비어 있어도 addSchedules 의 for 문이 그냥 0회 도니까요. 취향 문제라 그대로 두셔도 무방합니다.

Comment on lines +92 to +103
/**
* 삭제되지 않은 근무일정만 반환한다. (응답 노출 기준)
*/
public List<PostingSchedule> getActiveSchedules() {
if (ObjectUtils.isEmpty(this.schedules)) {
return List.of();
}

return this.schedules.stream()
.filter(schedule -> !PostingStatus.DELETED.equals(schedule.getStatus()))
.toList();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[낮음] CLAUDE.md 의 인자 전달 규칙과 어긋납니다 — 문서 갱신 필요

CLAUDE.md 의 "Controller → UseCase 인자 전달 규칙" 은 @RequestBody DTO 를 값 추출 없이 그대로 넘기라고 규정합니다. 이 PR 은 request.toCommand() 로 바꿨습니다.

방향 자체는 이 PR 이 맞다고 봅니다. 기존 규칙대로면 domain 패키지가 adapter.inbound DTO 를 import 해야 하고, 그건 CLAUDE.md 의 헥사고날 원칙(의존성은 안쪽으로만)을 정면으로 위반합니다. 실제로 이 PR 이 Posting 엔티티에서 그 import 세 줄을 걷어냈습니다.

다만 두 규칙이 충돌한 채로 두면 다음 작업자가 어느 쪽을 따를지 알 수 없습니다. 마무리하면서 아래를 함께 갱신해 주세요.

  • CLAUDE.md 의 인자 전달 규칙에 "도메인으로 넘길 때는 domain/*/command 의 Command record 로 변환한다" 를 명시
  • Obsidian 01-아키텍처/ 에 Command 패턴 문서 추가, 02-도메인/공고-도메인.md 의 UseCase 시그니처 갱신

getActiveSchedules() 자체는 깔끔합니다. updateSchedules · deleteSchedules 가 루프 안에서 매번 재평가하는 것도, 같은 ID 를 중복으로 보내면 두 번째가 NOT_FOUND 로 걸리는 바람직한 동작이 되네요.

examples = {
@ExampleObject(
name = "존재하지 않거나, 자신이 관리하지 않거나, 활성화되지 않은 업장",
value = "{\"code\" : \"B008\"}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[사소] 이 400 블록에 C001 이 섞여 있습니다 (기존 문제)

B008 을 400 블록에 넣으신 건 정확합니다. WORKSPACE_NOT_FOUND 가 400 이니까요.

다만 바로 아래 "서버 내부 오류 C001" 예시가 같은 400 블록에 들어 있는데, C001 은 500 입니다. 이 PR 이 만든 문제는 아니지만 바로 옆 줄을 건드리신 김에 500 블록으로 옮기시면 어떨까요.

qPosting.id.eq(postingId),
qWorkspace.managerUser.eq(managerUser)
qWorkspace.managerUser.eq(managerUser),
qPosting.status.ne(PostingStatus.DELETED)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[사소] 매니저 목록 2차 쿼리와 필터링 방식이 다릅니다

상세 조회에 DELETED 제외를 넣으신 건 맞습니다.

다만 전체적으로 보면 삭제 필터링이 세 가지 방식으로 섞여 있습니다.

  • 일반 목록: 조인 .on() 으로 SQL 에서 제외 (50 · 98 라인)
  • 매니저 목록: 387 라인 에서 DELETED 일정까지 fetch join 한 뒤 getActiveSchedules() 로 애플리케이션에서 제외
  • 상세: 여기처럼 WHERE 조건

최종 응답은 어느 경로든 getActiveSchedules() 를 거치므로 결과는 동일합니다. 급한 문제는 아니지만, 나중에 일정 필터 조건이 하나 더 생기면 세 곳을 각각 고쳐야 합니다.

ysw789 added 21 commits August 3, 2026 14:22
- 포트 시그니처에서 어댑터 DTO를 제거하고 도메인 Command를 받도록 변경
- Posting 엔티티의 adapter.inbound 의존 제거, 요일·시각 파싱을 어댑터 경계로 이동
- 공고 등록 시 매니저 스코프 업장 조회로 소유권 검증 추가
- 삭제된 공고의 내용 수정 차단
- 필수값 검증 보강: workspaceId·paymentType의 jakarta 어노테이션 교정,
  schedules NotEmpty, 수정 DTO의 description NotBlank,
  수정 스케줄의 workingDays NotEmpty·positionsNeeded Positive

소유권 검증과 Command 전환이 동일한 UseCase 시그니처를 건드려 한 커밋으로 묶음.
- 알바생 목록의 근무시간 필터가 삭제된 일정으로 매칭되던 문제 수정
  (건수·목록 쿼리 모두 스케줄 조인 시 DELETED 제외)
- 삭제된 공고의 매니저 상세 조회 차단
V4·V5·V6가 업종(#97)과 채팅(#95)에서 각각 중복돼 Flyway가 기동 단계에서
실패했다. 채팅 계열을 V11~V14로 옮긴다. 네 파일은 스키마 생성 → 백필 → 인덱스
순서로 서로 의존하므로 상대 순서를 유지한 채 함께 이동한다.
HttpMessageNotReadableException 핸들러가 없어 잘못된 요일·시각 값이 오면 상태는
400이지만 body가 Spring 기본 형식으로 나갔다. 앱의 오류 파싱이 깨진다.
같은 가드가 ManagerUpdatePosting과 ManagerUpdatePostingStatus에 중복돼 있었고,
Posting.updateContent 자체는 상태를 검사하지 않아 다른 호출부가 규칙을 우회할 수
있었다. validateModifiable()로 엔티티가 불변식을 지키게 한다.

UseCase 테스트의 삭제 공고 케이스는 mock으로는 검증할 게 없어져 PostingTests의
실제 엔티티 검증으로 대체한다.
일정을 전부 삭제해도 공고가 OPEN으로 남아 목록에 schedules 빈 배열로 노출됐다.
모집할 일정이 없으면 마감된 공고이므로 CLOSED로 넘긴다. 목록 쿼리가 모두
status=OPEN을 걸고 있어 자연히 빠지고, 지원도 함께 닫힌다.

판정은 삭제·수정·추가를 모두 마친 뒤에 한다. 전부 지우고 새로 추가하는 요청을
마감으로 잘못 처리하지 않기 위해서다.
createSchedules·updateSchedules만 null을 빈 목록으로 바꿔 넘기고 deleteScheduleIds는
그대로 넘어갔다. Command를 받는 쪽이 세 목록 중 둘만 non-null이라는 규칙을 알아야 했다.
존재하지 않는 공고(B007), 모집 종료·중복 지원(B001), 잠금 실패(429/E001)가 스펙에
빠져 있었다. B001 두 건은 코드가 같아 예시에 message를 함께 적는다.

@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

🤖 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/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java`:
- Around line 50-63: Apply element-level validation to the three DTOs: in
src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
lines 50-63, declare schedules as List elements with `@NotNull` and `@Valid`; in
src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java
lines 46-70, apply `@Valid` to elements of createSchedules and updateSchedules and
`@NotNull` to elements of deleteScheduleIds; in
src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
lines 26-48, declare workingDays as elements with `@NotNull`.

In `@src/main/resources/db/migration/V11__unify_chat_schema.sql`:
- Around line 6-7: Ensure the chat_rooms schema enforces that GROUP rooms always
have a non-null workspace_id, preventing type = 'GROUP' rows from bypassing
workspace-based queries and uniqueness protection. If V11__unify_chat_schema.sql
has already been deployed, add this constraint in a new migration rather than
modifying V11; otherwise update the V11 migration accordingly.

In `@src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql`:
- Around line 4-6: Flyway 설정에서 V14__unique_workspace_group_chat_room 마이그레이션을
트랜잭션 없이 실행하도록 구성하세요. 기존 uq_chat_group_room_per_workspace 인덱스 정의는 유지하고, 해당
마이그레이션에 대한 executeInTransaction 비활성화 설정이 적용되는지 확인하세요.

In
`@src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java`:
- Around line 49-92: Group the two CreatePosting.execute tests inside an `@Nested`
class annotated with `@DisplayName`("execute"), preserving their existing
assertions and setup. In the exception test execute_타매니저업장_예외발생, separate the
invocation under a // when section from the assertions under a // then section.

In
`@src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java`:
- Around line 37-39: Update both tests in GlobalExceptionHandlerTests to assert
the response message in addition to the existing code assertion, verifying
$.message equals "요청 본문을 해석할 수 없습니다." for each GlobalExceptionHandler response.
- Around line 18-52: GlobalExceptionHandlerTests의 관련 테스트를 `@Nested` 클래스(적절한
`@DisplayName` 포함)로 그룹화하고, 테스트 메서드명을 action_condition_expectedResult 형식으로 변경하십시오.
각 테스트에 Given-When-Then 주석을 명시적으로 추가하되, 기존 요청 본문과 검증 결과는 그대로 유지하십시오.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: f6d4d3d7-39bf-4f2e-989e-d75149c1ca00

📥 Commits

Reviewing files that changed from the base of the PR and between e25e297 and a370007.

📒 Files selected for processing (35)
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/controller/PostingControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingScheduleRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingScheduleQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java
  • src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/CreatePostingCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/PostingScheduleCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingScheduleCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/CreatePostingUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/ManagerUpdatePostingUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java
  • src/main/resources/db/migration/V11__unify_chat_schema.sql
  • src/main/resources/db/migration/V12__chat_message_content_nullable.sql
  • src/main/resources/db/migration/V13__backfill_workspace_group_chat.sql
  • src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql
  • src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java
  • src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java
  • src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java
  • src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java
💤 Files with no reviewable changes (1)
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java

Comment on lines 50 to +63
@Valid
@NotEmpty
private List<CreatePostingScheduleRequestDto> schedules;

public CreatePostingCommand toCommand() {
return new CreatePostingCommand(
workspaceId,
title,
description,
payAmount,
paymentType,
schedules.stream().map(CreatePostingScheduleRequestDto::toCommand).toList()
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
git ls-files | rg 'CreatePostingRequestDto|UpdatePostingRequestDto|UpdatePostingScheduleDto' || true

echo
echo "== annotations/imports and relevant snippets =="
for f in \
  src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java \
  src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java \
  src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,120p' "$f" | cat -n
done

echo
echo "== Search for constraint imports in relevant files =="
rg -n "import .*constraint|`@NotNull`|`@NotEmpty`|`@Valid`|List<@|List<[^>]*DayOfWeek|List<[^>]*Long" src/main/java/com/dreamteam/alter/adapter/inbound -g '*.java' || true

echo
echo "== Build files and Jakarta/Hibernate validator versions =="
git ls-files | rg '^(build.gradle|build.gradle.kts|settings.gradle|pom.xml|gradle.properties)$' || true
rg -n "hibernate-validator|jakarta.validation|validation-api|spring-boot-starter-validation" -S . --glob '!build/*' --glob '!**/target/**' --glob '!**/.gradle/**' || true

Repository: alter-app/alter-backend

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

for p in [
    Path("src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java"),
    Path("src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java"),
    Path("src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java"),
]:
    text = p.read_text()
    print(f"== {p} ==")
    print({
        "contains_List_at_field_level": any("List@" in line or "List<" in line for line in text.splitlines()),
        "annotations_lines_near_collections": [line.strip() for line in text.splitlines() if any(a in line for a in ["`@List`", "List<@", "`@NonNull`", "NotNull", "NotEmpty", "Valid"])]
    })
PY

Repository: alter-app/alter-backend

Length of output: 1166


🌐 Web query:

Jakarta Bean Validation element cascading @Valid applied to collection element List annotation type parameter

💡 Result:

In Jakarta Bean Validation (formerly Bean Validation), you can achieve cascading validation for elements within a collection (such as a List) using the @Valid annotation on the generic type argument [1][2]. To validate the elements of a List, place the @Valid annotation directly on the type argument of the List [1][3]. This instructs the validation engine to cascade validation to each element within the collection when the parent object is validated [1][2]. Example usage: private List<@Valid Employee> employees; When the object containing this list is validated, the validator uses a ValueExtractor (in-built for standard types like List, Iterable, and Map) to extract each element and apply cascading validation to the Employee objects [1]. Key concepts: - Cascading via Type Argument: By placing @Valid inside the angle brackets (e.g., List<@Valid T>), you specifically target the elements for validation rather than the collection itself [4]. - Recursion: Cascaded validation is applied recursively; if the elements (e.g., Employee) contain fields marked with @Valid, those will also be validated [5][2]. - Performance: It is more efficient to specify the validation intent explicitly on the type argument than to place @Valid on the collection container itself, which forces the validator to perform unnecessary metadata lookups to determine if the container type is a collection [4]. - Compatibility: This feature requires Jakarta Bean Validation 2.0 or later (which introduced support for type-use annotations on generic type arguments) [6][3][7]. Standard container types like List and Map have built-in ValueExtractor support in compliant implementations like Hibernate Validator [1][3].

Citations:


컬렉션 원소 제약을 세 DTO에 추가해 주세요.

@NotEmpty@Valid는 리스트 자체만 확인하므로 null 원소는 Command로 전달될 수 있습니다.

  • CreatePostingRequestDto.schedulesList<@NotNull @Valid CreatePostingScheduleRequestDto>로 선언해 주세요.
  • UpdatePostingRequestDto.createSchedules, updateSchedules에도 @Valid를 원소에 적용해 주세요. deleteScheduleIds에는 원소별 @NotNull를 추가해 주세요.
  • UpdatePostingScheduleDto.workingDaysList<@NotNull DayOfWeek>로 선언해 주세요.
📍 Affects 3 files
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java#L50-L63 (this comment)
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java#L46-L70
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java#L26-L48
🤖 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/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java`
around lines 50 - 63, Apply element-level validation to the three DTOs: in
src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
lines 50-63, declare schedules as List elements with `@NotNull` and `@Valid`; in
src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java
lines 46-70, apply `@Valid` to elements of createSchedules and updateSchedules and
`@NotNull` to elements of deleteScheduleIds; in
src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
lines 26-48, declare workingDays as elements with `@NotNull`.

@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

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 6

🤖 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/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java`:
- Around line 50-63: Apply element-level validation to the three DTOs: in
src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
lines 50-63, declare schedules as List elements with `@NotNull` and `@Valid`; in
src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java
lines 46-70, apply `@Valid` to elements of createSchedules and updateSchedules and
`@NotNull` to elements of deleteScheduleIds; in
src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
lines 26-48, declare workingDays as elements with `@NotNull`.

In `@src/main/resources/db/migration/V11__unify_chat_schema.sql`:
- Around line 6-7: Ensure the chat_rooms schema enforces that GROUP rooms always
have a non-null workspace_id, preventing type = 'GROUP' rows from bypassing
workspace-based queries and uniqueness protection. If V11__unify_chat_schema.sql
has already been deployed, add this constraint in a new migration rather than
modifying V11; otherwise update the V11 migration accordingly.

In `@src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql`:
- Around line 4-6: Flyway 설정에서 V14__unique_workspace_group_chat_room 마이그레이션을
트랜잭션 없이 실행하도록 구성하세요. 기존 uq_chat_group_room_per_workspace 인덱스 정의는 유지하고, 해당
마이그레이션에 대한 executeInTransaction 비활성화 설정이 적용되는지 확인하세요.

In
`@src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java`:
- Around line 49-92: Group the two CreatePosting.execute tests inside an `@Nested`
class annotated with `@DisplayName`("execute"), preserving their existing
assertions and setup. In the exception test execute_타매니저업장_예외발생, separate the
invocation under a // when section from the assertions under a // then section.

In
`@src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java`:
- Around line 37-39: Update both tests in GlobalExceptionHandlerTests to assert
the response message in addition to the existing code assertion, verifying
$.message equals "요청 본문을 해석할 수 없습니다." for each GlobalExceptionHandler response.
- Around line 18-52: GlobalExceptionHandlerTests의 관련 테스트를 `@Nested` 클래스(적절한
`@DisplayName` 포함)로 그룹화하고, 테스트 메서드명을 action_condition_expectedResult 형식으로 변경하십시오.
각 테스트에 Given-When-Then 주석을 명시적으로 추가하되, 기존 요청 본문과 검증 결과는 그대로 유지하십시오.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: f6d4d3d7-39bf-4f2e-989e-d75149c1ca00

📥 Commits

Reviewing files that changed from the base of the PR and between e25e297 and a370007.

📒 Files selected for processing (35)
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/controller/PostingControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingScheduleRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingScheduleDto.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingScheduleQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java
  • src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/CreatePostingCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/PostingScheduleCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/UpdatePostingScheduleCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/CreatePostingUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/ManagerUpdatePostingUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java
  • src/main/resources/db/migration/V11__unify_chat_schema.sql
  • src/main/resources/db/migration/V12__chat_message_content_nullable.sql
  • src/main/resources/db/migration/V13__backfill_workspace_group_chat.sql
  • src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql
  • src/test/java/com/dreamteam/alter/application/posting/usecase/CreatePostingTests.java
  • src/test/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingTests.java
  • src/test/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandlerTests.java
  • src/test/java/com/dreamteam/alter/domain/posting/entity/PostingTests.java
💤 Files with no reviewable changes (1)
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePostingStatus.java
🛑 Comments failed to post (2)
src/main/resources/db/migration/V11__unify_chat_schema.sql (1)

6-7: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

GROUP 방의 workspace_id 불변식을 DB에서도 강제하세요.

도메인 ChatRoom.createGroupworkspaceId를 필수로 요구합니다. 그러나 현재 스키마는 type = 'GROUP'workspace_id IS NULL 행을 허용합니다. 이 행은 업장 기반 방 조회와 유니크 인덱스의 보호 범위에서 벗어납니다.

수정 예시
 ALTER TABLE chat_rooms ADD COLUMN type varchar(20) NOT NULL DEFAULT 'DIRECT';
 ALTER TABLE chat_rooms ADD COLUMN workspace_id bigint NULL;
+ALTER TABLE chat_rooms
+    ADD CONSTRAINT ck_chat_rooms_group_workspace
+    CHECK (type <> 'GROUP' OR workspace_id IS NOT NULL);

V11이 이미 배포된 환경이면 이 제약은 새 마이그레이션으로 추가하세요.

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

ALTER TABLE chat_rooms ADD COLUMN type varchar(20) NOT NULL DEFAULT 'DIRECT';
ALTER TABLE chat_rooms ADD COLUMN workspace_id bigint NULL;
ALTER TABLE chat_rooms
    ADD CONSTRAINT ck_chat_rooms_group_workspace
    CHECK (type <> 'GROUP' OR workspace_id IS NOT NULL);
🧰 Tools
🪛 Squawk (2.61.0)

[warning] 6-6: Changing the size of a varchar field requires an ACCESS EXCLUSIVE lock, that will prevent all reads and writes to the table. Use a TEXT field with a CHECK constraint.

(prefer-text-field)

🤖 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/V11__unify_chat_schema.sql` around lines 6 -
7, Ensure the chat_rooms schema enforces that GROUP rooms always have a non-null
workspace_id, preventing type = 'GROUP' rows from bypassing workspace-based
queries and uniqueness protection. If V11__unify_chat_schema.sql has already
been deployed, add this constraint in a new migration rather than modifying V11;
otherwise update the V11 migration accordingly.
src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql (1)

4-6: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Flyway의 마이그레이션 트랜잭션 설정을 확인한다.
fd -HI -t f 'application.*' . -x rg -n -C 2 'flyway|executeInTransaction|spring\.flyway' {} \; || true

# 기존 마이그레이션의 인덱스 생성 방식과 대상 파일을 확인한다.
rg -n -C 2 'CREATE( UNIQUE)? INDEX|CONCURRENTLY' src/main/resources/db/migration

Repository: alter-app/alter-backend

Length of output: 2783


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## application files containing Flyway"
fd -HI -t f 'application.*' . -print

echo "## Flyway and db settings"
rg -n -C 3 'flyway|spring\.database|spring\.jdbc|executeInTransaction|ddl-auto|hibernate' -g 'application*.y*ml' -g 'application*.properties' . || true

echo "## V14 target file"
cat -n src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql

Repository: alter-app/alter-backend

Length of output: 361


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## application files containing Flyway"
fd -HI -t f 'application.*' . || true

echo "## Flyway and db settings"
rg -n -C 3 'flyway|spring\.database|spring\.jdbc|executeInTransaction|ddl-auto|hibernate' $(fd -HI -t f 'application.*' .) || true

echo "## V14 target file"
cat -n src/main/resources/db/migration/V14__unique_workspace_group_chat_room.sql

Repository: alter-app/alter-backend

Length of output: 8450


🌐 Web query:

PostgreSQL CREATE INDEX CONCURRENTLY requirements CREATE INDEX CONCURRENTLY cannot run inside a transaction block

💡 Result:

In PostgreSQL, the CREATE INDEX CONCURRENTLY command cannot be executed within a transaction block (i.e., inside a BEGIN... COMMIT block) because it is not a single atomic transaction [1][2]. The requirement that it must run outside of a transaction block exists because the operation itself involves multiple internal steps that must be committed independently to make the index visible to other concurrent transactions before the index build is complete [3][1]. If it were wrapped in a user-defined transaction block, the system would be unable to properly manage these intermediate states and visibility requirements [1]. Key requirements and characteristics of CREATE INDEX CONCURRENTLY include: 1. Outside a Transaction: You must execute the command in its own session or outside of any explicit BEGIN/COMMIT block [1][2]. 2. Longer Duration: The process performs two scans of the table and must wait for all existing transactions that could potentially modify or use the index to terminate [4][5]. Consequently, it takes significantly longer and requires more total work than a standard index build [4][6]. 3. Non-Blocking: It allows normal DML operations (INSERT, UPDATE, DELETE) to continue on the table because it does not take locks that prevent these operations [4][3]. It primarily holds a ShareUpdateExclusiveLock for most of the operation, which only blocks concurrent schema changes and VACUUM FULL [7]. 4. Failure Handling: Because it is not a simple transactional DDL, if the command fails, the index may be left in an INVALID state [4][7]. You must manually drop such invalid indexes using DROP INDEX before attempting to recreate them [7]. 5. Limited Concurrency: While multiple standard index builds can occur simultaneously, only one concurrent index build is allowed on a specific table at a time [5][2]. For detailed information on the internal phases of the build and locking requirements, refer to the official PostgreSQL documentation for the CREATE INDEX command [4][5].

Citations:


운영 배포에서 이 마이그레이션을 트랜잭션 밖으로 실행하세요.

PostgreSQL의 CREATE UNIQUE INDEX CONCURRENTLY는 CREATE INDEX CONCURRENTLY와 다르게 DML을 대기시킬 수 있습니다. 이 마이그레이션을 트랜잭션 밖으로 실행하도록 Flyway 설정을 확인하세요.

🧰 Tools
🪛 Squawk (2.61.0)

[warning] 4-6: During normal index creation, table updates are blocked, but reads are still allowed. Use concurrently to avoid blocking writes.

(require-concurrent-index-creation)

🤖 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/V14__unique_workspace_group_chat_room.sql`
around lines 4 - 6, Flyway 설정에서 V14__unique_workspace_group_chat_room 마이그레이션을
트랜잭션 없이 실행하도록 구성하세요. 기존 uq_chat_group_room_per_workspace 인덱스 정의는 유지하고, 해당
마이그레이션에 대한 executeInTransaction 비활성화 설정이 적용되는지 확인하세요.

Source: Linters/SAST tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants