feat: [alt-267] 업종 키워드 관련 기능 고도화 및 지원조건 버그 수정#96
Conversation
- /admin/posting-keywords 목록·생성·수정·삭제 API (약관 관리 패턴 재사용) - 쓰기 포트 PostingKeywordRepository 신설, 이름 중복·사용중 삭제 가드 - 초기 업종 Flyway 시드(V5, ON CONFLICT DO NOTHING 멱등)
- postings.custom_keywords jsonb 컬럼 추가(V4) - 마스터 선택(keywords) + 직접입력(customKeywords) 병행, 업종 최소 1개 검증(AssertTrue)
- GET /app/postings?keyword= : 마스터 업종명 OR 직접입력 라벨(jsonb) 부분일치 - filter-options에 업종 추천 facet 노출
- 목록·상세 응답(알바생/매니저)에 customKeywords 필드 추가 - 마스터 키워드 0개(직접입력 전용) 공고 조회 시 NPE 방지(getOrDefault)
모집중(OPEN) 공고 지원이 오히려 차단되던 조건 반전 버그 수정. if (OPEN) throw -> if (!OPEN) throw.
📝 WalkthroughWalkthrough관리자 게시 키워드 CRUD API와 키워드 관리 유스케이스가 추가되었습니다. 공고는 마스터 업종과 직접입력 업종을 함께 저장·검증하며, 목록 필터와 일반·관리자 응답에 직접입력 업종을 포함합니다. Changes게시 키워드 관리
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant AdminClient
participant AdminPostingKeywordController
participant AdminCreatePostingKeyword
participant PostingKeywordRepository
participant Database
AdminClient->>AdminPostingKeywordController: 게시 키워드 생성 요청
AdminPostingKeywordController->>AdminCreatePostingKeyword: 생성 커맨드 전달
AdminCreatePostingKeyword->>PostingKeywordRepository: 중복 확인 및 저장
PostingKeywordRepository->>Database: posting_keywords 조회/삽입
Database-->>PostingKeywordRepository: 생성된 키워드 반환
PostingKeywordRepository-->>AdminCreatePostingKeyword: 생성된 id 반환
AdminCreatePostingKeyword-->>AdminPostingKeywordController: 응답 데이터 반환
AdminPostingKeywordController-->>AdminClient: 201 Created
🚥 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: 10
🤖 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/admin/posting/controller/AdminPostingKeywordController.java`:
- Around line 56-62: 생성 응답이 요청값 대신 영속화된 엔티티 값을 사용하도록 수정하세요.
`AdminCreatePostingKeyword` 유스케이스와 관련 포트의 반환 타입을 `Long`에서 저장된 `PostingKeyword`
엔티티로 변경하고, `createPostingKeyword`에서는 반환된 엔티티를
`AdminPostingKeywordResponseDto::from`으로 매핑하세요.
- Around line 30-40: `@RequiredArgsConstructor가` 생성자 주입을 처리하므로
AdminPostingKeywordController의 네 개 final 의존성 필드에서 중복된 `@Resource`(name = "...")
어노테이션을 제거하세요.
In
`@src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java`:
- Around line 45-55: keywordMatches가 null인 경우에도 불필요한 키워드 테이블 조인이 실행됩니다.
PostingQueryRepositoryImpl의 카운트 쿼리와 커서 조회 쿼리에서 keywordMatches 존재 여부에 따라
qPostingKeywordMap 및 qPostingKeyword 조인을 조건부로 추가하거나 쿼리를 분기하고, 키워드 필터가 없을 때는 해당
조인을 생략하도록 수정하세요.
- Around line 597-610: Update keywordMatches to cast qPosting.customKeywords as
PostgreSQL text instead of string, and escape SQL LIKE wildcard characters (%)
and (_) and the escape character in keyword before constructing the ilike
pattern. Use the escaped keyword in the pattern while preserving the existing
masterMatch.or(customMatch) behavior.
In
`@src/main/java/com/dreamteam/alter/application/posting/usecase/CreatePosting.java`:
- Around line 33-38: Extract the duplicated keyword lookup and validation from
CreatePosting.execute() and ManagerUpdatePosting.execute() into a shared
PostingKeywordResolver (or equivalent) component with a resolveAndValidate
method. Inject and use this helper in both use cases, preserving empty-list
handling and INVALID_KEYWORD validation.
In
`@src/main/java/com/dreamteam/alter/application/posting/usecase/ManagerUpdatePosting.java`:
- Around line 37-43: ManagerUpdatePosting의 키워드 조회·검증 로직이 CreatePosting과 중복되어
있습니다. 두 유스케이스에서 공통으로 사용하는 키워드 검증 메서드 또는 서비스로 추출하고, CreatePosting과
ManagerUpdatePosting이 해당 공통 로직을 호출하도록 수정하세요.
In `@src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java`:
- Around line 11-18: Posting 도메인 엔티티에서 새 인프라 의존성을 제거하세요. Posting의 ObjectUtils
사용은 표준 Java null/빈 값 검사로 대체하고 import를 삭제하세요. JSONB 매핑을 위해 필요한 `@Type과`
JsonBinaryType은 도메인 엔티티에서 분리해 JPA 인프라 엔티티 또는 매핑 계층으로 이동하며, Posting에는 해당
Hibernate 의존성이 남지 않도록 하세요.
- Line 135: Posting의 updateContent 메서드에서도 전달받은 customKeywords 리스트를 직접 참조하지 않도록
수정하세요. 현재 ObjectUtils.isNotEmpty(customKeywords) 조건의 비어 있지 않은 경우에 새 리스트로 방어적 복사한
값을 할당하고, 비어 있거나 null인 경우에는 빈 ArrayList를 사용하도록 하여 해당 필드가 외부 리스트 변경의 영향을 받지 않게
하세요.
- Around line 95-97: Posting 엔티티의 customKeywords 할당에서 요청 리스트를 직접 참조하지 않도록 방어적
복사를 적용하세요. request.getCustomKeywords()가 비어 있지 않은 경우에도 새 ArrayList로 복사해 할당하고, 비어
있으면 기존처럼 빈 리스트를 사용하도록 수정하세요.
In `@src/main/java/com/dreamteam/alter/domain/posting/entity/PostingKeyword.java`:
- Around line 46-49: PostingKeyword.update 메서드가 name의 엔티티 불변식을 검증하도록 수정하세요.
name이 null이 아니고 최대 128자를 만족하는지 엔티티 내부에서 검증한 뒤에만 필드를 할당하며, 위반 시 프로젝트의 기존 도메인 예외
또는 검증 방식을 사용하세요. description 처리도 기존 도메인 규칙이 있다면 동일한 방식으로 검증하세요.
🪄 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: a56cd139-9bcc-459e-86b5-955b0b884a51
📒 Files selected for processing (38)
src/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/controller/AdminPostingKeywordController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/controller/AdminPostingKeywordControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/dto/AdminPostingKeywordRequestDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/dto/AdminPostingKeywordResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/CreatePostingRequestDto.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/PostingListFilterDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListResponseDto.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/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/application/posting/usecase/AdminCreatePostingKeyword.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/AdminDeletePostingKeyword.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/AdminGetPostingKeywordList.javasrc/main/java/com/dreamteam/alter/application/posting/usecase/AdminUpdatePostingKeyword.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/ManagerUpdatePosting.javasrc/main/java/com/dreamteam/alter/domain/posting/command/AdminCreatePostingKeywordCommand.javasrc/main/java/com/dreamteam/alter/domain/posting/command/AdminUpdatePostingKeywordCommand.javasrc/main/java/com/dreamteam/alter/domain/posting/entity/Posting.javasrc/main/java/com/dreamteam/alter/domain/posting/entity/PostingKeyword.javasrc/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminCreatePostingKeywordUseCase.javasrc/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminDeletePostingKeywordUseCase.javasrc/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminGetPostingKeywordListUseCase.javasrc/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminUpdatePostingKeywordUseCase.javasrc/main/java/com/dreamteam/alter/domain/posting/port/outbound/PostingKeywordQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/posting/port/outbound/PostingKeywordRepository.javasrc/main/resources/db/migration/V4__add_postings_custom_keywords_column.sqlsrc/main/resources/db/migration/V5__seed_posting_keywords.sql
| @Resource(name = "adminGetPostingKeywordList") | ||
| private final AdminGetPostingKeywordListUseCase adminGetPostingKeywordList; | ||
|
|
||
| @Resource(name = "adminCreatePostingKeyword") | ||
| private final AdminCreatePostingKeywordUseCase adminCreatePostingKeyword; | ||
|
|
||
| @Resource(name = "adminUpdatePostingKeyword") | ||
| private final AdminUpdatePostingKeywordUseCase adminUpdatePostingKeyword; | ||
|
|
||
| @Resource(name = "adminDeletePostingKeyword") | ||
| private final AdminDeletePostingKeywordUseCase adminDeletePostingKeyword; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
@Resource와 @RequiredArgsConstructor 중복은 불필요합니다.
@RequiredArgsConstructor로 생성자 주입이 이미 처리되며, 파라미터명이 빈 이름과 일치하므로 @Resource(name = "...")는 중복입니다. 제거를 고려해 보세요.
🤖 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/admin/posting/controller/AdminPostingKeywordController.java`
around lines 30 - 40, `@RequiredArgsConstructor가` 생성자 주입을 처리하므로
AdminPostingKeywordController의 네 개 final 의존성 필드에서 중복된 `@Resource`(name = "...")
어노테이션을 제거하세요.
| Long id = adminCreatePostingKeyword.execute( | ||
| new AdminCreatePostingKeywordCommand(request.getName(), request.getDescription()) | ||
| ); | ||
| return ResponseEntity.status(HttpStatus.CREATED) | ||
| .body(CommonApiResponse.of( | ||
| AdminPostingKeywordResponseDto.of(id, request.getName(), request.getDescription()) | ||
| )); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
생성 응답에 요청값이 아닌 영속화된 엔티티 값을 사용하세요.
createPostingKeyword에서 응답 DTO를 request.getName(), request.getDescription()로 구성하고 있습니다. 유스케이스가 입력값을 정규화(공백 제거 등)하는 경우, 응답과 실제 저장된 데이터가 불일치하게 됩니다. 유스케이스가 Long만 반환하므로 컨트롤러에서 from(PostingKeyword)을 사용할 수 없는 구조적 한계가 원인입니다.
생성 유스케이스가 영속화된 PostingKeyword 엔티티를 반환하도록 포트 시그니처를 변경하고, 컨트롤러에서 AdminPostingKeywordResponseDto::from으로 매핑하는 것을 권장합니다.
🤖 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/admin/posting/controller/AdminPostingKeywordController.java`
around lines 56 - 62, 생성 응답이 요청값 대신 영속화된 엔티티 값을 사용하도록 수정하세요.
`AdminCreatePostingKeyword` 유스케이스와 관련 포트의 반환 타입을 `Long`에서 저장된 `PostingKeyword`
엔티티로 변경하고, `createPostingKeyword`에서는 반환된 엔티티를
`AdminPostingKeywordResponseDto::from`으로 매핑하세요.
| QPostingKeywordMap qPostingKeywordMap = QPostingKeywordMap.postingKeywordMap; | ||
| QPostingKeyword qPostingKeyword = QPostingKeyword.postingKeyword; | ||
| QWorkspace qWorkspace = QWorkspace.workspace; | ||
|
|
||
| Long count = queryFactory | ||
| .select(qPosting.countDistinct()) | ||
| .from(qPosting) | ||
| .leftJoin(qPosting.schedules, qPostingSchedule) | ||
| .leftJoin(qPosting.workspace, qWorkspace) | ||
| .leftJoin(qPosting.keywords, qPostingKeywordMap) | ||
| .leftJoin(qPostingKeywordMap.postingKeyword, qPostingKeyword) |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff
키워드 필터 없을 때 불필요한 left join이 성능 저하를 유발합니다.
keywordMatches가 null을 반환하는 경우(키워드 필터 없음), qPostingKeywordMap 및 qPostingKeyword에 대한 left join이 여전히 수행됩니다. 카운트 쿼리와 커서 쿼리 모두에서 불필요한 조인 비용이 발생합니다.
조건부 조인 또는 별도 쿼리 분기를 통해 최적화를 권장합니다.
Also applies to: 105-106
🤖 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/outbound/posting/persistence/PostingQueryRepositoryImpl.java`
around lines 45 - 55, keywordMatches가 null인 경우에도 불필요한 키워드 테이블 조인이 실행됩니다.
PostingQueryRepositoryImpl의 카운트 쿼리와 커서 조회 쿼리에서 keywordMatches 존재 여부에 따라
qPostingKeywordMap 및 qPostingKeyword 조인을 조건부로 추가하거나 쿼리를 분기하고, 키워드 필터가 없을 때는 해당
조인을 생략하도록 수정하세요.
| /** | ||
| * 업종 필터: 입력 텍스트가 (마스터 연결 업종명 OR 직접입력 업종 라벨)에 부분일치. | ||
| * 직접입력 라벨은 jsonb(custom_keywords)라 text 캐스팅 후 ilike로 매칭한다. | ||
| */ | ||
| private BooleanExpression keywordMatches(QPostingKeyword qPostingKeyword, QPosting qPosting, String keyword) { | ||
| if (ObjectUtils.isEmpty(keyword) || keyword.isBlank()) { | ||
| return null; | ||
| } | ||
| BooleanExpression masterMatch = qPostingKeyword.name.containsIgnoreCase(keyword); | ||
| BooleanExpression customMatch = Expressions.booleanTemplate( | ||
| "cast({0} as string) ilike {1}", qPosting.customKeywords, "%" + keyword + "%" | ||
| ); | ||
| return masterMatch.or(customMatch); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: PostgreSQL에서 'string' 타입 캐스트 동작 확인
# Note: 프로젝트의 application.yml 또는 application.properties에서
# datasource URL을 확인하여 실제 DB가 PostgreSQL인지 검증 필요
# 프로젝트 DB 설정 확인
rg -n 'datasource.*url' src/main/resources/ --type yaml -g '!**/test/**'
rg -n 'spring.datasource' src/main/resources/ -g '!**/test/**'
# H2 의존성 확인 (테스트 환경)
rg -n 'h2' build.gradle* pom.xml 2>/dev/nullRepository: alter-app/alter-backend
Length of output: 210
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== target file context =="
sed -n '560,640p' src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/PostingQueryRepositoryImpl.java
echo
echo "== datasource / dialect references =="
rg -n "spring\.datasource|datasource.*url|hibernate\.dialect|dialect|postgres|h2" src/main/resources build.gradle* pom.xml settings.gradle* 2>/dev/null || true
echo
echo "== cast(... as string) usage =="
rg -n "cast\\([^\\)]* as string\\)|as string\\)" src/main/java src/test/java 2>/dev/null || true
echo
echo "== QueryDSL custom template usage =="
rg -n "booleanTemplate|stringTemplate|Expressions\\." src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence 2>/dev/null || trueRepository: alter-app/alter-backend
Length of output: 4591
cast(... as string)을 text로 바꾸고 검색 패턴을 이스케이프하세요
PostgreSQL에는 string 타입이 없어 이 경로에서 SQL 에러가 납니다. %/_도 이스케이프해야 의도치 않은 부분일치를 막을 수 있습니다.
🤖 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/outbound/posting/persistence/PostingQueryRepositoryImpl.java`
around lines 597 - 610, Update keywordMatches to cast qPosting.customKeywords as
PostgreSQL text instead of string, and escape SQL LIKE wildcard characters (%)
and (_) and the escape character in keyword before constructing the ilike
pattern. Use the escaped keyword in the pattern while preserving the existing
masterMatch.or(customMatch) behavior.
| List<PostingKeyword> postingKeywords = ObjectUtils.isEmpty(request.getKeywords()) | ||
| ? List.of() | ||
| : postingKeywordQueryRepository.findByIds(request.getKeywords()); | ||
| if (ObjectUtils.isNotEmpty(request.getKeywords()) | ||
| && postingKeywords.size() != request.getKeywords().size()) { | ||
| throw new CustomException(ErrorCode.INVALID_KEYWORD); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
키워드 검증 로직이 ManagerUpdatePosting과 중복됩니다.
CreatePosting.execute() (lines 33-38)와 ManagerUpdatePosting.execute() (lines 37-43)의 키워드 조회·검증 로직이 완전히 동일합니다. 공통 헬퍼 메서드로 추출하여 DRY 원칙을 준수하는 것을 권장합니다.
♻️ 공통 헬퍼 추출 제안
// PostingKeywordResolver.java (또는 유사한 공통 컴포넌트)
`@Component`
`@RequiredArgsConstructor`
public class PostingKeywordResolver {
private final PostingKeywordQueryRepository postingKeywordQueryRepository;
public List<PostingKeyword> resolveAndValidate(List<Long> keywordIds) {
if (ObjectUtils.isEmpty(keywordIds)) {
return List.of();
}
List<PostingKeyword> keywords = postingKeywordQueryRepository.findByIds(keywordIds);
if (keywords.size() != keywordIds.size()) {
throw new CustomException(ErrorCode.INVALID_KEYWORD);
}
return keywords;
}
}이후 CreatePosting와 ManagerUpdatePosting에서 주입하여 사용:
- List<PostingKeyword> postingKeywords = ObjectUtils.isEmpty(request.getKeywords())
- ? List.of()
- : postingKeywordQueryRepository.findByIds(request.getKeywords());
- if (ObjectUtils.isNotEmpty(request.getKeywords())
- && postingKeywords.size() != request.getKeywords().size()) {
- throw new CustomException(ErrorCode.INVALID_KEYWORD);
- }
+ List<PostingKeyword> postingKeywords = postingKeywordResolver.resolveAndValidate(request.getKeywords());🤖 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/posting/usecase/CreatePosting.java`
around lines 33 - 38, Extract the duplicated keyword lookup and validation from
CreatePosting.execute() and ManagerUpdatePosting.execute() into a shared
PostingKeywordResolver (or equivalent) component with a resolveAndValidate
method. Inject and use this helper in both use cases, preserving empty-list
handling and INVALID_KEYWORD validation.
| List<PostingKeyword> postingKeywords = ObjectUtils.isEmpty(request.getKeywords()) | ||
| ? List.of() | ||
| : postingKeywordQueryRepository.findByIds(request.getKeywords()); | ||
| if (ObjectUtils.isNotEmpty(request.getKeywords()) | ||
| && postingKeywords.size() != request.getKeywords().size()) { | ||
| throw new CustomException(ErrorCode.INVALID_KEYWORD); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
CreatePosting과 동일한 키워드 검증 로직 중복.
Lines 37-43의 키워드 조회·검증 로직이 CreatePosting.java lines 33-38과 완전히 동일합니다. 상세 제안은 CreatePosting.java 리뷰 코멘트를 참조하세요.
🤖 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/posting/usecase/ManagerUpdatePosting.java`
around lines 37 - 43, ManagerUpdatePosting의 키워드 조회·검증 로직이 CreatePosting과 중복되어
있습니다. 두 유스케이스에서 공통으로 사용하는 키워드 검증 메서드 또는 서비스로 추출하고, CreatePosting과
ManagerUpdatePosting이 해당 공통 로직을 호출하도록 수정하세요.
| import com.vladmihalcea.hibernate.type.json.JsonBinaryType; | ||
|
|
||
| import java.time.DayOfWeek; | ||
| import java.time.LocalTime; | ||
| import jakarta.persistence.*; | ||
| import lombok.*; | ||
| import org.apache.commons.lang3.ObjectUtils; | ||
| import org.hibernate.annotations.Type; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
도메인 계층에 새로운 인프라 의존성이 추가되었습니다 — 경로 지침 위반.
@Type(JsonBinaryType.class), JsonBinaryType, ObjectUtils가 도메인 엔티티에 추가되었습니다. 경로 지침은 도메인 계층에 "ZERO infrastructure dependencies (no Spring, no JPA annotations, no external libs)"를 요구합니다. 기존 코드도 JPA 어노테이션을 사용 중이나, 이 PR은 새로운 위반을 추가합니다.
ObjectUtils는 순수 Java 검사로 대체 가능해 도메인에서 외부 라이브러리 의존성을 제거할 수 있습니다. @Type/JsonBinaryType은 JSONB 매핑에 필요하므로 도메인-JPA 엔티티 분리 없이는 회피하기 어렵습니다.
♻️ ObjectUtils 의존성 제거 제안
import com.vladmihalcea.hibernate.type.json.JsonBinaryType;
import java.time.DayOfWeek;
import java.time.LocalTime;
import jakarta.persistence.*;
import lombok.*;
-import org.apache.commons.lang3.ObjectUtils;
import org.hibernate.annotations.Type;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;- posting.customKeywords = ObjectUtils.isNotEmpty(request.getCustomKeywords())
- ? request.getCustomKeywords()
- : new ArrayList<>();
+ posting.customKeywords = request.getCustomKeywords() != null && !request.getCustomKeywords().isEmpty()
+ ? new ArrayList<>(request.getCustomKeywords())
+ : new ArrayList<>();- this.customKeywords = ObjectUtils.isNotEmpty(customKeywords) ? customKeywords : new ArrayList<>();
+ this.customKeywords = customKeywords != null && !customKeywords.isEmpty() ? new ArrayList<>(customKeywords) : new ArrayList<>();As per coding guidelines, src/main/java/com/dreamteam/alter/domain/** 경로 지침은 "ZERO infrastructure dependencies (no Spring, no JPA annotations, no external libs)"를 요구합니다.
Also applies to: 76-78
🤖 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 11 - 18, Posting 도메인 엔티티에서 새 인프라 의존성을 제거하세요. Posting의 ObjectUtils 사용은 표준
Java null/빈 값 검사로 대체하고 import를 삭제하세요. JSONB 매핑을 위해 필요한 `@Type과` JsonBinaryType은
도메인 엔티티에서 분리해 JPA 인프라 엔티티 또는 매핑 계층으로 이동하며, Posting에는 해당 Hibernate 의존성이 남지 않도록
하세요.
Source: Path instructions
| posting.customKeywords = ObjectUtils.isNotEmpty(request.getCustomKeywords()) | ||
| ? request.getCustomKeywords() | ||
| : new ArrayList<>(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
customKeywords 할당 시 방어적 복사가 필요합니다.
request.getCustomKeywords() 리스트를 직접 할당하면, 호출자가 리스트를 수정할 경우 엔티티 상태가 변경될 수 있습니다. JPA 엔티티는 영속화 후 자체 상태를 보호해야 합니다.
🛡️ 방어적 복사 적용 제안
posting.customKeywords = ObjectUtils.isNotEmpty(request.getCustomKeywords())
- ? request.getCustomKeywords()
+ ? new ArrayList<>(request.getCustomKeywords())
: new ArrayList<>();📝 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.
| posting.customKeywords = ObjectUtils.isNotEmpty(request.getCustomKeywords()) | |
| ? request.getCustomKeywords() | |
| : new ArrayList<>(); | |
| posting.customKeywords = ObjectUtils.isNotEmpty(request.getCustomKeywords()) | |
| ? new ArrayList<>(request.getCustomKeywords()) | |
| : new ArrayList<>(); |
🤖 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 95 - 97, Posting 엔티티의 customKeywords 할당에서 요청 리스트를 직접 참조하지 않도록 방어적 복사를
적용하세요. request.getCustomKeywords()가 비어 있지 않은 경우에도 새 ArrayList로 복사해 할당하고, 비어 있으면
기존처럼 빈 리스트를 사용하도록 수정하세요.
| this.description = description; | ||
| this.payAmount = payAmount; | ||
| this.paymentType = paymentType; | ||
| this.customKeywords = ObjectUtils.isNotEmpty(customKeywords) ? customKeywords : new ArrayList<>(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
updateContent에서도 동일한 방어적 복사 누락.
Line 95-97과 동일한 문제입니다. 전달받은 customKeywords 리스트를 직접 할당합니다.
🛡️ 방어적 복사 적용 제안
- this.customKeywords = ObjectUtils.isNotEmpty(customKeywords) ? customKeywords : new ArrayList<>();
+ this.customKeywords = customKeywords != null && !customKeywords.isEmpty() ? new ArrayList<>(customKeywords) : new ArrayList<>();📝 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.
| this.customKeywords = ObjectUtils.isNotEmpty(customKeywords) ? customKeywords : new ArrayList<>(); | |
| this.customKeywords = customKeywords != null && !customKeywords.isEmpty() ? new ArrayList<>(customKeywords) : new ArrayList<>(); |
🤖 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` at line
135, Posting의 updateContent 메서드에서도 전달받은 customKeywords 리스트를 직접 참조하지 않도록 수정하세요.
현재 ObjectUtils.isNotEmpty(customKeywords) 조건의 비어 있지 않은 경우에 새 리스트로 방어적 복사한 값을
할당하고, 비어 있거나 null인 경우에는 빈 ArrayList를 사용하도록 하여 해당 필드가 외부 리스트 변경의 영향을 받지 않게 하세요.
| public void update(String name, String description) { | ||
| this.name = name; | ||
| this.description = description; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
update 메서드가 비즈니스 불변식을 검증하지 않고 필드를 직접 덮어씁니다.
name 컬럼은 nullable = false, length = 128 제약이 있지만, update 메서드는 null 또는 길이 초과 값을 그대로 할당합니다. DTO 검증이 현재 이를 보완하고 있으나, 엔티티가 자체 불변식을 캡슐화해야 합니다.
As per path instructions, src/main/java/com/dreamteam/alter/domain/**는 "Entities encapsulate business rules and invariants"를 요구합니다.
🛡️ 제안하는 불변식 검증 추가
public void update(String name, String description) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("업종명은 필수입니다.");
+ }
+ if (name.length() > 128) {
+ throw new IllegalArgumentException("업종명은 128자를 초과할 수 없습니다.");
+ }
+ if (description != null && description.length() > 255) {
+ throw new IllegalArgumentException("업종 설명은 255자를 초과할 수 없습니다.");
+ }
this.name = name;
this.description = description;
}📝 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.
| public void update(String name, String description) { | |
| this.name = name; | |
| this.description = description; | |
| } | |
| public void update(String name, String description) { | |
| if (name == null || name.isBlank()) { | |
| throw new IllegalArgumentException("업종명은 필수입니다."); | |
| } | |
| if (name.length() > 128) { | |
| throw new IllegalArgumentException("업종명은 128자를 초과할 수 없습니다."); | |
| } | |
| if (description != null && description.length() > 255) { | |
| throw new IllegalArgumentException("업종 설명은 255자를 초과할 수 없습니다."); | |
| } | |
| this.name = name; | |
| this.description = description; | |
| } |
🤖 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/PostingKeyword.java`
around lines 46 - 49, PostingKeyword.update 메서드가 name의 엔티티 불변식을 검증하도록 수정하세요.
name이 null이 아니고 최대 128자를 만족하는지 엔티티 내부에서 검증한 뒤에만 필드를 할당하며, 위반 시 프로젝트의 기존 도메인 예외
또는 검증 방식을 사용하세요. description 처리도 기존 도메인 규칙이 있다면 동일한 방식으로 검증하세요.
Source: Path instructions
변경사항
[기획] 구인구직 기능 정비 및 구현 계획
Notion 문서 7번(확인 필요·이슈) 중 "키워드(업종) 필터 부재"와 "지원 조건 로직 반전"을 해소합니다.
/admin/posting-keywords, Flyway V5)keywords) + 직접입력(customKeywords) 병행 지원 (postings.custom_keywordsjsonb, Flyway V4)GET /app/postings?keyword=, 마스터 업종명 OR 직접입력 라벨 부분일치) +filter-options업종 facetcustomKeywords노출 및 마스터 키워드 0개 공고 조회 시 NPE 가드Summary by CodeRabbit
새로운 기능
버그 수정