Skip to content

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

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

feat: [alt-267] 업종 키워드 관련 기능 고도화 및 지원조건 버그 수정#96
ysw789 wants to merge 5 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

  • 새로운 기능

    • 관리자가 게시 키워드를 조회, 생성, 수정, 삭제할 수 있습니다.
    • 게시글 작성 및 수정 시 직접 입력한 업종 키워드를 등록할 수 있습니다.
    • 게시글 목록과 상세 화면에 마스터 키워드와 직접 입력 키워드가 함께 표시됩니다.
    • 업종 키워드로 게시글을 검색하고 필터링할 수 있습니다.
  • 버그 수정

    • 모집 중인 게시글에서만 지원 절차가 진행되도록 동작을 바로잡았습니다.
    • 사용 중인 업종 키워드는 삭제할 수 없도록 제한했습니다.

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

📝 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 labels: FEAT

Suggested reviewers: hodoon, 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: 생성된 id 반환
  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 4.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 업종 키워드 관리자 CRUD, 직접입력/필터 추가와 모집중 지원 버그 수정이라는 변경사항을 잘 요약한 제목입니다.
✨ 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.

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

Comment on lines +56 to +62
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())
));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

생성 응답에 요청값이 아닌 영속화된 엔티티 값을 사용하세요.

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`으로 매핑하세요.

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 조인을 조건부로 추가하거나 쿼리를 분기하고, 키워드 필터가 없을 때는 해당
조인을 생략하도록 수정하세요.

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

Comment on lines +95 to +97
posting.customKeywords = ObjectUtils.isNotEmpty(request.getCustomKeywords())
? request.getCustomKeywords()
: new ArrayList<>();

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

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.

Suggested change
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<>();

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

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.

Suggested change
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를 사용하도록 하여 해당 필드가 외부 리스트 변경의 영향을 받지 않게 하세요.

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

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