Skip to content

[Feat] 공고 탐색 및 검색 / 상세 조회 API 구현#49

Open
sky-0131 wants to merge 18 commits into
developfrom
feat/#24
Open

[Feat] 공고 탐색 및 검색 / 상세 조회 API 구현#49
sky-0131 wants to merge 18 commits into
developfrom
feat/#24

Conversation

@sky-0131

@sky-0131 sky-0131 commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

#️⃣ 관련 이슈

#24

PR 유형

어떤 변경 사항이 있나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 테스트 추가, 테스트 리팩토링
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

PR Checklist

PR이 다음 요구 사항을 충족하는지 확인하세요.

  • 커밋 메시지 컨벤션에 맞게 작성했습니다.
  • 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).

🧩 작업 내용

  1. QueryDSL 설정 및 DTO 생성
  • QuerydslConfig.java: JPAQueryFactory를 빈으로 등록하여 Repository에서 동적 쿼리를 실행할 수 있도록 설정.

  • JobSearchRequest.java: 검색 필터 조건 바인딩 DTO.

  • JobSummaryResponse.java / JobDetailResponse.java: 공고 목록 조 및 상세 조회 전용 응답 DTO.

  • JobPostingConverter.java: 엔티티와 응답 DTO 간의 데이터 변환 매핑 메서드 추가 구현.

  1. Repository 동적 쿼리 구현
  • JobRepositoryCustom.java / JobRepositoryCustomImpl.java: QueryDSL을 활용해 키워드, 지역, 스킬 태그, 카테고리 등 다중 필터링 조건과 페이지네이션이 적용된 동적 검색 메서드 구현.
  • JobRepository.java: QueryDSL Custom 인터페이스 상속 추가.
  1. Service, Controller에 로직 추가
  • JobService.java: 공고 목록 검색/필터링 및 단건 상세 조회 비즈니스 로직 작성.
  • JobController.java: GET /api/v1/jobs (목록/검색) 및 GET /api/v1/jobs/{jobId} (상세 조회) REST API 엔드포인트 구현.
  1. 초기 데이터 적재 및 버그 수정
  • JobDataInitializer.java: 초기 공고 데이터 DB 자동 적재 로직 구현.
  • Job.java: Native Array 컬럼(skillTags) 타입 매핑 정리.
  • JobRepositoryCustomImpl.java: DB 배열 타입(skillTags) 검색 시 발생하던 HQL 타입 오류 수정 (array_contains 연산자 적용).
  • 엔티티 영속성 오류 수정: JSON 파싱 시 신규 생성된 Company 객체의 영속성 전이 처리.

테스트

  • 탐색
image image
  • 스킬 태그 (임시) 선택, 탐색
image image
  • 공고 상세 조회
image image

📣 To Reviewers

  • Reviewers : 팀 선택

  • Labels : 작업 유형, 자기 자신

  • 화면 기획서에서 요청한 대로 size = 24로 고정.

  • 기술 스택은 테스트를 위해 임시로 만들었습니다.

  • Job 엔티티 내 긴 문자열 필드들에 대해:
    크롤링 및 db에서 불러오는 과정에서 길이 초과로 인해 오류 발생.
    text 컬럼으로 임시 설정해 뒀습니다.
    원래 설계에 맞춰, 일부 내용만 불러오도록 수정을 시도했으나, 이 과정에서 오류가 너무 많았습니다.
    후순위로 미뤄도 될까요?

Summary by CodeRabbit

Summary by CodeRabbit

  • 새로운 기능

    • 키워드, 직무/카테고리, 기술 스택, 지역 조건으로 채용공고를 검색할 수 있습니다.
    • 채용공고 목록(기본 페이지/정렬 적용)과 상세 정보를 조회할 수 있습니다.
    • 기술 스택 필터(전체 목록)를 제공합니다.
  • 개선

    • 목록/상세 응답에서 회사명, 고용 형태, 경력 수준, 원격 여부, 마감일 및 기술 스택이 표준화되어 제공됩니다.

@sky-0131 sky-0131 self-assigned this Jul 26, 2026
@sky-0131 sky-0131 added the ✨ Feat 새로운 기능 추가 label Jul 26, 2026
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sky-0131, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fc0d2e4a-6f2c-4062-8f00-77f96fff775a

📥 Commits

Reviewing files that changed from the base of the PR and between 3f567d8 and 71f629f.

📒 Files selected for processing (6)
  • src/main/java/com/leets7th/job_is_be/domain/job/converter/JobPostingConverter.java
  • src/main/java/com/leets7th/job_is_be/domain/job/dto/JobDetailResponse.java
  • src/main/java/com/leets7th/job_is_be/domain/job/dto/JobSummaryResponse.java
  • src/main/java/com/leets7th/job_is_be/domain/job/repository/JobRepositoryCustomImpl.java
  • src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java
  • src/main/java/com/leets7th/job_is_be/global/status/ErrorStatus.java
📝 Walkthrough

Walkthrough

채용공고 검색·상세 조회 API와 기술 스택 메타데이터 API가 추가되었습니다. QueryDSL 동적 검색, 응답 DTO, JSONL 초기 데이터 적재, 엔티티 매핑 및 관련 Spring 설정이 함께 변경되었습니다.

Changes

채용공고 검색 및 데이터 흐름

Layer / File(s) Summary
검색 계약과 엔티티 매핑
src/main/java/.../domain/job/dto/*, src/main/java/.../domain/job/enums/TechStackType.java, src/main/java/.../domain/job/entity/*
검색 요청·응답 DTO와 기술 스택 enum을 추가하고 Job·Company의 JSON 및 JPA 매핑을 변경했습니다.
QueryDSL 검색 구현
src/main/java/.../domain/job/repository/*, src/main/java/.../domain/job/service/JobService.java
키워드·지역·기술 스택·카테고리 조건을 조합해 조인, 페이징, count 조회를 수행합니다.
공고 및 메타데이터 API
src/main/java/.../domain/job/controller/*, src/main/java/.../domain/job/converter/JobPostingConverter.java, src/main/java/.../domain/job/service/MetadataQueryService.java
공고 검색·상세 조회와 기술 스택 목록 조회 엔드포인트 및 응답 변환을 추가했습니다.
JSONL 초기 적재
src/main/java/.../global/init/JobDataInitializer.java, src/main/java/.../domain/job/repository/JobRepository.java
애플리케이션 시작 시 회사·공고 JSONL을 읽고 중복 데이터를 건너뛰어 저장합니다.
기반 설정 및 의존성
build.gradle, src/main/java/.../global/config/QuerydslConfig.java, src/main/java/com/leets7th/job_is_be/JobIsBeApplication.java, .gitignore
QueryDSL·캐시·Redis 의존성과 JPAQueryFactory, 페이지 DTO 직렬화 설정 및 Python 무시 규칙을 추가했습니다.

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

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JobController
  participant JobService
  participant JobRepositoryCustomImpl
  participant Database
  Client->>JobController: GET /api/jobs
  JobController->>JobService: 검색 조건과 Pageable 전달
  JobService->>JobRepositoryCustomImpl: searchJobs 호출
  JobRepositoryCustomImpl->>Database: QueryDSL 목록·count 조회
  Database-->>JobRepositoryCustomImpl: 결과 반환
  JobRepositoryCustomImpl-->>JobService: Page<JobSummaryResponse>
  JobService-->>JobController: PageResponse 변환
  JobController-->>Client: ApiResponse 응답
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: jihoonkim501

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.05% 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 공고 탐색·검색과 상세 조회 API 구현이라는 PR의 핵심 변경을 정확히 요약합니다.
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.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 12

🧹 Nitpick comments (3)
src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java (1)

15-17: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

JSONL 파일 읽기 시 UTF-8을 명시하세요.

프로젝트는 JDK 21 toolchain을 사용하지만, FileReader/JDK 기본 charset 설정이 보장되지 않을 때 한국어가 포함된 JSONL 입력에서 값 손상 또는 파싱 실패가 발생할 수 있습니다. StandardCharsets.UTF_8을 명시적으로 지정해 파싱 경로를 고정하세요.

-import java.io.FileReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
...
-        try (BufferedReader br = new BufferedReader(new FileReader(file))) {
+        try (BufferedReader br = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) {
🤖 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/leets7th/job_is_be/global/init/JobDataInitializer.java`
around lines 15 - 17, Update the JSONL reading path in JobDataInitializer to
explicitly use StandardCharsets.UTF_8 instead of the platform-default charset
used by FileReader, and add the required StandardCharsets import. Keep the
existing BufferedReader parsing flow unchanged.
build.gradle (1)

40-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cache/Redis 의존성 선언을 하나만 유지하세요.

40-41행이 43-44행과 동일합니다. 중복 선언을 제거해 의존성 구성을 단일화하세요. As per coding guidelines, “Make surgical changes: modify only what the task requires, match existing style, remove imports and variables orphaned by the change, and leave pre-existing dead code untouched.”

🤖 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 `@build.gradle` around lines 40 - 44, Remove the duplicated
spring-boot-starter-cache and spring-boot-starter-data-redis declarations in the
build.gradle dependencies block, keeping one pair in the existing style.

Source: Coding guidelines

src/main/java/com/leets7th/job_is_be/domain/job/controller/MetadataController.java (1)

51-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

컨트롤러에서 서비스의 변환 경로를 사용하세요.

MetadataQueryService.getAllTechStacks()와 동일한 enum 변환을 다시 구현하고 있습니다. metadataQueryService.getAllTechStacks()를 호출하고, 불필요해진 Arrays·TechStackType import를 제거하세요. As per coding guidelines, “Make surgical changes: modify only what the task requires, match existing style, remove imports and variables orphaned by the change, and leave pre-existing dead code untouched.”

🤖 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/leets7th/job_is_be/domain/job/controller/MetadataController.java`
around lines 51 - 53, Update MetadataController to obtain tech stack metadata by
calling metadataQueryService.getAllTechStacks() instead of locally mapping
TechStackType.values(). Remove the now-unused Arrays and TechStackType imports
and any orphaned local variable while preserving the existing response flow.

Source: Coding guidelines

🤖 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/leets7th/job_is_be/domain/job/controller/JobController.java`:
- Around line 51-67: The JobController methods searchJobs and getJobDetail
currently return raw payloads; update their response types and return statements
to use the standard ApiResponse envelope. Wrap the paginated search result in
PageResponse<JobSummaryResponse> via ApiResponse.success(...), and wrap the
detail DTO via ApiResponse.success(...), preserving the existing service calls
and pagination behavior.
- Around line 50-62: Update the JobController class-level request mapping to
/api/v1/jobs, then change the searchJobs method mapping from /search to the
class root so the endpoints become GET /api/v1/jobs and GET
/api/v1/jobs/{jobId}.
- Around line 52-54: JobController의 검색 흐름에서 요청으로 전달된 페이지 크기가 그대로 전파되지 않도록 수정하세요.
JobService.searchJobs() 호출 전에 페이지 번호와 정렬 정보는 기존 pageable에서 유지하고 크기만 24로 고정한 새
PageRequest를 생성해 전달하세요.

In
`@src/main/java/com/leets7th/job_is_be/domain/job/controller/MetadataController.java`:
- Around line 50-54: Update MetadataController.getAllTechStacks() to return
ApiResponse<List<TechStackResponse>> instead of a raw ResponseEntity list, and
wrap the result with ApiResponse.success(...) using
metadataQueryService.getAllTechStacks(). Remove the controller-side direct list
response while preserving the existing tech-stack data.

In `@src/main/java/com/leets7th/job_is_be/domain/job/dto/JobDetailResponse.java`:
- Around line 33-48: Update the JobDetailResponse.from method to map intro,
benefits, employeeCount, companyType, industry, and stockStatus from the
appropriate Job or Company fields before building the response. Preserve the
stored values in the detail response, or remove these fields from the response
contract if no source fields exist.

In `@src/main/java/com/leets7th/job_is_be/domain/job/dto/JobSummaryResponse.java`:
- Around line 33-34: 목록 응답의 두 변환 경로에서 저장된 썸네일 URL을 매핑하세요.
src/main/java/com/leets7th/job_is_be/domain/job/dto/JobSummaryResponse.java
33-34의 JobSummaryResponse 변환은 null 대신 job.getThumbnailUrl()을 사용하고,
src/main/java/com/leets7th/job_is_be/domain/job/converter/JobPostingConverter.java
90-91의 JobPostingConverter 변환도 동일하게 job.getThumbnailUrl()을 매핑하세요.

In
`@src/main/java/com/leets7th/job_is_be/domain/job/repository/JobRepositoryCustomImpl.java`:
- Around line 39-52: Update the query construction in JobRepositoryCustomImpl to
apply ORDER BY before offset/limit: convert pageable.getSort() using an
allowlist of supported Job fields, preserve each requested direction, and fall
back to the `@PageableDefault` createdAt DESC then id DESC ordering when no sort
is provided. Keep pagination and existing filters unchanged.

In `@src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java`:
- Around line 41-54: Update the JobDataInitializer loading flow so each row’s
JSON parsing and company save is handled independently: skip only expected
malformed rows, while propagating database or file-system failures instead of
printing and continuing. Ensure initialization does not silently leave partially
loaded data; use the existing transaction or rollback mechanism if available,
and apply the same behavior to the additional loading block referenced by the
comment.
- Around line 33-39: Update JobDataInitializer’s loadCompanies and corresponding
loadJobs loading paths so seed files are resolved through a configurable path or
packaged resource instead of relying on the process working directory. Remove
the silent file-not-found returns, and fail startup explicitly when the required
company or job seed data cannot be loaded, preserving automatic initial job-data
loading.
- Around line 70-71: loadJobs의 existsBySourceAndExternalId 후 save 방식은 동시
startup에서 중복 저장 경쟁을 유발하므로 제거하세요. findBySourceAndExternalId 기반 처리 또는 DB unique
constraint를 활용한 try-insert/upsert 전략으로 변경해 (source, externalId) 중복이 안전하게 처리되도록
하세요. 또한 loadJobs의 예외 catch에서 unique constraint 충돌을 포함한 예외가 stack trace와 함께 기록되도록
로깅을 추가하세요.
- Around line 29-31: In JobDataInitializer, stop mutating the shared
ObjectMapper returned by AppConfig; create or inject a separately named
JSONL-loading mapper and apply the SNAKE_CASE, unknown-property, and
case-insensitive-enum settings only to that dedicated mapper. Keep the Spring
MVC mapper unchanged and use the dedicated mapper throughout the JSONL import
flow.
- Around line 66-68: JobDataInitializer.loadJobs()에서 저장 전에 source와 externalId가
null이 아닌지 검증하고, 유효하지 않으면 해당 job을 건너뛰세요. existsBySourceAndExternalId 호출로 중복 식별자를
확인한 뒤 중복 데이터도 저장하지 않도록 기존 흐름을 유지하세요. 제목 검증은 null뿐 아니라 JobParserService와 동일하게
title.isBlank()인 경우도 건너뛰도록 수정하세요.

---

Nitpick comments:
In `@build.gradle`:
- Around line 40-44: Remove the duplicated spring-boot-starter-cache and
spring-boot-starter-data-redis declarations in the build.gradle dependencies
block, keeping one pair in the existing style.

In
`@src/main/java/com/leets7th/job_is_be/domain/job/controller/MetadataController.java`:
- Around line 51-53: Update MetadataController to obtain tech stack metadata by
calling metadataQueryService.getAllTechStacks() instead of locally mapping
TechStackType.values(). Remove the now-unused Arrays and TechStackType imports
and any orphaned local variable while preserving the existing response flow.

In `@src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java`:
- Around line 15-17: Update the JSONL reading path in JobDataInitializer to
explicitly use StandardCharsets.UTF_8 instead of the platform-default charset
used by FileReader, and add the required StandardCharsets import. Keep the
existing BufferedReader parsing flow unchanged.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 28dc155f-7f59-484b-b1d9-c6d9216e4a14

📥 Commits

Reviewing files that changed from the base of the PR and between 8f564d4 and b218646.

📒 Files selected for processing (20)
  • .gitignore
  • build.gradle
  • src/main/java/com/leets7th/job_is_be/JobIsBeApplication.java
  • src/main/java/com/leets7th/job_is_be/domain/job/controller/JobController.java
  • src/main/java/com/leets7th/job_is_be/domain/job/controller/MetadataController.java
  • src/main/java/com/leets7th/job_is_be/domain/job/converter/JobPostingConverter.java
  • src/main/java/com/leets7th/job_is_be/domain/job/dto/JobDetailResponse.java
  • src/main/java/com/leets7th/job_is_be/domain/job/dto/JobSearchRequest.java
  • src/main/java/com/leets7th/job_is_be/domain/job/dto/JobSummaryResponse.java
  • src/main/java/com/leets7th/job_is_be/domain/job/dto/TechStackResponse.java
  • src/main/java/com/leets7th/job_is_be/domain/job/entity/Company.java
  • src/main/java/com/leets7th/job_is_be/domain/job/entity/Job.java
  • src/main/java/com/leets7th/job_is_be/domain/job/enums/TechStackType.java
  • src/main/java/com/leets7th/job_is_be/domain/job/repository/JobRepository.java
  • src/main/java/com/leets7th/job_is_be/domain/job/repository/JobRepositoryCustom.java
  • src/main/java/com/leets7th/job_is_be/domain/job/repository/JobRepositoryCustomImpl.java
  • src/main/java/com/leets7th/job_is_be/domain/job/service/JobService.java
  • src/main/java/com/leets7th/job_is_be/domain/job/service/MetadataQueryService.java
  • src/main/java/com/leets7th/job_is_be/global/config/QuerydslConfig.java
  • src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java

Comment thread src/main/java/com/leets7th/job_is_be/domain/job/controller/JobController.java Outdated
Comment thread src/main/java/com/leets7th/job_is_be/domain/job/controller/JobController.java Outdated
Comment thread src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java Outdated
Comment thread src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java Outdated
Comment on lines +41 to +54
try (BufferedReader br = new BufferedReader(new FileReader(file))) {
String line;
while ((line = br.readLine()) != null) {
Company company = objectMapper.readValue(line, Company.class);

// 이미 존재하는 회사가 아니면 저장
if (company.getId() != null && companyRepository.existsById(company.getId())) {
continue;
}
companyRepository.save(company);
}
} catch (Exception e) {
e.printStackTrace();
}

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 | 🏗️ Heavy lift

한 행의 오류가 전체 적재를 중단시키고 부분 데이터로 시작됩니다.

while 전체가 하나의 try에 포함되어 있어 한 행의 JSON 오류나 저장 실패가 발생하면 이후 모든 행이 처리되지 않습니다. 또한 예외를 출력만 하고 반환하므로 이미 저장된 데이터는 남고 애플리케이션은 계속 시작합니다. 예상 가능한 malformed row만 행 단위로 건너뛰고, DB·파일 시스템 오류는 재던지거나 전체 적재를 원자적으로 처리해야 합니다.

PR의 초기 데이터 자동 적재 흐름이 부분 성공을 조용히 허용하지 않아야 한다는 전제에서 판단했습니다.

Also applies to: 61-76

🤖 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/leets7th/job_is_be/global/init/JobDataInitializer.java`
around lines 41 - 54, Update the JobDataInitializer loading flow so each row’s
JSON parsing and company save is handled independently: skip only expected
malformed rows, while propagating database or file-system failures instead of
printing and continuing. Ensure initialization does not silently leave partially
loaded data; use the existing transaction or rollback mechanism if available,
and apply the same behavior to the additional loading block referenced by the
comment.

Comment on lines +66 to +68
if (job.getTitle() == null) {
continue;
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Files =="
git ls-files | rg 'JobDataInitializer|Job|Source|External|Init|Repository|Entity' | sed -n '1,200p'

echo "== Target file outline =="
ast-grep outline src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java --view expanded || true

echo "== Target file lines =="
cat -n src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java | sed -n '1,130p'

echo "== Search for source/externalId fields usage =="
rg -n "source|externalId|External|Source" src/main/java/com/leets7th/job_is_be -g '*.java' | sed -n '1,240p'

Repository: Leets-Official/Job-is-BE

Length of output: 16539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Target file outline =="
ast-grep outline src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java --view expanded || true

echo "== Target file lines =="
cat -n src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java | sed -n '1,130p'

echo "== Search for source/externalId/null-title behavior in JobDataInitializer =="
python3 - <<'PY'
from pathlib import Path
p = Path('src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java')
lines = p.read_text().splitlines()
for i,l in enumerate(lines, 1):
    if 'title' in l or 'source' in l or 'externalId' in l or 'saveAll' in l or 'save(' in l:
        print(f'{i}: {l}')
PY

Repository: Leets-Official/Job-is-BE

Length of output: 3959


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Job entity =="
cat -n src/main/java/com/leets7th/job_is_be/domain/job/entity/Job.java | sed -n '1,230p'

echo "== Job category entity =="
cat -n src/main/java/com/leets7th/job_is_be/domain/job/entity/JobCategory.java | sed -n '1,220p'

echo "== JobParserService relevant section =="
cat -n src/main/java/com/leets7th/job_is_be/domain/job/service/JobParserService.java | sed -n '45,115p'

echo "== JobParserService constructor validation tests if any =="
rg -n "JobParserService|source|externalId|title|existsBySourceAndExternalId|findBySourceAndExternalId" src/test/java/com/leets7th/job_is_be -g '*.java' | sed -n '1,240p'

echo "== Java parser/read-only verifier: initializer entry guards =="
python3 - <<'PY'
from pathlib import Path
p = Path('src/main/java/com/leets7th/job_is_be/global/init/JobDataInitializer.java')
text = p.read_text()
checks = [
    "job.getTitle() == null",
    "job.getTitle().trim().isEmpty()",
    "job.getSource() == null",
    "job.getSource() != null && job.getSource().trim().isEmpty()",
    "job.getExternalId() == null",
    "job.getExternalId() != null && job.getExternalId().equals(0L)",
]
for c in checks:
    print(f"{c}: {c in text}")
print("next exists/save lines:")
print(text[text.find("job.setTitle"):text.find("jobRepository.save(job)")+800])
PY

Repository: Leets-Official/Job-is-BE

Length of output: 18066


중복 식별자와 제목 유효성을 저장 전에 검증하세요.

JobDataInitializer.loadJobs()는 null 제목만 건너뜁니다. existsBySourceAndExternalIdsource/externalId가 null이면 초기화 저장이 저장 경로에 진입하고, DB의 uk_jobs_source_external_id 제약/인덱스에도 영향을 줍니다. 또한 JobParserService처럼 제목이 공백일 경우도 무시하므로 title.isBlank()과 동일 조건을 추가하세요.

🤖 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/leets7th/job_is_be/global/init/JobDataInitializer.java`
around lines 66 - 68, JobDataInitializer.loadJobs()에서 저장 전에 source와 externalId가
null이 아닌지 검증하고, 유효하지 않으면 해당 job을 건너뛰세요. existsBySourceAndExternalId 호출로 중복 식별자를
확인한 뒤 중복 데이터도 저장하지 않도록 기존 흐름을 유지하세요. 제목 검증은 null뿐 아니라 JobParserService와 동일하게
title.isBlank()인 경우도 건너뛰도록 수정하세요.

Comment on lines +70 to +71
if (!jobRepository.existsBySourceAndExternalId(job.getSource(), job.getExternalId())) {
jobRepository.save(job);

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

🧩 Analysis chain

🏁 Script executed:

rg -n "UniqueConstraint|uniqueConstraints|externalId|external_id|source" src/main/java src/main/resources 2>/dev/null

Repository: Leets-Official/Job-is-BE

Length of output: 10247


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'JobDataInitializer.java|Job.java|JobParserService.java|JobRepository.java' . | sed 's#^\./##'

echo
echo "== Job entity header and fields =="
for f in $(fd 'Job.java|JobDataInitializer.java|JobParserService.java|JobRepository.java' src/main/java); do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,120p' "$f"
done

echo
echo "== init and service around checks =="
for f in $(fd 'JobDataInitializer.java|JobParserService.java' src/main/java); do
  echo "--- $f"
  sed -n '1,240p' "$f"
done

Repository: Leets-Official/Job-is-BE

Length of output: 26153


existssave만으로는 분산 startup 중 중복 적재를 막을 수 없습니다.

Job에는 (source, external_id) unique constraint가 있지만, 여러 인스턴스가 동시에 existsBySourceAndExternalId를 호출하면 각자 false를 보고 같은 데이터를 저장하려 합니다. existsBySourceAndExternalId 대신 findBySourceAndExternalId를 쓰거나 DB unique conflict를 처리하는 try-insert/upsert 전략으로 바꾸세요. loadJobscatch (Exception e) { e.printStackTrace(); }도 unique constraint 충돌 시 로그가 남지 않도록 추가하면 좋습니다.

🤖 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/leets7th/job_is_be/global/init/JobDataInitializer.java`
around lines 70 - 71, loadJobs의 existsBySourceAndExternalId 후 save 방식은 동시
startup에서 중복 저장 경쟁을 유발하므로 제거하세요. findBySourceAndExternalId 기반 처리 또는 DB unique
constraint를 활용한 try-insert/upsert 전략으로 변경해 (source, externalId) 중복이 안전하게 처리되도록
하세요. 또한 loadJobs의 예외 catch에서 unique constraint 충돌을 포함한 예외가 stack trace와 함께 기록되도록
로깅을 추가하세요.

yukyoungs
yukyoungs previously approved these changes Jul 26, 2026

@yukyoungs yukyoungs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

coderabbit 리뷰만 확인해줭~ 성현이 잘한다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feat 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants