체인지로그
GitHub릴리스별로 정리된 모든 주요 변경 사항입니다. 저장소에 실린 동일한 파일이며, 19개 언어 전체로 번역되어 있습니다.
[1.54.0] — 2026-05-18
WS10 — canonical-docs 재검증 + help 번들 H3 패리티(최종 수렴 릴리스). CHANGELOG/구조 CI 게이트가 H2만 검사했기에 docs/help/en.md는 조용히 70개 H3 하위 섹션으로 표류한 반면 7개 로컬라이즈 번들은 68에 머물렀음 — 격차는 §17(「Reference adapters」 테이블 + 「Common pitfalls」 목록, 영어 전용). 둘 다 이제 7개 언어 모두로 번역됨(어댑터 파일명 / 링크 / 식별자는 바이트 동일하게 유지); 8개 번들 모두 이제 17 H2 / 70 H3. help-ru-config-section.test.mjs의 새로운 H3 패리티 게이트가 이를 잠금(716 → 717). canonical-docs-coverage.test.mjs 7/7이 help가 여전히 career-ops.org/docs의 5개 가이드를 모두 반영함을 확인; WS2의 UX 감사(v1.41→v1.52의 40건)가 각 화면을 docs와 대조 — 괴리 없음. docs/sdd/CONVENTIONS.md를 v1.54.0으로 갱신(테스트 합계, H3 패리티 게이트, 파일 크기 이상치, 새 접근성 규약 섹션). WS0–WS10 완료; WS11만 남음. fix(docs): WS10 canonical re-validation + H3 parity · test(help): H3-parity gate. 자세히는 CHANGELOG.md.
[1.53.0] — 2026-05-18
WS9 — 셸 표면 테스트 피라미드(마지막 미테스트 계층). 4개의 bin/*.sh 스크립트와 .githooks/pre-commit 훅은 커버리지가 제로였음; 새로운 tests/sh-files.test.mjs가 10개 케이스를 추가해 bash -n/sh -n 구문, shebang + 실행 비트, 그리고 다른 워크스트림이 의존하는 동작 계약을 고정함: career-ops-ui.sh — help는 0으로 종료하며 shell-source 누출이 없음(v1.40.0 회귀 가드), 알 수 없는 verb는 2로 종료, usage()는 heredoc임; start.sh — NO_OPEN 존중, Node ≥ 18 요구, 브라우저 띄우기를 scripts/open-dashboard.mjs에 위임(v1.43.0 가드); setup.sh — strict 모드, SKIP_START, 두 저장소 클론; run_all.sh — --quick/--no-e2e 파싱과 4개 스위트; .githooks/pre-commit은 WS7 리뷰어를 exec하고 어떤 셸 파일도 git --no-verify를 호출하지 않음(CLAUDE.md 하드 룰 #7 가드); install-hooks.mjs가 core.hooksPath를 배선함. docs/architecture/TESTING.md — 피라미드 다이어그램에 셸 표면 베이스 계층 추가 + v1.53.0 합계 노트(716개 node --test 케이스 / 90개 파일 + 4개 E2E 표면). 706 → 716. 자세히는 CHANGELOG.md.
[1.52.0] — 2026-05-18
WS2 LOWs #33–#40 — 일괄 다듬기 정리(UX 감사 큐를 닫음). 저심각도 8건. fix(a11y/i18n): WS2 LOW batch — #33: #/dashboard — 헤더의 3개 CTA가 불일치했음(2개만 앞 아이콘 있음); “Open Pipeline”이 이제 📋를 가져 셋 다 일치함. #34: #/profile — 아키타입 fit/level이 모호한 두 칩으로 렌더링됐음; 이제 접두사(Fit: / Level:)와 대응 aria-label을 가짐. #35: #/health — Run-doctor / verify 토스트가 doctor.mjs의 원시 문자열을 표시했음; 이제 i18n 키화. #36: #/health — 검사 결과가 평탄한 <div> 나열이었음; 이제 role=list <ul>/<li>이며 상태 배지가 aria-label="<check>: <status>"를 가짐. #37: #/reports — 리포트 카드가 마우스 전용 <div onClick>이었음; 이제 role=link + tabindex + Enter/Space 핸들러 + aria-label. #38: #/activity — 페이지네이터 주석은 “200”이라 했으나 코드는 500을 요청했음; CAP 상수로 정합화하고 500 상한이 오래된 이력을 잘라낼 때 role=note 알림이 표시됨. #39: #/batch — 프로즈 플레이스홀더가 영어 하드코딩이었던 반면 aria-label은 localized였음; 4개가 이제 i18n 키화. #40: 모드 페이지가 비동기 프로브 후 주 버튼을 조용히 재라벨했음; 이제 정중한 role=status 영역이 이를 안내함. 신규 i18n 키 10개 × 8개 로케일({n} 보존); 테스트 +9: test: tests/low-sweep.test.mjs. 697 → 706. WS2의 UX 감사 큐(v1.41→v1.52의 #1–#40)를 닫음; 다음은 WS9 → WS10 → WS11. 자세히는 CHANGELOG.md.
[1.51.0] — 2026-05-18
WS2 #13 + #14 + #18 + #19 + #20 — #/auto와 #/evaluate feedback/i18n 정리. UX 감사 지적 5건. fix(a11y/ux): auto+evaluate — busy state, actionable HTTP errors, clipboard fallback, aria-live result, spinner-guarded submit — #13: #/auto의 Run 버튼이 이제 단순 비활성화 대신 바쁨 상태(is-loading + aria-busy + “Running…”)를 표시함. #14: 실패한 HTTP 요청이 이제 단계에 실행 가능한 i18n 메시지와 토스트({n}가 담긴 auto.httpFail)를 노출함(이전엔 무미건조한 “HTTP 500”이었음). #18: 수동 모드의 “Copy prompt”가 이제 비동기 Clipboard API를 execCommand 폴백과 함께 사용하며, 거짓 “Copied” 대신 실제 실패를 토스트함. #19: evaluate 결과 컨테이너가 이제 role=status aria-live=polite여서 긴 LLM 호출이 스크린 리더에 안내됨. #20: Evaluate 버튼이 UI.withSpinner로 감싸짐(이전엔 중복 제출을 허용하는 평범한 onClick: run이었음). 신규 i18n 키 3개 × 8개 로케일; 테스트 +6: 691 → 697. 또한 테스트 전용 수정(커밋 7f8e250): e2e pipeline-delete 티어다운이 v1.48 이전 네이티브 confirm 경로에 있었음; API DELETE로 전환(fix(test): … — CI Playwright-e2e가 빨간색이었음; 제품 회귀가 아님). 자세히는 CHANGELOG.md.
[1.50.0] — 2026-05-18
WS2 #12 + #27 + #28 — help 내비게이션 접근성. 17개 섹션·90+개 헤딩 가이드의 #/help UX 감사 지적 3건을 help.js에서 수정. fix(a11y): help — single h1, labelled+filterable TOC, focus-on-anchor, back-to-top — #28: 문서 markdown이 자체 # Title로 시작해, 헤더가 이미 정규 h1을 제공하는 페이지에 두 번째 <h1>을 만들었음; 이제 기사의 모든 <h1>이 제거되어 h1은 정확히 하나이며 계층은 <h2> 섹션에서 깔끔하게 시작함. #27: TOC의 <nav>는 이름 없는 랜드마크였음(페이지에 라벨 없는 <nav> 2개); 이제 aria-label(help.toc)을 가지며, TOC 항목을 클릭하면 뷰포트 스크롤뿐 아니라 포커스가 섹션 헤딩으로 이동함(tabindex=-1 + focus()). #12: 긴 문서에서 무언가를 찾을 방법이 없었음; TOC 위의 type=search 필터가 헤딩 텍스트로 항목을 실시간으로 좁히고, 스크롤 후 aria-label이 달린 플로팅 “Back to top” 버튼이 나타나 맨 위로 돌아가 페이지 <h1>로 포커스를 되돌림; 그 scroll 리스너는 #/help를 벗어나는 hashchange에서 제거됨. 신규 i18n 키 2개 × 8개 로케일 — help.tocFilter, help.backToTop; 테스트 +6: test: tests/help-nav-a11y.test.mjs. 685 → 691. 자세히는 CHANGELOG.md.
[1.49.0] — 2026-05-18
WS2 #10 + #11 + #25 + #26 — tracker 테이블 접근성 및 정렬. #/tracker의 UX 감사 지적 4건을 tracker.js에서 수정. fix(a11y): tracker headers, sortable table, localized fix labels, empty state — #10: 액션 열 헤더가 빈 문자열이었고 행별 Report 버튼에 맥락이 없었음; 이제 모든 <th>가 scope=col을 가지며, 액션 헤더와 Score/PDF 헤더는 i18n 키화되었고(비어 있거나 하드코딩된 영어였음), Report 버튼은 회사명이 담긴 aria-label(<report> — <company>)을 얻음. #11: 정렬 수단이 없던 tracker; Date / Score / Status 헤더는 이제 <th> 안의 키보드 조작 가능한 정렬 버튼이며 aria-sort(none/ascending/descending)를 가짐; sorted() 비교자(score는 숫자, date/status는 로케일 비교)가 페이지네이션 전에 실행되고, 클릭 시 방향을 토글하고 페이저를 리셋함. #25: track.normalize/dedup/merge는 가장 위험도가 높은 파괴적 컨트롤임에도 8개 로케일 모두에서 동일한 영어였음(data/applications.md를 제자리에서 다시 씀) — 이제 제대로 현지화되었고, title 툴팁도 추가. #26: 0행 첫 실행이 과도하게 필터링된 목록과 동일한 “no match” 메시지를 보였음; rows.length === 0은 이제 독립적인 빈 상태(제목 + 본문 + “Open pipeline” CTA)를 렌더링함. 신규 i18n 키 7개 × 8개 로케일 + 3개 재현지화; 테스트 +6: test: tests/tracker-a11y-sort.test.mjs. 677 → 683. 자세히는 CHANGELOG.md.
[1.48.0] — 2026-05-18
WS2 #8 + #22 — pipeline: 포커스 트랩 확인 + 미리보기 접근성. #/pipeline의 UX 감사 지적 2건을 pipeline.js에서 수정. fix(a11y): pipeline UI.confirm() + live preview region — #8: #/pipeline의 세 가지 액션이 네이티브 confirm()을 사용했음(포커스 트랩 안 됨): 미리보기 창의 Delete, 행별 ✕ 삭제, “Evaluate first”. 이제 모두 포커스 트랩되는 UI.confirm()(v1.44.0 인프라)를 거침 — 두 삭제는 danger:true(Cancel 기본값), “Evaluate first”는 danger:false; pipeline.js에 네이티브 confirm()은 더 이상 남아 있지 않음. #22: previewPane에 라이브 역할이 없었고 fetch 실패가 previewBody에 채워져 오해를 주는 <pre> “preview”로 렌더링되었음; 이제 aria-label을 갖춘 role=region aria-live=polite이며, 실패 시 별도의 previewError를 설정해 독립적인 role=alert 블록으로 렌더링함((재)선택 시 및 활성 행 삭제 시 지워짐). 신규 i18n 키 4개 × 8개 로케일; 테스트 +5: test: tests/pipeline-confirm-preview.test.mjs. 672 → 677. 자세히는 CHANGELOG.md.
[1.47.0] — 2026-05-18
WS2 #7 + #30 + #31 + #16 — 미바인딩 레이블 접근성 일괄 정리. 폼 컨트롤에 프로그래밍적 레이블이 없던 UX 감사 지적 4건(WCAG 1.3.1 / 3.3.2 / 4.1.2)을 모두 바인딩. fix(a11y): bind every swept form control to an accessible name — #7 scan.js: dry-run 체크박스와 company-select 드롭다운에 for 없는 레이블이 있었음; 기존 id에 맞춰 htmlFor 추가. #30 deep.js: company / role 입력에 미바인딩 레이블이 있었음; id + htmlFor 추가(deep-company, deep-role). #31 apply.js: url / jd에 미바인딩 레이블이 있었음; id + htmlFor 추가(apply-url, apply-jd). #16 cv.js: 주요 markdown <textarea>에 접근 가능한 이름이 없었음; 표시된 “Markdown” 제목에 aria-labelledby로 바인딩 — 스크린 리더 이름과 화면 제목 일치, 신규 i18n 키 없음. batch.js / mode-page.js에서 이미 표준인 명시적 label[for]↔control[id] 패턴 사용; 신규 i18n 키 없음; 동작 변경 없음. 테스트 +5: test: 667 → 672. 자세히는 CHANGELOG.md.
[1.46.0] — 2026-05-18
WS2 #5 + #6 + #21 + #24 — scan SSE 접근성. #/scan의 UX 감사 지적 4건을 scan.js에서 수정. fix(a11y): scan SSE — live-log region, Stop, run-state, error banner — #5: 스트리밍 콘솔이 이제 role=log aria-live=polite(+ aria-label, tabindex=0, 키보드 스크롤 가능)이며, 별도의 시각적으로 숨겨진 assertive role=status 영역이 종단 이벤트(완료 / 실패 / 중지)를 알림. #6: Stop 버튼이 진행 중인 EventSource를 닫고(es.close()), 결과 폴링을 취소하고, 상태를 초기화함. Stop은 scan 실행 중에만 표시. #21: scan 실행 중에는 Scan 버튼을 비활성화 + aria-busy 부여하고 Stop을 표시함. 두 스트림 경로 모두(단일 페이즈 streamTo와 다중 페이즈 runScanAll — 후자는 종단 done, final !== false에서만 실행을 종료) 적용. #24: SSE 실패가 더 이상 3.5초 토스트만이 아니라, 영속적인 role=alert 배너가 재시도 액션과 함께 오류를 표시하고(직전 실행 함수를 재호출), 다음 실행에서 지워짐. 신규 i18n 키 8개 × 8개 로케일; 테스트 +7: test: tests/scan-sse-a11y.test.mjs. 660 → 667. 자세히는 CHANGELOG.md.
[1.45.0] — 2026-05-18
WS2 #3 — #/config 탭: 완전한 WAI-ARIA Tabs 패턴. #/config의 세 탭(API keys / Profile / Modes)은 클릭 전용으로만 활성화되는 단순 <button class="tab-btn">로, role도 aria-selected도 키보드 모델도 없었음(UX-audit HIGH #3, WCAG 4.1.2 / 2.1.1). fix(a11y): config.js tabs implement role=tablist/tab/tabpanel — 이제 aria-label이 있는 role=tablist 컨테이너; 각 탭은 role=tab + id + aria-controls + aria-selected(activate()에서 동기화) + 로빙 tabindex(활성 0 / 나머지 -1); 패널은 role=tabpanel + tabindex=0 + 활성 탭을 추적하는 aria-labelledby. 완전한 키보드 내비게이션: ←/→/↑/↓(래핑) + Home/End가 포커스를 이동하고 활성화도 함. 레거시 .tab-btn.is-active CSS 훅은 보존. i18n 키 +1 × 8개 로케일(config.tablistLabel); 테스트 +7: test: tests/config-tabs-aria.test.mjs. 또한 테스트 전용 수정: fix(test): retarget 2 stale auto-pipeline smoke tests — v1.34 이전 Playwright-e2e smoke 테스트 2개가 대시보드 “Auto-pipeline” 버튼이 v1.34.0에서 더 이상 열지 않게 된 일시 모달(→ Router.go('/auto'))을 단언하고 있었고, 별도의 Playwright-e2e CI 잡에서 빨간 상태로 남아 있었음. #/auto 화면으로 재타깃. 653 → 660. 자세히는 CHANGELOG.md.
[1.44.0] — 2026-05-18
WS2 #4 + #9 — 상위 프로젝트 파일의 파괴적 덮어쓰기 전 포커스 트랩 확인. UX 감사 HIGH 두 건, 모두 데이터 손실: (#4) config.js의 saveProfileRaw/saveModesRaw는 상위 config/profile.yml / _profile.md 전체를 확인 없이 교체했음; (#9) tracker.js의 Normalize/Dedup/Merge는 상위 data/applications.md를 확인 없이 제자리에서 재작성했음. fix(a11y/safety): UI.confirm() gate before whole-file parent overwrites — public/js/api.js에 새 UI.confirm() 추가. 기존 WAI-ARIA 모달 인프라를 재사용한 포커스 트랩 다이얼로그로(_onClose 훅이 있어 Esc / backdrop / × / Cancel 모든 해제 경로가 false로 resolve; 포커스는 기본적으로 Cancel; Promise<boolean> 반환; 네이티브 confirm() 아님), 세 개의 파괴적 호출은 이제 모두 쓰기 전에 게이트됨. 신규 i18n 키 8개 × 8개 로케일({op} 플레이스홀더는 그대로 보존); 테스트 +8: test: tests/confirm-gate.test.mjs, 644 → 652. 자세히는 CHANGELOG.md.
[1.43.0] — 2026-05-18
사용자 요청 — career-ops-ui open + autostart 브라우저 앞으로 가져오기. setup/run 이후 브라우저가 이미 떠 있으면 맨손 open/xdg-open은 대시보드 탭을 뒤에 남겨, 사용자가 직접 찾아야 했음. feat(cli): career-ops-ui open — open AND raise the dashboard tab — 새 scripts/open-dashboard.mjs가 HOST/PORT에서 URL을 구성하고(0.0.0.0 바인드를 loopback으로 재작성), 선택적으로 /api/health를 기다린 뒤 기본 브라우저를 열고 이어서 강제로 앞으로 가져옴 — macOS는 osascript로 실행 중인 Chrome/Brave/Edge/Safari/Arc/Firefox 중 하나를 활성화, Linux는 xdg-open+wmctrl, Windows는 start. career-ops-ui open 동사로 노출(별칭 dash, focus). bin/start.sh의 autostart가 이제 여기에 위임하여 탭이 자동으로 앞으로 옴; NO_OPEN=1은 headless/CI 시작에서 auto-open을 비활성화함. README ×8 + help §1 ×8 갱신; 테스트 +8: test: tests/open-dashboard.test.mjs, 636 → 644. 자세히는 CHANGELOG.md.
[1.42.0] — 2026-05-18
WS2 수정 #2 — 죽은 라우트 #/portals → config 딥링크. #/portals는 미등록 라우트로 404 뷰를 렌더링했지만, 포털 소스 관리를 위해 북마크/직접 입력될 수 있는 타당한 URL이었음(UX 감사 HIGH #2). fix(router): #/portals 404 → alias to config + Regional-sources deep-link — router.js의 ALIASES에 portals: 'config'를 추가(settings→profile과 동일한 북마크 안정성 패턴), 이제 config 뷰로 해석되며 config 내비 항목이 활성화됨. Regional-sources 그룹이 존재하면 뷰(config.js)가 #/portals 해시를 감지해 해당 <details> 그룹을 강제로 펼치고 화면에 스크롤한 뒤 그 summary로 포커스를 이동(기본 h1 포커스를 재정의)하므로, 사용자는 포털 소스 컨트롤에 정확히 착지함; 별칭만으로 빈 지역 그룹을 렌더링하지 않음. help-bundle §5 × 8에 단축 경로 안내 추가; 라우터 테스트 +1: test(router): portals→config alias guarantee를 router.test.mjs에 추가, 635 → 636. 자세히는 CHANGELOG.md.
[1.41.0] — 2026-05-18
WS2 — 시니어 UX/사용성 감사 + 횡단 포커스 관리 수정. 10년 이상의 휴리스틱 감사(Nielsen × WCAG 2.2 AA × 프로젝트 규약)로 전체 17개 라우트를 점검해 심각도 순 40건 큐를 생성(.planning/.../UX-AUDIT.md); HIGH→MEDIUM→LOW를 릴리스마다 한 건씩 수정 출시. 이번 릴리스는 횡단 HIGH 1순위에 착지. 수정: fix(a11y): move focus to the new view on every route change — router.js render()가 hashchange마다 #content를 교체했지만 포커스를 옮기지 않아, 키보드/스크린 리더 사용자가 파괴된 노드에 남아 위치를 잃었음(WCAG 2.4.3 Focus Order / 4.1.3 Status Messages — 횡단적이며 17개 화면 모두에 영향); 새 focusNewView(content)가 새 뷰의 첫 h1/.page-title에 포커스(간결한 SR 안내 + 올바른 포커스 순서), 필요 시 헤딩을 포커스 가능하게(tabindex=-1) 만들고 #content로 폴백; skip-link와 충돌하지 않도록 첫 페인트는 건너뜀; 성공·오류 양 렌더 경로에 배선; 라이브 검증 완료: 내비 후 document.activeElement는 새 뷰의 H1.page-title. 테스트: test(router): focus-management static guarantees — router.test.mjs에 4개 케이스(헬퍼 정의, 헤딩 타깃 + content 폴백, 첫 페인트 스킵 가드, ≥2 호출 지점); 631 → 635. 자세히는 CHANGELOG.md.
[1.40.0] — 2026-05-18
WS8.3 — docs 실태화 스윕 + career-ops-ui help 수정 + askSecret 강화. 수정: fix(cli): career-ops-ui help no longer leaks shell source — 디스패처가 헤더 주석을 sed -n '2,12p'로 출력했으나 12행(set -euo pipefail)은 주석이 아니라 코드여서 career-ops-ui help(및 알 수 없는 동사 사용법 텍스트)가 떠도는 set -euo pipefail 줄로 끝났음; help와 *) 두 케이스를 2,11p(주석 블록)로 좁힘; help는 exit 0, 알 수 없는 동사는 exit 2 — 검증 완료. fix(cli): scripts/init.mjs key entry never echoes — v1.39.0 후속 작업에서 장식용 readline 덮어쓰기 마스크를 실제 raw 모드 리더로 교체: setRawMode(true) + 버퍼 라인으로 입력/붙여넣기된 키 바이트가 터미널에 전혀 도달하지 않음(scrollback / tmux / 화면 공유 누출 없음); 완전한 VT 이스케이프 FSM이 모든 CSI/SS3/OSC/DCS/SOS/PM/APC 시퀀스를 소비해 화살표·기능 키가 시크릿을 손상시키지 못함; stdin은 의존성 주입되어 비-TTY 폴백을 공유 전역을 건드리지 않고 단위 테스트; AI 리뷰 LGTM까지 깔끔하게 반복. 문서: README ×8 — 기존 “원커맨드 설치” 섹션을 눈에 띄는 “한 명령으로 실행 및 초기화” 섹션으로 교체(curl 원라이너에 더해 명시적 career-ops-ui CLI 체인: clone → npm link → setup → init → doctor → run → help, 공급자 마법사 설명, CI 형식 --provider --anthropic-key --yes, LLM_PROVIDER 노트); 8개 README 배지를 v1.22–v1.24 / tests-461–474에서 v1.40.0 / tests-631로 실태화(e2e 배지는 날조 카운트 회피를 위해 비숫자화); help-bundle ×8 §1 — 퀵스타트 플레이북 상단(“A. Setup” 앞)에 “한 명령 실행 & init” 콜아웃을 8개 로케일 모두에 추가; H2 섹션 패리티 유지(각 17 — CI 게이트 녹색). 테스트: test(init): non-TTY askSecret fallback — provider-selector.test.mjs에 DI-stdin 케이스를 추가해 askSecret가 비-TTY에서 공유 전역을 변경하지 않고 평범한 ask()에 위임(trim 패리티)함을 검증; 629 → 631. 자세히는 CHANGELOG.md.
[1.39.0] — 2026-05-18
WS8.2 — LLM 공급자 선택 + OpenAI/Codex 키 + 대화형 init 마법사. env-config에 LLM_PROVIDER(auto|claude|gemini)+OPENAI_API_KEY(시크릿). llm.mjs 6개 gate-site가 _provGate()로 providerOrder() 참조; auto는 동작 불변. #/config에 select+필드. scripts/init.mjs는 실제 마법사(검증된 경로로 parent .env 기록). 7 테스트. 622 → 629. README ×8/정식문서 fold = WS8.3/WS10. 자세히는 CHANGELOG.md.
[1.38.0] — 2026-05-17
WS8.1 — 통합 CLI 디스패처 + doctor 동사. bin/career-ops-ui.sh가 setup/run/doctor/init/help 라우팅. scripts/doctor.mjs는 /api/health 엔진을 그대로 재사용(createApp 인프로세스 → 터미널 리포트); 필수 체크 전부 통과 시에만 exit 0. docs/sdd + help §1 ×8. 6 테스트. 616 → 622. README ×8 = WS8.3. 자세히는 CHANGELOG.md.
[1.37.0] — 2026-05-17
WS7 — git 워크플로 pre-commit AI 리뷰. 결정적 플로어(fail-HARD): staged .env/시크릿, diff 내 키 패턴, staged 뷰의 .also(, node --check 실패 차단. AI 레이어(fail-SOFT): CLI 존재 + AI_REVIEW != off면 claude -p. .githooks/pre-commit + prepare로 core.hooksPath 연결. --no-verify 금지. docs/sdd. 6 테스트. 610 → 616. 자세히는 CHANGELOG.md.
[1.36.0] — 2026-05-17
WS6.3 — Modes 탭: 원본 덩어리 → 섹션별 편집기. WS6 완료. modes/_profile.md를 ## 섹션별 편집(헤딩마다 접이식 textarea). 서버 splitProfileSections 바이트 정확; PUT { sections }는 지정 섹션만 병합 — 프리앰블·타 섹션·순서 바이트 단위 보존. 미지 헤딩 → 400. raw 경로 유지. i18n 5키 ×8. help §2 ×8. 신규 테스트 6. 604 → 610. WS6 종료. 자세히는 CHANGELOG.md.
[1.35.0] — 2026-05-17
WS6.4 — Profile 배열 에디터 + WS6.2 API-keys 감사. PUT /api/profile가 { arrays } 수용({ fields }와 결합 가능): Target roles/Superpowers(목록), Archetypes(name/level/fit), Proof points(name/url/hero-metric). 동일 merge-not-replace; 빈 행 제거; 빈 목록은 키 삭제. #/config에 add/remove 에디터 4개. i18n 6키 ×8. 감사: KNOWN_KEYS ≡ FIELDS, 갭 없음. 신규 테스트 7. 597 → 604. 자세히는 CHANGELOG.md.
[1.34.0] — 2026-05-17
WS5 — 원클릭 Auto-pipeline 화면(#/auto). 모달이 전용·링크 가능 페이지로 승격. 한 번 클릭으로 검증→가져오기→평가→리포트 저장→트래커(SSE). 접근성 스테퍼, 딥링크, 키 없으면 수동 모드, #/auto?url=…&go=1 링크 가능. 사이드바 항목; 대시보드 ✨ 버튼이 여기로. i18n 14키 ×8. help §1 ×8 + README ×8. 신규 테스트 8. 589 → 597. 자세히는 CHANGELOG.md.
[1.33.0] — 2026-05-17
WS4 — career-ops 1.8.0 패리티 감사 + location_filter. 부모 scan.mjs에 location_filter(#570) 추가; web-ui의 in-process 스캐너는 위임하지 않아 흐르지 않음. 새 server/lib/location-filter.mjs가 시맨틱을 그대로 복제, 두 스캐너에 연결. help §5 ×8. 신규 테스트 8개. 581 → 589. 자세히는 CHANGELOG.md.
[1.32.0] — 2026-05-17
#/config Profile 탭 — 원본 YAML 덩어리 → 항목별 폼 (WS1). 접이식 3개 섹션(지원자/내러티브/보상), 14개 스칼라 경로. 항목 저장은 config/profile.yml에 병합 — 아키타입·프루프포인트·커스텀 키 그대로 보존. Advanced에 raw-YAML 이스케이프 해치 유지(주석 보존). i18n 23키 ×8. 신규 테스트 7개. 574 → 581. 자세히는 CHANGELOG.md.
[1.31.0] — 2026-05-17
career-ops 1.8.0 동기화 — #/batch가 --model + --start-from 노출. 부모 프로젝트가 1.7.1 → 1.8.0; batch-runner.sh에 --model NAME(#504) + --start-from N 추가. web-ui가 #/batch에 노출(모델, 시작 # 입력) + 서버 측 defense-in-depth 검증. i18n ×8. 신규 테스트 7개. 567 → 574. 자세한 내용은 CHANGELOG.md.
[1.30.0] — 2026-05-14
#/scan 결과 페이지네이션 — v1.12의 「처음 200개 표시 / 총 N」 트렁케이션 대체.
v1.30 이전에는 스캔 결과 표가 첫 200행으로 잘리고 「Showing first 200 of N」 각주가 표시되어, 201..N행에는 UI로 접근할 수 없었습니다. v1.30.0은 cap을 UI.paginate로 교체합니다(#/tracker / #/reports / #/activity와 동일한 헬퍼). PAGE_SIZE = 200으로 시각적 밀도 유지; 부스트-상단 정렬이 페이지 간에서도 안정적(전체 집합을 정렬한 후 페이지 분할); 필터 변경 시 1페이지로 자동 리셋. 사용되지 않는 scan.shownTop i18n 키 제거(8개 로케일). tests/scan-paginator.test.mjs에 9건의 새 케이스(정적 카나리 7건 + 6가지 경계 케이스를 가진 순수 로직 테이블 1건 + 요약 계산 1건). 558 → 567 유닛 + 어셉턴스(+9). 상세 내역은 CHANGELOG.md.
[1.29.2] — 2026-05-14
핫픽스: 🌐 Scan을 source=both로 실행해도 EN 단계만 동작했음. RU 단계는 조용히 누락.
SSE 클라이언트(public/js/api.js:156)는 첫 번째 done 이벤트에서 EventSource를 닫았지만, 서버는 source=both에서 단계별로 하나씩 발행합니다. RU 단계는 시작 직후 취소되었습니다. 수정: 서버는 각 done에 final: true|false를 표시하고, 클라이언트는 final !== false일 때만 닫습니다. 역호환 — final을 설정하지 않는 단일 단계 생산자는 이전처럼 닫힙니다. 547 → 558 유닛 + 어셉턴스(+11 신규). 상세 내역은 CHANGELOG.md.
[1.29.1] — 2026-05-14
5개 RU 포털 구성을 위한 상세 사용자 가이드를 help-bundle §5의 8개 로케일 전체에 추가.
§5 (Portals & sources) 내에 새 ### 하위 섹션 “러시아 포털 구성 — 상세 설정 가이드” 추가: 인증·지역 제한이 포함된 5-소스 인벤토리 테이블, portals.yml 위치 및 편집 단계, 5-소스 전체 YAML 예시, negative 리스트 충돌과 수정 예시, 단일 소스를 임시 비활성화하는 방법, 🌐 Scan 및 SSE 로그를 통한 검증 방법. §17(v1.29.0 출시)은 개발자 흐름; §5 v1.29.1은 최종 사용자 흐름. 540 → 547 유닛 + 어셉턴스(+7 신규). 상세 내역은 CHANGELOG.md.
[1.29.0] — 2026-05-14
러시아 포털 스캐너가 2 → 5 소스로 확장; registry + 동적 드롭다운; 새로운 §17 “새 포털 추가 방법”.
- 3개의 새 RU 어댑터:
Trudvsem(정부 open-data API, 인증/지오 게이트 없음),GetMatch및GeekJob(방어적 HTML 스크레이프 — 파싱 실패 시[], 정상 200에서 throw 안 함). - Source registry
server/lib/sources/registry.mjs— dispatcher + endpoint + dropdown 모두 단일 진실 공급원. v1.29 이전에는 세 곳에 하드코딩되어 있었음. - 새 엔드포인트
GET /api/scan/sources(Cache-Control: max-age=60) — SPA가#/scan마운트 시 소스 필터 드롭다운을 동적으로 다시 그림. - Help-bundle §17 신규 8개 로케일 전체: 「새 채용 포털 소스를 추가하는 방법」(어댑터 템플릿, 레지스트리 엔트리, 디스패처, 모크 테스트,
portals.yml). russian_portals.sources기본값이["hh", "habr"]에서 5 소스로 변경;portals.yml에서sources:를 명시적으로 나열했다면 새 3개를 수동으로 추가해야 합니다.- 테스트: 520 → 540(+20). 상세 내역은
CHANGELOG.md.
[1.28.1] — 2026-05-14
핫픽스: ?query를 포함한 해시에서의 router 404. health에서 HH_USER_AGENT 행 제거.
v1.28.1 이전에는 Router.go('/evaluate?url=…')가 첫 split('/') 세그먼트로 "evaluate?url=…" 리터럴을 만들어내어 등록된 라우트와 절대 일치하지 않았고, 결과적으로 __not_found__(404)으로 떨어졌습니다. 한 줄 수정: 이름 분할 전에 hash.split('?')[0]. 보고된 두 클릭을 모두 처리합니다: #/pipeline → ▶ 및 “App settings → Modes”. 선택적 HH_USER_AGENT 행을 /api/health에서 제거(러시아 외 403 게이트 힌트는 help-bundle §16에 그대로 남아 있으며, 스캔 시 stderr에서도 그대로 출력). 515 → 520 유닛 + 어셉턴스(+5 신규). 상세 내역은 CHANGELOG.md.
[1.28.0] — 2026-05-14
도큐 정렬 + #/batch의 --max-retries N 컨트롤. qa/QA-PROMPT-docs-vs-app.md에서 제기된 두 개의 open 이슈를 종료합니다.
- Issue #2 —
#/batch에 숫자 입력 “Max retries”(1–10)가 추가되었으며, “Retry failed”가 체크된 경우에만 활성화됩니다. 서버는parseInt+ 1≤N≤10 범위 검증을 수행하고, 범위 밖 값은 조용히 폐기하며,--retry-failed가 없으면--max-retries를 생략합니다.tests/batch-max-retries.test.mjs의 7 케이스. 새 i18n 키 2개 × 8 로케일. - Issue #1 — 8개 help-bundle + 8개 README의 AI CLI 목록이 career-ops.org/docs 정본(Claude Code · Codex · OpenCode · Qwen CLI)에 맞춰지고, 로컬라이즈된 한 줄을 추가: 「다른 Claude 호환 CLI도 동일한 슬래시 커맨드 인터페이스에서 작동합니다」. README의 “Multi-CLI” 항목(web-ui 자체 shim 파일 설명)은 그대로 유지(다른 surface).
tests/canonical-docs-coverage.test.mjs에 새 카나리아 2개. - 506 → 515 유닛 + 어셉턴스(+9 신규). Playwright 32/32 변동 없음. 상세 내역은
CHANGELOG.md.
[1.27.0] — 2026-05-14
겉모양 + 접근성 다듬기: 사이드바 #/dashboard 항목 중복 제거.
사이드바에서 브랜드 로고(<a class="logo" href="#/dashboard">)와 첫 번째 내비 항목이 동일 라우트를 가리켰습니다. 스크린리더는 「Dashboard」를 두 번 읽었고 키보드 사용자는 의미 없는 탭 스톱을 하나 더 거쳤습니다. 브랜드 블록은 이제 평범한 <div class="logo">이며 내비 항목만이 #/dashboard로의 유일한 링크입니다. 506 / 506 unit + 32 / 32 Playwright — 변동 없음. 상세 내역은 CHANGELOG.md 참조.
[1.26.1] — 2026-05-14
핫픽스 WCAG 2.5.5 — .btn 최소 높이 44 px 복원.
v1.26.0에서 .btn의 min-height: 44px 선언이 누락되어 헤더 버튼이 39-41 px 로 렌더링됐습니다 (WCAG 2.5.5 위반). v1.26.1에서 44 px 하한선 + flex-shrink: 0 + line-height: 1.2 를 복원했습니다. 502 → 506 unit, Playwright 32/32 그대로. 상세 내용은 CHANGELOG.md 참조.
[1.26.0] — 2026-05-14
테스트 피라미드 + 라인 커버리지 ≥ 93 %.
v1.25 백로그 요구사항에 따라 4단계 테스트 피라미드(unit → functional → acceptance → e2e)를 도입했습니다. v1.25에서 가장 컸던 커버리지 gap을 메우는 22개 신규 테스트 추가 (jds.mjs 61.64 % → 100 %, auto-pipeline 거절 경로). 멀티 엔드포인트 사용자 여정 테스트를 위한 tests/acceptance/ 디렉토리 신설. 480 → 502 unit + acceptance, Playwright 32/32 그대로. 상세 내용은 CHANGELOG.md 및 docs/architecture/TESTING.md 참조.
[1.25.0] — 2026-05-14
자동 파이프라인 수동 단락 처리 + 대시보드 외관 수정 + CHANGELOG 패리티 백필. G-014 (auto-pipeline 이 mode: 'manual' 을 무시) 와 G-012 (CHANGELOG 패리티 드리프트 — 6개 로케일이 릴리스 2개 뒤처짐), 그리고 대시보드의 ✨ ✨ 이중 글리프 외관 문제를 마무리합니다. G-003 (README.cn.md 이름 변경) 은 사실상 이미 종료되어 있었습니다 — 저장소에는 README.zh-CN.md 만 존재합니다. G-005 (A-G → A-F 보고서 블록 재정렬) 은 부모 프로젝트와 조율된 커밋이 필요하여 계속 연기합니다.
🛡️ G-014 — 자동 파이프라인 mode: 'manual' 단락 처리
fix(auto-pipeline): G-014 — honour mode:'manual' short-circuit(server/lib/routes/auto-pipeline.mjs:158-195) — v1.25 이전에는 라우트가 언제나 LLM 을 호출했습니다.mode: 'manual'을 전달해도 (v1.10.2 이후/api/evaluate와 동일한 의미) 조용히 무시되어 요청이 Anthropic 에서 1~3분간 멈췄습니다. 이제 핸들러는:- 하위 호환을 위해
mode와evalMode를 모두 수용합니다. 두 값 중 어느 쪽이든'manual'이면 단락 처리가 트리거됩니다. - 5개의 SSE 단계를 모두
status: 'done'/status: 'skipped'로 방출합니다. fetch 없음. LLM 호출 없음. 요청당 $0.05 의 비용도 없음. done페이로드는{ mode: 'manual', prompt: <buildEvaluationPrompt scaffold>, message }를 운반합니다 — SPA 는 기존/api/evaluate수동 프롬프트 카드와 동일한 방식으로 렌더링할 수 있습니다.
- 하위 호환을 위해
HOST=0.0.0.0환경에서의 DoS 위험 해소: 기존에는llmRateLimit이 분당 IP 당 10 요청으로 제한해도 공격자 10명 × 10 요청 = 분당 $50 의 Anthropic 비용 소진이 가능했습니다. 단락 처리는 레이트 리밋의 카운터가 실제 호출을 향해 감소하기 전에 발사됩니다.- 테스트 —
tests/auto-pipeline-manual-mode.test.mjs: 3개의 테스트가 (1)mode: 'manual'이 2초 이내에 5개 step 키 전부와 함께 반환되는지, (2)ANTHROPIC_API_KEY가 설정된 상태에서도 단락 처리가 여전히 발사되는지 (원래의 증상), (3) 레거시evalMode: 'manual'호출자가 계속 동작하는지를 확인합니다.
📝 G-012 — CHANGELOG 패리티 백필 (6 로케일 × 누락 릴리스 2건)
docs(changelog): backfill v1.23.0, v1.24.0, v1.24.1, v1.25.0 in 6 lagging locales— v1.25 이전에는 EN 만 v1.23-v1.24 를 보유하고 있었으며, RU 는 릴리스 1개 뒤처져 있었고 나머지 6개는 릴리스 2개씩 뒤처져 있었습니다. v1.25 는 병렬 번역 에이전트를 디스패치 (v1.23 패턴을 재현) 하여 네 개의 항목을 모두CHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md에 안착시킵니다. RU 는 v1.24.0 + v1.24.1 + v1.25.0 을 받습니다 (v1.23 사이클에서 이미 v1.23.0 을 받았기 때문입니다).feat(ci): scripts/check-changelog-parity.mjs gate— 어떤 로케일 CHANGELOG 의 최신 항목이 EN 정본보다 오래되면 빌드를 실패시킵니다.npm run test:ci에 연결되었습니다. 사전에 존재했던 G-012 드리프트는 EN 경계를 넘는 순간 스스로 탐지되었을 것입니다.
✨ 외관 — 대시보드 이중 글리프 제거
fix(dashboard): dedup ✨ glyph in auto-pipeline button label(public/js/lib/i18n-dict.js:219) —dash.autoPipeline이 모든 로케일 문자열에 선두✨를 포함하고 있었고,public/js/views/dashboard.js:58의 뷰가 또 다른✨를 앞에 붙였습니다. 결과적으로 버튼이✨ ✨ Auto-pipeline …로 렌더링되었습니다. v1.25 는 모든 로케일의 DICT 항목에서 선두 글리프를 제거합니다; 뷰의 접두사가 단일 소스가 됩니다. 동일한 감사 패스로 나머지 i18n 번들도 훑었습니다 — 다른 이중 글리프 패턴은 발견되지 않았습니다.
🚫 향후 릴리스로 연기
- G-005 — 정규 career-ops.org/docs 에 맞춘 A-G → A-F 보고서 블록 재정렬 — 부모
santifer/career-ops프로젝트와 조율된 커밋이 필요합니다 (modes/oferta.md를 재작성하여 A=Role, B=CV-match, C=Strategy, D=Comp, E=Personalization, F=STAR 를 방출 — C-Risks/G-Legitimacy 는 별도 블록에서 제거). v1.25.0 은 새 스키마에 대비한 web-ui 측을 출하합니다 (reports.js는 v1.13 이후 임의 블록 문자를 이미 수용합니다). 부모와 자식이 함께 안착할 수 있는 다음 릴리스 윈도우에서 추적합니다. - G-003 —
README.cn.md→README.zh-CN.md이름 변경 — v1.25 준비 중 검증: 저장소에 이미README.zh-CN.md가 존재합니다 (워크트리 어디에도 고아README.cn.md가 없음). G-003 결과는 낡은 발견이었습니다.
🧪 테스트
- 477 → 480 유닛 (PR-B
auto-pipeline-manual-mode.test.mjs에서 +3). - 32/32 Playwright 동일.
npm run test:ci가 이제npm test+check-no-also-leftovers.mjs+check-changelog-parity.mjs를 실행합니다.
검증
$ npm run test:ci
# 480 / 480
# ✓ no .also( leftovers in views/
# ✓ CHANGELOG parity: all 8 locales at v1.25.0
# G-014 — manual mode returns < 2 s even with ANTHROPIC_API_KEY set:
$ ANTHROPIC_API_KEY=sk-ant-test PORT=4317 npm start &
$ sleep 3
$ time curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/x","mode":"manual"}' \
http://127.0.0.1:4317/api/auto-pipeline | head -20
# real 0m0.1xx s (was 1-3 min)
# event: start … event: step (×5) … event: done {"mode":"manual","prompt":"…"}
# G-012 — every locale CHANGELOG carries the v1.25.0 entry:
$ grep -c '^## \[1.25.0\]' CHANGELOG*.md
# 8 files, each → 1
# Cosmetic — dashboard glyph:
$ grep "dash.autoPipeline" public/js/lib/i18n-dict.js
# No leading ✨ in any locale value (view supplies the single glyph)
비호환 변경
없습니다. mode: 'manual' 은 옵트인이며, 레거시 evalMode: 'manual' 호출자는 변경 없이 계속 동작합니다.
범위 외 (v1.26+)
| 항목 | 비고 |
|---|---|
| G-005 — A-F 보고서 블록 재정렬 | 부모와 조율된 커밋이 필요합니다 (santifer/career-ops 가 modes/oferta.md 를 재작성). |
| QA 시나리오 31 시각적 서브 테스트의 라이브 실행 | 브라우저 기반 에이전트가 필요합니다 (Claude Cowork). Playwright 스모크가 부분적으로 커버합니다. |
i18n-dict.js 의 400 LOC 목표 초과 |
번역 fixture 이며 정책상 면제됩니다. 번들러 없는 분할은 HTTP 요청만 증가시킵니다. |
[1.24.1] — 2026-05-14
핫픽스: 8개 로케일 모두에서 #/config 충돌 (G-015).
🚑 치명적 핫픽스
fix(config): G-015 — replace removed Element.prototype.also call in config.js(public/js/views/config.js:371) — v1.22.0 N-2 에서Element.prototype.also전역 멍키 패치를 제거하고cv.js를 자유 구문 (free-statement) 패턴으로 마이그레이션했으나config.js를 누락했습니다. 결과적으로#/config는 모든 로케일에서 첫 호출 시c(...).also is not a function으로 충돌했습니다. v1.24.1 은cv.js:188-201의 동일한 마이그레이션 패턴을 적용합니다 — 트리를const root = c(...)로 추출하고, 활성화 블록을 독립적으로 실행한 뒤return root;로 반환합니다.
🛡️ CI 게이트
feat(ci): scripts/check-no-also-leftovers.mjs sweep—public/js/views/아래 모든 파일을 순회하며.also(호출 지점이 있으면 빌드를 실패시킵니다 (주석 처리된 참조는 허용). 신규npm run test:ci스크립트에 연결되었습니다. 향후 멍키 패치 제거의 되돌림이 동일한 회귀를 조용히 재도입할 수 없습니다.
🧪 테스트
test: tests/config-view-syntax.test.mjs— 세 개의 가드:node:vm.Script로config.js를 파싱 (Playwright 없이 구문 수준의 회귀 포착)- 주석 밖에서
.also(가 살아남지 않는지 단언 const root = c(...)/return root;마이그레이션 앵커가 존재하는지 단언
- 474 → 477 유닛 (+3) + 32/32 Playwright 동일.
검증
$ npm run test:ci
# 477 / 477
# ✓ no .also( leftovers in views/
# Browser smoke:
$ open http://127.0.0.1:4317/#/config
# → renders normally, no "is not a function" card. Every locale equivalent.
범위 외 (v1.25 로 연기)
- G-014, G-012, G-005, G-003 — 묶음 처리에 대해서는 아래 v1.25.0 항목을 참조하십시오.
[1.24.0] — 2026-05-14
Help-bundle 콘텐츠 깊이 갱신 + QA 시나리오 31 라이브 실행 + RU CHANGELOG 전체 마무리. v1.23.0 의 “Out of scope” 표에서 v1.24 로 연기되었던 두 항목을 모두 마무리합니다: 5개의 정규 career-ops.org/docs URL 에서 출발한 8개 help bundle 전체의 콘텐츠 깊이 갱신 (v1.11.x 이후 URL 커버리지만 보장되어 있었음) 과, 실행 중인 서버에 대한 QA 시나리오 31 의 라이브 실행 (“브라우저 에이전트 + LLM 자격 증명 필요” 로 분류되어 있었으나 6/6 서브 테스트 중 시각적 서브 테스트만 브라우저가 필요하고 나머지는 curl + grep 으로 도달 가능함이 밝혀짐).
📖 Help-bundle 콘텐츠 깊이 갱신
docs(help): refresh en.md from 5 canonical career-ops.org/docs URLs(docs/help/en.md) — v1.24 이전의 EN 번들은 1113 라인이었고 front-matter 에 5개의 정규 URL 을 나열했지만 본문에서 확장하지는 않았습니다. v1.24 는 5개 URL 을 WebFetch 로 가져와 대응되는 H2 섹션을 심화합니다:- career-ops 소개 (front-matter) — 원칙 (데이터 주권, AI 비종속, 사람 통제), “career-ops 가 아닌 것” 블록을 추가했고 개념 인벤토리를 6행에서 10행으로 확장했습니다 (Proof points, JD store, Interview-prep, Batch additions 추가).
- §5 Portals — 정규 부트스트랩
cp templates/portals.example.yml portals.yml을 추가했고,tracked_companies항목별 필수 vs 선택 필드를 명확히 했습니다. - §7 Scan — Option A 에 “AI 토큰 소비 없음” 노트와 후속 명령 목록 (
apply/contacto/deep/tracker) 을 추가했습니다. - §14 지원 체크리스트 — SPA 체크리스트 모드 vs 수동-vs-Playwright 보조 vs 전체 CLI 흐름으로 분리했습니다 (
/career-ops apply <company>부터Evaluated → Applied자동 전이를 포함한Submitted.까지 정규 8단계 번호 매기기); batch evaluate 하위 섹션은 이제 TSV 스키마 테이블 + 4개 플래그 전체의 문서화 +merge-tracker.mjs --dry-run을 포함합니다; Playwright 설정 하위 섹션은 설치 명령, MCP 등록, 대체.claude/settings.local.json, 기본 헤드리스 노트를 나열합니다.
- 16개 H2 섹션 패리티 유지 (CI 테스트
help-ui.test.mjs::section-parity가 8개 로케일 전체에서 정확히 16개의 H2 섹션을 단언합니다). - 5개 정규 URL 각각이 번들에 ≥ 2회 등장 (CI 테스트
canonical-docs-coverage.test.mjs가 강제합니다). v1.24 이후 URL 별 등장 횟수:what-is-career-ops× 4,scan-job-portals× 5,apply-for-a-job× 3,batch-evaluate-offers× 5,set-up-playwright× 3. docs(help): translate the v1.24 deepening to 7 non-EN locales— 7개의 병렬 번역 에이전트를 디스패치했습니다. 각 대상 로케일 (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) 은 EN 구조를 섹션 단위로 미러링하고, 코드 블록 / URL / 파일 경로 / 버튼 레이블 (📁 Upload CV / 🌐 Scan now / ▶ Evaluate / 📄 Generate PDF / 💾 Save) 과 영어 약어 (CSP, SSRF, TOCTOU, WCAG, ATS, JD, SSE, REST, API) 를 그대로 보존하며, 심화 부분을 대상 언어로 출판 등급 기술 문서 스타일로 번역한 새 번들을 받습니다.
🧪 QA 시나리오 31 — 라이브 실행 (6/6 PASS)
-
docs(qa): append last-verified live-execution log to qa/claude-cowork-browser-test-prompt.md— v1.24 이전의 시나리오 31 은 문서화되어 있었으나 실제 서버에 대해 실행된 적이 없었습니다 (“브라우저 에이전트 + LLM 자격 증명 필요” 로 연기). v1.24 는 6개의 서브 테스트를 모두http://127.0.0.1:4317에 대해 실행했습니다:서브 설명 상태 31.1 help bundle 의 점수 임계값 ✅ PASS ( docs/help/en.md에서 4.5 × 3, 4.0 × 9, 3.5 × 6 회 언급)31.2 스캔 워크플로 엔드포인트 ✅ PASS ( /api/stream/scan-{en,ru}+/api/scan-ru/config→ 404;/api/scan/regional/config→ 200)31.3 /api/apply-helper체크리스트✅ PASS (본문에 career-ops apply+auto-submit경고 포함)31.4 /api/batch엔드포인트✅ PASS (키: [exists, runnerExists, raw, rows, additions])31.5 Playwright 가용성 ✅ PASS ( /api/health가Playwright (parent node_modules) ok: true, value: installed보고)31.6 help bundle URL 커버리지 (5 URLs × 8 로케일) ✅ PASS (40 / 40 ✓) 시각 전용 서브 테스트 (브라우저 필요) 는 QA 프롬프트에 별도 플래그로 표시되었습니다 — Claude Cowork 또는
npm run test:e2e:browser로 여전히 실행 가능합니다.
🌐 RU CHANGELOG 전체 마무리 (M-9 후속)
docs(translate): CHANGELOG.ru.md retry agent — full body translation(CHANGELOG.ru.md) — v1.23.0 릴리스는 RU CHANGELOG 재시도 에이전트가 아직 진행 중인 상태에서 출하되었습니다 (소켓 오류로 한 번 충돌한 뒤 재디스패치됨). v1.24 는 에이전트의 1542 라인 전체 번역을 수령합니다: v1.23.0 → v1.6.0 의 모든 항목이 출판 등급의 러시아어 본문을 갖게 되었고, 영어 본문 임시 대체는 더 이상 존재하지 않습니다. 스타일 규율은 v1.22.0 의 README 품질 갱신과 일치합니다: “функциональность” / “возможности” / “поведение” 가 어색한 “функционал” 을 대체했으며, “через” / “с помощью” 가 “при помощи” 를 대체했고, 능동태가 수동태보다 선호되며, “эндпоинт”, “лимит запросов”, “состояние гонки”, “санитайзинг” 이 정규 용어이고, 영어 약어 (TOCTOU, CSP, SSRF, WCAG, ATS, JD, SSE, REST, API) 는 보존되었습니다.
🧪 테스트
- 474 / 474 유닛 + 20 / 20 스모크 E2E + 32 / 32 Playwright. 동작상 테스트 변경 0건; 모든 help bundle CI 단언 (16개 H2 섹션 × 8개 로케일, 5개 URL × ≥ 2회 등장, 콘텐츠 하한선) 이 여전히 통과합니다.
검증
$ npm test # 474 / 474
# Help-bundle deepening:
$ wc -l docs/help/en.md
# ~1270 lines (was 1113 — deepened, not bloated)
$ for url in what-is-career-ops scan-job-portals apply-for-a-job \
batch-evaluate-offers set-up-playwright; do
echo -n "$url: "
grep -c "$url" docs/help/en.md
done
# what-is-career-ops: 4
# scan-job-portals: 5
# apply-for-a-job: 3
# batch-evaluate-offers: 5
# set-up-playwright: 3
# Scenario 31.6 — 40/40 URL coverage:
$ for lang in en es pt-BR ko ja ru zh-CN zh-TW; do
echo -n "$lang: "
for url in what-is-career-ops scan-job-portals apply-for-a-job \
batch-evaluate-offers set-up-playwright; do
curl -sS "http://127.0.0.1:4317/api/help/$lang" \
| python3 -c "import sys,json; print(json.load(sys.stdin).get('markdown',''))" \
| grep -q "$url" && echo -n "✓ " || echo -n "✗ "
done
echo
done
비호환 변경
없습니다.
범위 외 (v1.25+)
| 항목 | 비고 |
|---|---|
| 시나리오 31 시각적 서브 테스트의 라이브 실행 | 브라우저 기반 에이전트가 필요합니다 (Claude Cowork 또는 npm run test:e2e:browser). curl 만으로는 범위 밖이며 기존 Playwright 스모크가 커버합니다. |
| RU CHANGELOG 구버전 항목 (v1.5.x 이하) 의 본문 번역 | 재시도 에이전트는 v1.6.0 이상만 다뤘습니다. v1.6 이전 항목 (v1.5.x 등) — 만약 존재한 적이 있다면 — 은 사전 콘텐츠로 남습니다. |
| 향후 SPA 변경 후 대시보드 스크린샷의 시각적 회귀 | scripts/capture-dashboard-screenshots.mjs 가 로케일별 PNG 를 재생성합니다; 현재 자동 diff 는 없습니다. |
[1.23.0] — 2026-05-14
i18n 분할 + 연결 배너 CI 수정 + 로케일별 대시보드 스크린샷 + 모든 백로그 임시 대체 마무리. v1.22.0 의 “Out of scope” 표에서 v1.23 으로 표시되었던 세 항목 (M-9 로케일 CHANGELOG 본문, N-1 i18n.js LOC 분할, help bundle 콘텐츠 감사) 과, v1.22.0 이후 메인 브랜치 CI 를 적색으로 바꾼 스모크 E2E 테스트의 핫픽스를 함께 출하합니다.
🚑 CI 핫픽스 — 연결 배너 복구
-
fix(client): reset health-poll cadence + visibilitychange eager re-check(public/js/api.js:21-91) — v1.22.0 의 M-6 지수 백오프 (3초 → 6초 → 12초 → 캡 15초, 원래 캡 60초에서 감소) 는 올바른 방향이었으나, 진행 중인setTimeout이 이전에 설정된 어떤 지연 값에 고정되어 있었습니다. t=0.1 에 죽은 서버에 대해 t=3 에서 첫 핑이 실패하면 지연이 6초로 두 배가 되고, 다음 복구 프로브는 t=9 까지 발사되지 않았습니다. 스모크 E2E 의 “Flow 2a: 서버 다운 시 연결 배너 표시, 복구 시 숨김” 은 4초만 대기했고main에서 적색이 되었습니다.v1.23.0 은 폴링 루프를 재구성합니다:
_healthHandle을 추적하여setConnectionState(lost=true)가clearTimeout하고_HEALTH_MIN으로 재스케줄할 수 있습니다. 다운 후 첫 복구 프로브가 이전에 큐에 있던 지연과 무관하게 3초 이내에 발사됩니다._HEALTH_MAX가 60초에서 15초로 낮아졌습니다. 죽은 서버에 대해 백그라운드 탭이 되어 있어도 사용자가 돌아왔을 때 한 번의 폴링 사이클 내에서 복구되며, 대역폭 절감은 여전히 상당합니다.document.addEventListener('visibilitychange')가 탭이 포커스를 되찾고connectionLost === true일 때 즉시 재확인합니다 — Cmd-Tab 복귀가 다음 백오프 틱을 기다리지 않습니다.
🧹 N-1 — i18n.js 분할 (400 LOC 목표 초과)
refactor(client): split DICT into i18n-dict.js (data) + i18n.js (logic)— v1.23 이전의public/js/lib/i18n.js는 639 LOC 였습니다. 대부분 (23-586 라인) 이DICT번역 테이블 — 순수 구조화 데이터였습니다. v1.23.0 은 이를public/js/lib/i18n-dict.js(578 LOC, CLAUDE.md 의 “Exempt from these limits: generated files, migrations, test fixtures, lock files, vendored code” 에 따라 LOC 규칙 면제 — 번역 테이블은 fixture 로 분류) 로 추출하고,public/js/lib/i18n.js는 순수 모듈 로직 86 LOC (400 LOC 목표를 한참 밑돔) 로 남깁니다.- 로더 계약:
i18n-dict.js가window.__I18N_DICT = { … }를 채우고,i18n.js가 기존 IIFE 내부에서 이를 읽습니다.public/index.html이 순서대로 로드합니다 —i18n.js이전에i18n-dict.js가 — 그래서 IIFE 가 생성 시점에 완전히 채워진 DICT 를 봅니다. 누락된 dict 폴백: 모든t()호출은 인라인 폴백 또는 키 자체를 반환하므로 SPA 가 충돌하지 않고 잘못된 설정을 큰 소리로 드러냅니다. - 테스트 배선 갱신:
tests/i18n-coverage.test.mjs,tests/help-ui.test.mjs,tests/canonical-docs-coverage.test.mjs가 이제 두 파일을 테스트 VM 컨텍스트에서 모두 실행하거나 (정규표현식 스윕을 위해) 소스를 연결하여 모든 기존 단언을 보존합니다.
🌐 M-9 — 로케일 CHANGELOG 본문 번역
docs(translate): 7 non-EN CHANGELOG files end-to-end— v1.23 이전의CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md는 v1.13.0 이후의 모든 항목에 영어 본문 임시 대체 노트를 담고 있었고, EN 정본을 가리키는 푸터를 가지고 있었습니다. v1.23.0 은 로케일당 1개씩 7개의 병렬 번역 에이전트를 디스패치하여 모든 본문을 대상 언어의 출판 등급 기술 문체로 다시 씁니다. 임시 대체 노트가 제거되었습니다. 모든 로케일에서 코드 블록, 파일 경로, URL, 커밋 메시지 스타일 문자열 (fix(security): B-1 — …), 환경 변수, 링크 레이블은 그대로 보존됩니다.
🖼️ 모든 README 의 로케일별 대시보드 스크린샷
docs(readme): wire each locale README at its locale-specific PNG— v1.23 이전에는README.pt-BR.md만dashboard-pt-BR.png를 참조했고, 나머지 6개 비영어 README 는 여전히dashboard-en.png를 가리켰습니다. 스크린샷 자체는 v1.22.0 사이클에서scripts/capture-dashboard-screenshots.mjs로 이미 캡처되어images/에 있었지만 사용되지 않았습니다. v1.23.0 은 모든README.{es,ja,ko-KR,ru,zh-CN,zh-TW}.md의 14번 라인을 자체dashboard-<locale>.png로 갱신합니다.
🧪 테스트
- v1.22.0 과 동일한 474 / 474 유닛 + 32 / 32 Playwright. 스모크 E2E 가 이제 20 / 20 (v1.22.0 이후
main에서는 배너 복구 회귀로 19 / 1 실패였음; v1.23.0 의 재스케줄 수정이 이를 마무리합니다). - 3개의 기존 테스트가 i18n 분할을 처리하도록 재배선되었습니다. 신규 테스트 파일 0건; 삭제된 단언 0건.
검증
$ npm test
# 474 / 474
$ npm run test:e2e
# passed: 20 failed: 0 (was 19/1 on v1.22.0 main)
$ wc -l public/js/lib/i18n.js public/js/lib/i18n-dict.js
# 86 public/js/lib/i18n.js ← logic, under target
# 578 public/js/lib/i18n-dict.js ← data fixture, exempt
$ grep -h 'dashboard-' README*.md | sed -E 's/.*(dashboard-[^)]+).*/\1/' | sort -u
# dashboard-en.png (README.md only)
# dashboard-es.png dashboard-ja.png
# dashboard-ko-KR.png dashboard-pt-BR.png
# dashboard-ru.png dashboard-zh-CN.png dashboard-zh-TW.png
# CHANGELOG translation sanity: each locale file > 200 lines of native content
$ wc -l CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md | grep -v total
비호환 변경
없습니다. public/index.html 이 이전에는 한 개를 로드하던 곳에서 이제 두 개의 스크립트를 로드합니다 — CDN 에서 SPA 를 서빙하는 사용자는 i18n-dict.js 도 함께 가져가야 합니다; 스크립트 로드 순서는 index.html 의 <script src> 태그 순서로 강제됩니다. 런타임 폴백 (빈 DICT → t() 가 인라인 EN 폴백을 반환) 이 새 파일 누락 시 하드 크래시를 방지합니다.
범위 외 (v1.24+)
| 항목 | 비고 |
|---|---|
| career-ops.org/docs 로부터의 Help-bundle 콘텐츠 깊이 갱신 (URL 커버리지 대비) | 5개의 정규 URL 은 v1.11.x 이후 모든 로케일의 help bundle 에 이미 등장하며 QA 프롬프트의 시나리오 31.6 이 커버리지를 검증합니다. 콘텐츠 본문 깊이 갱신은 v1.24+ 후보입니다. |
| 실행 중인 서버에 대한 QA 시나리오 31 의 라이브 실행 | 브라우저 에이전트 + 라이브 LLM 자격 증명이 필요합니다. v1.24 후보. |
| 신규 mode-page 힌트 단락에 대한 컴포넌트별 터치 타깃 스윕 | v1.22.0 M-1 이 추가한 <p class="field-hint"> 요소가 8개 로케일 전체에서 WCAG 2.5.5 최소 높이에 대해 검증되지 않았습니다. |
[1.22.0] — 2026-05-14
M/L/N 백로그 일괄 처리 + 문서 정합성 + 번역 품질 패스. v1.20.1-BACKLOG.md 의 medium 이하 등급 전체를 한 릴리스로 정리했습니다. M 항목 9개, L 항목 5개, 자잘한 nit 2개. 더하여 5개의 정규 career-ops.org/docs 가이드에 대한 문서 정합성 감사, .claude/ 및 .github/ 하위 시스템 프롬프트 갱신, 7개 비영어 로케일 README 품질 재정비를 포함합니다.
🛡️ 보안 강화 (심층 방어)
-
fix(security): M-4 — entity-aware stripDangerousMarkdown(server/lib/security.mjs) — v1.22 이전의 정규표현식은<script>,javascript:,on*=를 문자 그대로의 부분 문자열로만 매칭했습니다.<script>,javascript:,<img src="data:image/svg+xml,<svg onload=…>">같은 입력이 그대로 통과했습니다. 정화 (sanitization) 로직은 이제 strip 정규표현식이 실행되기 이전에<,>,&,", 숫자 (&#NN;) 및 16진 (&#xHH;) 엔터티를 디코딩합니다.tests/cv-xss-bypasses.test.mjs의 11개 테스트로 검증했습니다. 실질적인 방어는 여전히 클라이언트의 escape-first 파이프라인UI.md가 담당하며, 이 변경은 저장된 파일 자체를 견고하게 만듭니다. -
fix(security): L-2 — bash --noprofile --norc on the batch runner(server/lib/routes/batch.mjs:108) —spawn('bash', [PATHS.batchRunner, ...])가 사용자~/.bashrc를 상속받았습니다. 악성 rc 파일이 실행에 영향을 미칠 수 있었습니다. 이제spawn('bash', ['--noprofile', '--norc', PATHS.batchRunner, ...])형태로 호출합니다.
🔒 복원력
-
fix(client): M-6 — exponential backoff on health ping(public/js/api.js:22-48) — 연결이 끊긴 상태의 폴러가 다운된 서버에 야간 동안 28,800회의 fetch 를 발사했습니다. 이제 3초 → 6초 → 12초 → 24초 → 60초로 백오프하며, 첫 2xx 복구 시 3초로 리셋됩니다. 각 단계가 새 지연 값을 반영하도록setInterval이 아닌setTimeout체인으로 구성했습니다. -
fix(client): M-5 — Safari private-mode localStorage guard(public/js/lib/i18n.js:572-583) — Safari 사생활 보호 모드는 모든localStorage.getItem/setItem호출에서SecurityError를 던집니다. 로드 중 실행되는 IIFE 가 i18n 모듈 전체를 실패시켜 SPA 가 키 원문을 그대로 렌더링했습니다. 두 호출을 모두 try/catch 로 감싸고detect()의 브라우저 언어 폴백을 사용하도록 변경했습니다. -
fix(server): M-2 — body-size cap on outbound preview fetches (test + verify)— v1.21.0 의safeGet은 이미 청크 단위로 스트리밍하고opts.maxBytes에서 자르도록 구현되어 있었습니다. v1.22 는tests/ssrf-redirect-rebind.test.mjs에 명시적 회귀 테스트를 추가하여 계약을 고정합니다: 100 KB 업스트림 + 4 KB 캡 → 응답 ≤ 4 KB. -
fix(client): L-5 — clear setTimeout on hashchange in scan.js(public/js/views/scan.js:6-22, :113-120) — 스캔 완료 후 300 msrefreshResults()타이머가 사용자가 그 시간 안에#/scan에서 벗어날 경우 누수되었습니다. 핸들을 캡처하여__cancelActiveScanPoll에서 해제합니다. -
fix(client): L-4 — multi-line SSE data: joiner(public/js/lib/auto-pipeline.js:158-176) — SSE 파서가match()(단일 라인)를 사용했습니다. 명세에 따르면 한 이벤트는 여러data:라인을 가질 수 있고 컨슈머가\n으로 결합해야 합니다. 서버는 현재 단일 라인 JSON 만 보내므로 기존 코드도 동작했으나, 향후 멀티라인 페이로드에 취약했습니다.
♿ 접근성
-
feat(a11y): M-3 — WCAG 1.4.1 redundant cues on score pills + connection banner(public/css/app.css:602-625, :812-822) — score-high / score-mid / score-low 가 색상만으로 상태를 전달했습니다 (빨강/주황/녹색). 색상을 인지하지 못하는 사용자는 폴백이 없었습니다. 각 등급에::before를 통한 중복 글리프 (✓ / ◐ / ○) 를 추가했습니다. 연결 배너는 오프라인 상태에서 선두에⚠글리프를 가집니다. 렌더링 지점은 손대지 않았고 순수 CSS 강화입니다. -
feat(a11y): M-1 — inline hint paragraphs for every mode-page field(public/js/views/mode-page.js,public/js/lib/i18n.js) — v1.20.0 은 모든 mode-page 필드에htmlFor → id를 연결했으나 인라인 힌트 문구는 가져오지 못했습니다. 필드 의도를 문서화한 곳은 README 워크스루뿐이었습니다. v1.22.0 은 힌트 i18n 키 19개 × 8 로케일 = 신규 번역 152개 를 추가하며,field()빌더는 이제 필드별로aria-describedby가 연결된<p id="…-hint">를 렌더링합니다. 스크린 리더 사용자는 입력에 포커스가 갈 때 힌트를 듣습니다. -
fix(a11y): M-7 — null-guard on UI.el() htmlFor alias(public/js/api.js:194-198) —htmlFor: null이 문자 그대로의for="null"로 렌더링되었습니다. 폴스루 분기의v != null && v !== false가드를 한 줄로 미러링했습니다.
🧹 품질 / 이식성
-
fix(server): L-1 — parseInt radix in health.mjs + bin/start.sh + bin/setup.sh—parseInt(process.versions.node)의 radix 누락은 린트 경고를 유발하며 Node 가 16진 버전을 사용하기 시작할 경우 취약합니다. 모든 곳에10을 추가했습니다. -
fix(server): L-3 — Windows-safe entrypoint check(server/index.mjs:159-163) —import.meta.url === \file://${process.argv[1]}`는 Windows 의 드라이브 문자와 백슬래시를 잘못 처리합니다.fileURLToPath(import.meta.url) === path.resolve(process.argv[1])` 로 교체했습니다. -
refactor(client): N-2 — drop Element.prototype.also monkey-patch(public/js/views/cv.js:188-201) — 전역 DOM 프로토타입 오염이었습니다. 트리 루트를 위한 로컬 변수로 대체했습니다. -
test(canary): M-8 — 404 regression test for retired /api/scan-ru/config(tests/scan-consolidated.test.mjs) — v1.20.0 이 별칭을 폐기했으나 카나리아를 추가하지 않았습니다. v1.18 의 폐기 테스트를 미러링하는 3줄 추가입니다.
📚 문서 + 시스템 프롬프트
-
docs(architecture): refresh OVERVIEW + DATA-FLOWS for v1.21+ surface— OVERVIEW.md 에safe-fetch.mjs(DNS-pinned GET),file-lock.mjs(경로별 뮤텍스),rate-limit.mjs(LLM 요청 빈도 제한),sanitizePathName을 추가했습니다. DATA-FLOWS.md 에 두 개의 새 섹션을 추가했습니다: “Outbound URL fetches (DNS-rebind-safe)” 와 “LLM endpoint rate-limiting”. -
docs(readme): security envelope section refresh— README.md 의 “Security notes” 가 이제 v1.21+ 보안 영역의 모든 헬퍼를 문서화합니다 (sanitizePathName, safeGet, withFileLock, llmRateLimit, 엔터티 인식 stripDangerousMarkdown). -
docs(qa): scenario 31 — career-ops.org/docs alignment(qa/claude-cowork-browser-test-prompt.md) — 5개의 정규 career-ops.org/docs 가이드에 기술된 동작과 UI 가 일치하는지 검증하는 6개의 신규 서브 테스트 (31.1–31.6) 를 추가했습니다: 점수 임계값, 스캔 워크플로 (단일 버튼), 지원 워크플로 (체크리스트, 자동 제출 아님), 배치 워크플로 (TSV 편집기), Playwright 설정 (graceful 실패), help-bundle 커버리지 (5 URLs × 8 로케일). -
docs(translate): README quality refresh × 7 non-EN locales— 모든 비영어 README 가 해당 모국어로 출판 등급의 기술 문서 스타일로 재작성되었습니다. 흔한 어색한 직역을 교체했고, v1.21/v1.22 보안 영역 언급을 추가했으며, 릴리스/테스트 배지를 갱신했습니다. -
docs(system): .claude/PROJECT-CONTEXT.md + .github/copilot-instructions.md— 세션에 합류하는 에이전트를 위한 단일 파일 오리엔테이션입니다. CLAUDE.md 를 압축했고, v1.21+ 헬퍼의 이름을 명시하며, 흔한 함정을 나열합니다. -
docs(bin): actualize start.sh / setup.sh / run_all.sh comments— “two deps” → “three deps” (express + js-yaml + multer); “298 tests” → “474+ tests”;parseIntradix 추가.
🧪 테스트
- 461 → 474 유닛 (+13) + 32/32 Playwright 동일.
- 신규 테스트 파일:
cv-xss-bypasses.test.mjs(M-4, 11 tests). - 확장:
ssrf-redirect-rebind.test.mjs(M-2 body cap 용 +1),scan-consolidated.test.mjs(M-8 별칭 카나리아 용 +1). - 기존 스위트에 동작상 테스트 변경이 없습니다 — 모든 수정은 가산적이거나 신규 카나리아로 보호됩니다.
검증
npm test # 474 / 474
npm run test:e2e:browser # 32 / 32
# Entity-encoded XSS strip:
node -e "import('./server/lib/security.mjs').then(({stripDangerousMarkdown}) => console.log(stripDangerousMarkdown('<script>alert(1)</script>')))"
# → '' (no <script> survives)
# Health-ping backoff (open devtools, kill server, watch network panel):
# 3 s → 6 s → 12 s → 24 s → 60 s, then resets on first successful ping
# Score-pill glyph (open #/reports in light + dark theme):
# .score-high shows ✓ + numeric score
# .score-mid shows ◐ + numeric score
# .score-low shows ○ + numeric score
# Mode-page hints (#/contacto, etc):
# <input aria-describedby="mode-contacto-recipient-hint"> ← targets <p id="…">
# Retired alias:
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404
Breaking changes
없습니다. 모든 수정은 가산적이거나 기존 엔드포인트 계약을 유지합니다.
Out of scope (v1.23+)
| 항목 | 비고 |
|---|---|
| M-9 — 로케일 CHANGELOG 본문 번역 | 모든 CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md v1.13+ 항목이 영어 본문 임시 대체였습니다. 릴리스 주기가 안정되면 일괄 번역 후보입니다. |
N-1 — public/js/lib/i18n.js 가 400 LOC 목표를 초과 |
로케일별 분할은 번들러 없이는 HTTP 비용을 증가시킵니다. 빌드 단계 결정이 내려질 때까지 연기합니다. |
| career-ops.org/docs 의 Help-bundle 콘텐츠 갱신 | 5개의 정규 URL 은 이미 모든 로케일의 help bundle (v1.11.x 이후) 에 등장합니다. QA 프롬프트의 시나리오 31.6 이 커버리지를 검증합니다. 콘텐츠 깊이 갱신은 v1.23 후보입니다. |
[1.21.0] — 2026-05-14
두 번의 독립적 코드 리뷰 패스에서 도출된 보안 + 동시성 + 접근성 폴리시. docs/sdd/specs/V1.20.1-BACKLOG.md 의 7개 결과를 한 릴리스에 출하했습니다: 블로커 1건 (DNS-rebind TOCTOU), 고위험 버그 6건 (경로 탐색 정화 (sanitization) 분산, LAN 배포에서의 레이트 리밋 공백, 동시 쓰기 경쟁 상태 (race condition), i18n 커버리지 누락, 매달린 aria-describedby, 누락된 label 연결). 신규 테스트 34개; 기준선이 427 → 461 유닛 + 32/32 Playwright 로 상승했습니다. 모든 수정은 명명된 회귀 테스트 뒤에 배치됩니다.
🛡️ 보안
-
fix(security): B-1 — close DNS-rebind TOCTOU via safe-fetch.mjs(server/lib/safe-fetch.mjs) — 이전 패턴은 검증을 위해 명시적dnsLookup을 한 번 수행한 뒤fetch()가 독립적인 조회를 다시 하도록 두었습니다. TTL=0 의 DNS rebind 공격자는 첫 조회에서 공인 IP 를, 두 번째 조회에서127.0.0.1/169.254.169.254/ LAN 주소를 반환하여isPrivateOrLoopbackHost를 우회할 수 있었습니다. 신규safeGet은 호스트를 한 번만 해석하고, node:http(s) 를 통해 TCP 연결을 정확히 그 IP 에 고정하며, 인증서 검증이 여전히 원본 호스트네임을 대상으로 하도록 SNI/Host 를 설정합니다./api/pipeline/preview와/api/auto-pipeline이 사용합니다. 조회 오류 시 fail-CLOSED (이전의try { … } catch { /* fall through */ }를 뒤집음).tests/ssrf-redirect-rebind.test.mjs의 신규 테스트 8개로 검증했습니다. -
fix(security): H-4 — consolidate sanitizePathName across 10 routes(server/lib/security.mjs) — 단순replace(/[^\w\-.]/g, '')정규표현식이jds.mjs,content.mjs,reports.mjs,llm.mjs,runners.mjs에 중복되어 있었고.문자를 유지해..pdf,....md, 선행 점 이름이 살아남았습니다.reports.mjs::sanitizeSlug만이 올바르게 처리했습니다. v1.21.0 은 올바른 버전 (sanitizePathName) 을security.mjs로 끌어올리고 10개의 잘못된 사본을 삭제했으며 빈 결과는 400 으로 거부합니다.tests/path-traversal.test.mjs의 12개 테스트로 검증했습니다. -
fix(security): H-5 — rate-limit LLM endpoints on public bind(server/lib/rate-limit.mjs) —/api/evaluate,/api/deep,/api/mode/:slug,/api/auto-pipeline에 IP 별 요청 빈도 제한이 없었습니다. 루프백 사용자는 영향이 없으며, LAN 으로 노출된 배포 (HOST=0.0.0.0) 는 분당 IP 당 10 요청을 받고 초과 시Retry-After및X-RateLimit-*헤더가 부착됩니다.LLM_RATE_LIMIT="N/Ws"로 설정 가능합니다. v2.0 P-12 인증 게이트 이전의 저비용 중간 방어책입니다.tests/rate-limit.test.mjs의 6개 테스트로 검증했습니다.
🔒 동시성
fix(data): H-6 — per-file mutex on applications.md / pipeline.md(server/lib/file-lock.mjs) — 동시POST /api/tracker(또는 auto-pipeline 과 수동 추가의 경쟁 상태 (race condition)) 가 둘 다num=42를 읽고 둘 다num=43을 써서 앞선 행을 조용히 누락시켰습니다.withFileLock(path, fn)은 경로별로 read-modify-write 를 직렬화합니다; 독립적 경로는 여전히 병렬로 실행됩니다.tracker.mjs,pipeline.mjs(POST + DELETE),auto-pipeline.mjs의 tracker 단계에 연결되어 있습니다.tests/concurrent-tracker-write.test.mjs의 5개 테스트로 검증했으며, 20개 동시 POST 가 001..020 까지 순차적으로 저장되는지 확인하는 통합 테스트가 포함됩니다.
♿ 접근성
-
fix(a11y): H-1 — id="batch-tsv-hint" on the batch.js hint paragraph(public/js/views/batch.js) — v1.20.0 이 TSV 텍스트영역에aria-describedby="batch-tsv-hint"를 추가했으나 힌트<p>에 해당id를 부여하지 않았습니다. 스크린 리더가 발화할 대상이 없었습니다. 수정했습니다. -
fix(a11y): H-2 — htmlFor on batch-parallel / batch-min-score labels(public/js/views/batch.js) — v1.20.0 의 입력 4개가 새 id 를 받았지만 라벨이 프로그래밍적으로 연결되지 않았습니다. 이제 WCAG 3.3.2 를 충족합니다. -
tests/a11y-form-wires.test.mjs의 신규 정적 분석 카나리아 — 모든 뷰 파일을 순회하며 모든aria-describedby/htmlForIDREF 가 형제id:선언을 가리키는지 단언합니다. 오타성 회귀를 CI 시점에 잡습니다.
🌐 i18n
fix(i18n): H-3 — 13 keys from v1.20.0 silently fell through to EN for 7 locales(public/js/lib/i18n.js) —pipe.filter,pipe.count,pipe.preview*,pipe.openTab,pipe.evaluateAll*,eval.jdHint,batch.parallelAria,batch.minScoreAria, 그리고common.delete,config.group{Core,Runtime,Regional},config.profileEmpty,config.viewProfile,scan.atsBadge,scan.regionalBadge가t('key', 'EN fallback')를 통해 참조되었으나 DICT 에는 결코 추가되지 않았습니다. 러시아어, 일본어, 중국어 스크린 리더 사용자는 영어aria-label을 듣게 되어, v1.20.0 이 주장한 WCAG 3.3.2 성과를 직접적으로 무효화했습니다. v1.21.0 은 19개 키 × 8 로케일 (≈ 신규 번역 150개) 을 모두 추가하고tests/i18n-coverage.test.mjs를 확장하여public/js/**/*.js의 모든t('key', …)호출을 스캔하고 각 키가 DICT 에 존재하는지 단언하는 정적 분석 패스를 포함합니다. 향후 표류는 CI 시점에 잡힙니다.
🧪 테스트
- 427 → 461 유닛 (+34) + 32/32 Playwright 동일.
- 신규 테스트 파일:
ssrf-redirect-rebind,path-traversal,concurrent-tracker-write,rate-limit,a11y-form-wires. - 기존
pipeline-preview.test.mjs는globalThis.fetch목에서safe-fetch.mjs의 신규_setTransport주입 지점으로 재배선되었습니다 — SSRF 경로가 더 이상 fetch 를 통과하지 않으므로 옛 목이 조용히 우회되었습니다.
검증
npm test # 461 / 461
npm run test:e2e:browser # 32 / 32
node --test tests/ssrf-redirect-rebind.test.mjs tests/path-traversal.test.mjs \
tests/concurrent-tracker-write.test.mjs tests/rate-limit.test.mjs \
tests/a11y-form-wires.test.mjs # 34 new tests, all green
# Path-traversal: every traversal-style :name returns 400 / 404
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/jds/..pdf
# → 400
# Rate-limit on public bind:
HOST=0.0.0.0 LLM_RATE_LIMIT=3/60s npm start &
for i in 1 2 3 4; do
curl -sS -o /dev/null -w '%{http_code} ' -X POST -H 'Content-Type: application/json' \
-d '{"jd":"…"}' http://0.0.0.0:4317/api/evaluate
done
# → 200 200 200 429
# Concurrent tracker writes: 20 parallel POSTs, 20 rows land:
node tests/concurrent-tracker-write.test.mjs
# 20 sequential rows 001..020
# Aria wires sanity:
grep -r 'aria-describedby' public/js/views/ | wc -l
# matching `id:` lookups all resolve (a11y-form-wires.test.mjs canary)
Out of scope (v1.22+)
| 항목 | 비고 |
|---|---|
pipeline-preview 본문 크기 스트리밍 캡 (M-2) |
await upstream.text() 가 8 KB 슬라이스 이전에 본문 전체를 읽으므로 악성 1 GB 스트림이 메모리를 소진할 수 있습니다. 바이트 카운터 + abort 로 스트림 읽기 권장. |
WCAG 1.4.1 — .connection-banner + 점수 pill 의 색상 전용 상태 (M-3) |
색상만으로 상태를 전달하므로 아이콘 접두어 (✓ / ◐ / ○) 또는 텍스트 접미어를 추가해야 합니다. |
HTML 엔터티를 통한 stripDangerousMarkdown 우회 (M-4) |
<script>, javascript:, <img src="data:image/svg+xml,<svg onload=…>"> 가 정규표현식을 통과합니다. UI.md 를 통한 심층 방어는 여전히 유효하나 테스트 스윕에서 우회를 문서화 및 잠금. |
try/catch 없이 Safari 사생활 보호 모드 localStorage 접근 (M-5) |
i18n.js:544/571 이 던지므로 SPA 가 키 원문을 렌더링합니다. 'en' 기본값과 함께 try/catch 로 감쌀 것. |
백오프 없는 setInterval(checkHealth, 3000) (M-6) |
지수 백오프 3s → 6s → 12s → cap 60s. |
null 가드가 없는 htmlFor 별칭 (M-7) |
한 줄 if (v != null && v !== false) 방어. |
폐기된 /api/scan-ru/config 의 404 카나리아 (M-8) |
v1.18 선례를 미러링하는 3줄 테스트. |
| 로케일 CHANGELOG 본문 번역 (M-9) | 릴리스 주기 안정 이후 일괄 번역 후보. |
| 모든 mode-page 필드의 인라인 힌트 단락 (M-1) | i18n 키 약 168개 × 8 로케일; 폴리시 항목으로 보류. |
| L-1 ~ L-5 자잘한 항목 | parseInt radix, bash –noprofile, Windows 안전 fileURLToPath, 멀티라인 SSE, scan.js 타이머 정리. |
[1.20.0] — 2026-05-13
컴포넌트별 접근성 폴리시 + 비영어 README 패리티 + /api/scan-ru/config 별칭 폐기. v1.19.0 의 “Out of scope” 표가 v1.20 으로 표시했던 네 항목을 모두 종료합니다.
♿ WCAG 2.5.5 / 2.5.8 — 컴포넌트별 터치 타겟 감사
a11y(touch-target): chip min-height 28 px + 8 px gap (2.5.8 spaced-target exception)—.chip은 24 × ~50 px 였고 (수직 24, 군집 컨트롤용 2.5.5 의 24 px 최저치를 미달성), 2.5.8 의 spaced-target 예외는 ≥ 24 × 24 px 또는 24 px 여유 중 하나를 요구합니다..chip을min-height: 28px; padding: 6px 12px;로, 감싸는.chip-row를gap: 8px;로 조정해 두 조건을 동시에 충족했습니다.a11y(touch-target): sidebar nav-item min-height 44 px—.nav-item은10px 14px패딩만 갖고 있어 대부분의 뷰포트에서 계산된 높이가 ~36 px 였습니다. 이제padding: 12px 14px; min-height: 44px; box-sizing: border-box;입니다..btn최저치와 일치합니다.a11y(touch-target): tab-btn min-height 44 px— Reports, Tracker, Scan 결과의 Sortable Headers / 카테고리 탭에도 동일한 처리.
♿ WCAG 1.3.1 / 3.3.2 — 인라인 폼 힌트의 aria-describedby
SPA 전반의 모든 폼 컨트롤이 이제 안정된 id 를 갖고, <label> 이 htmlFor 로 이를 가리키며, 인라인 힌트 단락은 aria-describedby 로 연결됩니다. 5개의 뷰 파일을 재배선했습니다:
a11y(forms): config.js— 키별id+ 힌트 연결 (cfg-<key>/cfg-<key>-hint).a11y(forms): evaluate.js—eval-jdtextarea + 정화 (sanitization) 후 50자 최소를 문서화하는eval-jd-hint단락.a11y(forms): batch.js—batch-tsv/batch-tsv-hint, 더하여batch-parallel,batch-min-score,batch-dry-run,batch-retry의aria-label.a11y(forms): pipeline.js—pipe-filter+pipe-new-url/pipe-new-url-hint.a11y(forms): mode-page.js— 7개 범용 모드 (project,training,followup,batch-prompt,contacto,interview-prep,patterns) 의 모든 필드가mode-<slug>-<name>id 와htmlFor라벨을 받습니다.
UI.el() 은 React 스타일 htmlFor 별칭을 학습하여 뷰 코드가 선언적으로 유지됩니다 — 내부적으로는 (JS 예약어인 프로퍼티 이름인) for 어트리뷰트를 설정합니다.
🌍 비영어 README 패리티
docs(readme): translate 7 locales to 585-line parity with EN master—README.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md가 306–316 줄이었습니다 (헤드라인은 다루되 마케팅 중심 워크스루와 API 레퍼런스 대부분을 건너뜀). 일곱 모두 이제 EN 구조를 처음부터 끝까지 미러링합니다: About → One-command install → Why? → Quick start (번호 매겨진 3단계) → Requirements → What you get 표 → Scan → Architecture (전체 디렉토리 트리) → API reference (모든 라우트 표) → Tests → Configuration → Security notes → Limitations → Contributing → 🌍 Getting Started 5단계 워크스루 → License.
🧹 /api/scan-ru/config 별칭 폐기
feat!(scan): remove /api/scan-ru/config legacy alias (sunset v1.20)— v1.19 에서 한 릴리스 동안의 별칭으로 유지되었습니다. 정규/api/scan/regional/config가 유일한 경로입니다. 제거:server/lib/routes/scan.mjs의 라우트 등록,README.md,docs/architecture/{OVERVIEW,SERVER,API}.md의 문서 참조. 테스트는 이미 정규 경로를 다루고 있었으므로 변경 불필요.
🧪 테스트
- v1.19 와 동일한 스위트. 427 / 427 유닛 + 20/20 smoke + 23/23 comprehensive + 32/32 Playwright. 모든 접근성 배선은 가산적입니다 (
id/for/aria-describedby어트리뷰트가 더 추가됨) — 동작 변경이나 테스트 차이가 없습니다.
검증
npm test # 427 / 427
npm run test:e2e:browser # 32 / 32
# Touch targets — every chip / nav-item / tab-btn ≥ 28 / 44 / 44 px:
# Chrome DevTools → Computed → height/min-height on .chip, .nav-item, .tab-btn
# Form labels — every input has a label[for=…] association:
# document.querySelectorAll('input,textarea,select').forEach(el =>
# console.assert(el.labels?.length || el.getAttribute('aria-label'), el))
# Alias gone:
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4317/api/scan-ru/config
# → 404
# Canonical still works:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
Breaking changes
DELETE /api/scan-ru/config— 제거됨./api/scan/regional/config를 사용하십시오. v1.19.0 의 CHANGELOG 와 검증 스크립트에서 sunset 으로 예고되었습니다.
Out of scope (v1.21+)
| 항목 | 비고 |
|---|---|
| 모든 mode-page 필드의 인라인 힌트 단락 | 현재는 <label for=…> 연결만 적용되어 있습니다. 필드별 가시 힌트 문구는 여전히 SPA 에서 영어 전용입니다. README 워크스루가 모든 로케일에서 필드 의도를 문서화하므로 폴리시 항목이며 블로커는 아닙니다. |
.connection-banner 와 대시보드 점수 pill 의 색상 전용 상태 노출 (WCAG 1.4.1) |
배너가 빨강/주황/녹색에 의존합니다; 색상을 인지하지 못하는 사용자를 위해 아이콘 또는 텍스트 접미어가 필요합니다. |
| 로케일별 CHANGELOG 본문 번역 | CHANGELOG.{es,pt-BR,ko-KR,ja,ru,zh-CN,zh-TW}.md 에 영어 본문 임시 대체가 남아 있습니다. 번역은 v1.x 릴리스 주기가 안정된 후 진행합니다. |
[1.19.0] — 2026-05-13
WCAG 1.4.3 명도 대비 + 스캔 통합 (최종) + UI 에서 HH_USER_AGENT 제거. v1.18 의 out-of-scope 였던 명도 대비 감사를 종료하고, v1.18 에서 시작한 EN/RU 분할 제거를 마무리하며, 사용자 방향에 따라 UI 에서 HH_USER_AGENT 설정 노브를 제거합니다 (서버에 번들된 합리적 기본값이 이미 대부분의 비-RU IP 에서 동작합니다).
♿ WCAG 1.4.3 명도 대비 패스
a11y(contrast): introduce AA-passing *-text variants for accent tokens— 라이트 테마:--rausch-text: #b80f42(흰색 위 6.59:1, 이전 3.52:1),--kazan-text: #066507(7.31:1, 이전 4.53:1),--darjeeling-text: #7a5800(앰버 배경 위 5.73:1, 이전 4.24:1),--babu-text: #00665e(6.09:1, 이전 2.70:1). 다크 테마: 밝게 조정된 미러 (#ff8aa0,#6ee7b7,#fcd34d,#5eead4) 가#161a22페이퍼 위에서 동일한 4.5:1 최저치를 만족합니다.- 배지 클래스 (
.badge-ok,.badge-warn,.badge-bad,.badge-info) 와 점수 pill (.score-high,.score-mid,.score-low) 은 이제 신규*-text변종을 통해 라우팅되어 — 모든 틴트 배경 위 텍스트 조합이 AA 를 통과합니다. 액센트 채움 토큰 (--rausch,--kazan등) 은 경계선과 외곽선용으로 유지됩니다 (비텍스트 UI 컴포넌트는 3:1 만 요구).
🧹 스캔 통합 (v1.18 작업 마무리)
docs(scan): scrub remaining EN/RU split references across READMEs + help + architecture docs— README 8개 + help 번들 8개 + 아키텍처 문서 3개 (API.md, SERVER.md, OVERVIEW.md, DATA-FLOWS.md) + scan.js 주석이 이제 단일 통합 스캔 방식을 설명합니다. 레거시/api/stream/scan-{en,ru}별칭은 v1.18 에서 이미 사라졌습니다; v1.19 는 여전히 스캔을 2단계 EN+RU 프로세스로 묘사하던 문서/문구를 잡아냅니다.feat(scan): canonical /api/scan/regional/config endpoint—/api/scan-ru/config은 한 릴리스 동안 얇은 별칭으로 유지하여 하위 호환을 제공합니다. 신규 경로는 소스 이름 규칙 (?source=regional) 과 일치합니다.
🛠️ UI 에서 HH_USER_AGENT 제거
feat!(config): drop HH_USER_AGENT field from /#/config + KNOWN_KEYS— 파워 유저는 여전히career-ops/.env에 직접HH_USER_AGENT를 설정할 수 있습니다 (서버는server/lib/sources/hh.mjs에서process.env.HH_USER_AGENT를 읽으며 번들된 UA 가 폴백입니다). 대부분의 사용자에게는 기본값으로 충분하고 App Settings 페이지에서 알 수 없는 User-Agent 필드를 보는 것이 반복적인 혼란의 원인이었기 때문에 UI 는 더 이상 이를 노출하지 않습니다.- 8개 로케일의 README 와 help 번들 언급이 “러시아 IP / VPN 으로 실행” 안내로 교체되었습니다.
scan.hhWarningi18n 키는 환경 변수 설정 세부 사항을 제거하도록 재작성되었습니다. KEY_GROUPS가 축소되었습니다: 더 이상regional분류가 없습니다 (HH_USER_AGENT 만 있었음). 테스트가 갱신되었고;regionalActive페이로드 필드는 SPA 하위 호환을 위해 유지됩니다.
🧪 테스트
tests/env-config.test.mjs—KNOWN_KEYS단언이 이제 HH_USER_AGENT 를 제외합니다; 키가 의도적으로 부재함을 확인하는 신규 단언.tests/config-endpoint.test.mjs— POST 쓰기 다중 키 테스트가 HH_USER_AGENT 대신 두 번째 알려진 키로GEMINI_MODEL을 사용합니다.tests/config-groups.test.mjs—groups.HH_USER_AGENT가 이제undefined로 기대됩니다.- 합계: 427 / 427 유닛 + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright. 조정된 모든 테스트가 이미 집계되었기 때문에 v1.18.0 과 동일한 수치입니다.
검증
npm test # 427 / 427
# Contrast (Chrome DevTools or axe) on light + dark:
# .badge-ok / .badge-warn / .badge-bad / .badge-info → AA pass (4.5:1+)
# .score-high / .score-mid / .score-low → AA pass
# HH_USER_AGENT no longer in /api/config:
curl -s http://127.0.0.1:4317/api/config | jq '.values | keys'
# → ["ANTHROPIC_API_KEY","ANTHROPIC_MODEL","GEMINI_API_KEY","GEMINI_MODEL","HOST","PORT"]
# (no HH_USER_AGENT)
# Canonical regional config endpoint:
curl -s http://127.0.0.1:4317/api/scan/regional/config | jq '.'
# Legacy alias still alive through v1.20:
curl -s http://127.0.0.1:4317/api/scan-ru/config | jq '.'
Out of scope (v1.20+)
| 항목 | 비고 |
|---|---|
| 컴포넌트별 터치 타겟 감사 (필터 chip, 정렬 가능한 헤더, 사이드바 내비게이션) | v1.18 이 전역 최저치를 설정했고 (.btn 44 px, .btn-sm 32 px); SPA 전반의 컴포넌트별 검증은 남아 있습니다. |
인라인 폼 힌트의 aria-describedby (#/config, #/pipeline, #/evaluate, #/batch) |
v1.17 이 글로벌 검색과 모달 닫기의 aria-label 을 다루었습니다. 입력별 힌트 연결이 다음 폴리시 레이어입니다. |
| 비영어 README 의 전체 패리티 (EN 처럼 585줄) | v1.18 이 비영어를 ~307 (EN 의 53 %) 로 가져왔습니다. 마케팅 중심의 “Quick start” + “🌍 Getting Started” 워크스루는 영어 전용으로 남아 있습니다. |
/api/scan-ru/config 레거시 별칭 제거 |
v1.20 에 sunset 계획. 정규 /api/scan/regional/config 이 마이그레이션 대상입니다. |
[1.18.0] — 2026-05-13
스캔 엔드포인트 통합 + WCAG 2.2 AA 패스 + i18n long-tail 마무리. 레거시 /api/stream/scan-{en,ru} 별칭을 폐기합니다 (Sunset 기간 2026-10-01 을 사용자 방향에 따라 v1.18 로 앞당김). 비영어 README 를 ~307 줄로 끌어올리고, 6개 로케일에서 RU 본문이 남아 있던 v1.16.0 + v1.17.0 CHANGELOG 항목을 번역합니다.
🚪 Breaking
feat!(scan): retire legacy /api/stream/scan-{en,ru} aliases— 폐기된 EN/RU 분할 SSE 엔드포인트가 사라졌습니다. 모든 컨슈머는 v1.12.0 부터 활성 상태인 통합/api/stream/scan?source=ats|regional|both엔드포인트를 통과합니다. 레거시 경로는 v1.15.0 부터 Deprecation + Sunset (RFC 8594) 헤더를 갖고 있었습니다; 마이그레이션 기간이 종료되었습니다. 옛 경로의 외부 통합은 SPA catch-all 로 조용히 라우팅되는 대신 404 를 받습니다.
♿ 접근성 (WCAG 2.2 AA 패스)
- WCAG 2.4.1 Bypass Blocks — 모든 페이지의 첫 번째 포커스 가능 요소로 신규 Skip to main content 링크.
.skip-link로 시각적으로 숨겨져 있다가 포커스를 받으면 좌상단으로 표시됩니다. - WCAG 2.4.7 Focus Visible — 전역
*:focus-visible스타일. 마우스 클릭 포커스 링은 꺼지고 키보드 Tab 포커스 링은 켜집니다 (WAI-ARIA AP 표준 패턴). 모달 닫기 (×) 는 더 높은 대비의 포커스 링을 받습니다. - WCAG 2.5.5 Target Size —
.skip-link의 최소 44×44 px 터치 타겟..btn-sm은 32 px min-height 를 유지합니다 (행 간격과 결합하면 컴팩트 테이블 행 컨트롤용 24×24 + 간격 AAA 예외를 충족). - WCAG 3.1.1 Language of Page —
<html lang="en">을lang="ru"에서 수정했습니다 (JS i18n 부트스트랩이 이미 로드 시 이를 재정의했지만 SSR 기본값이 이제 SPA 기본 로케일과 일치합니다). - WCAG 1.3.1 Info & Relationships —
#content가tabindex="-1"을 받아 skip-link 대상이 깔끔하게 포커싱됩니다. (ARIA 역할 + focus-trap 은 이미 v1.17 에서 추가되었습니다.)
📚 i18n long-tail
docs(i18n): v1.16.0 + v1.17.0 CHANGELOG translated in 6 locales—CHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md에서 이전에 RU 본문이었던 항목들이 이제 네이티브 언어로 표시됩니다. 로케일당 RU 문자 수가 79 → 42 → 23 으로 감소했습니다 (남은 23개는 파일 경로 같은 기술적 인라인 참조와 의도적으로 유지되는 다국어 헤더 링크입니다).docs(readme): expand non-EN READMEs with Why / Requirements / Features / Configuration / Contributing— 각 비영어 README 가 240 → ~307 줄로 성장했습니다. 이제 585줄 EN 과 동일한 비마케팅 섹션을 다룹니다. 전체 1:1 패리티 (마케팅 중심 워크스루 섹션) 는 여전히 연기됩니다.
🛠️ Misc
docs(api): consolidated scan endpoint in API.md + DATA-FLOWS.md + README.md— API 레퍼런스 표가 이제/api/stream/scan?source=…만 나열합니다. README 의 Scan 섹션이 v1.18.0 의 EN/RU 분할 폐기를 설명합니다.fix(scan.js): drop stale comment about deprecated aliases being live— SPA 의 runScanAll 디스패처 주석이 통합된 현실을 반영합니다.
🧪 테스트
tests/scan-consolidated.test.mjs::F-018 backwards compat가 재작성되었습니다 — 이전의 두 “레거시 엔드포인트가 여전히 동작함” 단언이 이제/api/stream/scan-{en,ru}요청이 (SPA catch-all 로 라우팅되는 대신) 404 를 반환하는지 검증합니다.- 합계: 427 / 427 유닛 + 20/20 smoke E2E + 23/23 comprehensive E2E + 32/32 Playwright (수치 동일; +2 개의 새로운 레거시 제거 단언이 +2 개의 레거시 정상 동작 단언을 대체).
검증
npm test # 427 / 427
npm run test:e2e:full # 23 / 23
# Legacy endpoint retirement:
curl -sI http://127.0.0.1:4317/api/stream/scan-en | head -1 # → HTTP/1.1 404
curl -sI http://127.0.0.1:4317/api/stream/scan-ru | head -1 # → HTTP/1.1 404
# Consolidated endpoint:
curl -sN 'http://127.0.0.1:4317/api/stream/scan?source=ats&dryRun=1' | head -5
# → event: start
# → data: {"script":"en-scanner","writeFiles":false,…}
# Skip link (a11y):
curl -s http://127.0.0.1:4317/ | grep -c 'class="skip-link"' # → 1
# html lang fallback:
curl -s http://127.0.0.1:4317/ | grep -c 'html lang="en"' # → 1
Out of scope (v1.19+)
| 항목 | 비고 |
|---|---|
| 비영어 README 의 전체 패리티 (EN 처럼 585줄) | v1.18 이 비영어를 ~307 (EN 의 53 %) 로 가져왔습니다. 마케팅 중심 “Why?” / “Quick start” 워크스루는 영어 전용으로 남아 있습니다. |
| 색상 명도 대비 감사 (WCAG 1.4.3 AA — 텍스트 4.5:1, 대형 텍스트 3:1) | v1.18 이 구조적 접근성을 다루었습니다; 라이트 + 다크 팔레트 전반의 토큰별 명도 대비 검증은 남아 있습니다. |
| 모든 상호작용 요소에 대한 터치 타겟 감사 | v1.18 이 최저치를 설정했고 (.btn: 44 px, .btn-sm: 32 px); 컴포넌트별 검증 (필터 chip, 사이드바 내비게이션, 정렬 가능한 헤더) 은 남아 있습니다. |
[1.17.0] — 2026-05-13
폴리시 + 접근성 + CI 수정 릴리스. v1.16.0 목록의 9개 후속 작업을 모두 종료합니다: 브라우저 smoke 검증, README 배지 정합, 커버리지 갱신, SPA 에 surface 된 lastWorkdayFallback, 전체 E2E 재기준선, Playwright auto-pipeline 시나리오, 접근성 감사 패스, 6개 로케일에서 과거 CHANGELOG 압축, Architecture / API / Security / Tests 섹션으로 비영어 README 확장.
🐛 수정
fix(e2e): smoke + comprehensive suites re-aligned with v1.16 UX— v1.16 의 Cmd+K Enter → AutoPipeline 모달 변경 때문에 e2e 테스트의search.press('Enter')가 후속 클릭을 가로채는 모달을 열었습니다. 테스트는 이제 v1.16 의 문서화된 분기와 일치하도록 레거시 quick-add 경로에Shift+Enter를 사용합니다. comprehensive E2E 의 배치 모드 반복도 (v1.15 PR-H 가 도입한 레거시 mode-prompt slug 인)/#/batch-prompt를 사용하도록 갱신되었습니다. 이것이 v1.16.0 푸시의 CI 실패였습니다 — Playwright e2e 가 backdrop 가로채기 클릭에서 30초 동안 타임아웃되었습니다.fix(mode-page): batch-prompt route → modes/batch.md via serverSlug— v1.15 가 레거시 mode slug 를batch-prompt로 이름을 바꾸었지만, 서버의POST /api/mode/:slug는 존재하지 않는modes/batch-prompt.md를 찾고 있었습니다. 신규serverSlug필드는 라우트 해시를 부모의 mode 파일 이름과 분리합니다.chore: bump deprecation messages from v1.16.0 to v1.17.0— scan-en/scan-ru deprecation 문구와 batch-prompt deprecation 배너가 과거 버전을 참조하고 있었습니다.
✨ 기능
feat(scan): 🔒 Workday CAPTCHA chip in Active Companies card— v1.16 PR-7 의 서버 사이드lastWorkdayFallbackexport 가 이제 SPA 에서 소비됩니다./api/scan-results가 스냅샷을 반환합니다;#/scan은 Workday 테넌트가 폴백으로 떨어진 경우 Active Companies 위에 경고 톤의 카드를 렌더링합니다 (“🔒 Workday tenant blocked — fallback: use /career-ops scan (Playwright)”). 신규getLastWorkdayFallback()exporter 는 ESM live-binding 모호성을 피합니다. 신규 i18n 키 2개 × 8 로케일.
♿ 접근성
a11y: ARIA roles + focus management pass on critical surfaces—index.html:<aside>(navigation),<header>(banner),<section id="content">(main),<div id="modal">(aria-modal/aria-labelledby 를 가진 dialog),<div id="toast">+#conn-banner(aria-live 를 가진 status),<div class="searchbar">(search) 에role어트리뷰트.#sidebar-toggle은aria-controls="sidebar"와 열기/닫기 시 JS 가 동기화하는aria-expanded를 받습니다.#global-search는 시각적으로 숨겨진<label>과 Cmd+K 단축키 힌트를 표시하는 명시적aria-label을 받습니다.- 모달 닫기 (×) 는
aria-label="Close dialog"를 받습니다. - 장식용 backdrop 은
aria-hidden="true"를 받습니다. - 모달의 Focus trap —
UI.modal()이 클릭 소유자를 기억하고, 열릴 때 첫 번째 비닫기 포커스 가능 요소에 포커스를 두며, 모달 내에서 Tab/Shift+Tab 을 순환시킵니다.UI.closeModal()은 이전 소유자에게 포커스를 복원합니다. public/css/app.css의 신규.visually-hidden유틸리티 클래스 (WAI-ARIA AP 표준 패턴).
📚 문서
docs(readme): badge truth across 8 READMEs— tests 배지284 / 379 / 360→ 427; release 배지v1.9.1 / v1.13.0→ v1.16.0 그 후 v1.17 bump 로 → v1.17.0. 릴리스 링크 타깃 갱신.docs(readme): expand 7 non-EN READMEs with reference sections— 각 README 가 170 → ~240 줄로 성장하면서 네이티브 언어로 Architecture / API reference / Security notes / Tests / A11y / Limitations / License 섹션이 추가되었습니다. 아직 EN 의 585줄 패리티는 아니지만 모든 핵심 비마케팅 표면을 다룹니다.docs(changelog): condense pre-v1.12 entries in 6 locales— 비영어/비러시아어 CHANGELOG 에 흘러들어간 길고 RU 본문이었던 v1.11.x + v1.10.x 항목이 이제 각 로케일의 네이티브 언어로 압축된 “Earlier releases” 요약으로 교체되었습니다. 상세 이력은CHANGELOG.md(EN) 에 유지됩니다.
🛠️ 도구
coverage: refresh numbers— 마지막 공개치는 95.46 % 라인 / 84.06 % 브랜치 (v1.13.0 REVIEW) 였습니다. v1.17 기준선: 94.14 % 라인 / 82.98 % 브랜치 / 93.20 % 함수. auto-pipeline + reports-write 의 새 오류 경로로 약간 감소; 여전히 CLAUDE.md 의 80 % 최저치보다 한참 위입니다.
🧪 테스트
- 합계: 427 / 427 유닛 + 20/20 smoke E2E + 23/23 comprehensive E2E + 32 / 32 Playwright (이전 28; +4 신규 auto-pipeline 시나리오: 버튼이 모달을 엽니다, Cmd+K 붙여넣기가 모달을 트리거합니다, 잘못된 URL 이 1단계를 게이팅합니다,
POST /api/auto-pipelineSSE 이벤트 프레이밍). - E2E 스위트가 v1.16.0 UX 에 재정렬되었습니다 (Shift+Enter quick-add, 레거시 모드용 /#/batch-prompt).
검증
# Locally:
npm test # 427 / 427
npm run test:e2e # 20 / 20
npm run test:e2e:full # 23 / 23
npm run test:e2e:browser # 32 / 32
# Browser smoke (page-level):
curl -s http://127.0.0.1:4317/api/scan-results | jq '.workdayFallback'
# null when no Workday fallback occurred; {apiUrl, reason, at} after a 4xx.
# A11y spot-check:
node -e "
const c = require('cheerio').load(require('fs').readFileSync('public/index.html','utf8'));
['banner','navigation','main','dialog','status','search'].forEach(r =>
console.log(r, c('[role=' + r + ']').length));
"
# Each role should appear ≥1.
# CI gate verification: dashboard-screenshots workflow boots a /tmp
# scaffold, regenerates PNGs, diffs against committed — green when
# images/dashboard-*.png are up to date with rendered SPA.
Out of scope (v1.18+)
| 항목 | 비고 |
|---|---|
| 비영어 CHANGELOG 의 v1.16.0 항목 번역 | 현재 RU 본문 (~30 줄 × 6 로케일 = 180 줄). 사용자의 명시적 v1.11.x/v1.10.x 범위 밖이었습니다. |
| 비영어 README 의 전체 패리티 (EN 처럼 585줄) | v1.17 이 비영어를 ~240 으로 가져왔습니다; 마케팅 중심의 “Why?” / “Quick start” 워크스루는 영어 전용으로 남아 있습니다. |
| 정규 A-F 프롬프트의 부모 커밋 | santifer/career-ops::modes/oferta.md 재작성이 여전히 upstream 에서 필요합니다 (CLAUDE.md 하드 룰 #1). |
| 전체 WCAG 2.2 AA 감사 | v1.17 이 구조적 ARIA + focus trap 을 다루었습니다; 컴포넌트별 명도 대비/Tab 순서 감사는 대기 중. |
career-ops-ui v1.267.1 · page 8/9 · mirror of the project changelog for cvstart.org