[BE-23] 점핏 애그리게이터 어댑터
작업 내용 (설계 의도)
근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “애그리게이터 어댑터 설계”, “방안 8”
근거 조사: 20260722-채용소스-조사-브리프.md “전체 플랫폼 조사 결과 / 채택 가능 — 규약 청정” (실측: jumpit-api.saramin.co.kr/api/positions?page=N&size=200&jobCategory={code}, JSON 무인증, id 안정 ID, 변경 감지 필드 없음, closedAt ISO / alwaysOpen 불리언, 백엔드 118건, /positions 미차단·GPTBot만 차단)
변경 사항
점핏 JSON API를 검색 조건으로 수집하는 애그리게이터 어댑터입니다. 조사에서 확인된 두 특성 — 변경 감지 필드 부재와 회사가 목록에 없고 상세에만 존재 — 가 이 어댑터의 설계 난점입니다(FR-68).
능력 선언(SourceCapabilities):
| 항목 | 값 | 근거 |
|---|---|---|
sourceType | AGGREGATOR | — |
changeSignalKind | FIELD_HASH | 변경 감지 필드 없음 → 주요 필드 해시 (FR-68) |
providesDeadline | true (조건부) | closedAt, 단 alwaysOpen 우선 |
companyIdentifierInDetailOnly | true | 목록에 회사 없음 → 상세 조회로 식별 (FR-68) |
| 규약 정책 | UA·요청 지연·size 상한 200·허용 경로 /api/positions | FR-69 |
핵심 설계 의도:
- **변경 감지가 없으므로
ChangeSignature.FieldHash**를 씁니다 — 제목·마감일·회사 등 주요 필드를 정규화해 SHA-256 해시하고, 이전 회차 해시와 비교해 변경을 판단합니다(FR-68). 전체 본문 해시(BodyHash, 인크루트)와 구분되는 변이입니다 — 목록 필드만으로 계산해 상세 조회 없이도 변경 판단이 가능합니다. - 마감일 정규화(FR-67):
alwaysOpen=true면closedAt을 무시하고deadlineAt=null(상시채용)입니다.alwaysOpen을 보지 않고closedAt을 그대로 쓰면 상시채용 공고가 마감일 있는 공고로 잘못 분류됩니다(브리프 경고).alwaysOpen=false면closedAtISO를 파싱합니다. - 회사 식별(FR-68): 목록 응답에 회사가 없으므로 상세 조회로
sourceCompanyName을 채웁니다.companyIdentifierInDetailOnly=true가 이 동작을 선언하고, 상세 조회는SourceRequestExecutor의 지연 정책을 적용받습니다. 상세 조회 실패 건은 회사 미상으로 남기되 회차를 실패시키지 않습니다(부분 실패 격리). id→sourceJobId,structuredTags는emptyList()(②만).- 모든 외부 호출은
SourceRequestExecutor(BE-02) 경유(FR-69). - robots·약관 확인 결과를 KDoc에 기록(NFR-9·NFR-11).
supportedCategories()로 지원 직무 카테고리를 선언합니다(FE 요청 #12) — 브리프 실측 코드(jobCategory=1서버/백엔드 등) 기반.
의존
- BE-02 (SPI·
ChangeSignature.FieldHash·SourceRequestExecutor)
다이어그램
처리 흐름
sequenceDiagram participant R as JobSourceGatewayImpl participant A as JumpitAggregatorAdapter participant E as SourceRequestExecutor participant J as jumpit-api R->>A: fetchList(Aggregator descriptor) loop 페이지 (size≤200) A->>E: get(/api/positions, page) E->>J: GET positions?jobCategory&page&size J-->>E: JSON (회사 없음) end loop 공고별 (회사 식별) A->>E: get(상세) E->>J: GET position detail J-->>A: 회사명 → sourceCompanyName end A->>A: alwaysOpen→null / closedAt 파싱 · 필드 해시 A-->>R: Fetched(RawJobPosting[])
클래스 의존
flowchart LR subgraph Domain["domain/posting"] Raw[RawJobPosting] Sig[ChangeSignature.FieldHash] Cap[SourceCapabilities] Outcome[SourceCollectionOutcome] end subgraph Adapter["infrastructure/posting/collector/aggregator/jumpit"] Ad[JumpitAggregatorAdapter] Dto[JumpitPositionResponse] Hash[FieldHashCalculator] Deadline[JumpitDeadlineNormalizer] end subgraph Common["collector/common"] Exec[SourceRequestExecutor] end Ad --> Exec Ad --> Dto Ad --> Hash Ad --> Deadline Hash --> Sig Deadline --> Raw Ad --> Cap Ad --> Outcome
테스트 케이스
- 실제 응답 fixture를 파싱하면
id가sourceJobId로 매핑된다 alwaysOpen=true이고closedAt이 존재해도deadlineAt=null(상시채용)로 정규화된다alwaysOpen=false이면closedAtISO가 마감일로 파싱된다- 주요 필드가 동일하면 같은
FieldHash가 생성되어 변경 없음으로 판정된다 - 제목이 바뀌면
FieldHash가 달라져 변경으로 판정된다 - 목록에 회사가 없으므로 상세 조회로
sourceCompanyName이 채워진다 - 상세 조회가 실패한 공고는 회사 미상으로 남되 회차는 실패하지 않는다
size가 200을 넘지 않고 요청 지연·UA가 적용된다- API가 5xx를 반환하면
Failed를 반환한다 supports(JUMPIT)가 true,sourceType이AGGREGATOR,changeSignalKind가FIELD_HASH를 반환한다supportedCategories()가 코드↔라벨 목록(예:1↔서버/백엔드)을 반환한다