feat: [alt-267] 업장 업종 관리 시스템 재설계#97
Conversation
모집중(OPEN) 공고 지원이 오히려 차단되던 조건 반전 버그 수정. if (OPEN) throw -> if (!OPEN) throw.
|
Warning Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories. 📝 WalkthroughWalkthrough업종을 독립 엔티티로 도입하고 관리자 CRUD API와 워크스페이스 등록·조회 흐름을 업종 ID 및 상세 정보 기반으로 변경했습니다. 공고 필터에 업종 선택을 추가했으며, 기존 공고 키워드 모델·조회·응답 경로를 제거했습니다. Changes업종 관리 및 워크스페이스 전파
공고 필터 및 키워드 제거
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (81)
src/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/controller/AdminBusinessTypeController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/controller/AdminBusinessTypeControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/dto/AdminBusinessTypeRequestDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/dto/AdminBusinessTypeResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/workspace/dto/AdminWorkspaceRequestResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/common/dto/BusinessTypeResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/ManagerPostingListWorkspaceResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingDetailResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingFilterOptionsResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingKeywordListResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListFilterDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListWorkspaceResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingMapListResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/controller/UserWorkspaceRequestController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/controller/UserWorkspaceRequestControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/CreateWorkspaceRequestDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/PostingDetailWorkspaceResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/WorkspaceRequestResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/controller/ManagerPostingControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingDetailResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/ManagerPostingListResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/posting/dto/UpdatePostingRequestDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceRequestController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceRequestControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceListResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/ManagerWorkspaceResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingKeywordQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingDetailResponse.javasrc/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/ManagerPostingListResponse.javasrc/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingDetailResponse.javasrc/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/readonly/PostingListResponse.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessTypeJpaRepository.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessTypeQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessTypeRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceRequestQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/readonly/ManagerWorkspaceListResponse.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/readonly/ManagerWorkspaceResponse.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/readonly/WorkspaceRequestResponse.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/CreatePostingApplication.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/GetPostingFilterOptions.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/GetPostingKeywordList.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/AdminCreateBusinessType.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessType.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/AdminGetBusinessTypeList.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/AdminUpdateBusinessType.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/GetBusinessTypeList.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatus.javasrc/main/java/com/dreamteam/alter/domain/posting/entity/Posting.javasrc/main/java/com/dreamteam/alter/domain/posting/entity/PostingKeywordMap.javasrc/main/java/com/dreamteam/alter/domain/posting/port/inbound/GetPostingKeywordListUseCase.javasrc/main/java/com/dreamteam/alter/domain/posting/port/outbound/PostingKeywordQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/command/AdminCreateBusinessTypeCommand.javasrc/main/java/com/dreamteam/alter/domain/workspace/command/AdminUpdateBusinessTypeCommand.javasrc/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessType.javasrc/main/java/com/dreamteam/alter/domain/workspace/entity/Workspace.javasrc/main/java/com/dreamteam/alter/domain/workspace/entity/WorkspaceRequest.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminCreateBusinessTypeUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminDeleteBusinessTypeUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminGetBusinessTypeListUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AdminUpdateBusinessTypeUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetBusinessTypeListUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessTypeQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessTypeRepository.javasrc/main/resources/db/migration/V4__create_business_types.sqlsrc/main/resources/db/migration/V5__migrate_workspace_business_type.sqlsrc/main/resources/db/migration/V6__drop_posting_keyword_tables.sqlsrc/test/java/com/dreamteam/alter/application/posting/usecase/GetPostingFilterOptionsTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/AdminCreateBusinessTypeTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessTypeTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/AdminUpdateBusinessTypeTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/UpdateWorkspaceRequestStatusTests.javasrc/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
| @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(); | ||
| } |
There was a problem hiding this comment.
🗄️ 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/usecaseRepository: 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/entityRepository: 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.javaRepository: 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.
| @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; | ||
|
|
There was a problem hiding this comment.
📐 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
| @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; |
There was a problem hiding this comment.
🎯 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
| 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; |
There was a problem hiding this comment.
🩺 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
'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 도메인 단위테스트를 신설한다.
There was a problem hiding this comment.
♻️ 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, "@Nestedclasses 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
📒 Files selected for processing (8)
.coderabbit.yamlsrc/main/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequest.javasrc/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessType.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/AdminCreateBusinessTypeTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/AdminDeleteBusinessTypeTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/AdminUpdateBusinessTypeTests.javasrc/test/java/com/dreamteam/alter/application/workspace/usecase/CreateWorkspaceRequestTests.javasrc/test/java/com/dreamteam/alter/domain/workspace/entity/BusinessTypeTests.java
변경사항
[BE] 업장 업종 관리 시스템 재설계
업종(業種)은 업장(Workspace) 등록 시 확정되는 속성인데, 기존에는 free-text로 저장되고 사실상 PostingKeyword(공고 키워드)가 그 역할을 대행하고 있었습니다. 업종을 업장의 마스터 FK로 재설계하고 PostingKeyword를 폐기합니다.
/admin/business-types, '기타'·사용중 업종 삭제 방지)businessTypeIds) 및 filter-options facet 추가Breaking (프론트 협의 필요)
type→businessTypeId+businessTypeDetailkeywords제거GET /manager/postings/available-keywords제거Summary by CodeRabbit