[BE-28] 잡코리아 애그리게이터 어댑터 (규약 회색지대 · 경로 화이트리스트)
작업 내용 (설계 의도)
근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “애그리게이터 어댑터 설계 — 규약 회색지대 4종”, “규약 강제 3중 방어”
근거 조사: 20260722-채용소스-조사-브리프.md “잡코리아” (실측: 목록 jobkorea.co.kr/recruit/joblist?menucode=duty&dutyCtgr=·상세 /Recruit/GI_Read/{gno} JSON-LD JobPosting, identifier·hiringOrganization·validThrough(ISO), 백엔드 1,958건. robots.txt가 ClaudeBot·anthropic-ai 등 AI 크롤러를 Disallow: /로 명시 차단, 키워드 검색 경로는 일반 크롤러에게도 차단, 허용 경로는 목록 1페이지 수준)
변경 사항
이 프로젝트에서 규약 준수가 가장 까다로운 어댑터입니다. robots가 AI 크롤러를 명시 차단하고 키워드 검색을 막으므로, 허용 경로 화이트리스트로 금지 경로 호출을 구조적으로 불가능하게 하는 것이 최우선입니다.
능력 선언(SourceCapabilities):
| 항목 | 값 | 근거 |
|---|---|---|
sourceType | AGGREGATOR | 직무 카테고리(dutyCtgr) |
changeSignalKind | BodyHash | 등록일만 제공 → 상세 본문 해시 |
providesDeadline | true | JSON-LD validThrough |
requiresDetailFetch | true | 상세 /Recruit/GI_Read/{gno}의 JSON-LD |
| 규약 정책 | 허용 경로 화이트리스트 ["/recruit/joblist", "/Recruit/GI_Read"]만, UA 일반 식별자, 요청 지연, 목록 1페이지 + 개별 상세로 제한 | robots |
핵심 설계 의도 — 규약 강제가 본체:
- 경로 화이트리스트(FR-69-1) —
SourceCompliancePolicy.allowedPathPrefixes=["/recruit/joblist", "/Recruit/GI_Read"].SourceRequestExecutor가 이 prefix에 없는 경로 호출을 예외로 차단합니다. 키워드 검색 경로(예:/Search/)를 어댑터가 실수로 때리는 것이 코드 레벨에서 불가능합니다. 이 테스트가 이 티켓의 최우선 검증입니다. - 수집 범위 제한 — 허용 경로에서 페이지네이션이 실질 1페이지만 동작한다는 브리프 사실을 반영해, 직무 카테고리 목록 1페이지 + 그 안의 개별 상세로 제한합니다. 전체 1,958건을 긁지 않습니다 — 목록 1페이지에 노출된 공고만 대상.
- UA는 일반 식별자 — AI 크롤러 UA(ClaudeBot 등)가 아닌 일반 식별 UA를 씁니다(robots가 AI 크롤러만 차단하므로). 봇 차단은 없습니다.
- 마감일 정규화(FR-67) — JSON-LD
validThroughISO 파싱 /상시채용문자열 또는validThrough가 등록일 기준 약 +1년이면 상시채용(null)으로 정규화. - 변경 감지 — 등록일만 있어
BodyHash(상세 JSON-LD 정규화 본문 해시). 상세 조회는 지연 정책 적용. data-gno(목록) / JSON-LDidentifier(상세) →sourceJobId.hiringOrganization→sourceCompanyName.- robots·약관 확인 결과와 AI 크롤러 차단·경로 제한 근거를 KDoc에 명시(NFR-9·NFR-11).
supportedCategories()로dutyCtgr↔라벨 선언.
롤백: disabled_at으로 소프트 비활성화(robots 정책 변경 시 즉시).
의존
- BE-02 (SPI·
SourceRequestExecutor경로 화이트리스트·ChangeSignature.BodyHash)
다이어그램
처리 흐름
sequenceDiagram participant R as JobSourceGatewayImpl participant A as JobkoreaAggregatorAdapter participant E as SourceRequestExecutor participant J as jobkorea.co.kr R->>A: fetchList(Aggregator descriptor) A->>E: get(/recruit/joblist?dutyCtgr) — 화이트리스트 통과 E->>J: GET joblist (1페이지) J-->>A: 목록 (data-gno) loop 공고별 (지연) A->>E: get(/Recruit/GI_Read/{gno}) — 화이트리스트 통과 E->>J: GET 상세 (JSON-LD) J-->>A: validThrough·hiringOrganization end Note over A,E: 키워드 검색 경로 호출 시 Executor가 예외로 차단 A->>A: validThrough 정규화 · 본문 해시 A-->>R: Fetched(RawJobPosting[])
클래스 의존
flowchart LR subgraph Domain["domain/posting"] Raw[RawJobPosting] Sig[ChangeSignature.BodyHash] Cap[SourceCapabilities] Policy[SourceCompliancePolicy] end subgraph Adapter["infrastructure/posting/collector/aggregator/jobkorea"] Ad[JobkoreaAggregatorAdapter] ListP[JobkoreaListParser] JsonLd[JobkoreaJsonLdParser] Hash[BodyHashCalculator] end subgraph Common["collector/common"] Exec[SourceRequestExecutor] end Ad --> Exec Ad --> ListP Ad --> JsonLd JsonLd --> Hash Hash --> Sig Cap --> Policy Ad --> Cap
테스트 케이스
- 키워드 검색 경로(예:
/Search/) 호출을 시도하면SourceRequestExecutor가 예외로 차단한다 (최우선) - 허용 경로
/recruit/joblist·/Recruit/GI_Read호출은 통과한다 - 목록 1페이지 + 개별 상세로만 수집하고 전체 페이지를 순회하지 않는다
- JSON-LD
identifier가sourceJobId,hiringOrganization이sourceCompanyName으로 매핑된다 validThroughISO가 마감일로 파싱된다상시채용문자열 또는validThrough가 등록일 +1년 근사면deadlineAt=null로 정규화된다- 상세 본문이 동일하면 같은
BodyHash로 변경 없음으로 판정된다 - UA가 AI 크롤러가 아닌 일반 식별자로 부착된다
- 상세 요청 사이에 지연이 적용된다
- 목록 조회 자체가 실패하면
Failed를 반환한다 supports(JOBKOREA)가 true,changeSignalKind가BodyHash를 반환한다supportedCategories()가dutyCtgr↔라벨 목록을 반환한다