Skip to content

feat: [alt-267] 업종 키워드 관련 기능 고도화 및 지원조건 버그 수정#96

Closed
ysw789 wants to merge 9 commits into
devfrom
feat/alt-267
Closed

feat: [alt-267] 업종 키워드 관련 기능 고도화 및 지원조건 버그 수정#96
ysw789 wants to merge 9 commits into
devfrom
feat/alt-267

Conversation

@ysw789

@ysw789 ysw789 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

변경사항

[기획] 구인구직 기능 정비 및 구현 계획

Notion 문서 7번(확인 필요·이슈) 중 "키워드(업종) 필터 부재"와 "지원 조건 로직 반전"을 해소합니다.

  • 업종(PostingKeyword) 마스터 관리자 CRUD 및 초기 시드 추가 (/admin/posting-keywords, Flyway V5)
  • 공고 등록·수정 시 마스터 선택(keywords) + 직접입력(customKeywords) 병행 지원 (postings.custom_keywords jsonb, Flyway V4)
  • 알바생 공고 목록 업종 필터 추가 (GET /app/postings?keyword=, 마스터 업종명 OR 직접입력 라벨 부분일치) + filter-options 업종 facet
  • 공고 응답(목록·상세, 알바생·매니저)에 customKeywords 노출 및 마스터 키워드 0개 공고 조회 시 NPE 가드
  • fix: 공고 지원 조건 반전 버그 수정 — 모집중(OPEN) 공고 지원이 차단되던 문제

Summary by CodeRabbit

  • 새로운 기능

    • 관리자용 게시 키워드(업종) 관리 API 추가(목록/생성/수정/삭제).
    • 공고 작성·수정 시 직접 입력 업종을 함께 등록 가능.
    • 공고 목록/상세 및 필터에서 마스터 키워드와 직접 입력 키워드 동시 제공, 업종 기반 검색·필터 지원.
  • 버그 수정

    • 모집 중인 공고에서만 지원 절차가 진행되도록 수정.
    • 사용 중인 업종 키워드는 삭제 불가하도록 제한하고, 직접 입력 키워드 입력 검증을 강화했습니다.

ysw789 added 5 commits July 8, 2026 00:36
- /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.
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1589c32a-6f70-4405-8224-82d38930bc77

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

관리자 게시 키워드 CRUD API와 키워드 관리 유스케이스가 추가되었습니다. 공고는 마스터 업종과 직접입력 업종을 함께 저장·검증하며, 목록 필터와 일반·관리자 응답에 직접입력 업종을 포함합니다.

Changes

게시 키워드 관리

Layer / File(s) Summary
키워드 계약과 관리자 CRUD 흐름
src/main/java/com/dreamteam/alter/domain/posting/{command,port}/..., src/main/java/com/dreamteam/alter/application/posting/usecase/Admin*PostingKeyword.java, src/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/...
관리자 게시 키워드 생성·조회·수정·삭제 계약, 유스케이스, 요청·응답 DTO, /admin/posting-keywords API가 추가되었습니다.
직접입력 업종 저장 및 검증
src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java, src/main/java/com/dreamteam/alter/adapter/inbound/{general,manager}/posting/dto/*RequestDto.java, src/main/java/com/dreamteam/alter/application/posting/usecase/{CreatePosting,ManagerUpdatePosting}.java, src/main/resources/db/migration/V4__add_postings_custom_keywords_column.sql
customKeywords를 JSONB 컬럼에 저장하고, 마스터 업종 또는 직접입력 업종 중 하나 이상을 요구하는 검증과 생성·수정 처리가 적용되었습니다.
마스터·직접입력 업종 필터링
src/main/java/com/dreamteam/alter/adapter/inbound/general/posting/dto/PostingListFilterDto.java, src/main/java/com/dreamteam/alter/adapter/outbound/posting/persistence/*QueryRepositoryImpl.java
공고 검색어 필터가 마스터 업종명과 JSONB 직접입력 업종의 부분일치를 조회하도록 확장되었습니다.
업종 응답 매핑과 초기 데이터
src/main/java/com/dreamteam/alter/adapter/{inbound,outbound}/.../posting/*Response*.java, src/main/java/com/dreamteam/alter/application/posting/usecase/GetPostingFilterOptions.java, src/main/resources/db/migration/V5__seed_posting_keywords.sql
필터 옵션과 일반·관리자 목록 및 상세 응답에 직접입력 업종과 마스터 업종을 매핑하고, 게시 키워드 초기 데이터를 시드합니다.

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

Suggested reviewers: juny0955

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: 저장 결과 반환
  AdminCreatePostingKeyword-->>AdminPostingKeywordController: 응답 데이터 반환
  AdminPostingKeywordController-->>AdminClient: 201 Created
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.00% 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 업종 키워드 CRUD, 필터/응답 확장, customKeywords 추가, 지원 조건 버그 수정 등 변경 사항을 적절히 요약한 제목입니다.
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 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.

@ysw789 ysw789 changed the title feat: [alt-267] 업종 DB 관리·직접입력·필터 지원 및 지원조건 버그 수정 feat: [alt-267] 업종 키워드 관련 기능 고도화 및 지원조건 버그 수정 Jul 10, 2026

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

📥 Commits

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

📒 Files selected for processing (38)
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/controller/AdminPostingKeywordController.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/controller/AdminPostingKeywordControllerSpec.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/dto/AdminPostingKeywordRequestDto.java
  • src/main/java/com/dreamteam/alter/adapter/inbound/admin/posting/dto/AdminPostingKeywordResponseDto.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/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/PostingListFilterDto.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/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/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/application/posting/usecase/AdminCreatePostingKeyword.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/AdminDeletePostingKeyword.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/AdminGetPostingKeywordList.java
  • src/main/java/com/dreamteam/alter/application/posting/usecase/AdminUpdatePostingKeyword.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/ManagerUpdatePosting.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/AdminCreatePostingKeywordCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/command/AdminUpdatePostingKeywordCommand.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java
  • src/main/java/com/dreamteam/alter/domain/posting/entity/PostingKeyword.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminCreatePostingKeywordUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminDeletePostingKeywordUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminGetPostingKeywordListUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/inbound/AdminUpdatePostingKeywordUseCase.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/outbound/PostingKeywordQueryRepository.java
  • src/main/java/com/dreamteam/alter/domain/posting/port/outbound/PostingKeywordRepository.java
  • src/main/resources/db/migration/V4__add_postings_custom_keywords_column.sql
  • src/main/resources/db/migration/V5__seed_posting_keywords.sql

Comment on lines +30 to +40
@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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

@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 = "...")
어노테이션을 제거하세요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인 결과, 이 패턴은 프로젝트 전역 컨벤션이라 유지합니다.

@Resource(name = "...") + @RequiredArgsConstructor 병용은 현재 inbound 어댑터 컨트롤러 48개 파일에서 동일하게 사용 중입니다(예: UserSelfController, ManagerPostingController, AdminUserController 등). UseCase 빈이 @Service("...")로 명시적 이름을 갖는 구조와 짝을 이뤄 주입 대상을 빈 이름으로 고정하려는 의도적 관례입니다.

이 파일만 제거하면 코드베이스 일관성이 깨지므로 반영하지 않습니다. 패턴 자체의 재검토가 필요하다면 별도의 전역 리팩토링으로 다루는 것이 적절합니다.

Comment on lines +45 to +55
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoff

키워드 필터 없을 때 불필요한 left join이 성능 저하를 유발합니다.

keywordMatchesnull을 반환하는 경우(키워드 필터 없음), qPostingKeywordMapqPostingKeyword에 대한 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 조인을 조건부로 추가하거나 쿼리를 분기하고, 키워드 필터가 없을 때는 해당
조인을 생략하도록 수정하세요.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인했습니다. 정확성 이슈는 없어 현 시점에는 반영하지 않습니다.

count 쿼리는 qPosting.countDistinct(), 커서 목록 쿼리는 groupBy(qPosting.id, ...)를 사용하므로, 키워드 1:N left join으로 행이 늘어나도 결과 집계에 영향을 주지 않습니다(중복 제거됨). 따라서 남는 것은 순수 조인 비용인데, 조건부 조인/쿼리 분기는 코드 복잡도를 늘리는 반면 현재 데이터 규모에서의 이득은 작습니다(지적하신 "Poor tradeoff" 라벨에 동의).

추후 프로파일링에서 실제 병목으로 확인되면 최적화하겠습니다.

Comment on lines +597 to +610
/**
* 업종 필터: 입력 텍스트가 (마스터 연결 업종명 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);
}

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 | 🟠 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/null

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

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

Comment on lines 33 to 38
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);

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 | ⚡ 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;
    }
}

이후 CreatePostingManagerUpdatePosting에서 주입하여 사용:

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

Comment on lines 37 to 43
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);
}

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 | ⚡ 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이 해당 공통 로직을 호출하도록 수정하세요.

Comment on lines +11 to +18
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;

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인 결과, 이 프로젝트의 실제 컨벤션과 상충하여 반영하지 않습니다.

  • ObjectUtils: 프로젝트 가이드가 null/빈 값 검사에 org.apache.commons.lang3 유틸 사용을 명시적으로 권장합니다. 도메인/DTO 전반(CreatePostingRequestDto 등 포함)에서 이미 표준으로 쓰고 있어, 순수 Java 검사로 바꾸면 오히려 컨벤션에서 벗어납니다.
  • @Type / JsonBinaryType: 이 코드베이스의 도메인 엔티티는 모두 JPA 엔티티(@Entity, @Table, @Column 등)이며, jsonb 매핑에 @Type이 필요합니다. 도메인–JPA 엔티티 분리는 이 PR 범위를 넘는 아키텍처 변경입니다.

.coderabbit.yaml의 domain "ZERO infrastructure dependencies" 지침은 현재 도메인 계층 전반과 이미 불일치(기존 엔티티 전부 JPA 애노테이션 사용)하므로, 지침 자체를 실제 컨벤션에 맞게 조정하는 편이 근본 해법입니다. 이는 별도로 처리하겠습니다.

Comment thread src/main/java/com/dreamteam/alter/domain/posting/entity/Posting.java Outdated
Comment on lines +46 to +49
public void update(String name, String description) {
this.name = name;
this.description = description;
}

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

Suggested change
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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

확인 결과, 프로젝트의 검증 레이어 컨벤션에 따라 반영하지 않습니다.

가이드 원칙: DTO(@Valid + Bean Validation)가 1차 방어선이며, 모든 호출 경로가 검증된 DTO를 통과하는 경우 엔티티 메서드에서 null/blank/길이를 재검증하지 않습니다(엔티티는 상태 전이 등 도메인 불변식만 가드).

update의 유일한 호출 경로인 AdminPostingKeywordRequestDto에 이미 다음 검증이 적용되어 있습니다.

  • name: @NotBlank + @Size(max = 128)
  • description: @Size(max = 255)

따라서 엔티티에 동일 검증을 추가하면 중복이 되며, DB 제약(nullable = false, length = 128)과 DTO 검증으로 이미 커버됩니다.

@ysw789

ysw789 commented Jul 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ysw789

ysw789 commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

작업 방향성 변경으로 PR Close 처리

@ysw789 ysw789 closed this Jul 12, 2026
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