[BE-30B] 잡코리아 회사 채용관(JRS) 파서 · 마감일 정규화
상태: 게이트 통과(PASS, 2026-07-27) — 파서 구현 진행. 근거 TDD:
20260727-jobkorea-dual-source-type-tdd.md“Open Questions #1”. 타당성 게이트 결과(PASS): 실채용관 5곳(wooribank·ibk·hanjucorp·nhbank·jobkorea) 실측 검증. “커스텀 셸이라 안정 ID 미노출” 우려는 반증 — 마케팅 셸(/{slug})이 아니라 **표준 라우트/{slug}/Main/AgiList**가 서버 렌더 HTML로 안정 식별자·마감일·상태를 노출한다. 본사이트(www.jobkorea.co.kr, robots 차단) 링크아웃 없음.
작업 내용 (설계 의도)
근거 TDD: 20260727-jobkorea-dual-source-type-tdd.md
근거 실측(2026-07-27): jrs.jobkorea.co.kr/{slug}/Main/AgiList가 공고 목록을 서버 렌더 HTML로 노출. 상세 href /{slug}/{sub-slug}/Agi/Index의 sub-slug가 회사 포털 내 유니크·영속 식별자. 마감일 YYYY.MM.DD HH:mm ~ YYYY.MM.DD HH:mm 클린 포맷.
변경 사항
jrs.jobkorea.co.kr/{slug}/Main/AgiList 회사 채용관 리스트의 순수 파싱·정규화 로직만 담당합니다(외부 호출·SPI 결선은 BE-30C). fixture HTML로 테스트해 SPI 의존 없이 병렬 개발 가능합니다.
- 진입점은 표준 리스트 라우트 — 마케팅 셸
/{slug}(이미지 플레이스홀더뿐, ID 없음)가 아니라/{slug}/Main/AgiList를 파싱 대상으로 한다. JobkoreaCompanyBoundListParser— 채용관 목록에서(sourceJobId, title, postingUrl, deadlineRaw, status)추출.- sourceJobId(안정 식별자): 상세 href 정규식
^/(?i){slug}/([A-Za-z0-9]+)/Agi/(Index|Invite)$→ group(1) = sub-slug. 키 ="{slug}:{sub-slug}"(예:wooribank:wooribank26) 또는 정규화 URL. slug 대소문자 정규화 필요(IBK 포털은 href에 대문자/IBK/). - status: 라벨
접수중/마감/심사.
- sourceJobId(안정 식별자): 상세 href 정규식
JobkoreaDeadlineNormalizer—YYYY.MM.DD HH:mm ~ YYYY.MM.DD HH:mm의 종료 datetime(group2)을ZonedDateTime으로 파싱.상시채용·마감일 부재는null(FR-13). (게이트 실측 결과 연·시각이 모두 명시돼 연도 보정은 대개 불필요하나, 연도 없는 변형이 나오면 시작 연도 기준 보정·12→01 경계 +1년.)ChangeSignature선택: 상세 본문은 수명주기에 따라 변하므로(종료 후 다른 페이지로 전환) 리스트의 status·마감일 기반으로 판정 — 상세 last-modified 신뢰 불가. sub-slug 집합 변화 = 신규, 동일 sub-slug의 status 전이/접수마감 경과 = 종료. 목록FieldHash(sub-slug+title+status+마감일).- 종료 공고 href 소실 예외: 일부 포털(hanjucorp)은 종료 공고 href가
javascript:alert(...)로 치환돼 sub-slug 소실. 신규 수집 스킵, 이미 수집된 공고는 기존 sub-slug 유지 + status=‘마감’ 기록. (진행 중 공고는 항상 실제 href라 수집 무영향.) - 순수 함수 —
RestClient·SourceRequestExecutor주입 없음(BE-30C에서 결선).
롤백: 파서 단독은 부작용 없음. 사용처(BE-30C)에서 disable()로 소프트 오프.
의존
- BE-30 (JobPlatform 겸용 — 파서가 반환 유형 판정에 참조)
다이어그램
처리 흐름
sequenceDiagram participant A as (호출부 BE-30C) participant L as JobkoreaCompanyBoundListParser participant N as JobkoreaDeadlineNormalizer A->>L: parse(html) L->>N: normalize("2026.05.26(화) - 06.08(월) 14:00까지") N-->>L: deadlineAt 또는 null(상시) L-->>A: List(sourceJobId, title, url, deadlineAt)
클래스 의존
flowchart LR ListP[JobkoreaCompanyBoundListParser] --> Norm[JobkoreaDeadlineNormalizer] ListP --> Raw[RawJobPosting] Norm --> Sig[ChangeSignature]
테스트 케이스
- fixture
AgiListHTML에서 공고 목록이(sourceJobId=slug:sub-slug, title, postingUrl, status)로 추출된다 - 상세 href
/{slug}/{sub-slug}/Agi/Index에서 sub-slug가 안정 식별자로 추출되고, 대문자 slug(/IBK/)도 정규화돼 매칭된다 2025.12.11 17:00 ~ 2025.12.26 13:00의 종료 datetime이ZonedDateTime으로 파싱된다상시채용문자열 또는 마감일 부재는deadlineAt=null로 정규화된다- 종료 공고 href가
javascript:alert(...)인 행은 신규 수집에서 스킵되고(빈 목록 조용 반환 아님), 진행 중 공고는 정상 추출된다 - status 라벨(
접수중/마감/심사)이 추출된다 - 동일 목록 필드(sub-slug+title+status+마감일)는 같은
FieldHash로 변경 없음으로 판정된다 - 마케팅 셸(
/{slug}, ID 없는 HTML)을 파싱 대상으로 넘기면 명시적 예외로 실패한다(진입점은AgiList여야 함)