Skip to content

feat: [alt-267] 업장 업종 관리 시스템 재설계#97

Open
ysw789 wants to merge 10 commits into
devfrom
feat/alt-267
Open

feat: [alt-267] 업장 업종 관리 시스템 재설계#97
ysw789 wants to merge 10 commits into
devfrom
feat/alt-267

Conversation

@ysw789

@ysw789 ysw789 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

변경사항

[BE] 업장 업종 관리 시스템 재설계

업종(業種)은 업장(Workspace) 등록 시 확정되는 속성인데, 기존에는 free-text로 저장되고 사실상 PostingKeyword(공고 키워드)가 그 역할을 대행하고 있었습니다. 업종을 업장의 마스터 FK로 재설계하고 PostingKeyword를 폐기합니다.

  • 업종(BusinessType) 마스터 테이블·엔티티 신설 및 기본 시드 추가 (Flyway V4)
  • 업장/업장 신청의 업종을 free-text에서 마스터 FK + 상세입력(businessTypeDetail)으로 전환, 기존 데이터 백필 (Flyway V5)
  • '기타' 업종 선택 시에만 상세 입력을 요구·저장 (관리자가 마스터 확충 필요성을 인지하도록)
  • 업장 등록 폼용 업종 목록 조회 API 추가 (app / manager)
  • 관리자 업종 관리 CRUD API 추가 (/admin/business-types, '기타'·사용중 업종 삭제 방지)
  • PostingKeyword 폐기 — 관련 코드 및 테이블(posting_keywords, posting_keyword_map) 제거 (Flyway V6)
  • 공고 응답에 업장 업종 노출 + 업종 기반 목록 필터(businessTypeIds) 및 filter-options facet 추가
  • fix: 공고 지원 조건 반전 버그 수정 (모집중(OPEN) 공고 지원이 차단되던 문제)

Breaking (프론트 협의 필요)

  • 업장 등록 요청 typebusinessTypeId + businessTypeDetail
  • 공고 등록/수정 요청 및 모든 공고 응답에서 keywords 제거
  • GET /manager/postings/available-keywords 제거

Summary by CodeRabbit

  • 새 기능
    • 관리자 업종 관리(조회·생성·수정·삭제) 기능이 추가되었습니다.
    • 사용자/매니저가 업종 목록을 조회할 수 있습니다.
    • 공고 검색에서 업종 필터를 선택할 수 있으며, “기타”는 상세 입력을 지원합니다.
  • 변경 사항
    • 공고 생성·수정 및 공고/매니저 조회 화면에서 키워드 정보가 제거되었습니다.
    • 업장 신청과 공고 필터가 업종 및 상세 값 중심으로 갱신되었습니다.
  • 버그 수정
    • 모집 종료 상태의 공고에는 지원할 수 없도록 검증이 개선되었습니다.

@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Warning

Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories.

📝 Walkthrough

Walkthrough

업종을 독립 엔티티로 도입하고 관리자 CRUD API와 워크스페이스 등록·조회 흐름을 업종 ID 및 상세 정보 기반으로 변경했습니다. 공고 필터에 업종 선택을 추가했으며, 기존 공고 키워드 모델·조회·응답 경로를 제거했습니다.

Changes

업종 관리 및 워크스페이스 전파

Layer / File(s) Summary
업종 도메인·저장소·관리자 API
src/main/java/com/dreamteam/alter/domain/workspace/**, src/main/java/com/dreamteam/alter/application/workspace/usecase/Admin*BusinessType.java, src/main/java/com/dreamteam/alter/adapter/**/workspace/**
업종 엔티티, 저장소, 관리자 CRUD 유스케이스와 관리자 REST API를 추가했습니다.
워크스페이스 등록·조회 반영
src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java, src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java, src/main/java/com/dreamteam/alter/adapter/**/workspace/**
워크스페이스와 요청에 업종 연관 및 상세 값을 저장하고, 승인·조회 응답과 등록 화면용 목록 API에 반영했습니다.
스키마 및 검증
src/main/resources/db/migration/V4__create_business_types.sql, src/main/resources/db/migration/V5__migrate_workspace_business_type.sql, src/test/java/com/dreamteam/alter/application/workspace/**, src/test/java/com/dreamteam/alter/domain/workspace/**
업종 테이블과 기존 문자열 업종의 FK 이전을 추가하고 업종 검증·정규화·승인 복사를 테스트했습니다.

공고 필터 및 키워드 제거

Layer / File(s) Summary
공고 업종 필터
src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/*, src/main/java/com/dreamteam/alter/application/posting/usecase/GetPostingFilterOptions.java, src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java
공고 목록 필터와 필터 옵션에 업종을 추가하고 QueryDSL 조회 조건과 Workspace fetch join을 변경했습니다.
키워드 경로 제거
src/main/java/com/dreamteam/alter/domain/posting/**, src/main/java/com/dreamteam/alter/application/posting/usecase/{CreatePosting,ManagerUpdatePosting}.java, src/main/java/com/dreamteam/alter/adapter/**/posting/**, src/main/resources/db/migration/V6__drop_posting_keyword_tables.sql
공고 생성·수정·조회 응답에서 키워드 매핑을 제거하고 키워드 API, DTO, 저장소, 매핑 엔티티와 테이블을 삭제했습니다.
필터 테스트
src/test/java/com/dreamteam/alter/application/posting/usecase/GetPostingFilterOptionsTests.java
필터 옵션에 지역과 업종 목록이 포함되는지 검증했습니다.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: FEAT

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 업장 업종을 BusinessType FK와 상세 입력 기반으로 재설계하는 이번 변경의 핵심을 잘 요약한 제목입니다.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/alt-267

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

🤖 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/application/workspace/usecase/AdminCreateBusinessType.java`:
- Around line 22-30: Update AdminCreateBusinessType.execute to catch the
DataIntegrityViolationException raised by saving a duplicate BusinessType and
translate it to CustomException with ErrorCode.CONFLICT, preserving the existing
pre-check while ensuring concurrent duplicate inserts return 409 instead of 500.

In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java`:
- Around line 40-51: Move the “기타” 업종의 상세 입력 필수 검증 from the application-layer
resolveBusinessTypeDetail helper into the BusinessType domain model, preferably
through a method such as resolveDetail. Update CreateWorkspaceRequest.execute
and any related WorkspaceRequest.create flow to use the domain behavior,
preserving the existing exception behavior for missing required details and
allowing reuse by other use cases.

In `@src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessType.java`:
- Around line 13-34: BusinessType retains JPA and Spring infrastructure
annotations, violating the domain layer’s dependency boundary. Keep this PR
unchanged for this existing cross-cutting pattern; track separation of
BusinessType into a pure domain model and dedicated persistence entity as a
broader refactor rather than modifying this class now.

In
`@src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequest.java`:
- Around line 60-65: WorkspaceRequest.create(...) 내부에서도 BusinessType이 “기타”인 경우
businessTypeDetail이 필수라는 도메인 불변식을 검증하도록 수정하세요. CreateWorkspaceRequest에만 의존하지 말고,
BusinessType의 “기타” 판별과 상세값 검증을 팩토리 메서드에 포함해 다른 생성 경로에서도 동일한 규칙이 적용되게 하며, 기존의 유효한
생성 흐름은 유지하세요.

In `@src/main/resources/db/migration/V5__migrate_workspace_business_type.sql`:
- Around line 35-47: 분리 마이그레이션을 적용해 workspace_requests와 workspaces의 FK를 먼저 NOT
VALID로 추가하고, 별도 단계에서 VALIDATE CONSTRAINT 및 데이터 정합성 확인 후 NOT NULL을 설정하세요. 기존
business_type 컬럼 삭제는 구버전 애플리케이션이 참조하지 않도록 배포가 완료된 뒤 별도 contract 마이그레이션으로 이동하고,
현재 마이그레이션에서는 DROP COLUMN을 제거하세요.

In
`@src/test/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessTypeTests.java`:
- Around line 25-115: Organize the tests in AdminDeleteBusinessTypeTests into
`@Nested` classes with `@DisplayName`, separating the four exception scenarios from
the successful deletion scenario. Keep each existing test’s setup, assertions,
and behavior unchanged while placing failure cases under one clearly named group
and execute_성공 under a success group.

In
`@src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java`:
- Around line 166-241: Group the four BusinessType-related
tests—execute_존재하지않는업종_예외, execute_기타_상세없음_예외, execute_기타_상세있음_저장, and
execute_비기타_상세무시—inside a dedicated `@Nested` test class with a descriptive
`@DisplayName`. Preserve each test’s existing setup, assertions, and 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

Run ID: ecd7bbce-b337-4283-95ec-a31840b0de94

📥 Commits

Reviewing files that changed from the base of the PR and between 333c799 and d070d70.

📒 Files selected for processing (81)
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/controller/AdminBusinessTypeController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/controller/AdminBusinessTypeControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/dto/AdminBusinessTypeRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/dto/AdminBusinessTypeResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/dto/AdminWorkspaceRequestResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/common/dto/BusinessTypeResponseDto.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/ManagerPostingListWorkspaceResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingDetailResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingFilterOptionsResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingKeywordListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListFilterDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListWorkspaceResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingMapListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/controller/UserWorkspaceRequestController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/controller/UserWorkspaceRequestControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/PostingDetailWorkspaceResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/WorkspaceRequestResponseDto.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/ManagerPostingDetailResponseDto.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/workspace/controller/ManagerWorkspaceRequestController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceRequestControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingKeywordQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.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/BusinessTypeJpaRepository.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessTypeQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessTypeRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/readonly/ManagerWorkspaceListResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/readonly/ManagerWorkspaceResponse.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/readonly/WorkspaceRequestResponse.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePostingApplication.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/GetPostingFilterOptions.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/GetPostingKeywordList.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/AdminCreateBusinessType.java
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessType.java
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/AdminGetBusinessTypeList.java
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/AdminUpdateBusinessType.java
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/GetBusinessTypeList.java
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/PostingKeywordMap.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/GetPostingKeywordListUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/outbound/PostingKeywordQueryRepository.java
  • src/main/java/com/dreamteam/alter/domain/workspace/command/AdminCreateBusinessTypeCommand.java
  • src/main/java/com/dreamteam/alter/domain/workspace/command/AdminUpdateBusinessTypeCommand.java
  • src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessType.java
  • src/main/java/com/dreamteam/alter/domain/workspace/entity/Workspace.java
  • src/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequest.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminCreateBusinessTypeUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminDeleteBusinessTypeUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminGetBusinessTypeListUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminUpdateBusinessTypeUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetBusinessTypeListUseCase.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessTypeQueryRepository.java
  • src/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessTypeRepository.java
  • src/main/resources/db/migration/V4__create_business_types.sql
  • src/main/resources/db/migration/V5__migrate_workspace_business_type.sql
  • src/main/resources/db/migration/V6__drop_posting_keyword_tables.sql
  • src/test/java/com/dreamteam/alter/application/posting/usecase/GetPostingFilterOptionsTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/AdminCreateBusinessTypeTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessTypeTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/AdminUpdateBusinessTypeTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatusTests.java
  • src/test/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequestTests.java
💤 Files with no reviewable changes (17)
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/GetPostingKeywordListUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/PostingKeywordMap.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/outbound/PostingKeywordQueryRepository.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingKeywordQueryRepositoryImpl.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingKeywordListResponseDto.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/GetPostingKeywordList.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingDetailResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingMapListResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingDetailResponseDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java

Comment on lines +22 to +30
@Override
public Long execute(AdminCreateBusinessTypeCommand command) {
if (businessTypeRepository.existsByName(command.name())) {
throw new CustomException(ErrorCode.CONFLICT, "이미 존재하는 업종입니다.");
}

BusinessType businessType = BusinessType.create(command.name(), command.description());
return businessTypeRepository.save(businessType).getId();
}

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 | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# V4 마이그레이션에서 UNIQUE 제약 조건 확인
cat src/main/resources/db/migration/V4__create_business_types.sql

# BusinessType.create() 메서드 검증
ast-grep outline src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessType.java --items all --match 'create'

# posting_keywords 테이블 참조 잔존 확인
rg -n "posting_keyword" --type java -g '!**/db/migration/**'

Repository: alter-app/alter-backend

Length of output: 1430


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== AdminCreateBusinessType ==\n'
cat -n src/main/java/com/dreamteam/alter/application/workspace/usecase/AdminCreateBusinessType.java

printf '\n== BusinessType entity ==\n'
cat -n src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessType.java

printf '\n== DataIntegrityViolationException handling ==\n'
rg -n "DataIntegrityViolationException|IntegrityViolation|`@RestControllerAdvice`|`@ExceptionHandler`|CustomException|ErrorCode.CONFLICT" src/main/java

printf '\n== Transaction annotations in application workspace usecases ==\n'
rg -n "`@Transactional`|`@Slf4j`|`@Service`\\(" src/main/java/com/dreamteam/alter/application/workspace/usecase

Repository: alter-app/alter-backend

Length of output: 50380


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Global exception handling ==\n'
rg -n "DataIntegrityViolationException|IntegrityViolation|ExceptionHandler|RestControllerAdvice|ControllerAdvice|CustomException" src/main/java/com/dreamteam/alter/common src/main/java/com/dreamteam/alter -g '!**/target/**' | head -n 200

printf '\n== Common exception package ==\n'
fd -a . src/main/java/com/dreamteam/alter/common/exception
cat -n src/main/java/com/dreamteam/alter/common/exception/CustomException.java
printf '\n---\n'
cat -n src/main/java/com/dreamteam/alter/common/exception/ErrorCode.java

printf '\n== Search for UNIQUE handling around business_types ==\n'
rg -n "business_types|unique = true|existsByName|findByName" src/main/java src/main/resources/db/migration

printf '\n== BusinessType-related use case references ==\n'
rg -n "AdminCreateBusinessType|AdminUpdateBusinessType|AdminDeleteBusinessType|BusinessType.create\\(" src/main/java/com/dreamteam/alter/application/workspace/usecase src/main/java/com/dreamteam/alter/domain/workspace/entity

Repository: alter-app/alter-backend

Length of output: 46069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== GlobalExceptionHandler ==\n'
cat -n src/main/java/com/dreamteam/alter/common/exception/handler/GlobalExceptionHandler.java

printf '\n== AdminCreateBusinessTypeCommand ==\n'
cat -n src/main/java/com/dreamteam/alter/domain/workspace/command/AdminCreateBusinessTypeCommand.java

printf '\n== Workspace business-type usecases ==\n'
sed -n '1,120p' src/main/java/com/dreamteam/alter/application/workspace/usecase/AdminUpdateBusinessType.java
printf '\n---\n'
sed -n '1,120p' src/main/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessType.java

Repository: alter-app/alter-backend

Length of output: 9105


existsByName는 여전히 경쟁 조건이지만, 중복 저장은 DB 제약으로 막고 예외를 409로 바꿔야 함

business_types.name에 UNIQUE 제약이 이미 있어서 중복 데이터가 실제로 저장되진 않습니다. 다만 existsByName() 이후 동시 insert가 들어오면 한쪽은 제약 위반으로 실패하고, 현재는 그 예외가 CONFLICT로 매핑되지 않아 500으로 노출될 수 있습니다. DataIntegrityViolationException을 409로 처리하거나, 중복 체크를 DB 제약 기준으로 통일하세요.

🤖 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/application/workspace/usecase/AdminCreateBusinessType.java`
around lines 22 - 30, Update AdminCreateBusinessType.execute to catch the
DataIntegrityViolationException raised by saving a duplicate BusinessType and
translate it to CustomException with ErrorCode.CONFLICT, preserving the existing
pre-check while ensuring concurrent duplicate inserts return 409 instead of 500.

Comment on lines +13 to +34
@Table(name = "business_types")
@Builder(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EntityListeners(AuditingEntityListener.class)
public class PostingKeyword {
public class BusinessType {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

@Column(name = "name", length = 128, nullable = false, unique = true)
@Column(name = "name", length = 64, nullable = false, unique = true)
private String name;

@Column(name = "description", length = 255, nullable = true)
private String description;

// '기타' 등 상세 입력을 요구하는 업종 식별 플래그. 시드로만 생성되며 관리자 CRUD로는 항상 false.
@Column(name = "requires_detail", nullable = false)
private boolean requiresDetail;

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 | ⚖️ Poor tradeoff

도메인 엔티티에 JPA/Spring 어노테이션이 그대로 포함됨

@Entity, @Table, @Column, @GeneratedValue, @EntityListeners(AuditingEntityListener.class) 등은 인프라 의존성으로, 도메인 계층은 "ZERO infrastructure dependencies (no Spring, no JPA annotations, no external libs)"를 요구합니다. 다만 이는 이 PR에서 새로 도입된 문제가 아니라 기존 PostingKeyword 및 다른 도메인 엔티티들에서 이미 사용되던 패턴을 그대로 이전한 것으로 보입니다. 별도 영속성 엔티티(JPA)와 순수 도메인 모델을 분리하는 것은 코드베이스 전체에 걸친 대규모 리팩터가 필요해 이번 PR 범위에서 다루기는 어려워 보이나, 참고 차원에서 기록합니다.

As per path instructions, domain layer should have "ZERO infrastructure dependencies (no Spring, no JPA annotations, no external libs)".

🤖 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/workspace/entity/BusinessType.java`
around lines 13 - 34, BusinessType retains JPA and Spring infrastructure
annotations, violating the domain layer’s dependency boundary. Keep this PR
unchanged for this existing cross-cutting pattern; track separation of
BusinessType into a pure domain model and dedicated persistence entity as a
broader refactor rather than modifying this class now.

Source: Path instructions

Comment on lines +60 to +65
@JoinColumn(name = "business_type_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private BusinessType businessType;

@Column(name = "business_type_detail", length = 128, nullable = true)
private String businessTypeDetail;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

'기타' 업종 상세 필수 불변식을 엔티티 레벨에서도 보호하는 것을 고려해보세요.

현재 "기타 업종 선택 시 상세 입력 필수" 검증은 CreateWorkspaceRequest 유스케이스에서만 수행되고, WorkspaceRequest.create(...)는 이미 검증된 값을 단순히 저장합니다. BusinessType이 "기타"인지 여부는 인프라 의존 없이 판단 가능한 순수 도메인 규칙이므로, 팩토리 메서드 내부에서 함께 검증하면 향후 다른 생성 경로가 이 불변식을 우회하는 것을 방지할 수 있습니다.

As per path instructions, domain 계층은 "Entities encapsulate business rules and invariants"를 요구합니다.

Also applies to: 96-125

🤖 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/workspace/entity/WorkspaceRequest.java`
around lines 60 - 65, WorkspaceRequest.create(...) 내부에서도 BusinessType이 “기타”인 경우
businessTypeDetail이 필수라는 도메인 불변식을 검증하도록 수정하세요. CreateWorkspaceRequest에만 의존하지 말고,
BusinessType의 “기타” 판별과 상세값 검증을 팩토리 메서드에 포함해 다른 생성 경로에서도 동일한 규칙이 적용되게 하며, 기존의 유효한
생성 흐름은 유지하세요.

Source: Path instructions

Comment on lines +35 to +47
ALTER TABLE workspace_requests
ALTER COLUMN business_type_id SET NOT NULL,
ADD CONSTRAINT fk_workspace_requests_business_type
FOREIGN KEY (business_type_id) REFERENCES business_types (id);

ALTER TABLE workspaces
ALTER COLUMN business_type_id SET NOT NULL,
ADD CONSTRAINT fk_workspaces_business_type
FOREIGN KEY (business_type_id) REFERENCES business_types (id);

-- 구 free-text 컬럼 제거 (미일치분은 '기타' 상세로 보존됨)
ALTER TABLE workspace_requests DROP COLUMN business_type;
ALTER TABLE workspaces DROP COLUMN business_type;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔵 Trivial | 🏗️ Heavy lift

단일 마이그레이션에서 NOT NULL/FK/DROP COLUMN을 한꺼번에 수행 — 운영 환경 락 위험

Squawk 정적분석 결과 기준으로, SET NOT NULL(36/41행)과 FK 제약 추가(37-38/42-43행)는 각각 테이블 풀스캔과 SHARE ROW EXCLUSIVE 락을 유발하고, DROP COLUMN(46-47행)은 무중단 배포 중 구버전 앱 인스턴스가 여전히 business_type 컬럼을 참조할 경우 장애를 유발할 수 있습니다. 테이블 크기가 크다면 이 모든 작업이 하나의 마이그레이션에서 실행되어 서비스 다운타임으로 이어질 수 있습니다.

Postgres는 ADD COLUMN, SET NOT NULL, DROP COLUMN 모두 ACCESS EXCLUSIVE 락을 획득하며, FK/제약 추가는 기본적으로 테이블 스캔을 동반합니다. 안전한 expand/contract 패턴으로 분리하는 것을 권장합니다:

  • FK는 NOT VALID로 추가 후 별도 트랜잭션에서 VALIDATE CONSTRAINT 실행
  • DROP COLUMN은 애플리케이션이 새 컬럼만 사용하도록 배포를 완전히 마친 이후의 별도 마이그레이션으로 분리
🛡️ 제안: FK를 NOT VALID로 분리
 ALTER TABLE workspace_requests
     ALTER COLUMN business_type_id SET NOT NULL,
-    ADD CONSTRAINT fk_workspace_requests_business_type
-        FOREIGN KEY (business_type_id) REFERENCES business_types (id);
+    ADD CONSTRAINT fk_workspace_requests_business_type
+        FOREIGN KEY (business_type_id) REFERENCES business_types (id) NOT VALID;

+ALTER TABLE workspace_requests VALIDATE CONSTRAINT fk_workspace_requests_business_type;

테이블 규모와 트래픽 패턴에 따라 리스크 수준이 달라지므로, 실제 운영 배포 전 테이블 크기와 배포 방식(무중단 여부)을 재확인해보시길 권장합니다.

🧰 Tools
🪛 Squawk (2.59.0)

[warning] 36-36: Setting a column NOT NULL blocks reads while the table is scanned. Make the field nullable and use a CHECK constraint instead.

(adding-not-nullable-field)


[warning] 37-38: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.

(constraint-missing-not-valid)


[warning] 37-38: Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes to each table. Add NOT VALID to the constraint in one transaction and then VALIDATE the constraint in a separate transaction.

(adding-foreign-key-constraint)


[warning] 41-41: Setting a column NOT NULL blocks reads while the table is scanned. Make the field nullable and use a CHECK constraint instead.

(adding-not-nullable-field)


[warning] 42-43: By default new constraints require a table scan and block writes to the table while that scan occurs. Use NOT VALID with a later VALIDATE CONSTRAINT call.

(constraint-missing-not-valid)


[warning] 42-43: Adding a foreign key constraint requires a table scan and a SHARE ROW EXCLUSIVE lock on both tables, which blocks writes to each table. Add NOT VALID to the constraint in one transaction and then VALIDATE the constraint in a separate transaction.

(adding-foreign-key-constraint)


[warning] 46-46: Dropping a column may break existing clients.

(ban-drop-column)


[warning] 47-47: Dropping a column may break existing clients.

(ban-drop-column)

🤖 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/V5__migrate_workspace_business_type.sql`
around lines 35 - 47, 분리 마이그레이션을 적용해 workspace_requests와 workspaces의 FK를 먼저 NOT
VALID로 추가하고, 별도 단계에서 VALIDATE CONSTRAINT 및 데이터 정합성 확인 후 NOT NULL을 설정하세요. 기존
business_type 컬럼 삭제는 구버전 애플리케이션이 참조하지 않도록 배포가 완료된 뒤 별도 contract 마이그레이션으로 이동하고,
현재 마이그레이션에서는 DROP COLUMN을 제거하세요.

Source: Linters/SAST tools

ysw789 added 3 commits July 15, 2026 16:27
'ZERO infrastructure dependencies (no JPA annotations)' 규칙이 실제 컨벤션과
불일치하여 매 PR마다 오탐을 생성했다. 도메인 엔티티 40개 전부가 JPA 매핑을
사용하고, CLAUDE.md 역시 Getter + private Builder + 정적 팩토리 패턴을 명시한다.

규칙을 실제 컨벤션(JPA 매핑 허용, Setter 금지, 불변식 캡슐화)으로 교체한다.
AdminCreate/Update/DeleteBusinessTypeTests 3종의 테스트를 execute 그룹으로 묶어
같은 폴더의 다른 유스케이스 테스트(CancelWorkspaceRequestTests 등)와 구조를 통일한다.

Mock/InjectMocks/ExtendWith는 외부 클래스에 유지하고 테스트 본문은 변경하지 않는다.
셋 다 flat 이었으므로 Delete 만 그룹화하면 오히려 일관성이 깨져 3종을 함께 처리한다.
'기타 업종이면 상세 입력 필수' 규칙은 requiresDetail 플래그를 소유한 BusinessType 의
불변식이므로, CreateWorkspaceRequest 의 private 헬퍼에서 BusinessType.resolveDetail()
로 옮긴다. 호출부가 1곳뿐이고 승인 시 copy 경로는 검증된 값을 그대로 전달하므로
동작은 동일하다.

테스트가 BusinessType 을 mock 으로 생성해 검증 규칙이 스텁아웃되던 문제도 함께
해소한다. businessType() 헬퍼를 실제 엔티티 생성으로 전환하여 업종 관련 테스트 4개가
유스케이스를 통해 실제 규칙을 검증하게 되고, 이미지 테스트 3개의 불필요한 스텁도
사라진다. BusinessTypeTests 도메인 단위테스트를 신설한다.

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

♻️ Duplicate comments (1)
src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java (1)

174-248: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

업종(BusinessType) 검증 테스트를 별도 @Nested 그룹으로 세분화하는 것을 권장합니다.

execute_존재하지않는업종_예외, execute_기타_상세없음_예외, execute_기타_상세있음_저장, execute_비기타_상세무시 4개는 모두 "업종 검증"이라는 공통 관심사를 다루지만 현재 ExecuteTests 최상위에 대표이미지 테스트들과 함께 나열되어 있습니다. 과거 리뷰에서 제안된 하위 @Nested 그룹화가 이번 커밋에서도 반영되지 않았습니다. As per path instructions, "@Nested classes group related tests with @DisplayName."

🤖 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/workspace/usecase/CreateWorkspaceRequestTests.java`
around lines 174 - 248, Create a dedicated `@Nested` class with an appropriate
`@DisplayName` inside ExecuteTests for BusinessType validation scenarios. Move
execute_존재하지않는업종_예외, execute_기타_상세없음_예외, execute_기타_상세있음_저장, and
execute_비기타_상세무시 into that group, keeping their setup and assertions unchanged.

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.

Duplicate comments:
In
`@src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java`:
- Around line 174-248: Create a dedicated `@Nested` class with an appropriate
`@DisplayName` inside ExecuteTests for BusinessType validation scenarios. Move
execute_존재하지않는업종_예외, execute_기타_상세없음_예외, execute_기타_상세있음_저장, and
execute_비기타_상세무시 into that group, keeping their setup and assertions unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e289cfbb-b2d6-47af-81fd-287a57039dc1

📥 Commits

Reviewing files that changed from the base of the PR and between d070d70 and c532c78.

📒 Files selected for processing (8)
  • .coderabbit.yaml
  • src/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.java
  • src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessType.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/AdminCreateBusinessTypeTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessTypeTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/AdminUpdateBusinessTypeTests.java
  • src/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.java
  • src/test/java/com/dreamteam/alter/domain/workspace/entity/BusinessTypeTests.java

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.

1 participant