체인지로그
GitHub릴리스별로 정리된 모든 주요 변경 사항입니다. 저장소에 실린 동일한 파일이며, 19개 언어 전체로 번역되어 있습니다.
[1.58.52] — 2026-05-20
fix(ux): UX-A5 — #/help TOC 스크롤 스파이가 정상 동작. v1.58.45 의 setTimeout(0) 이 라우터 마운트 전에 실행되었음. fix: headings 직접 참조 + 이중 requestAnimationFrame. 947 → 948 유닛. (UX-A5)
[1.58.51] — 2026-05-20
chore(docs): v1.58.51 — v1.58.37 → v1.58.50 사이클(14 releases) 최종 하우스키핑. 코드 변경 없음. qa/ 재정리(모든 버전 고정 문서 archive/v158-cycle/ 로 이동);6 개 perennial 이 루트에 남음. REGRESSION-FINAL §13 가 v1.58.37→.50 모든 불변 문서화. 베이스라인 불변(947/947). (housekeeping)
[1.58.50] — 2026-05-20
docs: DOC-1 — qa/REGRESSION-FINAL.md 에 §5a 추가(서버 에러 바디 영어-정책 명문화). NEW-D4 를 not-a-finding 으로 종료. FIX-PROMPT-FINAL-EXHAUSTIVE.md 의 v1.58.37 → v1.58.50 큐 완료(14 릴리스). 946 → 947 유닛. (DOC-1)
[1.58.49] — 2026-05-20
chore(tooling): TOOL-1 — make clean-test-fixtures 와 부모 프로젝트 data/pipeline.md 에서 example.com 라인을 제거하는 스크립트 추가. --dry-run 지원. CI-isolated 테스트 4 개. 942 → 946 유닛. (TOOL-1)
[1.58.48] — 2026-05-20
fix(ux/onboarding): UX-D-B — 프로필이 기본 템플릿일 때 #/dashboard 에 경고 배너 표시. /api/health 의 Profile customized: false 검출 시 .hero-banner--warning 표시. 새 i18n 키 onboarding.fixtureWarning + onboarding.fixProfile × 8. 941 → 942 유닛. (UX-D-B)
[1.58.47] — 2026-05-20
fix(ux/naming): UX-D-C — 상단바 “Quick scan” 을 Scan 열기 로 개명(실제 스캔을 시작하지 않고 단지 이동만 하므로). 8 언어 업데이트. 940 → 941 유닛. (UX-D-C)
[1.58.46] — 2026-05-20
fix(ux): UX-D-D — #/apply 체크리스트의 {company}-{role} 을 URL/JD 에서 추출한 슬러그로 치환. 이전엔 플레이스홀더가 그대로 표시. 새 extractSlugs + substitutePlaceholders 가 Greenhouse/Lever/Ashby/Workable/SmartRecruiters/Workday 인식. 폴백 [company]/[role]. 939 → 940 유닛. (UX-D-D)
[1.58.45] — 2026-05-20
fix(ux): UX-D-K — #/help 의 TOC 스크롤 스파이가 현재 섹션 강조. IntersectionObserver 가 보이는 H2 에 해당하는 TOC 링크에 .toc-current 부여. 938 → 939 유닛. (UX-D-K)
[1.58.44] — 2026-05-20
fix(ux): UX-D-L — #/deep 의 Saved-research 에서 열린 brief 에 인라인 × 닫기 버튼 추가. 이전엔 스크롤하거나 페이지를 떠나야만 닫을 수 있었음. 새 키 deep.closeBrief × 8. 937 → 938 유닛. (UX-D-L)
[1.58.43] — 2026-05-20
fix(ux): UX-D-F — #/evaluate 빈 제출 시 별도의 현지화 토스트 표시. 이전엔 “너무 짧음”과 같은 메시지. 새 키 eval.emptyJd × 8. 936 → 937 유닛. (UX-D-F)
[1.58.42] — 2026-05-20
fix(ux): UX-D-J — 모든 advisor 페이지에서 ETA 칩 일관성 확보. 이전엔 #/auto 만 “⏱ ~1–2 min” 표시. 이제 #/evaluate · #/deep · 5 개 mode 페이지도 ⏱ ~30s 표시(새 advisor.eta 키 × 8). 935 → 936 유닛. (UX-D-J)
[1.58.41] — 2026-05-20
fix(ux/truthfulness): UX-D-I — 비용 힌트가 탭 복귀 + providers-changed 이벤트에서 재조회. 이전엔 한 번만 가져왔으므로 다른 탭에서 프로바이더 변경 시 오래된 값이 표시. 934 → 935 유닛. (UX-D-I)
[1.58.40] — 2026-05-20
fix(ux/docs): UX-D-H — career-ops.org/docs/... 딥링크가 클릭 가능함을 보장하는 regression-lock. 새 tests/external-doc-links.test.mjs 가 views/.js 와 docs/help/.md 검사. 932 → 934 유닛. (UX-D-H)
[1.58.39] — 2026-05-20
fix(ux): NEW-D2 — 대시보드 헤더의 Refresh 버튼이 명시적 피드백 제공. 페이지 리로드 없이 재페치 + 재렌더. 2 개의 새 i18n 키. 931 → 932 유닛. (NEW-D2)
[1.58.38] — 2026-05-20
fix(a11y): NEW-D3 (WCAG 4.1.2) — #/tracker 검색 입력에 placeholder 와 다른 현지화 aria-label 추가. 이전엔 placeholder 만 있었고 SR 은 목적을 알 수 없었음. 새 i18n 키 track.searchAria × 8 언어, placeholder 와 다른 문자열. 930 → 931 유닛. (NEW-D3)
[1.58.37] — 2026-05-20
fix(i18n): NEW-D1 — #/pipeline H1 을 es/pt-BR/ru 에서 현지화 + RU 타이틀 누락 2 건 추가 수정. 새 tests/i18n-no-latin-leaks.test.mjs 가 contacto.title / health.title 의 RU 누락도 잡아 동시 수정. 928 → 930 유닛. (NEW-D1)
[1.58.36] — 2026-05-20
chore(docs): v1.58.36 — v1.58.x 사이클 종료 시점의 전체 하우스키핑 스윕. 코드 변경 없음. (1) qa/: 버전 고정 스냅샷 3 개(REGRESSION-END-TO-END-v1.58.16/33/35.md)를 qa/archive/v158-cycle/ 로 이동. (2) REGRESSION-FINAL.md 에 §12 추가(v1.58.4 → v1.58.35 모든 불변). (3) UX-AUDIT-PROMPT.md 에 30 행 추가. (4) docs/architecture/ 갱신(FRONTEND 드로어, TESTING 합계 928/62/20/23). (5) CLAUDE.md 에 “v1.58.x 사이클의 교훈” 섹션 추가. (6) README ×8 에 “알림 🔔” 행 추가 + 오래된 테스트 카운트 수정. 베이스라인 무변. (housekeeping)
[1.58.35] — 2026-05-20
fix(ui): v1.58.35 — 알림 드로어 자동 열림 버그 수정 + 도움말에 §18 “알림” 추가(사용자 보고). v1.58.34 버그: .notif-drawer { display: flex } 가 UA [hidden] { display: none } 를 덮어쓰고 있었음. .notif-drawer[hidden] { display: none } 명시. 드로어는 벨 클릭으로만 열림. 8 개 언어 도움말에 §18 추가(카테고리 표 + 키보드). 927 → 928 유닛. (사용자 보고)
[1.58.34] — 2026-05-20
feat(ui): v1.58.34 — 알림 드로어(U-13 완전 종료). v1.58.33 캡처 위에: 신규 UI.onToast(fn), 상단바 벨 🔔 + 안 읽음 배지, 우측 슬라이드 <aside role="dialog">, 로컬라이즈된 제목/빈 상태/항목 (notif.* × 8). Esc + 닫기 + 벨 재클릭으로 닫힘. 926 → 927 유닛. (U-13 follow-up)
[1.58.33] — 2026-05-20
fix(ux): U-13 + U-14 + U-15 — 토스트 저널(50 개 캡 + UI.getToastHistory()) + .page-header h1 + p 안전 규칙 + #/cv 미저장 변경 인디케이터. v1.58.x 사이클 마무리. 새 i18n 키 cv.unsaved × 8 언어. 925 → 926 유닛. (U-13/U-14/U-15)
[1.58.32] — 2026-05-20
fix(ux): U-12 — #/help TOC 필터 입력에 min-width: 16ch 적용. KO/JA 플레이스홀더가 잘리지 않도록 .help-toc__filter 클래스 추가. 924 → 925 유닛. (U-12)
[1.58.31] — 2026-05-20
fix(ux): U-11 — Tracker 의 Legitimacy 컬럼 헤더에 현지화 정보 칩 ⓘ + 툴팁으로 High/Caution/Suspicious 척도 설명. 새 i18n 키 track.col.legitimacy.help × 8 언어. 923 → 924 유닛. (U-11)
[1.58.30] — 2026-05-20
fix(ux): U-10 — Tracker 의 Normalize / Dedup / Merge 버튼이 data/applications.md 가 비었을 때 비활성화. 현지화 툴팁(track.fixEmpty × 8 언어)이 이유를 설명. 922 → 923 유닛. (U-10)
[1.58.29] — 2026-05-20
fix(ux): U-9 — #/pipeline 카운터 ↔ 필터 행이 좁은 뷰포트에서 세로로 쌓임. 새 .pipeline-controls 클래스와 @media (max-width: 720px) 로 필터 너비 100%. 921 → 922 유닛. (U-9)
[1.58.28] — 2026-05-20
fix(ux): U-8 — 7 개 모드 페이지에서 생성된 프롬프트 블록이 기본 접힘. <details class="prompt-block"> 로 래핑되고, 요약은 “Show prompt (N lines)” 가 현지화(prompt.show / prompt.lines × 8). Copy + Run-live 는 그대로 노출. 920 → 921 유닛. (U-8)
[1.58.27] — 2026-05-20
fix(ux): U-7 — verify-pipeline.mjs 의 === ASCII 구분자를 결과 모달에서 제거. 핸들러에서 ^={10,}$ 정규식으로 사전 제거. 919 → 920 유닛. (U-7)
[1.58.26] — 2026-05-20
fix(ux): U-6 — #/scan 의 “Active companies N/M” 칩이 툴팁 + aria-label 로 N 과 M 의 의미 설명. 새 i18n 키 scan.activeCo.help × 8 언어. 918 → 919 유닛. (U-6)
[1.58.25] — 2026-05-20
fix(ux/ia): U-5 — Dashboard CTA 중복 제거(헤더의 Open Pipeline 버튼과 Scan all sources 타일 제거). 사이드바와 히어로가 이미 두 경로를 다루며, v1.58.3 QA 의 4× Pipeline / 4× Scan 이 각 2× 로 줄어듦. 917 → 918 유닛. (U-5)
[1.58.24] — 2026-05-20
fix(ux): U-4 — 에러 토스트의 “(METHOD /path · HTTP NNN)” 후미를 접힌 <details> 안으로 이동. 기술 정보는 DOM 에 보존(BUG-006 불변), 헤드라인은 사람이 읽을 문장만 남음. 새 i18n 키 toast.details × 8 언어. 916 → 917 유닛. (U-4)
[1.58.23] — 2026-05-20
fix(ux): U-3 — #/followup 의 lastContact 플레이스홀더가 오늘 − 14 일로 동적 계산됨. 고정 2026-04-21 은 시간이 흐르면서 진부해졌음. new Date() 기반으로 setDate(getDate() - 14) 후 ISO YYYY-MM-DD 생성. 915 → 916 유닛. (U-3)
[1.58.22] — 2026-05-20
fix(ux): U-2 — #/auto H1 이 선행 ✨ 때문에 두 줄로 줄바꿈되던 문제 해결. auto.title 에서 ✨ 분리하여 <span class="page-icon" aria-hidden="true"> 로 이동. .page-header--icon 은 grid 레이아웃으로 이모지 전용 열 확보. 914 → 915 유닛. (U-2)
[1.58.21] — 2026-05-20
fix(ux): U-1 — #/cv H1 + 부제목이 다른 페이지와 통일(v1.56.0 UX-9 칩을 설계상 철회). .cv-breadcrumb 칩 제거, <h1 class="page-title"> + <p class="page-subtitle"> 복원. 단일 <h1> 불변 유지. 913 → 914 유닛. (U-1)
[1.58.20] — 2026-05-20
fix(i18n/platform): I-6 — 사이드바 풋터 단축키 힌트가 Mac 에서 ⌘K, 그 외 환경에서 Ctrl+K 로 표시되고 동사는 로컬라이즈. 수정 전엔 모든 플랫폼/언어에서 CTRL+K — search 리터럴이 노출되었다. top.langhint 가 {hotkey} — 검색 형식을 사용하고, applyFooterHotkey() 가 navigator.platform 에 따라 {hotkey} 를 치환. 915 → 916 유닛. (I-6)
[1.58.19] — 2026-05-20
fix(i18n): I-4 — 러시아어 #/followup 에서 라틴어 cadence / follow-up 누출 제거. RU followup 문자열(H1, 힌트)에 cadence, follow-up, scope, timeline 이 섞여 있었으나 러시아어 네이티브 표현으로 치환. 914 → 915 유닛. (I-4)
[1.58.18] — 2026-05-20
fix(i18n): I-3 — 도움말 TOC 항목 2/5/13/14, 비라틴 로케일에서 영어 잔재 제거. 수정 전 여러 로케일 도움말 번들에 ## 2. App settings & API keys, ## 5. Portals & Sources, ## 13. Mode prompts, ## 14. Apply checklist 가 남아 있었음. 이제 8 개 언어에서 완전히 현지화. 913 → 914 유닛. (I-3)
[1.58.17] — 2026-05-20
fix(i18n): I-2 — Saved-research 날짜 라벨을 Intl.RelativeTimeFormat 으로 로케일화. public/js/views/deep.js 의 formatRelative() 가 영어 today / 1d ago / Nd ago 를 하드코딩했었다. Intl.RelativeTimeFormat(I18n.getLang(), { numeric: 'auto' }) 로 교체 — 브라우저 네이티브의 로컬라이즈된 “오늘/어제/N일 전” 문자열 사용. 7일 초과 시 Intl.DateTimeFormat(locale, { dateStyle: 'medium' }) 로 폴백. 912 → 913 유닛. (I-2)
[1.58.16] — 2026-05-20
fix(ui): 브랜드 버튼 호버 깜빡임(사용자 보고). 원인: .btn-primary / .btn-danger 의 기본 배경은 linear-gradient(...), :hover 는 단색 var(--rausch-dark). CSS 는 그라데이션↔단색을 보간할 수 없어 180ms transition: background 가 스냅하면서 흰색/분홍 깜빡임이 보였다. 수정 위치 public/css/app.css: 호버에서도 그라데이션을 유지하고 filter: brightness(0.92) 로 어둡게 — filter 는 모든 브라우저에서 부드럽게 보간된다. .btn 의 transition 목록에 filter var(--transition) 을 추가해 어두워짐이 애니메이션되도록 함. 911 → 912 유닛. (사용자 보고)
[1.58.15] — 2026-05-20
fix(a11y/i18n): I-1 — 상단바 검색의 aria-label 과 시각적 숨김 <label> 을 i18n 화. 이전에는 모든 8 개 언어에서 영문 aria-label 이 스크린 리더에 노출되었다. public/js/app.js 에 범용 data-i18n-aria-label 훅 추가 — applyI18n() 이 언어 변경 시 data-i18n / data-i18n-placeholder 와 동일하게 aria-label 을 교체. 새 i18n 키 2 개(top.search.aria, top.search.label)를 8 언어에 추가. 어떤 미래 컨트롤에도 재사용 가능. 910 → 911 유닛. (I-1)
[1.58.14] — 2026-05-20
fix(ux): M-9 — 연결 배너의 새로고침 버튼에 피드백 추가(이전엔 무음 리로드). v1.58.13 까지 클릭 시 곧바로 location.reload() 를 호출했다. 이제 “새로고침 중…” 토스트를 즉시 표시하고, sessionStorage['refreshedToast'] 를 설정하며, 버튼을 disabled 처리해 빠른 더블클릭에서 토스트가 쌓이지 않게 하고, 리로드를 200ms 지연시켜 토스트가 그려진 뒤에 페이지가 새로 로드되도록 한다. 다음 부팅 때 app.js 가 플래그를 감지하고 성공 토스트 “새로고침됨” 을 출력. 새 i18n 키 2 개(common.refreshing, common.refreshed)를 8 언어에 추가. 909 → 910 유닛. (M-9)
[1.58.13] — 2026-05-20
fix(ux): M-8 — #/apply 체크리스트가 인터랙티브해짐. v1.58.13 이전에는 “▶ 체크리스트 생성”이 항목 0~7 을 모노스페이스 <pre> 블록으로만 보여주어 체크할 수 없었음. 이제 각 항목은 실제 <input type="checkbox"> 로 렌더링되며 <label> 로 감싸 클릭 영역이 행 전체(WCAG 2.5.5)가 됨. 상태는 URL 별로 localStorage['applyChecklist:'+slug] 에 영속화 — 3 개 체크 → 새로고침 → 3 개 그대로 유지. 버튼: 미체크 항목 복사(미해결 항목을 - markdown 으로 출력)와 초기화. 5 개 신규 i18n 키(apply.checklist.copyUnchecked, resetBtn, copied, copyFailed, reset)를 8 언어에 추가. 파서가 항목을 찾지 못한 경우 방어적 폴백 제공. 908 → 909 유닛. (M-8)
[1.58.12] — 2026-05-20
fix(ux): M-7 — 비용 힌트가 활성 제공자를 따라감 (OpenRouter 가 더 이상 잘못된 고정 금액으로 빠지지 않음). UI.providerCostHint() 는 이미 /api/status/providers 를 거쳐 제공자를 인지하고 있었으나, public/js/api.js 의 매핑이 anthropic/gemini/openai/qwen 만 포함. v1.57.0 의 5번째 제공자 OpenRouter 는 일반 폴백 0.03 에 떨어지고 표시 이름도 소문자 openrouter 였음. 이제 EST 에 openrouter: null 추가 (라우터가 모델을 선택하므로 비용 가변), === null 분기에서 로컬라이즈된 “cost varies (router picks)” 출력. NAME 에 openrouter: 'OpenRouter'. 새 i18n 키 cost.varies 를 8 언어에 추가. 907 → 908 유닛. (M-7)
[1.58.11] — 2026-05-20
fix(ux): M-4 — 저장된 리서치 카드의 제목↔날짜 간격을 구조적 CSS 로 (기존 인라인 margin). v1.58.3 MASTER 회귀에서 일부 카드가 software-engineer-generaltoday (제목과 날짜 사이 공백 없음)로 표시됨을 확인. 원인은 두 개의 <span> 사이의 style="margin-left: 8px" 인라인 의존이 특정 항목에서 무너지는 것. public/js/views/deep.js 수정 — 두 개의 <span> 을 .saved-card__title + 시맨틱 <time class="saved-card__date" datetime="…"> 로 교체하고 플렉스 컨테이너 .saved-card 로 감쌈. 간격은 gap: var(--space-2, 8px) 가 제어 → 더 이상 무너지지 않으며 <time> 으로 a11y/SEO 시맨틱도 확보. 906 → 907 유닛. (M-4)
[1.58.10] — 2026-05-20
fix(ux): M-2 — 결과 모달 열기 전에 진행 토스트 비우기. #/cv 에서 sync-check 클릭 시 “Running cv-sync-check.mjs…” 토스트가 우하단에 남은 상태로 결과 모달이 열려 좁은 화면에서는 시각적으로 겹쳤습니다. Health 페이지 Doctor / verify-pipeline 은 이미 UI.modal() 전에 UI.dismissToast() 를 명시 호출했으나 cv.js sync-check 만 누락. public/js/api.js 에서 UI.modal() 의 첫 실행 문장으로 dismissToast() 를 호출하도록 변경(경계에서의 심층 방어). cv.js 의 하드코딩 영문 문자열을 t('cv.syncCheckRunning') / t('cv.syncCheck') 로 교체해 BUG-008 불변 조건(모달 타이틀 == 로컬라이즈된 버튼 라벨)도 충족. 새 i18n 키 2 개를 8 개 언어에 추가. 905 → 906 유닛. (M-2)
[1.58.9] — 2026-05-20
fix(a11y): M-1 — 폼 필드에 가시적 :focus-visible 링 복원 (WCAG 2.4.7 Level AA). v1.58.3 MASTER 회귀에서 getComputedStyle(focusedInput) 이 outline: rgb(255,255,255) none 1.5px 를 반환 — none 키워드가 모든 폼 필드의 링 너비를 0 px 로 붕괴시킴을 확인. 근본 원인: .input, .textarea, .select { outline: none } 와 .searchbar input { outline: none } 의 베이스 규칙이 전역 *:focus-visible 보다 명시도가 높아 페이지당 88 개 포커스 가능 요소에서 키보드 포커스 링을 조용히 제거. public/css/app.css 에 명시적 .input:focus-visible/.textarea:focus-visible/.select:focus-visible 와 .searchbar input:focus-visible 규칙 추가, outline: 2px solid var(--rausch) + 반투명 box-shadow 부여. 마우스 포커스(:focus)는 그대로 유지. 904 → 905 유닛(정적 계약 가드); Playwright 60 → 61(Tab 순회). (M-1)
[1.58.8] — 2026-05-20
feat(health): OPENAI_API_KEY / QWEN_API_KEY / OPENROUTER_API_KEY 를 #/health 에 표시 (GEMINI_API_KEY 와 동일). v1.57.0 에서 OpenRouter 가 5번째 헤드리스 live-eval 공급자로 추가되었고, v1.55.3 (UX-2) 에서 4-공급자 온보딩 배너가 도입됐지만 #/health 페이지는 여전히 GEMINI_API_KEY 와 ANTHROPIC_API_KEY 만 노출했습니다. 사용자 요청: “set / unset (manual mode)” 행 패턴을 모든 헤드리스 공급자로 확장. server/lib/routes/health.mjs 에 3 개의 선택적 체크 행을 추가하며 /api/status/providers 와 동일한 isUsableKey 게이트를 사용. Health 뷰는 body.checks 를 순회하므로 8 개 언어 문자열 변경 불필요. 903 → 904 유닛. (사용자 요청)
[1.58.7] — 2026-05-20
fix(security): NEW-2 — isValidJobUrl 가 쌍을 이룬 템플릿 플레이스홀더 구문(${…}, {{…}})을 거부하도록 수정, 오류 메시지와 일치. POST /api/pipeline 의 400 응답은 “contain no script or template characters” 라고 안내하지만, v1.58.3 MASTER 회귀에서 실제로는 [<>"'\\s]가드의 부수 효과로 ASP/EJS 형식<%…%>만 차단되고 JS 템플릿 리터럴${TEST}과 Mustache/Handlebars{{TEST}}는 통과하던 것을 확인. fix-prompt Option A(정규식을 메시지에 맞춤): [server/lib/security.mjs](https://github.com/Fighter90/career-ops-ui/blob/main/server/lib/security.mjs) 에TEMPLATE_PATTERNS배열 추가,new URL(…)전에hasTemplatePlaceholder(url) 호출. **쌍을 이룬** 형식만 거부({normal}` 같은 단일 중괄호 ATS 패턴은 유지). 901 → 903 유닛. (NEW-2)
[1.58.6] — 2026-05-20
fix(a11y/i18n): BUG-008-tb — 상단바 Doctor 모달 타이틀이 로컬라이즈된 버튼 라벨과 일치하도록 수정. v1.58.0 에서 닫힌 원장 BUG-008(“모달 타이틀 == 로컬라이즈된 버튼 라벨”)은 Health 페이지 진입점에만 적용되어 있었으나, v1.58.3 MASTER 회귀에서 상단바 진입점이 불변 조건을 위반하고 있음이 확인되었습니다 — UI 언어와 무관하게 상단바 Doctor 클릭 시 모달 타이틀이 항상 doctor(영문 소문자)였습니다. public/js/app.js:118 에서 리터럴 'doctor' 를 I18n.t('top.doctor', 'Doctor') 로 교체. top.doctor 키는 8개 언어 모두에 이미 존재(EN Doctor · ES/pt-BR Diagnóstico · KO 진단 · JA 診断 · RU Диагностика · zh-CN 诊断 · zh-TW 診斷)하며 버튼이 data-i18n="top.doctor" 로 선언한 키와 동일합니다. tests/qa-report-fixes.test.mjs 에 정적 계약 가드 추가. 900 → 901 유닛; Playwright 60/60. (BUG-008-tb)
[1.58.5] — 2026-05-20
fix(ui): NEW-3 — #/followup Run-live 이중 POST 은 재현 불가 로 분류; Playwright 회귀 가드로 잠금. v1.58.3 MASTER 회귀에서 monkey-patched window.fetch 측정으로, #/followup Run live 한 번 클릭(회사/역할/메모 입력 완료, 날짜는 의도적으로 비움) 후 약 2 초 이내에 /api/mode/followup 로 동일한 POST 두 건이 관측되었습니다. fix-prompt 의 “먼저 재현” 원칙에 따라 public/js/views/mode-page.js::submit() 을 정밀 검토한 결과: (a) Run live 와 Generate prompt 는 모두 일반 <button> 으로 각각 단일 onClick 만 가지고 있으며, 부모 <form> 도 addEventListener('submit') 도 없어 이중 발화 경로가 존재하지 않고, (b) UI.withSpinner() (FIX-L1) 가 요청이 진행되는 동안 button.disabled = true 를 설정하여 두 번째 물리 클릭을 원천 차단합니다. tests/playwright-smoke.mjs 에 회귀 레시피를 그대로 따라가는 새 테스트를 추가했습니다 — 회사/역할/메모를 채우고 날짜는 비운 채 Run live 와 동일한 submit() 을 호출하는 수동 버튼을 한 번 클릭하면, 3 초 윈도우에서 POST /api/mode/followup 가 정확히 1 건 임을 검증합니다. 로케일 안정 선택자(8 개 언어 모두 ▶ 글리프 동일) + 동일 브라우저 컨텍스트의 이전 언어 테스트 영향을 막기 위해 addInitScript 로 career-ops-ui:lang=en 사전 주입. Playwright 59 → 60. 원본 QA 관찰은 레시피로만 기록되며 제품 코드 변경은 필요 없습니다. (NEW-3)
[1.58.4] — 2026-05-19
fix(security): NEW-1 — 모든 응답에 Content-Security-Policy 전송 (이전에는 루프백에서 누락). v1.58.4 이전에는 isPubliclyExposed() 가 참일 때(HOST 가 루프백 밖에 바인딩)만 CSP 헤더를 추가했기 때문에 127.0.0.1 에서는 / 와 /api/health 모두 CSP 없이 응답했고, UI.md() 의 escape-first 계약이 유일한 XSS 방어였습니다. v1.58.3 MASTER 회귀 테스트(§5)가 이를 stop-ship 불변 조건으로 지적했습니다. 이제 CSP 는 바인드 주소와 무관하게 모든 응답에서 무조건 동일하게 전송됩니다: default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'. script-src 는 'unsafe-inline'/'unsafe-eval' 를 절대 허용하지 않습니다. 디렉티브 집합은 이전의 노출 전용 정책에서 변경되지 않아(이미 SPA 에 맞게 구성 — Inter 용 Google Fonts 허용) 시각·기능 회귀가 없습니다. tests/security-headers.test.mjs 를 다시 작성했고, Playwright 경로 순회(en/ru/ja/zh-TW × 7 경로)에서 CSP 위반 0 을 검증합니다. 유닛 900 · Playwright 58→59 · e2e 20/20+23/23. 후속 fix-prompt 항목은 프로젝트 원칙에 따라 one-fix 릴리스로 순차 배포됩니다. (NEW-1)
[1.58.3] — 2026-05-19
fix(deep): R-2 / FIX-C1 — 조사 출력에서 고아/불균형 에이전트 스캐폴딩 태그 제거. v1.58.0 cleanLlmMarkdown는 쌍 블록과 말단 여는 태그만 제거했음. v1.58.2 심층 회귀에서 여는 짝 없는 고아 </tool_response>(</thinking>)가 저장된 #/deep 브리프에 그대로 노출. 최종 보수적 스윕이 단독 스캐폴딩 토큰(여닫기·균형 무관), Anthropic 도구 XML(<invoke>/<parameter>/antml:*), tool_* 펜스를 제거. 순수·멱등. 실제 <https://…> 자동링크/코드 보존. FIX-C2는 재현 안 됨(i18n.js가 이미 <html lang> 설정 + navigator.language 감지). 둘 다 회귀 가드로 고정. 896 → 900 유닛 · Playwright 58/58. v1.58.3 fix-prompt 잔여는 one-fix ship 대기(배치 금지).
[1.58.2] — 2026-05-19
fix(i18n): I18N-011 — #/help 목차를 비-EN 7개 로케일에서 현지화. TOC는 docs/help/<lang>.md의 ## 제목으로 생성됩니다. 3/4/6/7/8/9/10/11/12 섹션이 es/pt-BR/ko/ja/ru/zh-CN/zh-TW에서 여전히 영어 제목이라 사이드바는 번역됐는데 TOC는 영어였습니다. 각 제목을 사이드바 nav.* 키와 동일한 용어로 현지화(단일 진실원 — TOC ↔ 사이드바 일치), 섹션 번호와 (#/route …)는 원문 유지. EN 불변. v1.58 QA의 유일한 i18n 백로그 해소. 문서 전용; 896/896 유닛 · 33/33 help · Playwright 58/58.
[1.58.1] — 2026-05-19
fix(test): CI 격리 checkProfileCustomized 가드 (v1.58.0 패치). v1.58.0은 advisory pre-commit는 통과했지만 ci.yml(Node 18/20/22)에서 실패: 테스트가 cache-bust 동적 import + PATHS 재작성을 사용했으나 paths.mjs는 프로젝트 루트를 프로세스당 한 번 해석. 견고한 정적 가드(allow-list + ^(…)$/i 앵커 정규식; “test” 포함 실명 오탐 없음)로 교체. 프로덕션 코드 변경 없음; publish-package.yml도 해제. 896/896 유닛 · Playwright 58/58. qa/v158-regression/ 참고.
[1.58.0] — 2026-05-19
fix(qa): 외부 QA 리포트 버그 일괄 수정 + 깔끔하게 포맷된 조사 출력. 수정: BUG-001 #/followup 선택 날짜를 클라이언트에서 ISO YYYY-MM-DD 검증; BUG-003 블록 인용 내부에서도 **굵게**/`코드`/링크 렌더링(모든 Help 페이지); BUG-005 중복 URL은 「이미 대기열에 있음 — 건너뜀」; BUG-006 잘못된 URL 메시지 사람 친화적으로((POST /api/pipeline · HTTP 400) 컨텍스트는 의도적으로 유지); BUG-007/008 「Running doctor.mjs…」 토스트를 모달 전에 제거(새 UI.dismissToast()), 모달 제목 = 버튼의 현지화 라벨; BUG-010 #/reports 빈 상태에 부제목; BUG-002/UX-032 checkProfileCustomized()가 테스트 픽스처 이름을 「미설정」으로 표시(부모 profile.yml/cv.md 미변경 — 규칙 #1); I18N-012/013 러시아어 Deep research 실제 번역. 신규: cleanLlmMarkdown()가 에이전트 스캐폴딩(<tool_call>{…}</tool_call>, <tool_response>, <thinking> …)을 #/deep·저장된 조사에서 제거(모든 제공자 + 저장 파일 제공 시); #/outreach→#/contacto 별칭(BUG-004); 클라이언트 네트워크 오류 I18n.t() 현지화(8개 로케일; 서버 details는 의도적으로 영어 진단). 테스트: 새 tests/qa-report-fixes.test.mjs(10)·tests/llm-output.test.mjs(5), 881 → 896 유닛, Playwright 58/58. 미변경(근거 포함): BUG-009(#/cv H1 — 설계상 WCAG single-h1), 부모 데이터(parent-owned), minor i18n/UX 롱테일은 백로그. 자세한 내용은 CHANGELOG.md.
[1.57.2] — 2026-05-19
fix(config): /#/config「validation failed」의 진짜 원인 — SPA가 주입하는 lang 필드. public/js/api.js는 모든 JSON POST 본문에 lang을 자동 첨부합니다(LLM 라우트가 UI 로케일을 얻도록). /api/config는 LLM 라우트가 아니고 lang은 설정 키가 아니므로, validateConfig의 (정확하고 보안상 중요한) 미지 키 거부가 매 저장마다 400을 반환했습니다: validation failed — lang: not a known config key. 브라우저 전용 증상으로, curl/인프로세스 재현은 lang을 보내지 않아 v1.57.0/.1은 메시지만 개선하고 원인은 남았습니다. 이제 설정 라우트는 검증 전 전송용 lang을 제거합니다. KNOWN_KEYS 쓰기 필터는 여전히 진짜 미지 키를 버려 — 인젝션 가드 불변. 실제 저장 버튼을 누르는 새 Playwright 폼 순회로 발견. 테스트: 새 tests/playwright-forms.mjs(26, npm run test:e2e:browser에 포함)로 모든 폼 순회, config-endpoint에 브라우저 동등 케이스. 879 → 881 유닛, Playwright 32 → 58. 자세한 내용은 CHANGELOG.md.
[1.57.1] — 2026-05-19
fix(ux): 모든 API 오류가 무엇이 실패했고 어디서 왜인지 표시; 입력 오류 문구도 최대한 설명적으로. 서버는 이전부터 { error, details: ["필드: 사유", …] }를 반환했지만 폼들은 상단 줄(「validation failed」)만 표시해 /#/config(및 전 화면)에서 어느 필드가 문제인지 알 수 없었습니다. 이제 api.js가 필드별 details를 메시지에 사이트 전역으로 합치고(한 곳 수정으로 모든 폼이 혜택), 요청 컨텍스트 (메서드 /경로 · HTTP NNN)(어디서)를 덧붙이며, 비 JSON 응답은 원문 일부를 표시하고, 네트워크 오류에도 메서드+경로를 포함합니다. err.details도 노출. validateConfig 메시지는 최대한 설명적(무엇이 잘못됐고 어떻게 고치는지)으로 변경. 시크릿 키는 입력값을 절대 에코하지 않음(글자 수만) — 실제 키 오타가 토스트/로그로 새지 않습니다. PORT 범위도 실제로 검증(99999 거부). /#/config의 PORT/HOST는 실제 기본값(4317 / 127.0.0.1)으로 미리 채움. 오류 토스트는 더 오래(9–20초) 표시되고 잘리지 않고 줄바꿈/스크롤됩니다. 테스트: 새 tests/config-validation-detail.test.mjs(12), 874 → 879. 자세한 내용은 CHANGELOG.md.
[1.57.0] — 2026-05-19
feat(provider): OpenRouter를 5번째 헤드리스 라이브 평가 공급자로 추가 + fix(config): 어떤 API 키를 저장해도 「validation failed」 발생하던 버그 수정. 붙여넣은 키는 흔히 끝에 줄바꿈이나 공백이 붙어 옵니다(OS 클립보드, 공급자 콘솔의 「복사」 버튼). 1.57 이전에는 이것이 모든 공급자에서 줄바꿈 가드를 건드렸고, $ 종단 앵커 ANTHROPIC_API_KEY 정규식이 실제 Anthropic 키를 잘못 거부했습니다. 이제 validateConfig는 검증 전에 모든 값을 정규화(트림)하고, 라우트는 트림된 값을 저장(런타임 인증 성공, \n로 인한 .env 손상 없음)하며, Anthropic 검사는 견고한 sk-ant- 접두사 + 길이로 변경(공통 isUsableKey() ≥ 20자 기준이 진짜 「실제 키인가」 게이트). 내부 줄바꿈은 계속 거부(.env 인젝션 가드). OpenRouter가 1급 공급자가 됨: /#/config의 OPENROUTER_API_KEY 하나로 300개 이상 모델 접근. auto 순서의 마지막(Anthropic → Gemini → OpenAI → Qwen → OpenRouter)이라 기존 설정이 조용히 재라우팅되지 않으며, LLM_PROVIDER=openrouter로 고정. OpenAI/Qwen과 동일한 _tailProvider() 경로로 /api/evaluate·/api/deep·/api/mode/:slug에 연결, /api/status/providers + Health 대시보드에 노출. OpenAI 호환 클라이언트(새 의존성 없음 — 직접 fetch, AbortController 타임아웃, 키 미기록), 권장 HTTP-Referer/X-Title 헤더 포함. 모델 드롭다운은 라이브: OPENROUTER_MODEL은 GET /api/openrouter/models(OpenRouter 공개 카탈로그의 서버 측 프록시 — CSP connect-src 'self' 유지)에서 채워지며, 카탈로그 불가 시 선별 폴백, 10분 메모리 캐시. 8개 로케일에 새 i18n 키(config.openrouter*). 테스트: 새 tests/openrouter-route.test.mjs·tests/openrouter-model-selector.test.mjs, env-config/openai/provider-selector 확장. 831 → 855. 자세한 내용은 CHANGELOG.md.
[1.56.4] — 2026-05-19
feat(ui): UX-N2 — 전역 검색 입력에 플랫폼 인식 가시 ⌘K / Ctrl K 힌트. Cmd/Ctrl+K(검색 포커스) 단축키가 aria-label/소스에만 있어 시각 사용자는 발견하지 못했고 앱이 실제보다 느리게 느껴졌다. 이제 옅은 <kbd class="kbd-shortcut">가 검색 알약 끝에 표시되며 부팅 시 플랫폼 판정(navigator.platform/userAgent)으로 data-mac/data-other에서 채워진다: macOS/iOS는 ⌘K, 그 외는 Ctrl K. aria-hidden="true"(기존 aria-label이 이미 AT에 알리므로 중복 안내 방지)이며 pointer-events:none(장식). 기존 Cmd/Ctrl+K 키바인딩 불변. 새 i18n 키 없음(글리프는 공통); 배지는 기존 .searchbar의 flex 자식(래퍼/절대 위치 불필요 — input은 이미 flex:1). 테스트: 새 CI 격리 소스 정적 스위트 tests/cmdk-hint-visible.test.mjs(5): .searchbar 안에 <kbd class="kbd-shortcut">; aria-hidden="true" + data-mac/data-other 둘 다; app.js가 navigator 판정으로 채움; (e.ctrlKey||e.metaKey)&&e.key==='k' → search.focus() 바인딩 유지(회귀 가드); app.css가 .kbd-shortcut를 스타일링하고 display:none 아님. 826 → 831. feat(ui) · test: tests/cmdk-hint-visible.test.mjs. 자세히 — CHANGELOG.md.
[1.56.3] — 2026-05-19
fix(reliability): 프로바이더 키 감지가 빈 문자열뿐 아니라 플레이스홀더 / 너무 짧은 값도 거부. 부모 .env의 플레이스홀더 GEMINI_API_KEY가 “✓ set”으로 표시되고 유효한 ANTHROPIC_API_KEY를 제치고 활성 프로바이더로 선택되었다. effectiveEnv()는 undefined/''만 거부해 10자 쓰레기가 실제 키로 취급됨: 온보딩 배너가 GEMINI ✓ set, GET /api/status/providers가 activeProvider: "gemini" 반환, 유효한 108자 Anthropic 키를 무시하고 죽은 키로 모든 라이브 ⚡ 평가가 조용히 실패. 새 순수 함수 isUsableKey()(env-config.mjs)는 시크릿을 ≥ 20자(지원 프로바이더 키 중 더 짧은 것 없음 — Gemini AIza… ≈ 39, Anthropic sk-ant-… ≈ 100+, OpenAI ≥ 40, Qwen ≈ 35)이고 알려진 플레이스홀더(your_*_here, changeme, placeholder, <…>, 동일 문자 반복…)가 아닐 때만 설정된 것으로 간주. hasAnthropicKey()/hasGeminiKey()(anthropic.mjs), hasOpenAIKey()/hasQwenKey()(openai.mjs), GET /api/health의 GEMINI_API_KEY/ANTHROPIC_API_KEY 행(원시 process.env에서 동일한 effective+plausible 뷰로 이전)에 일괄 적용 — health 페이지·프로바이더 엔드포인트·OR 라우터가 항상 일치. selectActiveProvider()는 불변(올바른 keysConfigured를 받을 뿐). 테스트: 새 CI 격리 스위트 tests/key-detection-rejects-placeholder.test.mjs(5): isUsableKey 단위 + in-process createApp()로 보고 시나리오 재현(임시 .env에 10자 GEMINI_API_KEY + 실제 ANTHROPIC_API_KEY) — gemini는 keysConfigured에 없고 activeProvider === "anthropic", /api/health 행도 일치. 기존 effective-env 계층 테스트 4건은 너무 짧은 스텁을 현실적 길이로 연장(계약 불변). 821 → 826. fix(reliability) · test: tests/key-detection-rejects-placeholder.test.mjs. 자세히 — CHANGELOG.md.
[1.56.2] — 2026-05-19
feat(a11y): UX-N1 — 라우트별 로케일 인식 document.title(멀티탭 식별 + 스크린리더 페이지 변경 안내). 수정 전에는 24개 라우트가 모두 index.html의 정적 <title>(“career-ops — command center”)을 유지해 탭 이름이 동일하고 북마크가 일반적이며 “페이지 변경” 안내도 매번 같았다. public/js/router.js의 focusNewView()가 뷰 자체의 현지화된 <h1 class="page-title">에서 제목을 도출 — “뷰 — career-ops” — 하므로 자동 번역(새 i18n 키 불필요)되고 라우트별로 고유하다. 첫 페인트 가드 이전에 설정해 초기 탭에도 제목이 붙는다(v1.56.0 UX-12의 tabindex 설정과 동일한 순서). 제목이 없는 뷰는 career-ops — command center로 폴백. 테스트: 새 CI 격리 소스 정적 스위트 tests/document-title-per-route.test.mjs(4): focusNewView가 document.title 할당; 제목은 <h1>에서 파생(라우트별 + 현지화, 단일 리터럴 아님); 할당이 !firstPaintDone 이전; 제품 기본값 존재. 817 → 821. feat(a11y) · test: tests/document-title-per-route.test.mjs. 자세히 — CHANGELOG.md.
[1.56.1] — 2026-05-19
fix(a11y): 라우터가 관리하는 tabindex="-1" 제목 포커스에서 나타나던 가짜 브랜드 포커스 링 제거. public/js/router.js는 모든 클라이언트 내비게이션에서 대상 뷰의 제목에 tabindex="-1"을 부여하고 .focus()를 호출한다(스크린리더가 새 페이지를 읽도록). tabindex="-1" 요소는 키보드로 도달할 수 없지만 Chromium의 :focus-visible 휴리스틱이 여전히 전역 브랜드 링(*:focus-visible { outline: 2px solid var(--rausch) })을 그려, 내비게이션마다 페이지 제목 둘레에 빨간 사각형(예: #/dashboard의 “Command Center”)이 표시되었고 images/dashboard-*.png 히어로 스크린샷에도 박혔다. 수정은 한 개의 범위 한정 규칙 [tabindex="-1"]:focus, [tabindex="-1"]:focus-visible { outline: none }(WAI-ARIA APG 관리 포커스 패턴). 실제 키보드 포커스는 전역 *:focus-visible 링을 유지(WCAG 2.4.7 유지); skip-link 링은 영향 없음(<a>이며 tabindex="-1"이 아니고 더 높은 특이도). 8개 images/dashboard-*.png 재생성 — 빨간 테두리 사라짐. 테스트: 새 CI 격리 소스 정적 스위트 tests/managed-focus-no-ring.test.mjs(4): 전역 *:focus-visible 링 유지(WCAG 2.4.7 비퇴행); [tabindex="-1"]:focus,:focus-visible ⇒ outline:none; 억제 규칙이 전역 규칙 뒤(캐스케이드 안전); 수정 범위 한정(전역 *:focus{outline:none} 없음). tests/dashboard-initial-focus.test.mjs와 짝. 813 → 817. fix(a11y) · test: tests/managed-focus-no-ring.test.mjs. 자세히 — CHANGELOG.md.
[1.56.0] — 2026-05-19
feat(ux): LOW 다듬기 묶음 — UX-9 / UX-10 / UX-11 / UX-12 (하나의 묶음 마이너 릴리스). UX-9 #/cv: 페이지 제목을 조용한 .cv-breadcrumb 칩으로 격하하고 시끄러운 부제는 <h1>의 title 툴팁으로 이동 — 사용자 CV(미리보기의 이름)가 시각적 우선권을 가짐. F-V54-A 불변 유지 — 여전히 정확히 하나의 <h1>, 여전히 .page-title. UX-10 새 공유 헬퍼 UI.providerCostHint(t)를 #/auto·#/evaluate·#/deep·모든 #/<mode>의 ⚡ 라이브 실행 옆에 표시; GET /api/status/providers(v1.55.3) 재사용: 키 있으면 “예상 비용: OpenAI gpt-5-codex · ~$0.04/eval”(자릿수 추정, “~”), 키 없으면 ⚡가 수동 프롬프트를 복사(API 비용 없음)임을 명시; fail-soft. UX-11 #/help: TOC 필터가 정확히 1개 섹션으로 좁혀지면 300ms 유휴 후 해당 섹션으로 스크롤(디바운스; 0개나 2개 이상은 안 함). UX-12 #/dashboard: 첫 페인트에서 <h1>을 포커스 가능(tabindex="-1")으로 만들고 #content는 aria-live="polite" 유지(부팅 시 안내) — 포커스는 훔치지 않음(skip-link 충돌 회피, v1.41.0 결정). 새 i18n 키 cost.estimate, cost.manual ×8; 새 .cv-breadcrumb/.cost-hint CSS. 테스트: 4개 새 소스-정적 CI-격리 스위트(cv-breadcrumb 3, run-cost-line 4, help-toc-autoscroll 4, dashboard-initial-focus 3); 기존 cv-single-h1/help-nav-a11y 락 갱신(불변 보존). 800 → 813. 4개 라이브 Playwright 프로브, 콘솔 에러 0. feat(ux) · 4 test suites. 자세히는 CHANGELOG.md.
[1.55.8] — 2026-05-19
feat(tracker): 서버 사이드 페이지네이션 + 클릭 가능한 퍼널 칩 (UX-8). 서버: GET /api/tracker 가 선택적 ?page / ?pageSize / ?status 쿼리 파라미터를 가짐. 없으면 응답은 기존 { rows: [...] } 와 바이트 단위로 동일(모든 기존 호출자/테스트 무영향). 있으면 { rows: slice, total, page, pageSize, funnel } 반환 — pageSize 는 [1,500], page 는 ≥1 로 클램프, status 는 rows+total 필터, funnel 은 전체 이력의 상태→개수 분포(페이지/필터와 무관하므로 칩이 항상 정확). #/tracker: 상단에 새 클릭 가능한 퍼널 칩 바 — “모든 상태 · N · Applied · N · Interview · N …”(순서 Applied → Responded → Interview → Offer → Rejected → Discarded → Evaluated → SKIP). 칩 클릭은 Status 필터 설정(활성 칩 재클릭 시 해제); 활성 칩은 aria-pressed + 하이라이트. 8개 로케일 새 i18n 키 track.funnelAria; 새 .tracker-funnel/.tracker-chip/.tracker-chip--active CSS. test: tests/tracker-server-paged.test.mjs(신규, 7 케이스, CI-격리, 임시 포트 인프로세스 Express + 임시 CAREER_OPS_ROOT applications.md — CLAUDE.md #2/#8): back-compat(파라미터 없으면 정확히 {rows}); ?page&pageSize 슬라이스 + total/page/pageSize/funnel 합 N; 마지막 부분 페이지 비중복; 범위 밖 페이지 ⇒ 빈 rows + 유효 total; ?status= 가 total/rows 필터하되 funnel 은 전체 이력; pageSize 캡; + 칩 바 소스-정적 잠금. 793 → 800. feat(tracker) · test: tests/tracker-server-paged.test.mjs. 자세히는 CHANGELOG.md.
[1.55.7] — 2026-05-19
feat(pipeline): >1000행에서 vanilla-JS 행 가상화 (UX-7). #/pipeline 은 모든 행을 렌더(filtered.forEach(list.appendChild(urlRow)))했음 — 스캔은 큐를 수천 개 URL로 채우므로 수천 개 행 노드(각각 flex div + <a> + 버튼 2개)가 필터 키 입력마다 동기적으로 생성되어 DOM 과 접근성 트리를 폭주시켰음. 새 vanilla-JS 가상화(react-window 등가, 의존성 없음): VIRTUALIZE_THRESHOLD = 1000 초과 시 #/pipeline 은 고정 높이(70vh) 스크롤 뷰포트가 되고, 줄어들지 않는 스페이서(flex:0 0 auto, height = 행수 × 56px)가 전체 목록의 실제 스크롤바를 보존하며, rAF 스로틀 스크롤 리스너가 뷰포트 ± 5행 버퍼만 렌더(한 번에 N개가 아니라 ~16–19 노드). 임계값 이하에서는 기존 단순 렌더를 바이트 단위로 유지하므로 일반 파이프라인과 기존 테스트/e2e 가 영향 없음. 각 가상화 행은 URL 로 구분된 ▶/✕ aria-label 유지(F-V54-B 회귀 잠금). 윈도우 계산은 순수 computeWindow() 헬퍼. test: tests/pipeline-virtualize.test.mjs(신규, 5 케이스, CI-격리, 소스-정적): ~1000 수치 임계값; ≤임계 분기는 forEach→appendChild 유지; >임계 분기는 rAF 스크롤 리스너 + 스페이서로 slice(start,end) 렌더; computeWindow() 가 [0,total] ± 버퍼 클램프; 행이 ▶/✕ aria-label 유지. 788 → 793. 라이브 Playwright 프로브(1200-URL 픽스처): scrollHeight≈67248, DOM 에 ~16–19 노드만, 윈도우가 스크롤을 끝까지 추적, 콘솔 에러 0. feat(pipeline) · test: tests/pipeline-virtualize.test.mjs. 자세히는 CHANGELOG.md.
[1.55.6] — 2026-05-19
feat(scan): 보조 필터를 “고급 필터” 디스클로저 뒤로 정리 (UX-4). #/scan 은 모든 필터 — 자유 텍스트, 원격/하이브리드/온사이트, 범위, 소스, 스캔 후 stack/level/dynamic 패싯 칩 — 를 동일 가중치로 쌓아 컨트롤의 벽이었음. 이제 일상 필터는 보이게 유지(자유 텍스트 + 원격/하이브리드/온사이트; 🌐 스캔 버튼은 이미 컨트롤 카드에 별도)하고 보조 필터는 <details class="scan-advanced"><summary>고급 필터</summary> 뒤로 접힘: 범위 + 소스 셀렉트, 그리고 별도로 패싯 칩 클러스터(이제 새 결과 집합이 칩 벽이 아니라 테이블로 시작하며, 칩 행이 하나 이상일 때만 렌더). 8개 로케일 새 i18n 키 scan.advancedFilters; 새 .scan-advanced 요약 스타일(은은한 ⚙ 어포던스, 마커 없음, 열림 시 굵게). test: tests/scan-advanced-disclosure.test.mjs(신규, 6 케이스, CI-격리, 소스-정적): .scan-advanced 훅 + scan.advancedFilters 라벨의 <details>/<summary> 존재; 자유 텍스트 + 원격 가시 유지; 범위 + 소스 디스클로저 내부; chipsContainer 가 <details>; .scan-advanced summary 스타일; scan.advancedFilters ×8. 782 → 788. feat(scan) · test: tests/scan-advanced-disclosure.test.mjs. 자세히는 CHANGELOG.md.
[1.55.5] — 2026-05-19
feat(dashboard): 2개 P0 CTA + 초점 최근 활동 힌트의 히어로 (UX-3). #/dashboard 는 동일 가중치의 ~30개 노드로 열려 “다음에 뭘 할지”가 불명확했음. 새 .dash-hero 블록이 이제 페이지 헤더 바로 아래에 위치: 두 P0 여정 — ✨ URL 자동 파이프라인, 🌐 지금 스캔 — 을 큰 .btn-hero 버튼으로 승격하고, 단일 초점 최근 활동 힌트(“최근 평가: <점수> — <제목>”, 리포트로 링크; 콜드 스타트에서는 dash.heroNoEval 안내 빈 상태)가 재방문 사용자에게 멈춘 지점을, 신규 사용자에게 가장 중요한 단 하나의 행동을 알려줌. 두 기본 버튼은 헤더에서 제거(보조 “📋 파이프라인 열기”만 잔류)해 행동 중복 방지. 상태 버킷은 두드러진 .badge 에서 조용한 .dash-chip 알약으로 격하. 8개 로케일 새 i18n 키 dash.lastEval, dash.heroNoEval; 새 .dash-hero/.btn-hero/.dash-chip CSS. test: tests/dashboard-hero.test.mjs(신규, 5 케이스, CI-격리, 소스-정적): .dash-hero 존재 및 Quick-actions 그리드 선행; 두 P0 CTA 가 /auto+/scan 라우트의 .btn-hero; 초점 dash.lastEval + 빈 상태 dash.heroNoEval; 버킷이 .dash-chip; CSS 존재; dash.lastEval+dash.heroNoEval ×8. 777 → 782. feat(dashboard) · test: tests/dashboard-hero.test.mjs. 자세히는 CHANGELOG.md.
[1.55.4] — 2026-05-19
feat(ux): Run 옆 정직한 auto-pipeline ETA + 스캔 중 눈에 띄는 Stop (UX-6). #/auto: 새 .auto-eta 힌트 — “⏱ ~1–2분”(키 auto.eta, title은 auto.etaTitle) — 이제 Run 버튼 옆에 표시되어, 사용자가 누르기 전에 원클릭 약속이 소요 시간에 대해 정직하도록 함; 문구는 career-ops.org/docs(“URL 붙여넣기 → 1–2분 내 전체 리포트”)와 일치. #/scan: 수 분짜리 크롤이 실행 중(aria-busy)일 때 Stop 을 저대비 고스트 버튼에서 눈에 띄는 파괴적 버튼으로 승격(새 .btn-danger — 채움, 고대비 흰색 온 코랄, 굵기 600). setScanRunning(running) 이 scan-stop-btn 을 btn-danger(실행 중)와 btn-ghost(유휴, 어차피 숨김) 사이에서 전환하여, 부하 상황에서도 사용자가 Stop 을 찾고 신뢰하게 함. 8개 로케일 새 i18n 키 auto.eta, auto.etaTitle; 새 .btn-danger/.auto-eta CSS. test: tests/auto-eta-stop.test.mjs(신규, 4 케이스, CI-격리, 소스-정적): #/auto 가 runBtn 옆에 .auto-eta 클래스로 t('auto.eta') 렌더; auto.eta ×8; setScanRunning(running) 이 Stop 을 btn-danger 로 승격; .btn-danger 가 고대비 흰색 텍스트로 존재. 773 → 777. feat(ux) · test: tests/auto-eta-stop.test.mjs. 자세히는 CHANGELOG.md.
[1.55.3] — 2026-05-19
feat(onboarding): 화면상 4-제공자 OR 상태 — 콜드 스타트 배너 + 활성 제공자 칩 (UX-2, HIGH). 새 읽기 전용 엔드포인트 GET /api/status/providers → { activeProvider, activeModel, keysConfigured }. keysConfigured 는 llm.mjs 게이트와 동일한 effective-env 뷰(process.env ∨ 부모 .env); activeProvider 는 OR-라우터가 실제로 고를 값 — env-config.mjs 의 새 순수 헬퍼 selectActiveProvider() 가 providerOrder() 를 순회(키 없는 LLM_PROVIDER 핀은 null). 비밀은 반환 안 함 — 제공자 이름 + 모델 id 만. SPA 셸이 전역 온보딩 영역(#onboarding-banner, app.js 가 채움, CSP 안전 DOM)을 렌더: 0 키 → 빨간 배너 + #/config?tab=api-keys CTA; ≥1 키 → 은은한 칩 활성 제공자+모델. 대표 차별점(“Anthropic / Gemini / OpenAI / Qwen 중 하나, 자동 순서”)을 시행착오 대신 화면에서 발견 가능하게 함. 8개 로케일 onboarding.* i18n 키; 새 .onboarding-warn/.onboarding-ok CSS. test: tests/onboarding-key-banner.test.mjs(신규, 9 케이스, CI-격리): selectActiveProvider 의미; GET /api/status/providers 인프로세스(임시 포트 + 임시 CAREER_OPS_ROOT .env 로 실제 부모 키 비참조 — CLAUDE.md #2/#8); 정적 SPA 배선 + onboarding.* ×8 커버리지. 764 → 773. feat(onboarding) · test: tests/onboarding-key-banner.test.mjs. 자세히는 CHANGELOG.md.
[1.55.2] — 2026-05-18
fix(cv): #/cv 마크다운 편집기에 서술적이고 자체 완결된 접근 가능 이름 부여 (F-V55-H / UX-5). #/cv 의 기본 편집기 <textarea id="cv-editor"> 는 이제 새 cv.editorAria 키를 통해 서술적 aria-label — “CV 마크다운 편집기 — 마크다운 형식의 전문 이력서” — 을 가지며, 보이는 “Markdown” 섹션 제목에서 물려받던 간결한 이름을 대체함. 참고: F-V55-H 증상(aria-label/labels 만 검사)과 달리 이 필드는 이름이 없지 않았음 — v1.47.0(WS2 #16)이 이미 aria-labelledby → <h3 id="cv-md-heading">Markdown</h3> 로 바인딩하여 스크린 리더가 “Markdown, 편집, 여러 줄” 로 안내했음. v1.55.2 는 그 간결한 “Markdown” 을 자체 완결 레이블로 개선함. 중복 aria-labelledby 는 제거됨(남으면 죽은 마크업 — ARIA 우선순위상 aria-label 이 이김); 보이는 <h3>Markdown</h3> 은 비장애 사용자를 위해 유지됨. WCAG 1.3.1 + 4.1.2; v1.54.5 batch-tsv 수정(F-V54-C)과 평행. test: tests/cv-editor-a11y.test.mjs(신규, 3 케이스, CI-격리, auto-stepper-prerender.test.mjs 식 소스-정적): #cv-editor 는 비어있지 않은 폴백과 함께 t('cv.editorAria', …) 로 자신을 명명; cv.editorAria 는 8개 로케일 전부에 존재하고 비어있지 않음; 요소에 중복 aria-labelledby 없음. 761 → 764. fix(cv) · test: tests/cv-editor-a11y.test.mjs. 자세히는 CHANGELOG.md.
[1.55.1] — 2026-05-18
fix(auto): #/auto 마운트 시 5단계 파이프라인 스테퍼를 사전 렌더 (F-V55-E / UX-1, 시니어 관찰 S-4 재개). #/auto 는 이제 문서화된 다섯 단계 개요 — 검증 → 가져오기 → 평가 → 리포트 저장 → 트래커 추가 — 를 화면이 마운트되는 순간에 보여줌. 이전에는 첫 SSE 이벤트 전까지 빈 상태였음. 예전에는 <ol class="auto-stepper"> 가 display:none 으로 생성되고 renderStepper() 가 setStep() / run() 에서만 도달되어, 콜드 스타트 사용자는 Run 클릭 전에 문서가 약속한 파이프라인을 결코 보지 못했음. 스테퍼는 이제 마운트 시 다섯 단계 모두 pending 상태로 표시되며 aria-label(auto.stepperAria)을 가져 보조 기술이 해당 영역을 안내함. F-V55-E(a11y/정적 보장 렌즈)와 UX-1(약속 충실도 렌즈)을 닫음 — 동일 수정, 두 렌즈. test: tests/auto-stepper-prerender.test.mjs(신규, 4 케이스, CI-격리, router.test.mjs 식 소스-정적): STEPS 배열은 정확히 5개 표준 단계가 순서대로; stepperEl 은 마운트 시 display:none 이 아니고 auto.stepperAria 를 가짐; 마운트 범위 renderStepper() 호출이 function setStep( 보다 앞섬; auto.stepperAria 는 8개 로케일 전부에 존재. 757 → 761. fix(auto) · test: tests/auto-stepper-prerender.test.mjs. 자세히는 CHANGELOG.md.
[1.55.0] — 2026-05-18
feat(llm): 헤드리스 라이브 평가가 “OR” 로 동작 — Anthropic | Gemini | OpenAI | Qwen, 어떤 키가 설정됐는지에 따라 자동 선택. 사용자 요청에 따라 web-ui 의 ⚡ 라이브 평가는 이제 Anthropic/Gemini 뿐 아니라 설정된 어떤 API 키로도 동작함. LLM_PROVIDER 에 openai 와 qwen 추가; auto(기본값)는 키가 존재하는 첫 제공자를 사용하며 Anthropic → Gemini → OpenAI → Qwen 순으로 선호함. 명시값은 하나를 고정함; 키 없이 강제된 제공자는 여전히 수동 프롬프트 경로로 떨어짐. 새 server/lib/openai.mjs — 의존성 없는 OpenAI 호환 Chat Completions 클라이언트(anthropic.mjs 와 동일한 안전한 직접 HTTPS 패턴: AbortController 타임아웃, 키 결코 로깅 안 함, effectiveEnv() 키 해석으로 부모 .env 의 키가 재시작 없이 동작). 하나의 코어(runOpenAICompatible)가 runOpenAI(api.openai.com)와 runQwen(Alibaba DashScope 의 OpenAI 호환 모드; 중국 본토 호스트는 raw .env 에서 QWEN_BASE_URL 로 엔드포인트 재정의)를 뒷받침함. SDK 없음, 임의 CLI 실행 없음 — 부모 프로젝트는 CLI 비종속 유지(Claude Code · Codex · Gemini · OpenCode · Qwen · Copilot · Kimi); 이것은 헤드리스 API-키 경로만 확장함. OpenAI/Qwen 꼬리는 모든 평가 표면에 연결됨: /api/evaluate, /api/deep, /api/mode/:slug, /api/auto-pipeline SSE — auto 선호를 보존하기 위해 Anthropic(인라인) + Gemini(서브프로세스) 분기 뒤에서 참조되며, Anthropic 이 쓰는 것과 동일한 번들 컨텍스트 인라이닝 사용. env-config.mjs: QWEN_API_KEY(비밀) + QWEN_MODEL(비밀 아님)을 KNOWN_KEYS/KEY_GROUPS.core 에 추가; LLM_PROVIDERS 와 providerOrder() 확장; OPENAI_API_KEY 는 이제 일급 헤드리스 제공자 키(이전에는 저장 전용). #/config API-키 탭: LLM_PROVIDER 셀렉트에 openai/qwen 추가; 새 QWEN_API_KEY + QWEN_MODEL 필드(큐레이팅된 qwen-max/qwen-plus/qwen-turbo/qwen2.5-* 목록); 탭 상단의 새 노트가 CLI 비종속 부모 vs web-ui 헤드리스 평가와 OR 순서를 설명함. 8개 로케일 전체에 새 i18n 키. test: tests/openai.test.mjs(신규, 9 케이스, CI-격리): OpenAI/Qwen 성공 + 블록 배열 콘텐츠, Bearer 인증, 기본 및 QWEN_BASE_URL 재정의 엔드포인트, 4xx/5xx/형식 오류, max_tokens 클램프, 타임아웃, effectiveEnv 키 감지, 키 무유출 카나리. tests/provider-selector.test.mjs 는 v1.55.0 의 providerOrder/LLM_PROVIDERS/SECRET 표면 + OpenAI/Qwen 꼬리 배선용으로 갱신됨. 748 → 757. feat(llm) · test: tests/openai.test.mjs · test: tests/provider-selector.test.mjs. 자세히는 CHANGELOG.md.
[1.54.10] — 2026-05-18
fix(auto-pipeline): SSE 클라이언트 연결 해제 위생 — 불안정한 Playwright e2e 잡을 제거. Playwright e2e 잡이 간헐적으로 빨강이 되었음(개별 테스트 32/32 통과하지만 not ok 2 - tests/playwright-smoke.mjs): #/auto SSE 스트림이 진행 중일 때 페이지를 닫으면 서버의 다음 res.write() 가 EPIPE/"aborted" 로 거부되었고 —응답에 'error' 리스너가 없어서— Node 가 이를 uncaughtException 으로 격상시켜 node:test 가 “asynchronous activity after the test ended” 로 보고했음. auto-pipeline.mjs 의 openSse() 는 이제 no-op res.on('error') 를 등록하고 send() 를 res.writableEnded || res.destroyed 로 보호함(try/catch 로 감쌈) — 사라진 클라이언트는 예외가 아니라 예상된 일임. 이것은 단순한 테스트 수정이 아니라 올바른 프로덕션 SSE 위생임. tests/playwright-smoke.mjs: Cmd+K 테스트가 실제 외부 URL(https://example.com/jobs/123)을 사용했지만 모달이 나타나기만 기다렸으므로, closePage() 가 테스트 종료 후 서버의 진행 중인 safeGet() 을 중단시켰음. 이제 파이프라인이 종단 상태에 도달할 때까지 기다림(닫기 전에 fetch 가 정상적으로 해소되도록). 공유 closePage() 헬퍼(window.stop() 후 닫기)와 after 훅의 server.closeAllConnections() 는 심층 방어로 유지됨. 검증: 연속 8/8 그린 실행(6× node --test + 2× browser-smoke), 이전에는 ~2회 중 1회 빨강. tests/auto-pipeline.test.mjs +1 정적 케이스로 openSse 연결 해제 위생 계약을 고정(res.on('error') 리스너 + writableEnded||destroyed 가드 + try 로 감싼 쓰기). 747 → 748. fix(auto-pipeline) · test: tests/auto-pipeline.test.mjs. 자세히는 CHANGELOG.md.
[1.54.9] — 2026-05-18
fix(llm): 요청 시점에 부모 .env 의 LLM 키를 존중함 — 오래되었거나 잘못된 제공자로의 오라우팅을 중단. 라이브 평가가 ANTHROPIC_API_KEY 가 구성된 제공자였음에도 “Gemini API error: API key not valid” 로 실패할 수 있었음. 근본 원인: hasAnthropicKey() / hasGeminiKey()(그리고 runAnthropic 의 키/모델 조회)가 부팅 시점의 process.env 스냅샷만 읽었음. 서버가 시작된 후 Anthropic 키가 부모 .env 에 추가되면 실행 중 프로세스는 그것을 결코 보지 못함 → Anthropic 감지가 false 가 되고, 평가는 process.env 에 실제로 있던 오래된 키(흔히 낡고 잘못된 GEMINI_API_KEY)로 폴백했음. Gemini 실행 경로(부모 Node 서브프로세스)는 이미 부모의 라이브 .env 를 읽었으므로 두 제공자가 키를 일관성 없게 해석했음. env-config.mjs 의 새 effectiveEnv(key, envFilePath): 비어 있지 않은 process.env 값이 우선(셸 export 와 POST /api/config 의 라이브 적용을 커버), 그렇지 않으면 현재 부모 .env 파일을 참조함. anthropic.mjs 는 이제 ANTHROPIC_API_KEY, ANTHROPIC_MODEL, 그리고 Gemini 키 검사를 이를 통해 해석하므로, 부모 .env 에 설정된 키가 서버 재시작 없이 존중되고 키 감지가 요청이 실제로 보내는 키와 항상 일치함. 제공자 순서는 변경 없음(auto → Anthropic-그-다음-Gemini); 이것은 감지만 고침. 키는 결코 로깅되거나 반환되지 않음(REVIEW-B4 무유출 테스트는 여전히 통과). tests/anthropic.test.mjs 는 CI-격리되도록 재작성됨(temp CAREER_OPS_ROOT, 동적 import) — 정확한 버그를 재현하는 2개 새 케이스 포함(키가 부모 .env 에만 있음 → 감지됨; process.env 가 미설정일 때 runAnthropic 가 부모 .env 키 + 모델을 전송). tests/env-config.test.mjs +3 effectiveEnv 케이스(process.env 우선, 빈-문자열-을-미설정으로 포함한 .env 폴백, 파일-없음 / 키-없음 / 경로-없음 → undefined) — 새 분기의 100%. 742 → 747. fix(llm) · test: tests/anthropic.test.mjs · test: tests/env-config.test.mjs. 자세히는 CHANGELOG.md.
[1.54.8] — 2026-05-18
feat(config): Modes 필드 폼이 (비어 있거나 stub 인 파일에서도) 항상 career-ops.org 필드 안내와 함께 정규 스키마를 렌더링함. v1.54.3 Modes 필드 폼은 이미 존재하는 ## 섹션에 대해서만 필드를 렌더링했음 — 그래서 갓 생성되었거나 비어 있거나 스키마 비준수인 modes/_profile.md(예: 흔한 1줄 stub)에서는 “No ## sections found — use the raw editor below.” 로 폴백했고 사용자는 필드를 전혀 얻지 못했음. 사용자 요청(“разбей по полям … описание полей возьми из career-ops.org/docs”)에 따라, 폼은 이제 항상 문서화된 순서로 5개 정규 필드를 렌더링함(Target Roles, Adaptive Framing, Exit Narrative, Comp Targets, Location Policy), 파일에 있으면 미리 채워지고 없으면 비어-있지만-편집-가능 — 그래서 완전히 새 프로필도 폼을 통해 전부 작성 가능. 각 필드는 career-ops.org 정규 Quick Start §Step-5 에서 가져온 설명(Target Roles / Adaptive Framing / Exit Narrative / Comp Targets / Location Policy 에 무엇을 넣을지)을 표시하며, 스크린 리더용으로 aria-describedby 로 연결됨. 헤딩 변형 허용: 템플릿의 ## Your Target Roles(등)는 ## Target Roles 와 동일한 정규 필드로 매핑되어, 템플릿도 서버 스캐폴드 관행도 폼을 깨지 않음. collect() 는 이제 태깅된 페이로드임: 렌더링된 헤딩이 파일의 기존 헤딩과 정확히 일치할 때의 비파괴적 { sections } 병합(서두 + 손대지 않은 + 커스텀 섹션이 바이트-안정으로 보존됨), 또는 파일에 스키마가 없었을 때 스키마 준수 문서를 부트스트랩/정규화하는 { markdown } 전체 파일 재구성. 재구성 경로는 config.js 에서 확인-게이트됨(부모 파일을 대체함 — WS2 #4 파괴적-저장 불변식), 기존 서두(또는 문서화된 기본값)를 보존하고 비정규 섹션을 verbatim 으로 유지함. 8개 로케일 전반에 6개 새 i18n 키(config.modesDescTargetRoles … config.modesDescLocationPolicy + config.modesFormRebuildBody). tests/modes-form.test.mjs 는 v1.54.8 계약에 맞춰 재작성됨: 스키마 + 정규 순서, config.js 페이로드/확인 배선, 8개 로케일에서 각 필드의 문서-기반 설명 존재, canonicalKey “Your X” 허용, 리스트 라운드트립 안정성, 부트스트랩-항상-렌더링 보장, 그리고 데이터-안전성을 갖춘 태깅된 sections-vs-markdown collect(). 실제 부모 stub 파일에 대해 라이브 검증됨(5개 필드 + 설명 표시, 콘솔 에러 0)과 격리된 stub 픽스처(채우기 → 확인-게이트 저장 → 5개 정규 섹션 모두 영속화). feat(config) · test: tests/modes-form.test.mjs. 자세히는 CHANGELOG.md.
[1.54.7] — 2026-05-18
fix: W-001 — 코드/스타일 자산 + SPA 셸이 Cache-Control: no-store 로 제공됨(배포 위생). SPA는 api.js / router.js / 모든 뷰를 버전 쿼리 스트링 없는 평범한 <script src> 로 로드하며 빌드 단계가 없음(콘텐츠 해싱 없음). 그래서 배포 후 브라우저가 캐시된 옛 번들을 몇 시간 동안 계속 제공할 수 있었음 → 쿼리 스트링 라우트에서 stale-cache 404(v1.29.2 회귀 중 라이브 관찰됨; 회귀 런 W-001). server/index.mjs 가 이제 express.static 의 setHeaders 훅을 통해 .js / .mjs / .css / .html 에 Cache-Control: no-store 를 설정하고, SPA 셸 catch-all(sendFile 을 사용해 setHeaders 를 우회함)에도 명시적으로 설정하여, 브라우저가 라우팅을 구동하는 코드를 항상 재검증하도록 함. 비코드 정적 자산은 express.static 의 기본 캐싱을 유지함. 보안 헤더(CSP / nosniff / frame-deny / referrer-policy)는 변경 없음 — 기존 security-headers 스위트(8 케이스)가 새 테스트와 함께 그린으로 실행되어 검증됨. 테스트 파일 tests/asset-cache-control.test.mjs +1 — 4 케이스(JS 자산 no-store, CSS no-store, 정적 index.html no-store, SPA catch-all 딥 라우트 셸 no-store), 격리된 CAREER_OPS_ROOT 에 대해 실제 앱을 부팅함. 더해 tests/playwright-smoke.mjs 의 플레이키 teardown 수정(별도 test(e2e) 커밋): 자동 파이프라인 SSE 스모크 테스트가 이제 finally 에서 리더를 취소 + fetch 를 중단하고 after 훅이 잔존 소켓을 강제로 닫아, v1.54.6 Playwright e2e 잡을 붉게 만들던 teardown 이후 “Error: aborted” 를 제거함. 738 → 742. fix · test: tests/asset-cache-control.test.mjs. 자세히는 CHANGELOG.md.
[1.54.6] — 2026-05-18
fix(a11y): S-7 — #/help 의 back-to-top 버튼이 정규 선택자 클래스 back-to-top 를 가짐. #/help 의 플로팅 back-to-top 버튼은 올바르게 작동했으나(라이브 검증됨) 그 클래스 목록(btn btn-primary help-back-top)이 spec §2 #28 테스트가 겨냥하는 .back-to-top 선택자 컨벤션 밖에 있었음 — 더 엄격한 선택자였다면 플레이키했을 것(회귀 런 S-7, “쉬운 승리”). 이제 버튼은 정규 back-to-top 클래스도 가짐. 순수 가산적이며 CSS no-op: help-back-top(기존 CSS 훅)은 변경 없고 back-to-top 에는 CSS 규칙이 없음 — 안정적인 테스트/자동화 핸들일 뿐. 라이브 검증됨: document.querySelector('.back-to-top') 가 버튼을 해석, aria-label 유지, 콘솔 오류 0. tests/help-nav-a11y.test.mjs 의 기존 #12 케이스를 back-to-top 버튼의 클래스 목록이 정규 back-to-top 선택자를 포함한다는 어서션으로 확장(새 파일 없음). fix(a11y) · test: tests/help-nav-a11y.test.mjs. 자세히는 CHANGELOG.md.
[1.54.5] — 2026-05-18
fix(a11y): F-V54-C — #/batch TSV 편집기에 접근 가능한 이름이 있음. #/batch의 TSV <textarea>는 aria-describedby로 연결된 힌트는 있었으나 접근 가능한 이름이 없었음 — <label htmlFor> 없음, aria-label/aria-labelledby 없음(회귀 런 F-V54-C; WCAG 1.3.1 Info & Relationships / 4.1.2 Name, Role, Value). aria-describedby는 이름이 아니라 설명을 제공하므로, 스크린 리더가 레이블 없는 “edit text”를 안내했음. 이제 textarea는 새 i18n 키 batch.tsvAria를 통한 aria-label을 가지며, 이미 *Aria 키를 쓰는 형제 런-컨트롤 입력과 일관됨; 기존 describedby 힌트는 보존됨. 라이브 검증됨: aria-label 존재 + 현지화, aria-describedby 유지, 콘솔 오류 0. 새 i18n 키 batch.tsvAria를 8개 로케일 전반에 추가. 테스트 파일 tests/batch-tsv-accessible-name.test.mjs(2개 케이스: batch-tsv 블록이 describedby 힌트를 유지하면서 t(batch.tsvAria)를 통한 aria-label을 가짐; batch.tsvAria가 8개 로케일에 정의됨) +1; 736 → 738. fix(a11y) · test: tests/batch-tsv-accessible-name.test.mjs. 자세히는 CHANGELOG.md.
[1.54.4] — 2026-05-18
fix(a11y): F-V54-B — #/pipeline 행 액션 버튼에 접근 가능한 이름이 있음. #/pipeline의 행별 ▶(평가) 및 ✕(삭제) 버튼은 title 속성만 있는 아이콘 전용이었음(회귀 런 F-V54-B; WCAG 4.1.2 Name, Role, Value). title은 신뢰할 수 있는 접근 가능한 이름이 아니므로, 스크린 리더 사용자는 구별 불가능한 “button”의 긴 나열을 들었고 삭제가 어느 행에 적용될지 알 수 없었음. 이제 두 버튼 모두 새 shortUrl() 헬퍼를 통한 압축 URL(host + …/ + 마지막 2개 경로 세그먼트; 파싱 불가 입력에는 후행 슬라이스 폴백)로 명확화된 명시적 aria-label을 가지므로, a11y 트리가 예컨대 *“Delete: hh.ru/…/vacancy/12345”*로 읽힘. 새 i18n 키 없음 — common.delete / pipe.evaluateBtn + URL을 재사용. 라이브 검증됨: 1385개 행, 각 버튼 이름이 행마다 고유, 콘솔 오류 0. 테스트 파일 tests/pipeline-row-action-names.test.mjs(4개 케이스: 두 버튼 모두 shortUrl(url)로 연결 + 정확히 두 개의 그런 레이블, shortUrl이 사용 전에 선언됨, 동일 호스트 다른 채용 URL은 합쳐지지 않음, 베어 호스트 / 파싱 불가 / 빈 폴백) +1; 732 → 736. fix(a11y) · test: tests/pipeline-row-action-names.test.mjs. 자세히는 CHANGELOG.md.
[1.54.3] — 2026-05-18
feat(config): #/config “Modes” 탭을 위한 구조화된 필드 폼(더 이상 원시 마크다운 아님). “Modes” 탭은 modes/_profile.md를 ## 섹션마다 하나의 원시 <textarea>로 편집했음(v1.36.0 섹션 단위 세분성). 사용자 요청에 따라, 이제 문서화된 스키마에서 파생된 구조화된 필드 폼을 렌더링함(career-ops.org Quick Start §Step-5): Target Roles / Adaptive Framing / Comp Targets → 추가/제거 가능한 반복 레이블 라인 입력(필드마다 역할/앵글/comp 한 줄, + Add line / 행마다 aria-label이 있는 ✕); Exit Narrative / Location Policy → 단일 레이블 산문 <textarea>. 각 필드는 i18n 섹션 이름과 함께 <label htmlFor>로 바인딩된 실제 컨트롤임. 새 public/js/lib/modes-form.js(window.ModesForm)가 parse → render → collect() 로직을 소유함; 이는 기존 PUT /api/modes/_profile { sections } 머지 경로에 공급되므로, 프리앰블, 순서, 그리고 폼이 건드리지 않는 모든 섹션이 바이트 안정적으로 유지됨(머지-비교체, 서버 강제). 데이터 안전성: 본문이 순수 불릿 목록이 아닌 정규 목록 섹션(사용자가 거기에 산문을 넣음)과 비정규 ## 섹션은 설명 노트가 있는 레이블된 그대로의 <textarea>로 폴백됨 — 임의 콘텐츠는 그대로 round-trip되며, 결코 조용히 재구성되거나 손실되지 않음. Round-trip 안정성 입증됨: serialise(parse(body))가 동일하게 재파싱됨. 전체 파일 원시 마크다운 편집기는 섹션 추가/제거 및 프리앰블 편집을 위한 확인 게이트가 있는 Advanced 디스클로저로 남아 있음(WS2 #4 파괴적 저장 게이트 변경 없음). 8개 로케일 전반에 새 i18n 키 10개(config.modesTargetRoles … config.modesUnknownNote). 테스트 파일 tests/modes-form.test.mjs(7개 케이스) +1; 725 → 732. 격리된 CAREER_OPS_ROOT 픽스처에 대해 라이브 검증됨: 정규 섹션 5개가 필드로 렌더링됨 + 커스텀 섹션 1개가 레이블된 폴백으로, 편집-후-저장 round-trip이 프리앰블 + 커스텀 섹션을 보존함, 콘솔 오류 0. feat(config) · test: tests/modes-form.test.mjs. 자세히는 CHANGELOG.md.
[1.54.2] — 2026-05-18
feat(config): #/config의 OpenAI / Codex 모델 선택기. #/config에는 OpenAI / Codex 모델을 고를 방법이 없었음 — OPENAI_API_KEY가 이미 부모 멀티-CLI(Codex / OpenCode) 흐름용으로 노출되어 있었는데도 ANTHROPIC_MODEL과 GEMINI_MODEL에만 드롭다운이 있었음. 이제 OPENAI_MODEL이 일급 환경 키임: env-config.mjs의 KNOWN_KEYS(OPENAI_API_KEY 바로 뒤에 정렬)와 core 키 그룹에 추가되었고, SECRET_KEYS에는 의도적으로 미포함 — 자격 증명이 아니라 모델 id이므로 결코 마스킹되지 않음. config.js는 큐레이트된 OPENAI_MODELS 목록(기본값 gpt-5-codex, 이어서 gpt-5 / gpt-5-mini / gpt-4.1 / o4-mini / o3)과 OpenAI 키 바로 뒤에 렌더링되는 OPENAI_MODEL <select> 필드를 추가하며, Anthropic/Gemini 모델 필드를 정확히 그대로 따름. 8개 로케일 전반에 새 i18n 키 config.openaiModel + config.openaiModelHint. 테스트 파일 tests/openai-model-selector.test.mjs(4개 케이스) +1; 721 → 725. 라이브 검증됨: #/config → 6개 옵션을 가진 OPENAI_MODEL select, 기본값 gpt-5-codex, 레이블 바인딩됨, 콘솔 오류 0. feat(config) · test: tests/openai-model-selector.test.mjs. 자세히는 CHANGELOG.md.
[1.54.1] — 2026-05-18
fix(a11y): F-V54-A — #/cv를 단일 <h1>로. CV markdown 자체의 # Name이 페이지 제목 <h1>CV</h1> 옆에 두 번째 최상위 <h1>로 렌더링되었음(회귀 실행 F-V54-A; WCAG 1.3.1 정보와 관계 / 2.4.6 제목). 이제 cv.js는 CV 미리보기의 모든 주입 지점(초기 렌더, 파일 가져오기 시 새로고침, 에디터 라이브 동기화)을 제목을 한 단계 낮추는(h1→h2 … h6→role="heading" aria-level="7") 범위 한정 cvMd()를 통과시켜 페이지가 정확히 하나의 <h1>을 유지함. cv.js로 의도적으로 범위 한정 — UI.md는 help/reports/deep/evaluate가 공유하며 각자 제목을 자기 방식으로 관리하기 때문. 테스트 파일 tests/cv-single-h1.test.mjs(4개 케이스) +1; 717 → 721. 라이브 검증됨: #/cv → <h1> 1개, 사용자의 # Name은 이제 <h2>, 콘솔 오류 0. fix(a11y): F-V54-A · test: tests/cv-single-h1.test.mjs. 자세히는 CHANGELOG.md.
career-ops-ui v1.267.1 · page 7/9 · mirror of the project changelog for cvstart.org