[BE-22] 사람인 애그리게이터 어댑터
작업 내용 (설계 의도)
근거 TDD: 20260722-타깃-공고-알림-및-지원-히스토리-tdd.md — “애그리게이터 어댑터 설계”, “방안 8”
근거 조사: 20260722-채용소스-조사-브리프.md “전체 플랫폼 조사 결과 / 채택 가능 — 규약 청정” (실측: saramin.co.kr/zf_user/jobs/list/job-category?cat_kewd={code}&page=N&page_count=100, HTML UTF-8, rec_idx 안정 ID, 수정일 YY/MM/DD 존재, 백엔드 2,594건, robots 미차단·Crawl-delay 없음, UA 헤더 필수)
변경 사항
검색 조건(직무 카테고리 + 선택 키워드)으로 수집하는 애그리게이터 어댑터입니다(FR-60). 회사에 종속되지 않는 전역 소스이며, CollectionDescriptor.Aggregator를 받습니다.
능력 선언(SourceCapabilities):
| 항목 | 값 | 근거 |
|---|---|---|
sourceType | AGGREGATOR | 검색 조건으로 수집 |
changeSignalKind | UPDATED_AT | 목록의 수정일 YY/MM/DD (FR-68) |
providesDeadline | true | 목록에 마감일 노출 |
providesStructuredWorkArrangement | false | ②(JD 본문)만 |
companyIdentifierInDetailOnly | false | 목록에 회사명 존재 |
| 규약 정책 | UA 필수·요청 지연 1000ms·page_count 100·허용 경로 /zf_user/jobs/list | FR-69, 브리프 |
핵심 설계 의도:
- 마감일 정규화(FR-67)가 이 어댑터의 핵심 난점입니다. 사람인은 연도가 없는
~MM/DD표기와채용시·상시채용문자열을 섞어 씁니다.~MM/DD는 연도를 추론합니다 — MM/DD가 오늘 이전이면 내년, 이후면 올해(연말·연초 경계 처리).채용시·상시채용문자열은deadlineAt=null(상시채용)로 정규화합니다. 정규화 실패 시 예외 대신null+ 경고 로그(회차를 실패시키지 않음). - 회사명은 목록에서 확보해
RawJobPosting.sourceCompanyName에 채웁니다 — 크로스 소스 dedup(BE-24)과 발견 회사 자동 등록(BE-25·06)의 입력입니다. rec_idx→sourceJobId, 공고 제목 → 직무 매칭 대상,structuredTags는emptyList()(②만).- 모든 외부 호출은
SourceRequestExecutor(BE-02)를 경유해 UA·지연·페이지 상한·허용 경로를 강제받습니다 — 어댑터가 직접 HTTP 클라이언트를 만들지 않습니다(FR-69). 기본 UA는 307로 막히므로 정책의 UA가 반드시 부착돼야 합니다. - 페이지네이션 전량 수집(2,594건 = 26페이지). 총 건수와 수집 건수 불일치 시 회차
Failed(부분 수집이 미발견 오판정으로 번지는 것 방지). - robots·약관·요청 정책 확인 결과를 어댑터 KDoc에 기록(NFR-9·NFR-11).
supportedCategories()로 지원 직무 카테고리(코드↔라벨)를 선언합니다(FE 요청 #12) — 브리프 실측 코드(cat_kewd=84백엔드 등)를 근거로 목록을 하드코딩하고, 이 목록이 등록 API의 코드 검증·FE Select의 SSOT가 됩니다.
의존
- BE-02 (SPI·
CollectionDescriptor·SourceRequestExecutor·SourceCapabilities)
다이어그램
처리 흐름
sequenceDiagram participant R as JobSourceGatewayImpl participant A as SaraminAggregatorAdapter participant E as SourceRequestExecutor participant S as saramin.co.kr R->>A: fetchList(Aggregator descriptor) loop 페이지 (지연 1000ms · UA 부착) A->>E: get(list URL, page) E->>S: GET job-category?cat_kewd&page S-->>E: HTML (UTF-8) E-->>A: 문서 end A->>A: 마감일 정규화(~MM/DD 연도 추론 · 문자열→null) · 회사명 추출 alt 총 건수 == 수집 건수 A-->>R: Fetched(RawJobPosting[] with sourceCompanyName) else 불일치 A-->>R: Failed(reason) end
클래스 의존
flowchart LR subgraph Domain["domain/posting"] Raw[RawJobPosting] Sig[ChangeSignature.UpdatedAt] Cap[SourceCapabilities] Outcome[SourceCollectionOutcome] end subgraph Adapter["infrastructure/posting/collector/aggregator/saramin"] Ad[SaraminAggregatorAdapter] Parser[SaraminListParser] Deadline[SaraminDeadlineNormalizer] end subgraph Common["collector/common"] Exec[SourceRequestExecutor] end Ad --> Exec Ad --> Parser Ad --> Deadline Parser --> Raw Deadline --> Raw Ad --> Cap Ad --> Outcome
테스트 케이스
- 실제 응답 fixture를 파싱하면
rec_idx가sourceJobId로, 회사명이sourceCompanyName으로 매핑된다 수정일 25/07/16이ChangeSignature.UpdatedAt으로 매핑되고 왕복 복원된다- 마감일
~08/15가 오늘(예: 08/16)보다 이전이면 내년으로 추론된다 - 마감일
~12/31이 오늘(예: 08/16)보다 이후면 올해로 추론된다 채용시·상시채용문자열은deadlineAt=null(상시채용)로 정규화된다- 마감일 형식이 예상과 다르면
null+ 경고 로그로 처리되고 회차는 실패하지 않는다 - 페이지 전량을 수집해 총 건수와 일치시킨다
- 총 건수와 수집 건수가 불일치하면
Failed를 반환한다 - 요청 사이에 1000ms 지연이 적용되고 정책 UA가 부착된다
- 목록 페이지 조회가 실패하면
Failed를 반환한다 supports(SARAMIN)가 true,sourceType이AGGREGATOR를 반환한다CollectionDescriptor.CompanyBound가 전달되면 명시적 예외를 던진다(애그리게이터 전용)supportedCategories()가 코드↔라벨 목록(예:84↔백엔드)을 반환한다