체인지로그
GitHub릴리스별로 정리된 모든 주요 변경 사항입니다. 저장소에 실린 동일한 파일이며, 17개 언어 전체로 번역되어 있습니다.
변경 로그
career-ops-ui 의 모든 주요 변경 사항을 기록합니다. 형식은 Keep a Changelog 를 따르며, 버전은 Semantic Versioning 을 준수합니다.
번역: 🇬🇧 English · 🇪🇸 Español · 🇧🇷 Português · 🇯🇵 日本語 · 🇷🇺 Русский · 🇨🇳 简体中文 · 🇹🇼 繁體中文 · 🇫🇷 Français · 🇵🇱 Polski · 🇺🇦 Українська · 🇩🇰 Dansk · 🇸🇦 العربية · 🇩🇪 Deutsch · 🇮🇹 Italiano · 🇹🇷 Türkçe · 🇮🇳 हिन्दी
i18n 노트 — 이 파일은 완전히 한국어로 번역되었습니다. 모든 버전 항목의 본문이 출판 등급의 한국어로 제공되며, 영어 본문 임시 대체 표기는 더 이상 사용되지 않습니다.
[1.236.0] — 2026-09-18
상위 동등성 — career-ops main @ 6a9c84c (VERSION 1.33.0, 커밋 128개). 새 소스 두 개, 그리고 미러링된 수정 세 건 — 그중 두 건은 여기서 공고를 소리 없이 잃고 있었습니다.
추가
스캐너 소스 두 개, 92 → 94개(89 EN + 5 RU). Python.org Jobs(pythonorg)는 게시판의 공개 피드를 읽습니다. 고용주 표기가 필수이므로, 제목에 고용주가 명시되지 않은 행은 포털 항목의 이름으로 채워 넣는 대신 버려집니다. Generalist World(generalist-world)는 게시판의 카드를 스크레이핑하며, 파싱할 수 없는 마크업에서는 큰 소리로 실패합니다 — 카드 형태가 바뀌면 빈 게시판으로 읽히는 대신 예외를 던지는데, 이것이 바로 스크레이핑 소스를 신뢰할 수 없게 만드는 실패 방식입니다. 둘 다 #/scan의 Source 필터에 표시되고, 둘 다 호스트 고정(앵커된 정규식, HTTPS 전용)되어 있으며, 둘 다 가짜 전송 스위트로 처음부터 끝까지 구동됩니다(각각 43건과 36건).
수정
여러 도시에 열려 있는 Workday 공고가 위치 필터에 의해 버려졌습니다. Workday의 목록 엔드포인트는 다른 모든 공고가 장소를 담는 자리에, 다중 위치 공고에 대해서는 개수로 답합니다: "53 Locations". 이것은 장소가 아니며, buildLocationFilter는 대소문자를 구분하지 않는 부분 문자열로 일치시킵니다 — 그래서 Austin과 그 밖의 52개 도시에 동시에 열려 있는 공고는 어떤 allow: [austin] 항목과도 일치하지 않아 버려졌고, 같은 공고가 단독으로 나열되었을 때는 통과했습니다. 상위 저장소에서 테넌트 세 곳에 걸쳐 측정한 결과: 291건 중 53건의 공고가 자리표시자였으므로, 이는 예외적인 사례가 아니라 통상적인 사례입니다. 공고의 실제 장소는 CXS 상세 문서에만 존재하므로, 이제 자리표시자마다 그 문서에 대한 추가 GET 요청이 하나씩 들며, 항목당 200건으로 상한이 걸립니다 — 상한을 넘으면 자리표시자를 꾸며내지 않고 그대로 남겨 둡니다. 해결되지 않은 공고가 해결된 것처럼 읽혀서는 절대 안 되기 때문입니다. 실패했거나 장소 정보가 없는 상세 문서는 공고를 목록이 돌려준 그대로 남겨 둡니다: 눈에 보이는 "53 Locations"는 무엇을 해결하지 못했는지를 정직하게 드러내지만, 비워진 위치였다면 모든 필터를 소리 없이 통과했을 것입니다. 같은 GET 요청은 startDate도 함께 담고 있어서, 보강된 공고는 목록 엔드포인트의 상대적 표현("Posted 30+ Days Ago" → undefined) 대신 정확한 날짜를 갖게 됩니다. 순수한 YYYY-MM-DD 형태가 아닌 값은 Date.parse에 넘기지 않고 그대로 둡니다 — 이 함수의 비-ISO 대체 동작은 구현에 따라 다르기 때문입니다.
명시된 등급이 있으면 더 이상 두 개의 채용 요청을 하나의 재게시로 뭉개지 않습니다. "Insurance Specialist II"는 이 채용 요청이 형제 항목이 아니라는 고용주 자신의 진술입니다 — 하나의 기본 직함 아래 있지만 급여 등급, 범위, 요청 번호가 서로 다릅니다. 토크나이저는 이를 볼 수 없었습니다: w.length > 3은 VIII까지의 모든 로마 숫자와 모든 한 자리 숫자를 걸러내므로, "Insurance Specialist"와 "Insurance Specialist II"는 동일하게 토큰화되어 완벽한 Jaccard 비율을 기록했고, 재게시 탐지기는 후자를 전자의 재등록으로 표시했습니다. web-ui의 role-matcher.mjs에는 등급 처리가 전혀 없었으므로, 이번에는 상위 저장소의 메커니즘 전체를 그대로 들여옵니다: 로마 숫자와 아라비아 숫자를 하나의 숫자로 접어 등급을 추출하고("Nurse II"와 "Nurse 2"는 같은 진술입니다 — 이 짝은 이전에는 잘못 다르게 취급되었습니다), 명시된 두 등급은 반드시 일치해야 하며, 한쪽에만 등급이 있는 경우는 각 제목이 상대에게 없는 단어를 함께 지니고 있지 않은 한 느슨한 재작성으로 간주됩니다 — 이는 장식이 아니라 두 가지 불일치이기 때문입니다.
필터로 고정한 Avature 게시판이 결국 전체 글로벌 게시판을 훑고 있었습니다. Avature의 검색 UI는 불투명한 패싯 파라미터를 생성하며(?42386=[812132]는 국가 필터입니다), portals.yml에서 좁혀진 게시판을 고정한 운영자는 스캔이 그 게시판을 훑기를 기대합니다. resolveSearch는 URL을 ${origin}${path}로 재구성하면서 쿼리 문자열을 통째로 버렸습니다 — 그래서 첫 요청을 포함한 모든 요청이 테넌트의 전체 게시판을 훑었습니다: 더 많은 페이지, 더 많은 요청, 그리고 운영자가 의도적으로 걸러 냈던 공고들까지. 이제 패싯은 페이지네이션되는 모든 요청에 실리며, 우리 자신의 jobOffset을 마지막에 적용해 동일한 이름을 가진 낯선 패싯이 페이지네이션을 가로챌 수 없도록 합니다.
보안
devalue 5.8.1 → 5.9.2(astro를 통한 전이 의존성), 열려 있던 Dependabot 경고 하나를 닫았습니다 — 잘못된 형식의 입력에 대한 서비스 거부. 사이트 전용 의존성이며 런타임 코드는 변경되지 않았습니다.
참고
이식하지 않은 항목과 그 이유. 부모 저장소는 공유 기본값 10초 대비 lever에는 30초, manfred에는 25초의 타임아웃을 주었습니다 — 이는 이식 대상이 아닙니다: web-ui의 스캔 전송은 이미 60초(DEFAULT_SCAN_TIMEOUT_MS)를 허용하므로, 두 게시판 모두 저쪽에서 그 수정이 주는 것보다 이쪽에서 더 넉넉한 여유를 갖고 있습니다. iCIMS의 이중 careers- 접두사 문제도 여기서는 일어날 수 없습니다: web-ui는 테넌트 원본(origin)을 api/careers_url에서 곧바로 가져오며 테넌트 이름으로부터 호스트를 만들어 내는 일이 전혀 없습니다. 그 새로운 폴백 호스트 순회는 부모 저장소의 더 느슨한 설정을 위한 기능이지, 결함 수정이 아닙니다. deutschebahn과 manfred는 일시적 중단 시 재시도 기능을 얻었는데, web-ui의 60초 상한이 있는 한 이는 훨씬 덜 시급합니다 — 이식하지 않고 참고로만 남겨 두어, 향후 장시간 순회에서 타임아웃이 발생하면 가장 먼저 시도해 볼 방법을 알 수 있게 했습니다. 128개 커밋 델타의 나머지는 모두 CLI, 검증기, 대시보드 또는 web/(부모 저장소 자체의 프런트엔드)입니다: liveness-browser, check-jd-archive, verify-ats/verify-portals/verify-cv-facts, update-system, set-status, audit-portals, discover-ats, scaffolder, test-all, 플러그인 매니페스트.
pull 이후 포크 분기점을 다시 확인했습니다: providers/telegram-channel.mjs의 키릴 문자 \p{L} 수정, 포크 고유의 providers/telegram.mjs, web/src/lib/clis.ts의 hermes. 상위 career-ops-hq/main은 완전히 병합되었습니다(누락된 커밋 0개). 3066 → 3164개 테스트. 도움말은 이제 17개 번들 모두에서 94개의 어댑터(89 EN + 5 RU)를 명시하며, 헤딩 구조는 32 H2 / 122 H3로 변함없습니다.
[1.235.0] — 2026-09-16
상위 동등성 — career-ops main @ 68e6b94 (VERSION 1.33.0, 커밋 42개). 미러링된 수정 2건 — 그중 하나는 여기서 MokaHR 공고를 소리 없이 잃고 있었습니다.
수정
같은 MokaHR 게시판의 두 번째 공고가 추가되지 않고 사라졌습니다. url-key.mjs는 공고 URL을 비교용 키로 축약하는데, 프래그먼트를 무조건 버렸습니다 — 대부분의 프래그먼트가 순전히 표시용(#apply, #section-2)이라는 점에서는 합리적입니다. 하지만 MokaHR은 우리 레지스트리 안에서 예외입니다: 이곳 공고는 …/social-recruitment/{tenant}/{id}#/job/{n} 형태이며, 한 테넌트의 모든 공고가 경로를 공유하고 공고 id는 오직 프래그먼트 안에만 존재합니다. 그래서 이들 모두가 하나의 키로 정규화되었습니다. 수정 전 측정 결과: 서로 다른 MokaHR 공고 두 건을 파이프라인에 붙여넣으면 한 줄만 남습니다 — 오류도 경고도 없이, 알아챌 만한 어떤 흔적도 없이. 스캐너의 신선도 필터(en-scanner.mjs)가 같은 키를 읽기 때문에, 해당 테넌트의 공고 하나가 스캔 이력에 들어가는 순간부터 나머지 전부가 “이미 본 것”으로 버려졌습니다. 이제 normalizeUrl은 인식된 #/job/{id} 또는 #/jobs/{id} 라우트를, 프래그먼트가 버려지기 전에 비교 전용 쿼리 키로 승격시킵니다 — MokaHR은 기존에 써 오던 이름인 mokahr_job_id를 그대로 유지해 이전에 기록된 키와 계속 비교 가능하도록 하고, 그 외 호스트는 내부용 _career_ops_fragment_job_id를 받습니다. 승격된 id는 설정이 아니라 추가됩니다: 이미 그 파라미터를 갖고 있는 URL은 다른 공고의 id를 담고 있는 것이므로, 이를 덮어쓰면 이 키가 애초에 갈라놓으려던 바로 그 두 가지를 하나로 합쳐버리게 됩니다. 인식되지 않는 프래그먼트는 여전히 모두 붕괴되며, 이는 원래부터 옳았던 절반입니다.
accept-encoding이 이제 모든 소스 요청에 고정됩니다. 설정하지 않으면 Node는 자신의 undici 빌드가 제공하는 대로 협상하는데, Node ≥ 23부터는 여기에 zstd가 포함되며, amazon.jobs의 zstd 응답은 상태 코드 200을 유지한 채 정확히 1024바이트에서 잘려서 돌아옵니다 — 그래서 실패가 전송 계층 버그가 아니라 손상된 API인 것처럼 Unterminated string in JSON at position 1024로 읽힙니다. 이제 모든 요청은 gzip, deflate, br을 보냅니다. 우리 런타임에서는 이것이 예방 조치이지 수리가 아닙니다: Node 18/20/22 — CI 매트릭스와 서버(22.22.1) — 는 gzip, deflate를 보내며, 이는 가정이 아니라 직접 검증한 사실이므로 여기서는 아무것도 잘리지 않았습니다. 오늘 바뀌는 것은 br이 제안된다는 점이며, 이 버전들은 기본적으로 이를 요청하지 않지만 올바르게 디코딩합니다. 기본값은 대소문자를 구분하지 않고 적용되며, 이 부분이야말로 검토할 가치가 있습니다: 헤더 이름은 대소문자를 구분하지 않고 fetch는 이름이 같은 두 항목을 하나의 콤마 구분 값으로 합치므로, Accept-Encoding: identity를 넘긴 호출자는 그렇지 않았다면 gzip, deflate, br, identity를 보내게 되었을 것입니다.
참고
헤더는 fetchImpl 경계에서 평범한 객체로 유지되며, 부모 저장소는 이 지점에서 fetch에 Headers 인스턴스를 넘깁니다. 이는 실수가 아니라 의도적인 분기입니다: web-ui의 fetch 헬퍼들은 주입 가능한 fetchImpl을 받고, 45개의 소스 스위트가 자신이 캡처한 값에서 opts.headers['User-Agent']를 읽습니다 — Headers 인스턴스에는 대괄호 접근이 없으므로, 충실한 이식 대상은 구현이 아니라 동작(대소문자 구분 없는 오버라이드, 호출자가 우선함)입니다. 호출자의 객체는 복사될 뿐 절대 변형되지 않는데, 여러 소스가 하나의 헤더 상수를 공유하기 때문입니다. 부모 저장소의 짝을 이루는 수정 — 기본 user-agent를 대소문자 구분 없이 적용하는 것 — 은 이식하지 않습니다: web-ui는 애초에 기본 UA를 주입하지 않고, 각 소스가 자신의 게시판이 요구하는 UA를 직접 설정하므로, 호출자의 User-Agent와 충돌할 기본값 자체가 없습니다.
새 스위트 두 개와 케이스 세 개가 이 쌍을 고정합니다: tests/http-accept-encoding.test.mjs(정확히 고정된 값, 네 개의 fetch 헬퍼 각각, 그리고 세 가지 대소문자 표기 아래에서의 호출자 오버라이드 — 병합 버그가 두 개를 만들어냈던 것과 달리 단 하나의 accept-encoding 키만 살아남는지 검증)와, 키가 아니라 사용자가 보는 접점을 다루는 tests/parsers-pipeline-hash-route.test.mjs(서로 다른 MokaHR 공고 두 건이 모두 파이프라인에 도달해야 하고, 같은 공고를 네 가지 표기로 다시 붙여넣어도 여전히 한 줄로 붕괴해야 하며, 장식용 프래그먼트가 식별자가 되어서는 안 됨)가 그렇습니다. 모두 먼저 실패함을 확인했습니다. 이 고정은 해당 헬퍼뿐 아니라 전송 계층에 있습니다. 92개 소스 중 25개는 주입된 fetchImpl을 직접 호출하며 헬퍼를 전혀 거치지 않으므로, 두 스캐너가 실제로 주입하는 래퍼인 makeTimeoutFetch가 이를 적용합니다 — 모든 소스가 공유하는 유일한 통과 지점입니다. (부모에는 대응물이 필요 없습니다. providers/_http.mjs가 유일한 전송 계층이기 때문입니다.) 이제 구조적 가드가, 새 소스가 건네받은 전송 대신 fetch를 직접 호출하면 CI를 실패시키고, 동작 검증 케이스는 25개 중 하나인 greenhouse를 래퍼로 구동해 헤더가 실제로 전송되는지 증명합니다.
이식하지 않은 항목과 그 이유: liveness-browser.mjs(헤드풀 Playwright 재사용 가드 — web-ui는 헤드풀 브라우저를 구동하지 않음), check-jd-archive.mjs(web-ui 중계 없음), test-all.mjs(부모 저장소의 하네스; 포크의 vpFixtureEnv 분기는 그대로 유지), web/(부모 저장소 자체의 프런트엔드이며 미러링 대상 아님), 플러그인 매니페스트와 SIGNATURES.md(저장소 전용). batch/batch-runner.sh(앰퍼샌드를 sed 보간 전에 이스케이프)와 jd-skill-gap.mjs(데이터 루트를 기준으로 cv.md를 해석)는 읽기 전용으로 중계됩니다 — web-ui는 부모 저장소의 사본으로 셸 아웃하므로 여기서는 코드 변경 없이 둘 다 반영되며, jd-skill-gap 쪽은 /api/jds/:id/skill-gap 중계를 더 정확하게 만듭니다: cv.md는 이제 프로세스가 어디서 실행되었든 상관없이 데이터 루트를 기준으로 해석됩니다. pull 이후 포크 분기점을 다시 확인했습니다: providers/telegram-channel.mjs의 키릴 문자 \p{L} 수정, 포크 고유의 providers/telegram.mjs, web/src/lib/clis.ts의 hermes. 소스 개수는 92개(EN 87 + RU 5)로 변함없어 도움말도 그대로입니다. 3042 → 3066개.
[1.234.0] — 2026-09-15
상위 동등성 — career-ops main @ 56cce8f (VERSION은 여전히 1.32.0, 1.32.0 이후 커밋 58개). 15개 소스에 이식된 수정 1건과, 재발을 막는 가드.
수정
잘못된 공고 id 하나가 더 이상 페이지 전체를 비우지 않습니다. encodeURIComponent는 UTF-16 서로게이트 단독 문자에서 URIError를 던지며, JSON 페이로드는 그런 문자를 담을 수 있습니다 — JSON.parse('"\\uD800"')가 그것을 그대로 유지합니다. 열다섯 개 소스는 파싱 루프 안에서 호스트가 제어하는 id나 슬러그로 각 공고의 URL을 만들었기 때문에, 잘못된 공고 하나가 루프를 중단시켜 그 페이지의 모든 공고를 잃었습니다. 예외가 회사별 catch가 뭔가 보고하기도 전에 풀려나갔으므로 소스 실패로도 기록되지 않았습니다. 부모 저장소는 이를 providers/_safe-url.mjs에서 고쳤습니다(safeEncodeURIComponent → URIError에는 null을 반환하고 그 외에는 다시 던짐) 그리고 루프의 각 id를 이 함수에 통과시켜 null이 나오면 그 공고 하나만 버립니다. 그 헬퍼는 이제 server/lib/sources/_safe-url.mjs이며, 같은 변경이 이식된 열세 개 소스 — alibaba, arbeitsagentur, bamboohr, feishu-jobs, garena, jibeapply, manfred, meituan, mokahr, phenom, thehub, tkms, vdab — 와, 새 가드가 첫 실행에서 찾아낸 web-ui 전용 소스 두 곳인 jobstreet과 trudvsem에도 적용되었으며, 이들 역시 같은 루프 형태와 같은 잠재적 throw를 갖고 있었습니다. 범위는 의도적입니다: API에서 온 id만 이 헬퍼를 거칩니다. 설정에서 파생된 값은 엄격한 인코딩을 유지하며 크게 실패합니다 — garena의 office(portals.yml 세그먼트)는 throw하는 urlSegment를 그대로 쓰고, API id는 null을 반환하는 idUrlSegment를 씁니다. csod의 corpName과 4dayweek의 SLUG_RE로 검증된 슬러그는 이유와 함께 이름으로 허용목록에 올랐습니다.
rheinmetall의 제목 대체 로직이 잘못된 href에서 throw할 수 있었습니다. 카드에 제목이 없으면 URL 슬러그로부터 decodeURIComponent를 통해 제목을 재구성하는데, 스크래핑한 href에 잘못된 퍼센트 시퀀스가 있으면 여기서 throw합니다 — 디코딩 쪽에서 벌어지는, 한 개의 잘못된 행이 페이지 전체를 죽이는 같은 형태입니다. 이제 방어적으로 디코딩하며 실패하면 원본 슬러그로 대체합니다.
소스 레지스트리가 _로 시작하는 파일을 건너뜁니다. 자동 탐색은 server/lib/sources/ 안의 모든 .mjs를 자기 자신만 제외하고 임포트했고, export const meta가 없는 파일에는 경고를 냈습니다 — 그래서 이 헬퍼가 부팅 시 경고를 만들어냈습니다. 이제 부모 저장소의 관례를 그대로 따릅니다: 앞에 밑줄이 붙으면 소스가 아님을 뜻하며, 그런 파일은 임포트되지도 경고를 받지도 않습니다. meta가 없는, 선언되지 않은 파일은 여전히 경고를 받으며, 탐색 테스트는 이제 양쪽 모두를 검증합니다(옛 필터로 실패함을 확인했습니다). 사이트 빌드의 sync-assets.mjs도 같은 규칙으로 디스크의 어댑터 파일을 세므로, 헬퍼가 더 이상 조용한 누락 감지 가드를 건드리지 않습니다.
참고
tests/sources-url-encoding-surrogate.test.mjs는 부모 저장소의 테스트 스위트를 세 부분으로 그대로 옮깁니다: 헬퍼의 계약(짝을 이룬 서로게이트 — 평범한 이모지 — 는 여전히 인코딩되며, 값 자신의 toString에서 나온 throw는 호출자의 버그이므로 그대로 전파됨), 열다섯 개 전체 소스별 동작(잘못된 id 하나 + 정상 id 하나 → throw 없음, 정상은 유지되고 잘못된 것은 버려짐 — 각 소스가 내보내는 순수 파서를 통해, thehub의 경우 가짜 전송으로 그 fetcher를 통해 구동), 그리고 소스 수준 가드: 변환된 모든 파일은 헬퍼를 임포트하고 그리고 호출해야 하며, 임포트 없이 이를 참조할 수 없고, 검토되지 않은 파일은 순수 encodeURIComponent만으로 채용 URL을 만들 수 없습니다 — 옛 형태를 가진 새 소스는 문제가 된 줄 번호와 함께 CI에서 실패합니다. 이식 전에 열다섯 개 소스별 사례 모두가 실패함을 확인했습니다.
부모 저장소 델타의 나머지는 이식하지 않았으며, 이유는 다음과 같습니다: batch/batch-runner.sh --cli는 CLI 전용 플래그입니다(web-ui는 배치 러너로 셸 아웃하지 않음); invite-match.mjs는 이제 코드 루트에서 set-status.mjs를 찾는데 — web-ui는 이를 중계하지 않습니다; company-funded.mjs는 내부적으로 같은 safe-url 변경을 얻었지만 /api/company-funded가 파싱하는 --json 출력 형태는 그대로입니다; test-all.mjs는 타임아웃 예산만 옮겼습니다(포크의 vpFixtureEnv 분기는 그대로 유지됩니다); web/은 부모 저장소 자체의 프런트엔드이며 미러링 대상이 아닙니다; AGENTS.md/SIGNATURES.md는 저장소 전용입니다. pull 이후 포크 분기점을 다시 확인했습니다: providers/telegram-channel.mjs의 키릴 문자 \p{L} 수정, 포크 고유의 providers/telegram.mjs, web/src/lib/clis.ts의 hermes. 소스 개수는 92개(EN 87 + RU 5)로 변함없어 도움말도 그대로입니다. 3022 → 3042개.
[1.233.2] — 2026-09-10
수정 — 심각도 높음 CodeQL js/remote-property-injection 두 건을, 이미 있던 불변식을 주장이 아니라 구조로 만들어 닫았습니다.
수정
POST /api/config는 재시작 없이 반영되도록 저장값을 process.env로 옮기는데, 그 루프는 요청 본문에서 만들어진 Object.entries(safe)를 돌면서 안쪽에 __proto__ / constructor / prototype 방어를 두고 있었습니다. 코드는 안전했습니다 — safe는 스무 줄 위에서 KNOWN_KEYS만 훑어 채워지므로 속성 이름이 사용자 손에 들어갈 일이 없습니다. 그러나 루프만 떼어 읽으면 요청 본문으로 만든 객체에서 나온 이름의 속성에 대입한다로 읽히고, CodeQL은 두 루프에 걸친 제약을 따라가지 못합니다. 이제 상수 배열을 직접 돌며 safe에서 값을 꺼내므로 속성 이름이 모듈 수준 리터럴임이 증명됩니다. 프로토타입 방어도 함께 사라졌습니다. 동작은 동일하며, 경고 날짜가 9월 9일인 것은 v1.232/v1.233이 줄 번호를 옮겼기 때문일 뿐입니다.
참고
두 건을 오탐으로 기각하는 길도 있었고 저장소에는 그 관행도 있습니다. 여기서는 고치는 쪽을 골랐습니다 — 주석 속 단언은, 분석기와 다음 독자가 스무 줄을 따라가지 않고도 검증할 수 있는 형태보다 값이 낮습니다.
옛 방어를 고정하던 테스트는 소스 텍스트 grep이라 문자열의 존재만 증명했습니다. 더 강한 둘로 교체했습니다: process.env[k]에 이르는 경로가 KNOWN_KEYS 순회임을(그리고 요청 유래 객체를 순회하지 않음을) 보는 구조 테스트 — 옛 형태에서 빨간불 확인 — 과, __proto__·constructor·prototype·미등록 이름을 원시 JSON으로 보내는 동작 테스트. 객체 리터럴은 안 됩니다: 소스의 { __proto__: … }는 속성이 아니라 프로토타입을 설정하므로 JSON.stringify가 {}를 보내 아무것도 증명하지 못했을 것입니다. 3021 → 3022개.
[1.233.1] — 2026-09-10
수정 — 안내서에 v1.233.0이 더한 컨트롤 설명이 전혀 없었고, 같은 절에는 200개 릴리스 전에 사실이 아니게 된 지시가 남아 있었습니다.
수정
DOC-1 — 새로 더한 컨트롤이 안내서 어디에도 없었습니다. 이제 모든 드롭다운은 ’기본값 사용 (…)’으로 시작해, UI가 이전에는 나타낼 방법이 없던 상태 — 키가 설정되지 않음 — 을 표현합니다. 아래 목록에서 같은 값을 고르는 것과의 차이는 보이지 않으면서도 중대합니다: ’기본값 사용 (llama3.2)’은 아무것도 쓰지 않고 프로젝트가 기본값을 옮길 때마다 새 값을 계속 건네주지만, llama3.2를 명시적으로 고르면 OLLAMA_MODEL=llama3.2이 기록되어 다음에 기본값이 바뀔 때 조용히 옛 값에 남습니다. CONFIG-2와 CONFIG-3이 다룬 것이 바로 이 조용한 표류였고, 메커니즘은 코드에서 고쳤는데 읽을 곳이 없었습니다. 기존 ### Behavior 목록에 세 항목을 17개 번들 모두에 넣었습니다.
DOC-2 — 표의 한 행이 어떤 키도 이름하지 않았고, 그 안의 유일한 구체적 지시는 틀렸습니다. Key 칸에는 (server uses default UA)라는 괄호 주석이 들어가 v1.19.0이 제거한 HH_USER_AGENT를 설명했고, dev.hh.ru에 앱을 등록해 UA 문자열을 쓰라고 안내했습니다. v1.65.0에서 hh.ru 어댑터가 자체 브라우저 UA로 공개 검색 사이트를 읽도록 바뀐 뒤로 그 일은 아무 소용이 없습니다. 같은 정도로 낡은 언급이 둘 더 있었고, 셋 모두 17개 번들에서 사라졌습니다. 문제 해결 행은 삭제가 아니라 다시 썼습니다 — 절반은 여전히 참이기 때문입니다: 2026년 7월부터 hh.ru는 러시아 밖 IP에 HTTP 451을 돌려주므로 러시아 가정용 IP가 실제로 필요합니다.
키 표가 자기가 무엇인지 밝힙니다. ### Recognized keys는 45개 중 6개만 싣는데 제목은 등록부를 약속합니다. 한 문장 ×17로 선별임을 밝히고 완전한 목록은 #/config에 있다고 가리킵니다.
참고
tests/help-recognized-keys.test.mjs가 빠져 있던 게이트입니다. 실제로 키를 나열하는 도움말 표의 첫 열을 살아 있는 KNOWN_KEYS와 대조합니다. 범위 한정이 중요합니다 — 내보내기 표의 PDF처럼 첫 열이 백틱 대문자인 다른 표도 있어서, 자기 행 중 하나라도 진짜 키를 이름할 때만 키 표로 봅니다. 수정 전 번들에서 세 건 모두 실패함을 확인했습니다. 3018 → 3021개; 도움말은 17개 모두 32 H2 / 122 H3 그대로입니다.
[1.233.0] — 2026-09-10
수정 — #/config 저장 경로에서 세 건. CONFIG-3은 v1.232.1의 수정 자체가 불러온 것입니다.
수정
CONFIG-3 — 심어진 값과 우연히 같았던 의도적 선택이 ’건드리지 않음’으로 버려졌습니다. v1.232.1에서 제가 쓴 주석은 코드가 ’표시용 기본값과 같은 값을 일부러 고른 사용자’를 구분한다고 했지만, 구분하지 못합니다. initial은 두 출처를 뒤섞습니다: .env에서 심어진 경우(같으면 편집하지 않은 것 — 건너뛰는 게 옳음)와 defaultValue에서 심어진 경우(같음이 모호 — 열지 않았거나, 열고 동의했거나). HOST가 없으면 컨트롤은 127.0.0.1을 보여 주는데, 바로 그 값을 입력하고 저장하면 {}가 전송되고 키는 생기지 않았습니다. 고정하는 유일한 길은 틀린 값을 넣고 저장한 뒤 옳은 값으로 되돌리는 것 — 아무도 찾지 못할 막다른 길이었습니다. initial 옆에 **seededFromFile**을 기록해 해결했습니다.
키 삭제가 ’· 0’으로 보고되었습니다. 서버가 쓴 것만 돌려주어 삭제는 셀 것이 없었고, 실제로는 지워졌는데 ’아무 일도 없었다’로 읽혔습니다. 이제 removed도 응답하고 토스트가 둘 다 셉니다.
셀렉트는 설정은 되어도 비울 수 없었습니다. 18개 중 어느 것도 빈 항목이 없었습니다. 이제 각 셀렉트는 **‘기본값 사용 (…)’**으로 시작하고, 설정되지 않은 키는 기본값 자체가 아니라 그 항목을 선택합니다. i18n 키 하나 ×17.
참고
이번에 함께 확인하고 일부러 그대로 둔 것: lang은 접두 규칙이 아니라 문자 그대로의 허용 항목(langx, lang_evil, LANG 모두 400). facts.json은 설계상 배포물에 없습니다 — 22개 파일이 빌드 시 가져와 값이 HTML에 인라인됩니다. 확장자 없는 업로드를 텍스트로 다루는 것은 문서화된 의도이며, 쓰지도 실행하지도 않고 .exe/.zip은 명시적으로 거부됩니다.
유닛 기준선은 3018 그대로. 게이트는 브라우저 쪽이며 playwright-config-save-scope.mjs가 6건에서 11건으로(브라우저 합계 111 → 116), 새 5건 모두 옛 코드에서 먼저 실패함을 확인했습니다.
[1.232.1] — 2026-09-10
수정 — #/config의 저장이 사용자가 건드리지 않은 필드까지 기록했습니다.
수정
CONFIG-2. v1.57.1은 설정되지 않은 컨트롤에 defaultValue를 심어 서버가 실제로 쓰는 값을 보여 줍니다(옳고, 그대로 둡니다). 반면 save()는 dirty를 비밀 필드에만 물었고 나머지는 언제나 보냈습니다. 그래서 보여 주려고 넣은 값이 사용자의 선택인 양 서버로 갔습니다. 키 두 개가 설정된 픽스처에서 측정하니, 드롭다운 하나를 바꾸자 28개 키가 전송되고 아무도 열지 않은 18개가 고정되었습니다. 아무것도 바꾸지 않아도 27개가 갔습니다. 그날의 동작은 달라지지 않습니다 — 기록된 값이 곧 기본값이니까요. 달라지는 것은 각 설정의 상태입니다: ’설정되지 않음, 프로젝트 기본을 따름’에서 ’.env에 고정됨’으로. 이 목록들은 릴리스 사이에 움직이므로, 다음에 기본값을 바꾸면 한 번이라도 저장을 누른 사람에게는 조용히 닿지 않습니다.
참고
눈여겨볼 곳은 비교의 기준입니다. 불러온 cfg.values와 비교하는 것 — 가장 먼저 시도한 명백한 수정 — 으로는 부족합니다. 설정되지 않은 select는 자기 defaultValue를 보여 주고 저장값은 ''이라 둘은 정직하게 다르며, 열일곱 개 모델 드롭다운은 여전히 기록됩니다. 본문이 27개에서 19개로 줄었을 뿐 1개가 되지 않았습니다. 이제는 각 컨트롤이 무엇으로 심어졌는지와 비교합니다. dirty도 기준이 될 수 없습니다 — 값을 입력했다가 원래대로 되돌려도 켜지니까요.
함께 수정: 토스트의 키 개수가 t()의 폴백 인자 안에 있었고 config.saved는 17개 사전 모두에 있어서, 그 숫자는 한 번도 그려지지 않았습니다. 12개를 저장해도 1개를 저장한 것과 똑같아 보였습니다.
나머지는 그대로입니다. v1.57.1의 사전 채움은 유지되고, 비밀 필드는 여전히 건드렸을 때만 전송되며, v1.232.0의 서버측 유예는 폼을 거치지 않는 직접 API 호출을 위한 방어로 남습니다. 수정이 클라이언트 쪽이라 테스트 기준선은 3018 그대로이고, 게이트는 tests/playwright-config-save-scope.mjs의 브라우저 6건입니다(105 → 111). 여섯 건 모두 옛 코드에서 먼저 실패함을 확인했습니다.
[1.232.0] — 2026-09-09
수정 — 브라우저 QA에서 나온 두 건. 둘 다 v1.231.5에서 온 것이 아닙니다.
수정
PROFILE-1 — #/profile이 페이지 전체를 옆으로 스크롤시켰습니다. 값은 인라인 글꼴 스타일만 지닌 익명 <div>여서 계산된 overflow-wrap이 normal로 남았습니다. LinkedIn URL은 CSS에 적법한 줄바꿈 지점을 주지 않으므로 min-content 너비가 문자열 전체가 되고, 그리드 항목의 기본 min-width: auto는 그 아래로 줄어들기를 거부합니다 — 카드가 트랙을, 이어서 행과 본문 열과 문서를 밀어냈습니다(1280 px에서 실측 23 px의 가로 스크롤). 공유 클래스 .card-value에 overflow-wrap: anywhere를 주고 .card-row > * { min-width: 0 }을 받침으로 두어 해결했습니다. **break-word가 아니라 anywhere**입니다 — min-content 계산에 들어가는 것은 anywhere뿐입니다.
CONFIG-1 — POST /api/config가 select 필드에 어떤 값이든 받아들였습니다. {"LLM_PROVIDER":"not-a-provider"}가 200을 반환하고 .env에 그대로 기록되었습니다. 이후에도 아무것도 실패하지 않습니다 — 리졸버가 조용히 폴백하고, 사용자가 고른 설정은 효력 없이 멀쩡해 보입니다. 바로 그 19개 값을 담은 LLM_PROVIDERS는 줄곧 env-config.mjs에서 내보내지고 있었습니다. 검증기가 참조하지 않았을 뿐입니다.
참고
검증 대상은 LLM_PROVIDER 하나뿐이며, 이는 보고에 대한 의도적인 축소입니다. 모델 목록의 진짜 도메인은 공급자의 카탈로그에 있고 우리 릴리스와 무관하게 움직입니다. 잘못된 모델은 호출 시점에 큰 소리로 실패하고, 조용히 실패하는 것은 공급자 이름뿐입니다.
이미 저장된 값은 눈감아 줍니다. 엄격한 판본이 쓸 수 없다는 것을 브라우저 테스트가 보여 주었기 때문입니다. 설정 폼은 저장할 때마다 비밀이 아닌 모든 필드를 다시 보내므로, 기존의 잘못된 값을 거부하면 페이지 전체가 얼어붙습니다. 이제는 변경만 거부합니다.
함께 해결: OLLAMA_API_KEY는 API로는 쓸 수 있었지만 #/config에 필드가 없어 인증 프록시 뒤의 Ollama 사용자는 앱에서 설정할 수 없었습니다. 디스크립터를 추가했고(비밀, i18n 키 2개 ×17), 서버의 45개 키와 UI의 45개 디스크립터가 계속 같음을 테스트가 보장합니다. 3013 → 3018개.
[1.231.5] — 2026-09-09
보안 — 권고 일곱 건을 닫았고, 그중 하나는 치명적인 원격 코드 실행입니다. 소스 파일은 하나도 바뀌지 않았습니다.
보안
Dependabot에서 병합: multer 2.2.0 → 2.3.0(높음 — 중단 시 파일 디스크립터 누수로 인한 서비스 거부)과 사이트의 svgo 4.0.2 → 4.1.0(높음 및 보통 — removeScripts가 네임스페이스와 콘텐츠 처리를 통해 실행 가능한 링크를 통과시켰고, foreignObject 안의 실행 가능 HTML도 불완전하게 정화했습니다).
이어서 그 병합이 촉발한 재검사가 다섯 건을 더 드러냈습니다. 영향 범위는 모두 기존 ^ 지정자로 이미 충족되어 매니페스트 수정이 아니라 업데이트로 충분했습니다: 사이트의 astro 7.1.0 → 7.3.2 — 치명적, AVIF 이미지 최적화를 통한 원격 코드 실행(7.2.8에서 수정)과 설정된 base를 벗겨낼 때 경로 세그먼트 경계 검사 누락으로 인한 인가 우회(7.2.4); sharp 0.35.3 → 0.35.4 — 높음, libheif 취약점 두 건; 두 매니페스트 모두의 js-yaml 4.3.1 → 4.3.2 — 높음.
참고
여기서 “의존성뿐”은 “위험 없음”과 같지 않아, 감사 결과만으로 내보내지 않았습니다. js-yaml은 앱이 읽는 모든 설정을 파싱하고 multer는 모든 업로드를 받으며, Astro는 마이너 버전을 건넜습니다. 확인: npm run test:ci 3013/3013, 브라우저 스위트 4/4, 그리고 동일한 86개 페이지와 17개 미러, 올바른 facts.json을 낸 사이트 전체 재빌드. npm audit은 두 매니페스트 모두 취약점 0건.
[1.231.4] — 2026-09-07
수정 — v1.231.3은 Doctor 클릭의 뒷수습을 고쳤을 뿐, 그 순간을 고치지 않았습니다. 모바일과 데스크톱 양쪽에서 다시 신고되었습니다.
수정
요청이 진행되는 동안 withSpinner는 여전히 버튼을 다시 썼습니다. v1.231.3은 자식 노드를 스냅샷하고 finally에서 되돌리게 했고, 실제로 되돌아왔습니다 — 그러나 작업 중 표시는 여전히 button.replaceChildren(document.createTextNode('⏳ ' + original))을 실행했습니다. 그래서 doctor.mjs가 도는 내내 36 px 정사각형은 ⏳ 🩺Doctor 텍스트를 담은 넓은 알약이 되어 줄을 무너뜨렸고, 데스크톱에서는 ⏳🩺Doctor로 뭉쳐 보였습니다. 자식을 되돌린 것은 뒷수습이었을 뿐이고, 끝 상태만 보는 단언은 이를 그대로 통과시켰습니다. 요소 자식을 가진 버튼은 이제 전혀 다시 쓰지 않습니다. 표시는 .is-loading 클래스가 맡고, 이 클래스가 CSS에서 아이콘을 모래시계로 바꾸므로 형상이 달라질 수 없습니다: 320 px에서 대기 36 px, 진행 중 36 px, 이후 36 px.
참고
교훈은 코드가 아니라 단언에 있습니다. v1.231.3의 게이트는 aria-busy가 사라지기를 기다린 뒤에야 측정했으니, 구조상 finally가 이미 고쳐 놓은 상태만 볼 수 있었습니다. 새 브라우저 검사는 스스로 프로미스를 열어 둔 채 비행 중에 표본을 뜹니다. 3012 → 3013개. 두 검사 모두 v1.231.3 코드에서 실패함을 확인했습니다.
[1.231.3] — 2026-09-07
수정 — Doctor를 누르면 모바일 상단 바가 무너졌습니다. v1.231.2가 나간 그날 휴대폰에서 신고되었습니다.
수정
UI.withSpinner가 자기가 꾸미던 바로 그 버튼의 자식들을 납작하게 만들고 있었습니다. 작업 중 표시를 textContent로 처리했기 때문입니다 — button.textContent를 저장하고, '⏳ ' + original을 쓰고, 다시 되돌려 씁니다. textContent에 대입하면 모든 자식이 텍스트 노드 하나로 대체됩니다. 그런데 v1.231.2는 막 상단 바의 각 동작을 .btn-ico와 .btn-label로 나누고 레이블을 max-width: 900px 아래에서 숨긴 참이었습니다. 그래서 Doctor를 처음 누르는 순간 두 <span>이 영영 사라졌습니다. 모바일 규칙은 걸 대상을 잃었고, 레이블은 맨 텍스트로 돌아왔으며, 36 px 정사각형은 테마 전환 위로 올라타는 🩺Doctor 알약으로 부풀었습니다. 저절로 낫지 않았고 새로고침만이 마크업을 되돌렸습니다. 수정은 자식 노드를 스냅샷하고 replaceChildren으로 되돌립니다. 텍스트만 있는 버튼, 즉 24개 호출 지점의 대부분은 이전과 완전히 동일하게 동작합니다.
- FIND-3 — 320 px 화면에서 검색 필드가 8 px로 찌그러져 있었습니다. 상단 바를 한 줄로 만들면서 searchbar가 유일한 신축 요소로 남았고, 지난 릴리스의 변경 기록이 그대로 적어 두었습니다 — “아무것도 남지 않을 때까지 줄어들 수 있다”. 정말로 그렇게 됐습니다: 320 px에서 입력 8 px, 340 px에서 28 px — 21자 자리표시자 중 한 글자. 누를 수도 입력할 수도 있지만 더는 필드로 읽히지 않습니다.
min-width로는 안 됩니다. 320 px 바에는 여백 32 px, 메뉴 40 px, 동작 162 px가 들어 있어 내줄 120 px이 없습니다. 그래서 420 px 미만 — 필드가 스스로 100 px를 넘기는 폭 — 에서는 돋보기 뒤로 숨고, 누르면 바 전체로 펼쳐집니다: 320 px에서 182 px, 360 px에서 222 px, 390 px에서 252 px, 어느 폭에서도 넘침이 없습니다. 이 버튼은 접근 가능한 이름을 하나로 유지하고 상태는aria-expanded로 알립니다. 🔍/✕ 전환은 순수 CSS입니다 — 이 버튼에 JS가 쓰지 않으며, 같은 릴리스에서 Doctor를 망가뜨린 것이 바로 그 쓰기였습니다. 420 px 위로는 달라지는 것이 없습니다. 라이브 빌드를 상대로 한 브라우저 회귀에서 발견되었습니다.
참고
이는 v1.231.2에서 적어 둔 applyI18n() 함정과 같은 결함 부류입니다 — 이제 요소 자식을 가진 요소에 textContent를 대입하는 코드 — 다만 다른 호출자를 통해 닿았습니다. 교훈은 일반화됩니다: 어떤 요소의 내용을 자식 <span>으로 쪼개는 순간, 그 요소를 향한 기존의 모든 textContent 기록자가 잠복 버그가 됩니다. 그리고 그 기록자가 지금 편집 중인 파일에 있으리라는 보장은 없습니다.
두 겹으로 잠갔습니다. node:vm에서 가짜 버튼에 진짜 withSpinner를 돌리고, 320 px 실제 브라우저에서 Doctor를 클릭합니다. 둘 다 이전 코드에서 실패함을 확인했습니다. 3009 → 3012개 (브라우저 케이스는 선택이며 test:ci에 없습니다).
[1.231.2] — 2026-09-06
수정 — 모바일 상단 바가 두 줄로 접히던 문제, 서버가 더 오래된 상위 프로젝트로 돌던 문제, 그리고 cvstart.ru 리디렉션이 이미 언어를 포함한 모든 경로에서 404를 내던 문제.
수정
- 모바일 상단 바가 다시 한 줄이 되고, 동작 버튼은 아이콘만 남았습니다. 동작 버튼들이 전체 너비의 두 번째 줄로 강제되어, 휴대폰에서는
[☰ · 검색]아래에[🔔 🌙 Doctor Scan 열기]라는 넓은 알약 두 개가 별도의 줄을 차지했습니다. 이제는 종과 테마 전환 옆의 36 px 정사각형(🩺 / ⚡)이며, 320 px에서도 전부 한 줄에 들어갑니다. 눈대중이 아니라 실측입니다: 문서 너비가 320·360·390·430 px에서, 그리고 예전에 버튼이 검색창을 덮던 560~760 px 구간 전체에서 뷰포트와 같습니다. 로케일은 이제 모바일 레이아웃에 전혀 영향을 주지 않습니다 — 레이블이 숨겨지고 버튼이 고정 정사각형이기 때문입니다. 900 px를 넘으면 레이블이 그대로 돌아옵니다. 접근 가능한 이름은aria-label에서 옵니다.display:none레이블은 이름 계산에서 빠지기 때문입니다. - 릴리스는 상위 1.32.0을 기준으로 만들었는데 서버는 상위 1.31.0으로 돌고 있었습니다. resumecraft.ru의
/api/health가parentVersion: 1.31.0을 보고했습니다. 런타임 95개 파일을 동기화하고 2개를 삭제했습니다. 운영 환경을 대상으로 한 브라우저 QA가 찾아냈습니다. cvstart.ru는 경로에 무엇이 있든 보지 않고/ru/를 앞에 붙였습니다./ru/help는/ru/ru/help가 되고/en/help는/ru/en/help가 되어 둘 다 404였습니다. 앞의 것은 본 사이트에서 URL을 복사하면 생기고, 뒤의 것은 더 심각해서 영어권 방문자가 영어판에 도달할 길을 완전히 막았습니다. 이제 16개 로케일 접두사 중 하나를 이미 가진 경로는 그대로 통과하고,en은 제거되며(영어는 루트에 있고/en/디렉터리가 없습니다), 나머지는 여전히/ru/를 받습니다.
추가
- 대체된 QA 프롬프트 98개를
qa/archive/superseded-prompts/로 옮겼습니다.qa/README.md는 v1.137.0부터 최상위에는 현재 릴리스의 프롬프트만 둔다고 기록해 왔지만, 이 규칙은 95개 릴리스 동안 지켜지지 않았습니다. - 다섯 개의 프롬프트가 더 최상위에서 내려왔습니다 — 스스로를 현행이라 칭했지만 아니었습니다.
REGRESSION-FINAL.md는 자신을 “유일하게 권위 있는, 버전 비의존 회귀 프롬프트”라 불렀지만, §§11–15는 닫힌 다섯 개의 사이클 대장(v1.55.x → v1.59.7)이고 명시한 도움말 기준은 16개 로케일에 28 H2 / 103 H3 — 오늘의 32 / 122, 17에 대비해서입니다. 이를 따르는 QA 에이전트는 §9 첫걸음에서 넘어졌을 것입니다.QA-REGRESSION-PROMPT.md는 버전 없는 이름에 v1.226.0 내용을 담고 있었습니다.QA-REGRESSION-PROMPT-v1.76.0-FULL.md는 155개 릴리스 뒤에 고정되어 있었고,REGRESSION-PROMPT-FINAL.md는 v1.58.52 → v1.59.10 사이클을 닫는 문서이며,QA-FULL-REGRESSION.md는 v1.131.1 기준으로 그 모두를 중복했습니다.PARENT-SYNC-WORKLIST-v1.26.0.md도 함께 갔고(상위는 이미 1.32.0), 날짜가 붙은UX-AUDIT-2026-07-06.md는reports/로 옮겼습니다. 드라이버 넷이 남았고, 각자의 기준은 v1.231.2 실측값으로 갱신했습니다.qa/README.md는 실제로 존재하는 폴더를 설명하도록 다시 썼습니다.
참고
- 상단 바를 고치며 빠졌던 명시도 함정 두 가지를
app.css에 주석으로 남겼습니다: 데스크톱 규칙이max-width: 900px블록보다 아래에 선언되어 있어 명시도가 같으면 뒤가 이깁니다 — 모바일gap은 조용히 24 px로 남았고(이것이 320 px를 넘치게 했습니다) 아이콘은 계속 숨겨져 버튼이 빈 정사각형이 되었습니다. applyI18n()이el.textContent를 할당하므로 i18n 키를 안쪽<span>으로 옮겼습니다. 버튼에 두면 언어를 바꿀 때마다 아이콘이 지워집니다.- 상위 프로젝트의 첫 rsync는 로컬에 더 이상 없는 파일에서 전송 도중 중단되고도 성공을 보고했습니다. 상위 배포는 이제 종료 코드가 아니라 체크섬으로 검증합니다.
[1.231.1] — 2026-09-06
수정 — v1.231.0의 카운트 일괄 치환이 모든 링크의 계정 이름을 바꿔 놓았습니다.
수정
- 모든
Fighter90링크가Fighter92가 되었습니다. v1.231.0의 소스 카운트 치환은 숫자 경계 보호 아래90을92로 바꿉니다. 이 보호는290과901은 올바르게 건너뛰지만,Fighter90안의90에는 일치합니다. 앞은 알파벳이고 뒤는 그냥 토큰이 끝나기 때문입니다. 17개 README 전부, 17개 도움말 묶음 전부, 사이트의 도움말 미러에 걸쳐 562개 링크와 위키의 138개가 존재하지 않는 계정을 가리켰고,providers.png배너와 모든 “GitHub에서 편집” 링크가 404였습니다. 이미지가 보이지 않는다는 사용자 제보로 발견했습니다. - 과거 릴리스 참조 5건이 어긋났습니다. 같은 릴리스의
85 → 87치환이v1.85.0을v1.87.0으로 바꿔, 낸 적 없는 릴리스를 만들어냈습니다.
참고
- 숫자 경계만으로는 부족합니다. 이웃한 숫자로부터는 지켜 주지만, 그 숫자가 식별자나 버전 문자열의 일부인 경우는 막지 못합니다. 카운트 자체는 정확하며 바뀌지 않았습니다: 소스 92, EN 어댑터 87, 테스트 3009.
- 상위 저장소는 영향을 받지 않았습니다.
[1.231.0] — 2026-09-05
추가 — 상위 career-ops v1.32.0에서 온 스캔 소스 두 개(Collage, Telegram 엄격), Gem의 REST 모드, 그리고 이식 과정이 상위 자체 코드에서 찾아낸 결함 하나.
추가
- 새 소스 두 개, 90 → 92(EN 87 + RU 5),
ALL_ADAPTERS85 → 87. 둘 다 토큰이 필요 없습니다.- Collage(
provider: collage) — Collage HR의 공개 채용 사이트 API. 경로 구간은 테넌트가 직접 고른 사이트 주소이지 회사명에서 만든 슬러그가 아니므로,api:또는secure.collage.co/jobs/<주소>채용 URL에서 읽어오며 결코 추측하지 않습니다. 추측한 주소는 남의 게시판을 스캔하게 됩니다. - Telegram(엄격)(
provider: telegram-channel) — 기존telegram소스와 같은t.me/s/<채널>미리보기를 읽지만 반대의 교환을 합니다. 게시물이 행이 되는 것은 고용주를 명시하고 동시에 채용 페이지로 연결될 때뿐입니다. 모음 글, 고용주를 감춘 글, 연락처만 있는 글은 버립니다. 낮은 포괄률은 의도된 것으로, 상위에서 HR 운영 채널 기준 게시물의 32~77%가 통과하는 것으로 측정되었습니다. 대신 모든 행이 실제 고용주와 실제 링크를 가집니다. 상위와 마찬가지로 둘 다 제공됩니다.
- Collage(
- Gem에 선택형 REST 모드가 추가되었습니다(GraphQL 경로와 병존,
api: https://api.gem.com/job_board/v0/<board>/job_posts). 게시판 id에서 유도하는 일은 결코 없으며 GraphQL 경로가 기본으로 남습니다. 상위 #3783에서 이식.
수정
telegram-channel의 위치 필터가 키릴 문자를 보지 못해 도시 이름을 고용주로 반환했습니다. 상위의LOCATIONISH_RE는 ASCII 전용인\b를 사용했고, 이는 키릴 문자 옆에서는 결코 발동하지 않습니다. 그 결과 안에 있던 러시아어 후보(москва,спб,офис,удалёнк*)는 모두 도달 불가였고,Senior Engineer | Москва는 “Москва”를 고용주로 반환했습니다. 이는 귀속 정책 전체가 막으려는 “틀린 필드를 사실로 기록하는” 바로 그 동작이며, 하필 이 소스의 주 사용자층을 정면으로 칩니다. 러시아어 채널은 위치를 키릴 문자로 적기 때문입니다. web-ui 이식본은 유니코드 검사(\p{L}/\p{N})를 사용하며, 이는 상위가 바로 아래ROLE_WORD_RE에서 이미 하고 있는 것과 같습니다. ASCII 형태에서 실패하는 테스트로 고정했습니다. 상위에서는 아직 수정되지 않았습니다.
참고
- 상위의 Workday 패싯 복구 작업(#3851, #3874)은 이식이 필요 없었습니다. web-ui는 Workday를 페이징하거나 패싯으로 나누지 않습니다 —
offset: 0으로 POST 한 번뿐입니다. v1.229.0과 같은 결론이며, 가정하지 않고 다시 확인했습니다. - **
providers/_types.js**는 문서만 변경되었습니다. - 테스트: 2962 → 3009(+47).
[1.230.0] — 2026-09-05
변경 — 상위 career-ops v1.32.0에서 온 프로바이더 수정 3건과, 이식 과정 자체가 드러낸 결함 1건.
추가
- Welcome to the Jungle이 서버 측 Algolia
filters식을 받습니다(wttj: { filters: "offices.country_code:FR AND contract_type:full_time" }). 키워드만으로는 전역 게시판을 좁힐 수 없습니다. 어떤max_hits결과가 돌아올지는 스캐너의 필터가 아니라 Algolia의 관련도 순위가 정하며, 우리 필터는 그 뒤에 이미 도착한 것에 대해서만 작동하므로 상한을 넘어선 것은 아무리 잘 맞아도 보이지 않습니다. 필터는 재정렬이 아니라 결과 집합 자체를 줄이므로 질의당 상한이 200에서 1000으로 올라갑니다. 필터가 있으면queries는 선택 사항이 되고, 빈 질의는 “필터를 통과하는 모든 것”을 뜻합니다. 상위 #3757에서 이식.
수정
- 테넌트가 자체 배너의 주장보다 단지 더 적은 공고를 제공할 뿐인 경우, Radancy는 더 이상 “truncated” 경고를 내지 않습니다. 상위 프로젝트는
data-total-results를 실제 테넌트 9곳과 대조했습니다. 5곳은 일치했지만 4곳은 — 작은 곳과 아주 큰 곳 모두 — 중복 행이 단 하나도 없는 상태로 공표치보다 10~56% 적은 고유 공고를 제공했습니다. 배너는 테넌트 자신의 검색 색인이 실제로 제공하는 양을 과장하므로, 누적 집계를 그것과 비교하는 방식은 대다수 게시판에서 오탐이었습니다. 이제 경고는 우리 자신의max_jobs/max_pages가 순회를 멈춘 경우에만 나옵니다. 호출자가 조치할 수 있는 경우는 그것뿐이기 때문입니다.totalResults는 맥락으로 메시지에 남지만 판단에는 결코 관여하지 않습니다. #3839에서 이식. - Radancy는 모든 JSON 프래그먼트 요청에서 캐시를 우회합니다. 해당 경로 앞의 캐시 계층이 일부 테넌트에서 오래된 응답을 되돌려줍니다. 같은 공고가 몇 페이지 뒤에 다시 나타나는 반면 다른 공고는 한 번도 제공되지 않았고, 상류에서 9회 중 9회 재현되었습니다.
Cache-Control/Pragma헤더는 아무 차이도 없었고, 요청마다 무작위 매개변수를 붙이자 해결되었습니다. 이 값은 호출마다 고유해야 하며 페이지 번호에서 유도해서는 안 됩니다. 결정적인 매개변수는 캐시에 여전히 안정적인 키를 주고, 따라서 똑같이 안정적이면서 잘못된 대응을 남기기 때문입니다. #3839에서 이식. - Greenhouse 오피스 목록이 결정적 순서를 갖습니다. Greenhouse는 응답 간
offices[]의 순서를 보장하지 않으며,/offices트리에서 복원한 집합은 그 순회 순서를 따릅니다. 둘 다 공고의location필드에 도달하므로, 게시판이 오피스 순서를 바꾸면 그 문자열이 다시 쓰였고 — 아무것도 바뀌지 않은 공고가 새 공고로 읽혔습니다. 상위 프로젝트는 집합을 고쳤고(#3839), web-ui에는 상위에 없는 동일 결함의 두 번째 사례가 있었습니다 — 공고 자체에 위치가 없을 때 쓰이는offices[0]폴백입니다. 이제 둘 다 정렬됩니다.
참고
- 상위의 iCIMS 수정(#3728)은 이식이 필요 없었습니다. 이는 상세 페이지 JSON-LD의 모든
jobLocation항목을 읽습니다. 일부 테넌트가 실제 주소 앞에 전부UNAVAILABLE인 항목을 내보내기 때문입니다. web-ui의 iCIMS는 목록 페이지 HTML에서 위치를 가져오며 상세 페이지를 열지 않습니다. - 상위의
_http.mjs변경도 이식이 필요 없었습니다.isRefusedRedirectError를 분리해 두 번째 소비자와 판단을 공유하기 위한 것인데, web-ui에는 그 두 번째 소비자가 없고isRetryableError의 동작은 그대로입니다. - 위치 인식 중복 제거(#3751)는 반영하지 않습니다 — 상위 CLI의
scan.mjs파이프라인에 있는 기능입니다. - 레지스트리 변동 없음: 90개 소스(EN 85 + RU 5). 테스트: 2956 → 2962(+6).
[1.229.0] — 2026-09-03
추가 — 상위 career-ops v1.31.0에서 스캔 소스 네 개: Built In, Feishu Jobs, Garena, MokaHR.
추가
- 새 소스 4개, 86 → 90(EN 85 + RU 5),
ALL_ADAPTERS81 → 85. 모두 토큰이 필요 없습니다. Built In은 미국 기술 채용 보드의 아홉 개 시장을 아우르지만 기본 질의를 담지 않습니다:queries:도categories:도 없는 항목은 아무것도 스캔하지 않고 그렇다고 말합니다 — 공유 소스가 특정인의 검색어를 지녀서는 안 되기 때문입니다. 허용 목록에 없는 시장은 거부되며, 조용히 전국 보드로 대체되지 않습니다. Feishu Jobs는 호스트 형태를 둘만 받아들이고, 둘은 채용 페이지 경로가 서로 다릅니다. Garena는 단일 기업이며office는 링크를 만들 뿐 목록에는 영향을 주지 않습니다. MokaHR는 암호화된 응답(키가 함께 오는 AES-128-CBC 봉투)을 돌려줍니다 — 보안이 아니라 난독화지만, 복호화하지 않으면 아무것도 읽을 수 없습니다.
수정
htmlToText가 불완전한 태그 여는 문자를 텍스트로 흘릴 수 있었습니다.safe <img src=x onerror=1이safe <img src=x onerror=1로 나왔습니다. 이제 매 디코드 뒤에 제거가 이어지고, 끝의 여는 문자는 꺾쇠를 잃습니다. 상위 프로젝트에서 이식(#3491).
참고
- 상위의 Workday 수정에서는 아무것도 이식하지 않았습니다. 그것은 백엔드가 오프셋 2000에서 페이지네이션을 막는 테넌트를 구제하지만, web-ui는 Workday를 아예 페이지네이션하지 않습니다(
offset: 0에서 POST 한 번, 100행). verify-cv-facts와merge-tracker수정은 이식이 필요 없었습니다: web-ui는 그 스크립트들을 상위 체크아웃에서 실행합니다.- 테스트: 2909 → 2956(+47).
[1.228.5] — 2026-08-30
수정 — 보고되는 버전은 실행 중인 코드가 아니라 디스크의 파일을 가리켰습니다.
수정
-
/api/health가 매 요청마다package.json을 다시 읽어, 오래된 프로세스가 자신이 실행하지 않는 버전을 보고했습니다. QA는version: 1.228.4라고 답하면서 1.228.3에서 추가된/api/ping에는 404를 반환하는 로컬 인스턴스를 찾았습니다. 배포 전에 시작되어 옛 코드를 서비스하면서 새 파일의 버전을 보고하고 있었던 것입니다. 파일만 복사하고 재시작하지 않은 배포는 바로 이렇게 성공처럼 보입니다 — 운영자가 확인하는 유일한 문자열이 그 문제를 볼 수 없는 문자열입니다. v1.228.1의 심볼릭 링크와 같은 부류로, 그때도 rsync는 전송 도중 중단하고서 성공을 보고했습니다.버전은 이제 모듈 로드 시점에 포착되어 실행 중인 코드를 가리킵니다. 오래된 프로세스는 옛 버전을 보고합니다 — 그것이 진실이며 첫 요청에서 바로 보입니다.
parentVersion은 의도적으로 요청마다 읽습니다. 이 프로세스가 로드하지 않고 실행 중인 서버 아래에서 정당하게 바뀔 수 있는 상위 체크아웃을 가리키기 때문입니다.
[1.228.4] — 2026-08-30
수정 — 인증 없는 활성 상태 프로브가 매 요청마다 디스크 작업을 했습니다.
수정
GET /api/ping이 매 요청마다package.json을 읽고 파싱했습니다. 자격 증명 없이 도달할 수 있는 유일한 엔드포인트이므로, 요청당 디스크 읽기와 JSON 파싱은 누구에게나 건네진 서비스 거부 지렛대입니다 — CodeQL은 이를 속도 제한 누락으로 표시했습니다. 버전은 이제 라우트 등록 시 한 번만 읽습니다. 재시작 없이는 바뀔 수 없으므로 핸들러는 I/O를 전혀 하지 않습니다. 속도 제한은 피해에 상한을 두지만, 작업을 없애면 지렛대 자체가 사라집니다.- 루프백 외부에서 프로필 소유자의 실명을 가리는 동작이 이제 테스트로 고정되었습니다.
/api/health는 이미hidden으로 대체하고 있었지만, 그 행이 프로젝트 루트와hidden ?? value가드를 공유하기 때문일 뿐이었습니다. 아무것도 이를 검증하지 않았으므로, 그 행에 자체 값을 주는 수정이 있었다면 실존 인물의 이름이 아무도 모르게 LAN으로 새어 나갔을 것입니다.
[1.228.3] — 2026-08-30
Fixed — the docs assistant could not answer in English, Telegram company names arrived truncated, and a monitor had no way to see the service was up.
Fixed
-
The docs assistant answered “no matching help sections were found” in English while the same question worked in Russian. Retrieval was fine; the context builder was not.
buildAskPromptdidif (ctx.length + sec.body.length > MAX_CONTEXT) break— abreak, so one oversized section ended the loop and left the context empty, and the assistant truthfully reported it had nothing. The English §5 had grown to 16 081 bytes against a 14 336-byte budget (partly because v1.228.0 addedtelegram_channelsto it); the Russian §5 was 13 009 and fit. Same ranking in both languages — §5 was first — only the size differed.Two changes: a section past 6 KB is now split at its
###boundaries, sotelegram_channelsis its own retrievable chunk (1 340 bytes) that ranks first in both languages; and an oversized section is skipped or truncated rather than ending the loop. The guide went from 32 chunks to 75. -
A Telegram company label yielded the sentence after it, not the name.
Компания: FinCore Technology (продуктовая разработка, высоконагруженные системы)becameFinCore Technology (продуктовая разработка, высоконагруженны— capped at 60 characters, cut mid-word. Three of 141 rows. The name now ends at the first separator that introduces a description — a spaced dash (soCoca-Colasurvives), a comma, semicolon, pipe or opening parenthesis — and any remaining cap falls on a word boundary. This source attributes a company as fact; a truncated sentence is as wrong as a guessed name, only less obviously so.
Added
GET /api/ping— a liveness probe that leaks nothing.{ ok, version }and no more./api/healthreports absolute paths, the profile owner’s real name and which API keys are set: legitimate for an authenticated operator, never for the public internet. A monitor behind a Basic-auth gate previously had no way to see the service was up at all.
[1.228.2] — 2026-08-30
Fixed — a two-word search in /api/scan-results matched the phrase, not the words, and under-reported badly.
Fixed
-
?q=now matches every term independently instead of the literal phrase. The query was a singleincludes(q)over title + company + location, soпродакт менеджерfound only rows carrying those two words in that exact adjacency — 32 of the 162 that carry both.Менеджер продуктаandМенеджер по развитию продуктаwere invisible to it: the same words, reordered or with filler between them.This is how a narrow match becomes a wrong answer. The Telegram assistant asked for all Продуктовый менеджер reported 9 — faithfully, because 9 was what the endpoint said. The count it quoted was honest; the endpoint’s was not. Word order and the words between are not something a person typing two words intended to pin.
Matching is AND across terms, so widening precision costs nothing: a row must still carry every term.
менеджер разработчикreturns zero rather than the union.
[1.228.1] — 2026-08-29
Fixed — a dangling symlink in the repo that silently truncated deploys.
Fixed
-
.claude/skills/refero-designwas a tracked symlink to a path that has never existed here. It pointed at../../.agents/skills/refero-design, which resolves to.agents/at the repo root; there is no such directory, and the link arrived in the v1.118.0 parity pack from a machine where the surrounding folders nested differently. Anyone cloning the repo has been getting a broken link ever since.It was not inert. rsync aborts on it mid-transfer — a deploy today reported success while the server kept running the previous version, and the mismatch only surfaced because the version was checked afterwards. The parent project’s
node test-all.mjsalso crashes outright when a web-ui checkout sits inside its tree, because its fixture copy walks the working directory and cannot stat the link. Nothing referenced it: the onlyreferomatches in the repo are the Polish word preferowany.
[1.228.0] — 2026-08-29
추가 — 스캔 소스로서의 Telegram 채널과 /api/scan-results의 페이지네이션 질의 모드.
추가
-
Telegram 채널이 스캔 대상이 되었습니다.
portals.yml의telegram_channels:블록에 나열하며, 각 채널은 공개 웹 미리보기https://t.me/s/<채널>에서 읽습니다. 레지스트리: 85 → 86개 소스(EN 81 + RU 5),ALL_ADAPTERS80 → 81.Telegram은 RSS를 제공하지 않고, Bot API는 봇이 관리하지 않는 채널을 읽을 수 없습니다. 즉 뻔한 경로 두 개가 모두 막혀 있습니다.
/s/미리보기는 서버가 렌더링한 순수 HTML로 인증도 JS도 쿠키도 필요 없습니다. 16개 채널에서 검증: 15개가 맨 GET에 20건씩 반환했습니다.channel:은 어떤 표기든 받습니다. 파서는 Telegram 리디자인이 가장 나중에 옮길 세 가지 —data-post, 본문 컨테이너, ISO 날짜<time datetime>— 에 기댑니다. -
GET /api/scan-results에 페이지네이션 질의 모드가 생겼습니다.?q=<텍스트>&limit=50[®ion=ru|en][&set=filtered|fresh][&offset=N]가{ total, returned, offset, limit, set, rows }를 반환하며,total은 페이지네이션 이전의 개수입니다. 파라미터가 없으면 예전처럼 스냅샷 전체를 반환합니다 —#/scan테이블이 그 형태에 의존하기 때문입니다.
수정
- Telegram 어시스턴트는 데이터의 일부만 보고 답했습니다. 스킬은 이미
/api/scan-results를 가리키고 있었지만, 이 라우트는 스냅샷 전체만 반환할 수 있었고 실제 장비에서 약 2MB — 에이전트 컨텍스트에 들어가지 않습니다. “Продуктовый менеджер 전부 찾아줘”에 스냅샷에 수백 건이 있는데도 9건이라 답하고, “제한된 로컬 인덱스로 동작한다”고 말했습니다. 사실이었고, 원인은 엔드포인트가 보이지만 소화할 수 없다는 것이었습니다. 페이지네이션 모드로 같은 질문이 2KB로 끝나고 정직한total을 돌려줍니다. 스킬도 다시 썼습니다: 새 게시물을 요약하라고 되어 있어filtered(남은 전부)가 아니라fresh(직전 실행분)를 가리켰기 때문입니다.
참고
- 채널 게시물은 산문이지 채용 레코드가 아닙니다. 구조화된 필드가 없습니다: 제목은 첫 실질적 줄에서, 회사·근무지·연봉은 명시적 라벨에서 취하고 본문은 설명에 넣습니다. 진짜 공고를 광고·다이제스트와 가르는 일은 기존
title_filter— 다른 소스용으로 이미 조정된 바로 그것 — 에 맡깁니다. 단서이지 목록이 아닙니다. - 회사는 절대 추측하지 않습니다. 라벨이 없으면 행은 채널(
@rabotaphp)에 귀속됩니다. 잘못된 고용주는 트래커에 사실로 들어가기 때문입니다. - 파싱 결과 0은 오류이지 “공고 없음”이 아닙니다. 비공개·부재 채널에 t.me는 리디렉션으로 답합니다. 여기서 0건을 반환하면 조용한 하루로 읽히고 설정의 오타를 영원히 감춥니다. 원래 목록의
jobGeeks가 정확히 그러하며(HTTP 302) 의도적으로 설정하지 않았습니다. - 원격 판정은
\b대신 유니코드 단어 경계를 씁니다.\b는 ASCII 전용이라 키릴 문자 앞의 공백은 경계가 되지 않습니다 — v1.227.3에서 제목 필터가 밟은 바로 그 함정이며, 이번엔 출시 전에 테스트가 잡았습니다. - 채널 15개 설정 및 실데이터 검증: 299개 게시물, 15/15 채널. 신규 테스트 28개. 테스트: 2865 → 2895.
- The README test counts jumped by hundreds, and no, this release did not add them. The localized READMEs carried unit-test totals frozen at 1856 / 1945 / 1955 / 549 / 419 / 284 / 2527 depending on the file — each stale since a different release, and several contradicting the badge at the top of the same page. They now all read 2895, verified by running the suite rather than by copying a badge. Playwright (70 / 90 / 12 → 101), e2e smoke (20 → 21), route modules (12 / 32 → 37), smoke flows (12 → 22) and the test-file count (218 → 329) were stale in the same way. The
Totalrow also repeated the unit count, so it under-summed the three suites below it; it now reads the real sum (3040).
[1.227.5] — 2026-08-29
수정 — 지역 스캔이 서버 메모리를 고갈시켰고, Playwright 스위트 하나가 자체 teardown에서 불안정하게 실패했습니다.
수정
- 지역 스캔이 OOM으로 서버를 죽였습니다.
ru-scanner는 모든 쿼리의 원시 결과를 전부 쌓아두고 중복 제거와 필터링을 맨 마지막에만 수행했습니다. 쿼리 목록은 의도적으로 유의어투성이라(Golang,Go разработчик,Golang разработчик,Senior Go…) 같은 공고가 쿼리마다 돌아오고, 배열은 고유 건수의 몇 배를 들고 있었습니다 — 게다가 채용 객체의 대부분을 차지하고 필터가 거의 다 버리는 설명문까지 함께. 실측: 21개 쿼리 실행에서 힙 742MB. 서버는 Node 힙을 490MB로 제한하며(V8이 머신의 956MB RAM에서 산정), 스캔은FATAL ERROR: Reached heap limit로 서비스를 죽였습니다 — 하루에 네 번. 이제 중복 제거와 필터링이 쿼리마다 이뤄져 같은 실행이 177MB로 내려갔습니다(76% 감소). 보고되는 수치는 그대로입니다: 고유 합계는 URL 문자열Set이 담습니다. net::ERR_SOCKET_NOT_CONNECTED가 CI를 무작위로 떨어뜨렸습니다. 세 개의 Playwright 스위트가 콘솔 오류가 없음을 확인하기 전에window.stop()으로 진행 중인 요청을 중단합니다. Chromium은 이 하나의 의도적 동작을 소켓 상태에 따라 세 가지 이름으로 보고합니다: 취소 시ERR_ABORTED, 전송 중ERR_EMPTY_RESPONSE, 소켓이 이미 사라졌으면ERR_SOCKET_NOT_CONNECTED. 공용 필터는 앞의 둘만 알고 있었습니다. 로케일 스윕의tr에서 드러났고, 역시window.stop()을 부르는playwright-forms와playwright-smoke도 운 나쁜 한 번이면 같은 실패였습니다.
참고
- 왜 이제서야 실패했는가. 프로덕트 매니저 검색을 추가하며 쿼리가 14개에서 21개가 되었습니다. 이전 구현의 메모리는 총 결과 수에 비례했기에 쿼리 +50%가 머신의 한계를 넘겼습니다.
- 중복 제거 변경은 동작을 보존하며, 주장이 아니라 테스트로 담보합니다.
tests/ru-scanner-dedup-order.test.mjs가 까다로운 방향 — 뒤에 온 중복이 필터를 통과하지 못하면 앞서 통과한 것을 밀어내야 한다 — 을 고정하고, 옛 구현과의 등가성을 200회 무작위 시행으로 검증합니다. - 콘솔 필터는 좁게 유지됩니다. 취소된 요청 계열만 무해합니다; 연결 거부, DNS 실패, 리셋, 모든 5xx, 잡히지 않은 JS 예외는 여전히 단언을 실패시킵니다.
- 두 가설은 검증 후 폐기했고 릴리스에 넣지 않았습니다: Caddy 타임아웃(설정은 깨끗하고 SSE용
flush_interval -1도 정확)과 Node 기본 5분requestTimeout(축소 재현 결과 긴 스트리밍 응답을 끊지 않음). 실제 증거는 저널의Reached heap limit이었습니다. 테스트: 2860 → 2865.
[1.227.4] — 2026-08-29
수정 — word: / stem:가 content_filter에서는 여전히 무시되고 있었습니다. v1.227.3은 제목 필터만 고쳤습니다.
수정
content_filter가 접두사를 존중하지 않았습니다. v1.227.3은title_filter에 상위의word:/stem:접두사를 가르쳤지만,content_filter는 자체의 날것lower.includes(k)그대로여서 접두사가 붙은 항목이 여전히 문자 그대로"word:java"로 대조되었습니다 — 같은 조용한 no-op이 필터 하나 건너에 남아 있었던 셈입니다.content_filter는 채용 공고의 설명을 읽으며 기본값은 늘 단순 부분 문자열이었습니다. 그래서 맨 부정java는 「JavaScript」를 언급만 해도 전부 걸러냅니다. 접두사는 그 기본값에서 특정 항목 하나만 빼는 수단입니다. 상위의compileContentKeyword를 이식했습니다. 접두사 machinery는 공유하되 짧은 키워드는 의도적으로 자동 고정하지 않습니다 — 제목 필터가 2~3글자 약어를 고정하는 이유는 「Coordinator」 속 「COO」가 항상 오답이기 때문이지만, 설명문 단락 속 짧은 문자열은 보통 의도된 것이기 때문입니다(aws,gcp,sql,go).
참고
- v1.227.3 PR의 AI 리뷰에서 발견되었습니다. 코드가 자기 주석이 주장하는 바를 충족하지 못한다는 지적이었습니다 — 주석은 「
title_filter/content_filter」라고 적혀 있었지만 연결된 것은 제목 경로뿐이었습니다. 이제 주석이 명시적으로 구분합니다: 접두사는 두 필터 모두에서 동작하고, 약어 자동 고정은 제목에서만. - 같은 리뷰에서 검증했고 변경 없음: 2~3글자 약어 규칙을
\b에서 유니코드 경계로 옮긴 것은 ASCII에서 동작이 동일하며 필요한 곳에서만 더 엄격합니다 —COO_lead와coo1은 계속 유지(밑줄과 숫자는 양쪽 모두 단어 문자),Coordinator유지, 「Директор COO」와 「VP «Продукт»」는 올바르게 제외. 14개 경계 사례 확인. - 접두사 없는
content_filter항목은 이전 일치를 바이트 단위로 유지하고, 비었거나 없는 설명은 여전히 통과하며, 내용 없는word:/stem:는 전부가 아니라 아무것과도 일치하지 않습니다.tests/content-filter.test.mjs에 6개 케이스 추가. 테스트: 2852 → 2858.
[1.227.3] — 2026-08-29
수정 — 제목 필터의 word: / stem: 접두사가 조용히 무시되어 해당 필터 줄이 사실상 무효였습니다.
수정
- web-ui는 상위 프로젝트의
word:/stem:접두사를 구현하지 않았습니다.title_filter(및content_filter)는 기본적으로 대소문자를 구분하지 않는 부분 문자열 일치라서, 맨intern부정 항목은 「International Product Manager」와 「Internal Tools Engineer」까지 걸러냅니다. 이 기본값을 뒤집으면 설정된 모든 설치가 깨지므로, 상위 프로젝트는 항목별로 정밀도를 선택하게 했습니다:word:intern은 온전한 단어만,stem:agent는 단어의 시작에 일치하되 이어질 수 있습니다(「Agentforce」와 「Reagents」를 구분). web-ui에는 둘 다 없어 항목을 문자 그대로"word:intern"으로 대조했고, 그런 문자열이 든 채용 제목은 없습니다. 그 줄은 조용한 no-op이 되어, 같은portals.yml이 CLI에서는 제대로 걸러지고 여기서는 전혀 걸러지지 않아, 제외했다고 여긴 인턴 공고가 모두#/scan표에 도달했습니다. 상위의title-keywords.mjs에서 이식했으며, ASCII 전용이라 악센트·키릴 제목에서 단어 중간에 일치하던\b대신 유니코드 인식 단어 경계(\p{L}\p{M}\p{N}_)도 함께 가져왔습니다.
참고
- 실제
portals.yml을 확인하다 발견했습니다. 맨intern이 「International Product Manager」를 떨어뜨리지 않게 하려고 바로 그 목적으로word:intern을 막 도입한 상태였습니다. CLI에서는 접두사가 작동했고 web-ui는 조용히 모든 인턴 공고를 남겼습니다. 이제 동작이 동일합니다. - 기본값은 그대로입니다. 접두사 없는 키워드는 이전 일치 방식을 유지합니다: 구절이나 비문자를 포함한 항목(
.NET,SAP,L&D)은 부분 문자열, 2~3글자 약어는 단어 경계 고정. 내용이 없는word:/stem:은 오타로 보고 아무것과도 일치하지 않습니다 — 부정으로 쓰이면 빈 패턴이 모든 제목에 일치해 콜론 하나로 스캔 전체를 무효화하기 때문입니다. - 2~3글자 약어 규칙도 같은 유니코드 경계를 쓰도록 통일했습니다. 이전에는
\b를 써서 비ASCII 제목에서coo와word:coo가 어긋났습니다 — 한 규칙의 두 가지 표기였습니다. tests/title-filter.test.mjs에 6개 케이스 추가. 테스트: 2846 → 2852.
[1.227.2] — 2026-08-29
수정 — 폭이 좁은 휴대폰에서 #/config가 가로로 밀려났습니다.
수정
- MOBILE-1: 352 px 이하 뷰포트에서
#/config가 가로로 넘쳤습니다. 모든 select 계열 필드가 인라인min-width: 300px를 달고 있었습니다. 인라인 스타일은 스타일시트를 이기므로 컨트롤이 컨테이너에 맞춰 줄어들 수 없었고, 320 px 뷰포트에서는 select의 오른쪽 끝이 353 px에 놓여 페이지에 가로 스크롤이 생기고 컨트롤이 오른쪽 밖으로 밀려났습니다. 프로바이더/모델 select 18개 전부(cfg-llm-provider와 모든cfg-*-model)가 영향을 받았습니다. 이제min-width: min(300px, 100%)+max-width: 100%로, 공간이 있을 때만 300 px 폭이 적용됩니다. 임계값 이하 기기: iPhone SE(1세대, 320 px), 구형 Android, Galaxy Fold 외부 화면.
참고
- 이전 모바일 점검이 모두 놓친 이유. 360 px와 375 px에는 넘침을 가릴 만큼의 여유가 딱 있고, Playwright 스위트는 1280 px 창에서 돕니다. CI에서 375 px보다 좁은 뷰포트를 측정한 적은 한 번도 없었습니다. 새
tests/playwright-narrow-viewport.mjs는 폼이 있는 라우트를 320 px에서 훑고 가로 넘침이 있으면 실패하며, 메시지에 가장 오른쪽으로 튀어나온 요소를 지목합니다. 두 번째 케이스는#/config의 어떤 select도 뷰포트를 넘지 않음을 고정합니다. 먼저 버그를 재현하는지 확인했습니다(scrollWidth 353 > clientWidth 320, 오른쪽 끝 353인 select 18개). Playwright: 99 → 101. - 알려진 사항이며 의도적으로 아직 게이트 대상 아님: 280 px(Galaxy Fold 외부 화면)는 여전히 약 5 px 넘칩니다. 원인은 다릅니다 — 설정 카드의
<details>블록 안의 긴 콘텐츠와 280 px 뷰포트에 대한 256 px 사이드바이며, 이번 릴리스가 고친 인라인min-width가 아닙니다. 주류 휴대폰이 보고하지 않는 폭에 맞춘 재배치는 별도 작업이고, 스위트의WIDTHS목록은 준비되어 있습니다. - v1.227.1 QA 프롬프트도 수정했습니다. 팔로업 라우트를
#/modes/followup이라 적었지만, mode 페이지는 슬러그를 그대로 등록하므로(Router.register(cfg.slug)) 라우트는#/followup이며#/modes/followup은 정상적으로 404입니다. 문서만 변경. - 유닛 스위트는 2846로 변동 없음(이번 추가 커버리지는 브라우저 레벨).
[1.227.1] — 2026-08-29
수정 — 도움말의 소스 내역이 스스로 맞지 않았고, GitHub용 배너가 앱에서 원시 마크업으로 출력되었습니다.
수정
- §17의 합계와 내역이 맞지 않았습니다. 모든 도움말 번들이 “
server/lib/sources/레지스트리는 85개의 어댑터를 포함합니다 — 영문 78개 + 러시아어 5개”라고 적고 있었습니다. 78 + 5 = 83이지 85가 아니며, ja/ko는 더 벌어져 77(= 82)이었습니다. 새 소스가 추가될 때마다 합계만 올리고 항은 그대로 두어, 두 숫자는 여러 릴리스에 걸쳐 어긋나 있었습니다. 이제 내역은 17개 로케일 모두에서 EN 80 + RU 5입니다. 이는 미관 이상의 문제입니다.docs/help/<lang>.md는 “Ask the docs”가 근거로 삼는 유일한 코퍼스이므로, 지원 소스 수를 물은 사용자는 한 문장에서 서로 모순되는 두 숫자를 받았습니다. - 같은 문장이 오래된 버전 앵커를 고정하고 있었습니다. “v1.213.0 기준으로 레지스트리는 …”이 16개 로케일에 남아 있는 동안 카운터는 두 번 움직였습니다(v1.219.0에서 83, v1.226.0에서 85). 이 앵커는 v1.210.1에서 이미 수작업으로 고쳐졌다가 다시 밀렸습니다. 버전 없는 표현으로 대체해 제거했습니다 — 카운터 자체는 살아 있는 레지스트리와 대조해 검증되며, 낡을 수 없습니다.
- 공급자 배너가
#/help맨 위에 이스케이프된 마크업으로 출력되었습니다. v1.225.0/v1.225.1은 “UI.md()에 이미지 지원이 없어 앱에서 제거된다”고 기록했습니다. 이는 서로 다른 두 가지를 뒤섞은 것입니다.UI.md()는 이스케이프 우선이라 markdown 변환 전에 소스의 모든 바이트를 이스케이프하며, 이는 올바른 XSS 경계지만 제거와는 반대입니다. 아무도 제거를 시도하지 않았기에 아무도 제거하지 않았고, 17개 로케일 모두가#/help를 268자의 literal<p align="center"><img src="https://…로 열었습니다. 새로운 공용 입구(server/lib/help-markdown.mjs::stripGithubOnlyBlocks)가 읽는 시점에 단독 HTML 이미지 블록을 제거합니다 —GET /api/help/:lang과 docs-assistant 코퍼스 양쪽 모두에서. 파일에는 배너가 남으므로 GitHub 쇼케이스는 그대로 렌더링됩니다. - 케이던스 보드의 “#” 열은 v1.117.0 이후 계속 비어 있었습니다.
entry.appNum을 읽었지만followup-cadence.mjs가 내보내는 것은num이며,appNum은 페이로드에 존재한 적이 없습니다. 이제 둘 다 읽습니다. v1.227.0의 “Next up” 줄도 같은 실수를 물려받았고 함께 고쳤습니다.
참고
- 네 건 모두 게이트가 없어 발견되지 못했습니다. 도움말 게이트(
canonical-docs-coverage,help-ui,help-ru-config-section)는 제목을 셉니다 — 32 H2 / 121 H3 — 그리고 문단 안의 숫자를 읽거나 도움말 입구가 실제로 무엇을 제공하는지 본 테스트는 없었습니다. 새 스위트 두 개가 이를 막습니다:tests/help-source-counts.test.mjs(명시된 합계가SOURCES.length와 같고, 항이 살아 있는 EN/RU 분할과 같으며 합계가 되고, 문단이 버전을 고정하지 않음)와tests/help-banner-strip.test.mjs(배너는 GitHub용으로 디스크에 남고, strip 후에는 절대 살아남지 않으며, 각 번들의 첫 렌더 줄이 제목이고, strip이 좁게 유지되며, 두 소비자가 모두 그곳을 통과함). 둘 다 커밋 전에 바로 그 버그들에 대해 실패하는지 확인했습니다. 테스트 스위트: 2837 → 2846. - 레지스트리·스캐너·라우트 계약 변경 없음. 소스는 85개(EN 80 + RU 5),
ALL_ADAPTERS는 80 그대로입니다.
[1.227.0] — 2026-08-28
추가 — SEEK 홍콩(JobsDB), 팔로업 기한 도래만 보기, 그리고 비공개 고용주가 이를 중개한 에이전시를 표시합니다.
추가
- 기존 Jobstreet/SEEK 소스를 통한 JobsDB 홍콩. 홍콩은 JobsDB 브랜드로 운영되지만 같은 v5 엔드포인트 뒤의 동일한 SEEK 플랫폼 위에 있어 별도 소스가 필요 없습니다 — 호스트 허용 목록의
hk.jobsdb.com과siteKey: HK-Main이면 충분합니다. 레지스트리는 그대로 85개 소스(EN 80 + RU 5),ALL_ADAPTERS80 — 기존 어댑터에 추가된 새 시장이지 새 어댑터가 아닙니다. - 케이던스 보드의 기한 도래만 보기(
#/modes/followup). 새 체크박스가 보드를 지금 조치가 필요한 항목으로 좁힙니다. 기한 도래 여부는 상위 프로젝트의followup-cadence.mjs가 이미 계산한urgency(urgent/overdue)에서 읽으며, 트래커의status는 절대 쓰지 않습니다 — 그것은applied/responded/interview라 아무것도 일치하지 않습니다. 기한 도래 항목이 없으면 보드는 빈 화면 대신 가장 가까운 예정 팔로업을 알려 줍니다. 로직은 상위의followup-view.mjs를 옮긴 순수 모듈public/js/lib/followup-view.js에 있습니다. - 비공개 고용주는 이를 중개한 에이전시로 표시됩니다. 고용주가 비공개인 공고는 트래커에
?로 기록되고 리크루터는Via열에 들어갑니다.#/tracker는 맨?만 보여 주어 여러 비공개 행을 구분할 수 없었고 로고 리졸버도 맞출 대상이 없었습니다. 이제 그런 행은 **“비공개 · <에이전시> 경유”**로 표시되고, 로고를 에이전시 이름으로 해석하며, 검색창에서 그 이름으로 찾을 수 있습니다. 표시 전용입니다 — 정본company필드는 리크루터로 덮어쓰지 않습니다. 상위의company-presentation.mjs를 옮긴 새 순수 모듈public/js/lib/company-presentation.js.
수정
- Jobstreet/SEEK가 폐기된 엔드포인트를 호출하고 있었습니다. 소스가 여전히 SEEK가 중단한 chalice-search v4(
/api/chalice-search/v4/search)를 가리키고 있었습니다. 상위 프로젝트는/api/jobsearch/v5/search로 이전했지만 web-ui는 따라가지 않았습니다. 이제 모든 Jobstreet/SEEK 항목이 v5를 조회합니다. v5 항목 구조를 기본으로 읽고(id로 URL 생성,locations[0].label,advertiser.description,salaryLabel), v4 전용solrFields프로젝션 매개변수는 사라졌으며,portals.yml이 아직 죽은 v4 경로를 고정한 항목도 시장 호스트명은 유지한 채 경로만 v5로 재구성됩니다 — 사용자가 손댈 필요 없습니다.
참고
- career-ops 1.30.0 동등성(상위 HEAD
8d64f65→6cc46a4; 상위의VERSION은 아직 1.30.0 — release-please가main보다 뒤처집니다). 미이식 및 사유: iCIMS JSON-LD 위치 채우기 — web-ui의 iCIMS 소스에는enrichDate()상세 페이지 훅이 아예 없어(목록 페이지 전용) 고칠 경로가 없음; 비치명적 CLI stderr(#1974) — web-ui 러너는 종료 코드만으로 실패를 판정하며 그 휴리스틱이 없었음; Turbopack 경로 추적 — web-ui에는 번들러가 없음;cv-sync-check수정 — 읽기 전용 fail-soft 중계;scan.mjs의 Playwright 순차 실행, Block H 답변, updater/doctor/update-system가드, 재귀 문법 검사, Go 대시보드,jd-skill-gap/verify-cv-facts자체 테스트 추가 — web-ui가 호출하지 않는 CLI 전용 표면이거나 동작 변화가 없는 자체 테스트. 테스트: 2818 → 2837.
[1.226.0] — 2026-08-27
추가 — 베트남 채용 소스 2종: ITviec와 CareerViet.
추가
- ITviec + CareerViet(베트남). 호스트 고정된 베트남 보드 2종이
provider: itviec/provider: careerviet항목으로 스캐너에 합류합니다: ITviec(itviec.com, 기술 중심)와 CareerViet(careerviet.vn, 광범위). 둘 다 토큰 없는 HTML 스크레이퍼로 SSRF 호스트 고정·HTTPS 전용·회사별 fail-soft·페이지 상한을 갖춥니다. 레지스트리는 이제 85개 소스(80 EN + 5 RU),ALL_ADAPTERS80 — 둘 다#/scan소스 필터에 표시됩니다.
참고
- career-ops 1.30.0 패리티(parentVersion 1.29.0 → 1.30.0). 미포팅(CLI/커뮤니티 전용 또는 이미 반영): Hired Wall,
/calibrate, clean-markers, 템플릿 팩, doctor, pdf/LaTeX, web-ui가 호출하지 않는 eval/tracker/scan CLI들;_html-to-text수정과 detect-repostsroleFuzzyMatch는 이미 있었음;title-filter stem:접두사는 보류. 테스트 스위트: 2818.
[1.225.1] — 2026-08-27
수정 — 제공자 로고 배너가 이제 README와 도움말에 표시됩니다.
수정
- v1.225.0 배너가 GitHub에서 표시되지 않았습니다: 독립형
images/providers.svg의font-family에 중첩된 큰따옴표(GitHub가 거부)가 있었고,<img>가 저장소 상대src를 사용했으며(GitHub는 raw HTML<img>의 상대 경로를 해석하지 않음),<p>블록에 끝 빈 줄이 없어 다음 줄과 합쳐졌습니다. 이제 렌더링된images/providers.png를 절대 raw URL로 가리키며 빈 줄로 분리됩니다. SVG도 유효한 XML로 수정했습니다.
참고
- 문서만; 코드/테스트 변경 없음. cvstart.org 쇼케이스와 앱의 제공자 타일은 이미 정상이었습니다(인라인 SVG, 이 파일 아님). 2784
[1.225.0] — 2026-08-27
추가 — README와 도움말 가이드에 제공자 로고 배너.
추가
- 제공자 로고 배너. 하나의 자립형
images/providers.svg— 18개 LLM 제공자를 담아, 아이콘을 게시하는 11개는 실제 브랜드 로고(simple-icons, CC0), 나머지 7개는 브랜드 색상 모노그램 — 가 이제 README(×17)와 인앱 도움말 가이드 상단에 표시됩니다. (앱은 실제 로고 타일을 직접 렌더링하며, 배너는 GitHub에서 렌더링되고 앱의 XSS-안전 렌더러가 제거합니다.)
참고
- 문서만 — 코드/테스트 변경 없음; SVG는 인라인(원격 자원 없음). 스캔 소스 변경 없음: 83. 테스트 스위트: 2784.
[1.224.0] — 2026-08-26
변경됨 — LLM 제공자 타일이 이제 실제 브랜드 로고를 표시합니다.
변경됨
- 실제 제공자 로고. 제공자 타일 — 설정의 API 키 필드, 사용량 행, 대시보드 칩, 설정 “활성” 요약, ⚡ 평가 결과, 온보딩 배너 — 이 이제 오픈소스 아이콘을 게시하는 11개 제공자(Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub · DeepSeek · Kimi · MiniMax · Mistral · Ollama; simple-icons의 단일 경로 SVG, CC0)에 대해 실제 브랜드 로고를 렌더링합니다. 게시된 아이콘이 없는 7개(Hermes · GLM/Z.ai · Grok · Together · Fireworks · BytePlus Ark · Volcengine Ark)는 브랜드 색상 모노그램을 유지합니다. 여전히 구성상 CSP 안전: 로고 경로는 인라인 정적 상수 — 원격 자원 없음,
innerHTML없음.
참고
- 라우트/동작 변경 없음; 동일한
ProviderLogo.el(slug)API.provider-logo.js가 커졌습니다(인라인 로고 경로). 스캔 소스 변경 없음: 83. 테스트 스위트: 2784.
[1.223.0] — 2026-08-26
수정 — 성공한 ⚡ 라이브 평가가 오류로 표시되던 문제; 또한 OpenWorker coworker를 GitHub URL 또는 .zip에서 바로 설치.
수정
- ⚡ 평가 결과 배지 색상. 성공한 라이브 평가가 더 이상 실패로 표시되지 않습니다. 인프로세스 제공자(OpenRouter, DeepSeek, …)는 서브프로세스 종료
code를 반환하지 않으므로 엄격한code === 0이 거짓이 되어 모든 성공이 빨갛게(“exit 0”으로 표시되면서도) 나왔습니다. 이제 색상은 결과를 따릅니다 — 0이 아닌 코드로 종료된 서브프로세스만 빨갛게, 코드 없는 라이브 결과는badge-ok, “· exit N” 접미사는 실제 종료 코드가 있을 때만 표시됩니다.
변경됨
- OpenWorker coworker를 GitHub URL / .zip에서 설치. coworker 문서에 이제 한 줄 명령 설치기(
curl … | bash, 멱등 — 기존 career-ops / web-ui 재사용)와 OpenWorker의 세 가지 설치 경로(GitHub URL ·.zip· 단일 파일 가져오기)가docs/integrations/openworker.md, 인앱 도움말 §32(×17), README에 설명됩니다.
참고
- 오래된 커버리지 공백을 닫았습니다: Recruitee 소스에 v1.214.2 인용부호-각괄호 수정(태그 속성 안의
>가 설명으로 새면 안 됨)에 대한 종단 간 테스트가 추가되었습니다. 스캔 소스 변경 없음: 83. 테스트 스위트: 2783.
[1.222.0] — 2026-08-26
변경됨 — 확장 제공자(DeepSeek … Volcengine Ark) 필드 힌트가 이제 17개 언어로 현지화되었습니다.
변경됨
- 제공자 필드 힌트 현지화. v1.216.0–v1.217.0에서 추가된 11개 OpenAI 호환 제공자(DeepSeek · GLM/Z.ai · Kimi · MiniMax · Mistral · Grok · Together · Fireworks · Ollama · BytePlus Ark · Volcengine Ark)의 모든
config.<slug>Hint— 설정 → API 키의 API 키·모델·기본 URL 힌트 — 가 이제 영어로 폴백하지 않고 17개 언어 전체로 번역됩니다. 가입 URL, 모델 id, ⚡ 라이브 평가 마커는 그대로 유지됩니다.
참고
- 동작 변경 없음 —
field-specs.js의 영어hintFallback은 키 누락 시 여전히 보호하며, 새 계약 테스트가 모든 필드 서술자hintKey가 17개 언어에서 해석됨을 검증합니다. 스캔 소스 변경 없음: 83. 테스트 스위트: 2779.
[1.221.0] — 2026-08-26
보안 — 스캐너 fetch 경로의 DNS-rebinding 심층 방어.
보안
- 스캐너 HTTP 코어(
server/lib/http-json.mjs의fetchJson/fetchText)가 이제 실제 네트워크 경로에서 각 소스의 호스트를 해석하고 사설/루프백/링크로컬/CGNAT/클라우드 메타데이터 주소로 해석되면 연결을 거부합니다 — DNS-rebinding 벡터를 차단합니다. 스캐너 호스트는 이미 공개 도메인에 고정되어 있어 심층 방어이며, 사용자 URL 경로는 이미 더 강한 연결 고정(safe-fetch.mjs)이 있었습니다. 가드는 실제fetch전송에서만 실행되며, 주입된 테스트fetchImpl은 절대 해석되지 않습니다.
참고
- 정상 스캔에는 동작 변화 없음. 스캔 소스 변동 없음: 83. 테스트: 2775.
[1.220.0] — 2026-08-26
추가 — 하나의 항목으로 여러 Get on Board 카테고리를 스캔합니다.
추가
getonbrd항목에서programming피드만이 아니라categories: [programming, operations-management, machine-learning-ai](또는 단일category:)를 지정할 수 있습니다. 게시물은 카테고리 간 URL로 중복 제거되며 최대 12개로 제한됩니다. 기존 항목은 동일합니다(기본값은 여전히programming).
참고
- 서버 전용; SSRF 호스트 고정(www.getonbrd.com)과
redirect:'error'는 그대로이고, 잘못된 카테고리 슬러그는 요청 전에 거부됩니다. 스캔 소스 변동 없음: 83. 테스트: 2771.
[1.219.0] — 2026-08-26
추가 — Torre가 스캐너에 합류했고, 이제 모든 제공자 라벨이 올바릅니다.
추가
- Torre(
provider: torre) — torre.ai의 범-라틴아메리카 인재 마켓플레이스, Greenhouse/Lever/Ashby에 없는 원격 채용을 담은 무인증 공개 검색; 항목당 한 번의 제한된 요청. 레지스트리는 이제 83개 소스(영어 78 + 러시아어 5)를 제공합니다. 또한 온보딩 배너에 활성 제공자 모노그램.
수정
- a16z Speedrun 스캔이 순환하는 보드에서 조기에 멈췄습니다(스윕 도중 짧은 페이지를 마지막으로 간주해 ~300개 중 149개로 잘림). 이제 피드의
total_pages를 신뢰합니다. 또 온보딩 배너가 4개 항목 맵에서 제공자를 해석해 나머지 14개는 원시 슬러그를 보였는데, 이제 18개 라벨+모노그램을 사용합니다. 설정의 제공자 안내가 7→18로 바뀝니다.
참고
- Get on Board의 항목당 다중 카테고리 옵션은 아직 여기 없습니다(소스 수준 리팩터 필요). 테스트: 2768.
[1.218.0] — 2026-08-26
추가 — 제공자가 표시되는 모든 곳에 제공자 배지(와 올바른 이름).
추가
- 대시보드 칩, 설정 “활성” 요약, ⚡ 평가 결과 헤더의 제공자 모노그램.
수정
- 오래된 라벨: 세 화면이 5개 항목 맵에서 이름을 해석했고(평가 결과는 Anthropic이 아닌 모든 제공자를 “Gemini”로 잘못 표시), 이제 공유
ProviderStatus.label(18개)을 사용합니다. 키 개수가 “/ 7”로 표시되던 것을 18로 수정.LLM_PROVIDER힌트 슬러그 목록에 17개 언어 모두ark/arkcn추가.
참고
- 클라이언트 전용, CSP 안전. 서버 변경 없음. 스캔 소스: 82. 테스트: 2758.
[1.217.1] — 2026-08-26
테스트 강화 — 전체 제공자 목록의 엔드포인트 커버리지.
추가
- Kimi, MiniMax, Mistral, Fireworks의 엔드포인트·기본 모델·Bearer 인증을 검증하는 매개변수화된
run<Provider>테스트.
[1.217.0] — 2026-08-26
추가 — Ark가 LLM 목록에 합류(18개 제공자). ByteDance의 Doubao 모델인 BytePlus Ark와 Volcengine Ark가 이제 ⚡ 라이브 제공자입니다 — 동일한 OpenAI 호환 코어와 지역별 기본 URL을 사용합니다.
추가
- BytePlus Ark(
ARK_API_KEY, 국제)와 Volcengine Ark(ARK_CN_API_KEY, 중국) —runOpenAICompatible()을 통한 OpenAI 호환 Chat Completions.ARK_MODEL/ARK_CN_MODEL(Doubao 모델명 또는ep-…엔드포인트 id)을 설정하고,ARK_BASE_URL/ARK_CN_BASE_URL로 지역을 전환합니다. 둘 다auto순서(마지막)에 합류하며 설정과 사용량에 모노그램이 표시됩니다. 웹 UI는 이제 18개 라이브 제공자를 지원합니다.
참고
- 나머지와 동일한 신뢰된 설정 + http(s) 스킴 검사 경로(채용 URL용 SSRF 검증기는 사용 안 함). 스캔 소스 변동 없음: 82. 테스트: 2755.
[1.216.0] — 2026-08-26
추가 — LLM 제공자 9개가 더해졌고, 각각 브랜드 타일이 붙습니다. 이제 ⚡ 라이브 평가를 DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together, Fireworks 또는 완전 로컬 Ollama로 실행할 수 있습니다 — 키 하나면 되고, 설정의 각 필드 옆에 모노그램이 표시됩니다.
추가
- OpenAI 호환 제공자 9개. DeepSeek, GLM (Z.ai), Kimi (Moonshot), MiniMax, Mistral, Grok (xAI), Together AI, Fireworks AI, Ollama(완전 로컬, 키 불필요). 설정에서 키(또는 Ollama의 경우
OLLAMA_BASE_URL)를 지정하면 Hermes 다음의auto순서에 합류합니다.LLM_PROVIDER로 특정 제공자를 고정할 수 있습니다. Together는 Thinking Machines의 Inkling(thinkingmachines/Inkling)도 호스팅합니다. 웹 UI는 이제 16개 라이브 제공자를 지원합니다. - 제공자 모노그램 타일. CSP 안전한 브랜드 색상 이니셜 타일이 설정의 키 필드 옆과 사용량 페이지 행에 각 제공자를 표시합니다 — 인라인 SVG, 외부 로고 없음, 어떤 것도 기기를 떠나지 않습니다.
참고
- 제공자 기본 URL은
runOpenAICompatible()을 통해 http(s) 스킴 검사(Ollama 루프백 허용)로 처리되는 신뢰된 설정이며, 채용 URL용 SSRF 검증기를 거치지 않습니다. 스캔 소스 변동 없음: 82. 테스트: 2752.
[1.215.0] — 2026-08-26
추가 — OpenWorker에서 구직 전체를 실행하세요. 새 coworker가 Andrew Ng의 오픈소스 AI coworker 앱에서 이 파이프라인을 구동하며, 이제 인앱 도움말이 이를 다뤄 “문서에 묻기” 어시스턴트가 안내할 수 있습니다.
추가
- OpenWorker coworker. 코드 없는 OpenWorker coworker — Fighter90/career-ops-coworker — 가 구직 전체(스캔·평가·맞춤·추적·팔로업)를 실행하고 이 대시보드를 실행할 수 있으며, 모두 데스크톱에서 이뤄집니다. 17개 언어로 제공되는 한 파일짜리 페르소나로, OpenWorker 자체 로더로 설치 가능함이 검증됐습니다.
- 도움말 §32 “Run it from OpenWorker”(17개 로케일)가 이를 문서화하여, 인앱 문서에 묻기 어시스턴트와 떠 있는 도움말 버튼이 이에 답할 수 있습니다. 랜딩 푸터, README, 위키,
docs/integrations/openworker.md에도 링크됩니다.
참고
- 문서 + 사이트만 — 앱 코드 변경 없음. 스캔 소스는 82개로 변동 없음. 테스트 스위트: 2742.
[1.214.2] — 2026-08-25
수정 — 공유 HTML→텍스트 파이프라인(Greenhouse / Recruitee 설명)이 속성 값에 > 문자가 있을 때 태그 속성을 설명에 흘리지 않습니다.
수정
- 속성 안에
>가 있는 HTML 설명이 깔끔하게 제거됩니다. Greenhouse·Recruitee 설명 뒤의 태그 제거기는 첫>에서 멈추는 단순 매칭을 써서,<a title="salary > 100k">같은 태그가 뒤쪽 속성 텍스트(100k">)를 설명에 남겼습니다. 이제 따옴표로 감싼 속성 값(작은/큰따옴표)을 존중하고, 엔티티를 디코딩하기 전에 마크업을 제거하며(속성 안의 인코딩된 따옴표가 거짓 구분자가 되지 않도록), 평문의 리터럴<>는 그대로 보존합니다.
참고
- 테스트 스위트: 2742. 스캔 소스는 82개로 변동 없음. 이번 라운드에 부모에서 새로 온 것은 없습니다: 나머지 변경은 읽기 전용으로 중계되거나(follow-up / weekly-digest / rejection-latency / salary-gap 수정은 web-ui 변경 불필요) web-ui 표면이 아닙니다(외부 X/Xquik BYO-key 플러그인, CLI/verify 수정). 선택형 항목 — SmartRecruiters 상세 설명, detect-reposts 애그리게이터 건너뛰기,
stem:/word:제목 접두사, DNS 리바인딩 방어 — 은 계속 대기 중입니다.
[1.214.1] — 2026-08-25
수정 — Ashby 공고 설명이 이제 Greenhouse·Recruitee와 같은 길이로 잘려, 콘텐츠 필터가 세 보드에서 동일하게 동작하고 한 보드가 스캔 캐시를 부풀리지 못합니다.
수정
- Ashby 설명이 이제 다른 보드와 같은 길이 상한을 지킵니다. v1.214.0은 Ashby의 공고 본문을 콘텐츠 필터에 넣었지만, 4000자로 자르는 Greenhouse·Recruitee와 달리 Ashby는 상한 없이 그대로 매핑됐습니다(일부는 4배 더 길었음). 그래서 같은
content_filter키워드가 텍스트의 위치에 따라 Ashby에서는 매칭되고 Greenhouse에서는 안 될 수 있었고, 한 보드가#/scan이 불러오는 스캔 캐시에 수백 KB를 더할 수 있었습니다. Ashby도 이제 같은 4000자로 잘립니다(이미 평문이라 잘리기만 하고 태그가 제거되지는 않습니다).
참고
- 테스트 스위트: 2738. 스캔 소스는 82개로 변동 없음.
[1.214.0] — 2026-08-25
수정 — Greenhouse·Ashby·Recruitee의 공고 설명이 이제 콘텐츠 필터로 들어가 키워드 / 국가 / 비자 필터가 이 보드들에 실제로 매칭되고, “Associate <시니어 직함>”(학술 직급 포함)이 더는 엔트리급으로 잘못 분류되지 않습니다.
수정
- 콘텐츠 필터가 이제 Greenhouse·Ashby·Recruitee에서 작동합니다. 이 보드들은 이제 각 공고의 전체 본문을 설명으로 담으므로, 설명을 읽는
content_filter(또는 국가 / 비자 키워드 필터)가 보드를 그냥 통과시키지 않고 공고 본문에 매칭합니다. Ashby와 Recruitee는 목록 페이로드에 본문을 무료로 실어 주고, Greenhouse는 이미 그랬습니다. 셋 다 하나의 공유 HTML→텍스트 파이프라인으로 정리됩니다. - “Associate Director” / “Associate Professor”는 엔트리급이 아니라 시니어입니다. 시니어리티 분류기(
skip_tiers:가 사용)는 “associate”가 시니어 직함을 수식할 때도 이를 엔트리급 표시로 취급했습니다. 이제 “Associate <시니어 명사>”—학술 직급(Associate Professor, Associate Dean) 포함—를 시니어로 읽고, 진짜 주니어 변형(Associate Attorney, Associate Editor)은 엔트리급으로 유지하며, 앞에 오는 “Intern, Associate Dean”은 여전히 인턴십으로 분류합니다.
참고
- 테스트 스위트: 2736. 스캔 소스는 82개로 변동 없음. 후속으로 대기: 선택형 SmartRecruiters 설명 보강(공고별 상세 조회), detect-reposts의
aggregator: true보드 건너뛰기, 앵커드 키워드stem:/word:제목 접두사 — 각각 기본 동작 변경이 아니라 선택형 설정입니다. DNS 리바인딩 강화는 별도 보안 릴리스로 여전히 대기 중입니다.
[1.213.0] — 2026-08-22
추가 — 싱가포르 국가 채용 은행 MyCareersFuture를 스캔 소스로 추가. 수정 — Greenhouse 공고가 이제 전체 본문을 담아 콘텐츠 필터가 작동하고, 원격 Ashby 공고가 도시만 표시된 위치 뒤에 더는 가려지지 않습니다.
추가
- MyCareersFuture (싱가포르)(mycareersfuture.gov.sg) — Workforce Singapore가 운영하는 싱가포르 국가 공공 채용 은행을 위한 새 무토큰 스캔 소스입니다.
#/scan의 소스 필터에서 고르거나, 선택적keywords목록과 함께provider: mycareersfuture회사를 추가하세요(Job Bank처럼 생략 시 프로필의 목표 직무로 대체). 공개 검색 API를 호스트 고정으로, 키 없이 읽습니다.
수정
- Greenhouse 공고를 이제 콘텐츠로 필터링할 수 있습니다. Greenhouse 보드를 공고 전체 본문과 함께 가져와 공고 설명(description)으로 평문 디코딩합니다 — 그래서 설명을 읽는
content_filter(또는 국가/비자 키워드 필터)가 이제 Greenhouse 공고를 그냥 통과시키지 않고 실제로 매칭합니다. - 원격 Ashby 공고가 더는 도시 필터에 걸려 탈락하지 않습니다. Ashby는 근무 형태(Remote/Hybrid/Onsite)를 사무실 도시와 별개로 두므로, 완전 원격 공고도 예를 들어 “San Francisco”로 읽혔고 그 도시를 막는 위치 필터가 지원 가능한 공고를 숨겼습니다. 이제 원격일 때 위치에 “Remote”가 추가되며, 오래된
isRemote플래그보다workplaceType가 우선해 사무실 기반 하이브리드 공고를 잘못 라벨링하지 않습니다.
참고
- 스캔 소스: 82개(영어 77 + 러시아어 5). 테스트 스위트: 2724. DNS 리바인딩 방어(연결 전에 호스트의 확인된 주소 검증)는 직접 이식이 아니라 web-ui 전용 설계가 필요해 전용 릴리스로 대기 중입니다.
[1.212.1] — 2026-08-21
수정 — cvstart.org 랜딩이 스캐너의 채용 소스를 적게 셌습니다(80으로 표시하며 Job Bank (캐나다)를 누락). 이제 앱의 81과 다시 일치하며, 둘이 어긋나면 사이트 빌드가 요란하게 실패합니다.
수정
- 랜딩의 “채용 소스” 수가 앱과 다시 동기화됩니다. v1.212.0 이후 cvstart.org는 80개 보드를 보여주고 새 Job Bank (캐나다) 칩이 빠져 있었지만, 앱·스캔 드롭다운·도움말은 모두 81을 나열했습니다. 랜딩은 라이브 스캐너 레지스트리를 불러와 목록을 만드는데, 한 소스가 YAML 의존성을 가져오는 방식 때문에 그 빌드에서 로드에 실패해 조용히 누락됐습니다. 이제 Job Bank는 앱의 나머지가 스캔 시점에 하는 것과 같은 방식으로 그 의존성을 지연 로드하므로 항상 나타납니다.
- 사이트 빌드는 이제 어긋난 소스 수를 배포하지 않습니다. 레지스트리가 디스크에 존재하는 것보다 적은 소스를 열거하면(로드 실패 소스의 특징) 빌드가 잘못된 숫자를 조용히 배포하는 대신 명확한 메시지와 함께 실패합니다.
참고
- 앱 동작은 그대로입니다 — 스캐너에는 항상 81개 소스가 모두 있었고, 랜딩 페이지만 영향을 받았습니다. 스캔 소스: 81개(영어 76 + 러시아어 5) — 변동 없음. 테스트 스위트: 2687.
[1.212.0] — 2026-08-21
추가 — Job Bank (캐나다), 연방 국가 채용 보드. 제거 — EchoJobs(피드가 봇 차단됨). 수정 — Consider 기반 보드가 다시 결과를 반환하고, 다중 지역 Lever 공고가 더는 절반의 지역을 숨기지 않습니다.
추가
- Job Bank (캐나다)(jobbank.gc.ca) — 어떤 애그리게이터도 잘 다루지 못하는 대용량 보드인 캐나다 연방 국가 고용 서비스를 위한 새 무토큰 스캔 소스입니다.
#/scan의 소스 필터에서 고르거나, 선택적keywords목록과 함께provider: jobbankca회사를 추가하세요(생략 시 프로필의 목표 직무로 대체). 공개 ATOM 피드를 호스트 고정으로, 키 없이 읽습니다.
제거
- EchoJobs — 폐기. 공개 피드가 이제 봇 차단 뒤에 있어 아무것도 반환하지 않으므로, 유지해봤자 스캔 슬롯만 낭비했습니다.
수정
- Consider 기반 보드가 다시 결과를 반환합니다. 이제 Consider는 검색 요청을 받기 전에 익명 핸드셰이크(세션 쿠키 + CSRF 토큰을 심는 GET)를 요구합니다. 그것 없이는 요청이 조용히 거부되어 보드가 비어 보였습니다.
- 다중 지역 Lever 공고가 더는 절반의 지역을 숨기지 않습니다. Lever는 기본 도시 하나를
location에, 나머지를allLocations에 둡니다. 기본만 읽으면 바르셀로나 AND 몬테비데오에 열린 공고가 바르셀로나 전용으로 보였습니다(그리고 지역 필터에 잘못 걸려 탈락). 이제 둘을 병합합니다.
참고
- 페이지 간 페이싱을 더 완만하게(150 → 250 ms) 조정해 단일 호스트 채용 사이트에 예의를 지킵니다. 스캔 소스: 81개(영어 76 + 러시아어 5) — 변동 없음(Job Bank 추가, EchoJobs 제거). 테스트 스위트: 2685.
[1.211.0] — 2026-08-19
추가 — 대만 테크 채용 보드 Yourator. 수정 — 제목·회사명의 악센트 엔티티가 이제 어디서나 디코딩되고, 이름에 악센트가 있는 회사가 더는 잘못 표시되지 않습니다.
추가
- Yourator(yourator.co) — 대만 테크·디지털 채용 시장을 위한 새 무토큰 스캔 소스입니다.
#/scan의 소스 필터에서 고르거나provider: yourator회사를 추가하세요. 공개 JSON API를 읽고(키·브라우저 없음), 보드의 모든 페이지를 순회하며, 각 공고의 실제 고용주 링크(자체 ATS)를 추적 파라미터를 제거해 내보냅니다.
수정
- 악센트 명명 엔티티가 이제 어디서나 디코딩됩니다. 공유 HTML 디코더가 Latin-1 문자(
é→ é,ç→ ç, …)를 갖췄으므로,Développeur나Français를 쓰는 유럽 보드가 그 리터럴을 제목·트래커·생성 문서에 남기지 않습니다. (대문자는 대문자로 —É는 É이지 é가 아님 —&constructor;같은 조회는 이제 자기 자신으로 남습니다.) - 이름에 악센트가 있는 회사가 자체 도메인에 있다는 이유로 더는 잘못 표시되지 않습니다. “Işık”은 이제 “isik”으로 접혀 isik.com.tr과 일치하고, “Société Générale”은 societegenerale.com과 일치합니다. 이전 검사는 악센트 문자를 ASCII 기본형으로 접지 않고 지웠습니다.
참고
- 스캔 소스: 81개(영어 76 + 러시아어 5). 테스트 스위트: 2667.
[1.210.1] — 2026-08-19
수정 — “&“나 따옴표가 든 Habr Career 공고 제목·회사명이 더 이상 깨져서 들어오지 않습니다.
수정
- Habr Career 소스가 이제 제목과 회사명의 HTML 엔티티를 흘려보내기 전에 디코딩합니다. 서버 렌더링 카드는 이스케이프된 채로(“Changellenge >>”, “Demand Forecasting & Inventory Optimization”, “ООО "М-ТЕХ"”) 도착하므로, 디코딩되지 않은 “&“가 사용자의 “&” 제목 필터를 조용히 통과하지 못했고(직전 릴리스가 다섯 개 보드에서 닫은 바로 그 증상) 회사명이 트래커와 리포트에 깨진 채 도달했습니다. 이제 여섯 개 영향 소스 모두에서 엔티티 디코딩이 완결됩니다.
참고
- 테스트 스위트: 2644.
[1.210.0] — 2026-08-19
추가 — 스캐너의 첫 아프리카 채용 보드 Senjob(세네갈); 다섯 개 보드의 제목 매칭 개선.
추가
- Senjob(senjob.com) — 세네갈을 위한 새 무토큰 스캔 소스이자 스캐너의 첫 아프리카 보드입니다.
#/scan의 소스 필터에서 고르거나provider: senjob회사를 추가하세요. 공개 목록을 평범한 HTTP로 읽고(키·브라우저 없음), 모든 요청을 senjob.com에 고정하며 — HTML을 파싱하므로 — 갑자기 아무것도 나오지 않는 목록을 “채용이 없는 나라”가 아니라 깨진 보드(드러난 오류)로 처리합니다.
수정
- “&“가 든 제목이 다섯 보드에서 더는 공고를 떨어뜨리지 않습니다 — beesite, Cornerstone(csod), Hacker News “Who is hiring”, Phenom, TKMS는 제목이 HTML 이스케이프된 상태로 도착합니다. 그래서 “R&D Engineer” 같은 직무의 이스케이프된 “&“가 사용자의 “r&d” 키워드를 통과하지 못해 공고가 조용히 사라졌습니다(“sales & marketing” 부정 필터도 발동하지 않았습니다). 이제 제목 — 그리고 Phenom 위치 — 은 필터링 전에 디코딩됩니다.
참고
- 스캔 소스: 80개(영어 75 + 러시아어 5). 테스트 스위트: 2643.
[1.209.0] — 2026-08-17
추가 — 인앱 도움말이 이제 지원 결과 기록 방법을 다루며, “도움말에 물어보기”가 그곳으로 안내합니다.
추가
- 트래커 도움말(§11)에 17개 언어 모두 “결과 기록하기” 섹션이 추가되어 결과 버튼을 안내합니다: 무슨 일이 있었는지 선택(거절 / 오퍼 / 합격 / 거절 / 잠수 / 면접 진행)하고, 무엇을 할지 미리 본 뒤 기록하면 — 결과를 남기고, 제출한 CV와 커버레터를 보관하며, 행의 상태를 대신 동기화합니다. 떠다니는 “도움말에 물어보기” 도우미는 이 가이드를 읽으므로, 이제 상태를 직접 편집하라고만 하지 않고 그 버튼으로 안내합니다.
참고
- 각 도움말 번들은 이제 31 H2 / 119 H3입니다(기존 118); 패리티 가드를 맞춰 올렸습니다. 문서만 변경 — 코드나 동작 변경 없음. 스위트: 2625.
[1.208.2] — 2026-08-16
수정 — 휴대폰에서 알림·테마 버튼이 더 이상 검색창 위에 겹치지 않습니다.
수정
- v1.208.1은 상단 바 버튼이 페이지 제목을 가리는 문제를 없앴지만, 아주 좁지는 않은 좁은 화면에서 — 특히 버튼 라벨이 긴 언어에서 — 바 전체가 여전히 한 줄에 몰려서 🔔·🌙 버튼이 검색창 위에 놓일 수 있었습니다. 이제 동작 버튼(알림, 테마, 진단, Scan 열기)이 휴대폰에서는 항상 전체 너비의 둘째 줄로 내려가므로 검색창이 온전히 보이고 아무것도 겹치지 않습니다.
참고
- 휴대폰에서 상단 바 동작 버튼이 전체 너비의 둘째 줄로 이동하여, 레이아웃이 남은 음수 공간을 겹침으로 분배하던 취약한 “거의 꽉 찬 줄” 구간을 없앴습니다. Playwright 가드가 이제 정확한 유발 조건 — 긴 라벨 로케일 × 565–640px 구간 — 을 재현하고 상단 바 컨트롤이 픽셀을 공유하지 않음을 확인합니다. 스위트: 2621.
[1.208.1] — 2026-08-16
수정 — 휴대폰에서 상단 바 버튼이 더 이상 페이지를 가리지 않습니다.
수정
- v1.208.0에서 좁은 화면일 때 상단 바 동작 버튼(진단, Scan 열기, 알림, 테마)을 둘째 줄로 넘겼지만 바 높이가 고정이라, 넘어간 줄이 밖으로 삐져나와 페이지 제목 위에 겹쳤습니다. 이제 바가 줄에 맞춰 늘어나고 콘텐츠가 그 아래로 흐릅니다.
참고
- 상단 바의 고정
height를min-height로 바꿔 어떤 너비에서도 내용에 맞게 늘어납니다(데스크톱은 그대로). Playwright 가드가 이제 상단 바가 페이지 위로 삐져나오지 않는지도 확인합니다. 스위트: 2621.
[1.208.0] — 2026-08-16
수정 — 이제 앱이 휴대폰 화면에 맞습니다: 옆으로 스크롤되지 않습니다.
수정
- 좁은 화면에서는 앱 전체가 옆으로 밀렸습니다 — 상단 바, 표, 도움말 문서, 설정 탭이 모두 오른쪽 가장자리를 넘었습니다. 이제 모든 페이지가 어떤 너비에도 맞습니다: 상단 바 버튼은 둘째 줄로 넘어가고, 넓은 표와 코드 블록은 자체 박스 안에서 스크롤되며, 도움말은 목차를 본문 위에 쌓고, 버튼·탭 줄은 줄바꿈되며, 긴 경로나 URL은 페이지를 늘리지 않고 줄바꿈됩니다.
참고
- 근본 원인은 전형적인 flex/grid min-width: auto 함정과 감싸지지 않은 몇몇 넓은 요소였습니다. grid 항목의
min-width: 0, 마크다운·제목의overflow-wrap, 스크롤 가능한 마크다운 표, 모바일 브레이크포인트에서의 도움말 그리드 쌓기로 해결했습니다. Playwright 가드가 주요 경로에서 375px에서 가로 오버플로 0을 검증합니다.tests/playwright-smoke.mjs. 스위트: 2621.
[1.207.2] — 2026-08-16
수정 — AI 계획과 커리어 오리엔테이션 프로필이 더 이상 원시 코드 덤프로 표시되지 않습니다.
수정
- 일부 모델은 답변 전체를
markdown …코드 펜스로 감쌉니다. 그럴 때 개발 계획과 오리엔테이션 프로필이 제목·목록이 있는 문서 대신 고정폭 코드 블록으로 나왔습니다. 이제 감싸는 펜스를 제거합니다 — 답변 전체를 감싸고 언어가 명시적으로markdown/md일 때만, 그래서 진짜python/js/언어 없는 ``` 코드 답변은 그대로 둡니다.
참고
- 공용 LLM 정리 단계(
cleanLlmMarkdown)에서 한 번에 처리하므로 모든 AI 경로가 혜택을 받고, 감싼 답변 안의 코드 블록은 보존됩니다.tests/llm-output.test.mjs(+3). 스위트: 2621.
[1.207.1] — 2026-08-16
수정 — 작은 휴대폰에서 랜딩 페이지가 더 이상 옆으로 넘치지 않습니다.
수정
- 좁은 휴대폰에서 히어로 영역(제목, 소개 문장, 설치 터미널)이 오른쪽 가장자리에서 잘릴 수 있었습니다. 긴 설치 명령과 레이아웃 열이 화면에 맞게 줄어들지 않았기 때문입니다. 이제 어떤 너비에도 맞으며, 설치 명령은 자체 터미널 상자 안에서 스크롤됩니다.
참고
- 또한 일시적인 리소스 404로 실패할 수 있던 불안정한 E2E 스모크 검사를 강화했습니다 — 이제 형제 검사처럼 무해한 네트워크 잡음(파비콘 / 연결 / 리소스 실패)을 무시하면서 실제 스크립트 오류는 계속 잡습니다. 애플리케이션 동작은 바뀌지 않았습니다. 스위트: 2618.
[1.207.0] — 2026-08-15
추가됨 — 트래커에서 바로 지원 결과를 기록합니다.
추가됨
- 모든 트래커 행에 결과 동작이 생겼습니다: 무슨 일이 있었는지 고르고(불합격, 오퍼 수신, 입사, 오퍼 거절, 무응답, 면접 진행), 선택 메모를 추가하고, 결과 상태를 미리보기한 뒤 기록합니다. 기록하면 제출한 CV·커버레터 아티팩트를 보관하고 트래커를 표준 상태로 동기화합니다 — 손으로 편집하는 대신 결정적인 한 번의 동작입니다.
참고
- 새
POST /api/outcome가 결과 CLI를 중계합니다:dryRun:true는 읽기 전용 미리보기(행을 찾아 결과 상태를 알려주며 아무것도 쓰지 않음)이고, 실제 호출은 기록합니다. 쓰기 안전성: 결과 유형은 알려진 집합으로 화이트리스트되고 모든 텍스트 필드는 셸 호출 전에 제어 문자를 거부합니다(배열 인자, spawn — 셸 없음).tests/outcome-route.test.mjs. 스위트: 2618.
[1.206.0] — 2026-08-15
문서 — 앱 내 도움말 가이드가 이제 최신 기능 다섯 가지를 17개 언어 모두로 다룹니다.
추가됨
- 앱 내 도움말 가이드와 이를 근거로 답하는 «도움말에 질문» 도우미가 이제 최근에 추가된 다섯 기능을 문서화합니다: 설정 닥터(설정 — CV·프로필의 빠진 부분과 남은 예시 데이터를 점검), ATS 보드 찾기(포털 — 회사의 채용 보드를 자동으로 탐색), «아직 열려 있나요?» 확인(트래커 — 공고가 아직 열려 있는지), «이전 CV 재사용?» 힌트(CV 스튜디오 — 이미 맞춤한 CV가 새 공고에 맞는지 알림), 스킬 로그(분석 — 자가 평가 점수 기록). 새 하위 섹션 다섯 개를 17개 언어로 번역했습니다.
참고
- 도움말 구조가 31개 H2 / 118개 H3로 늘었고 모든 언어에서 패리티가 보장됩니다. 참조 문서 갱신:
docs/architecture/API.md가 이 기능들의 다섯 라우트를 문서화하고,CLAUDE.md와docs/sdd/CONVENTIONS.md의 라우트/버전 수치가 최신입니다(라우트 모듈 36개). 스위트: 2610.
[1.205.0] — 2026-08-15
추가 — 시험/평가 결과를 기록하는 스킬 로그.
추가됨
- 새 스킬 로그(분석 → 스킬 로그)로 자가 평가를 기록할 수 있습니다 — 회사, 플랫폼, 스킬, 점수 %, 선택 메모 —
data/assessments.tsv에 추가되고 최신순 목록으로 표시됩니다. 토큰 없이 결정론적이며, 파일 형식은 부모 CLI가 담당합니다.
참고
- 새
GET /api/assessments(assessment-log.mjs의 기본 JSON 목록 릴레이; 부드러운 실패{available:false}) +POST /api/assessments(명시적 쓰기: 필드를 배열 인자로assessment-log.mjs add에 전달). 쓰기 안전: 제어 문자가 든 텍스트 필드는 거부됩니다(TAB은 열을, 줄바꿈은 행을 깨뜨림) → 쓰기 전 400; 점수/기준은 0–100으로 제한, 길이 제한.tests/assessments-route.test.mjs. 스위트: 2610.
[1.204.0] — 2026-08-15
추가 — 설정에 “설정 닥터” 패널 추가, 불완전하거나 예시 데이터가 남은 CV/프로필을 표시합니다.
추가됨
- 설정 → 설정 닥터가 이제 토큰 없이
cv.md와config/profile.yml을 점검하여 차단 문제(누락 파일/필드)와 경고(남은 예시/자리표시 데이터, 하드코딩된 수치)를 나열합니다 — 불완전한 설정이 스캔·맞춤을 약화시키기 전에 잡습니다. 읽기 전용, 한 번 클릭으로 재실행.
참고
- 새 읽기 전용
GET /api/cv-sync-check가 부모 프로젝트의cv-sync-check.mjs를 릴레이합니다 — 이 스크립트는 사람이 읽는 텍스트 + 종료 코드를 출력하며--json이 없습니다. 라우트는 안정적인ERROR:/WARN:줄을 가볍게 파싱해{ok, errors[], warnings[]}로 만들며, 성공 여부는 종료 코드가 아니라 배너로 판단합니다. 독립 설치에서는{available:false}로 부드럽게 실패합니다.tests/cv-sync-check-route.test.mjs. 스위트: 2602.
[1.203.0] — 2026-08-15
추가 — CV 스튜디오에 “이전 CV 재사용?” 힌트.
추가됨
- CV 스튜디오에서 저장된 채용공고를 열면, 이제 앱이 저장해 둔 다른 공고들과 비교하여(결정론적 단어 겹침, 토큰 0) 가장 비슷한 것이 그 맞춤 CV를 재사용할 만한지, 수정 후 재사용할지, 새로 맞춤할지 알려줍니다 — 이미 노린 역할을 처음부터 다시 만들 필요가 없습니다.
참고
- 새 읽기 전용
GET /api/jds/:name/reuse가 부모 프로젝트의jd-similarity.mjs(자카드 겹침 + 시니어리티 가드; JSON{decision, score, reason})를 이전 공고마다 한 번씩 릴레이합니다(팬아웃 25 제한, 최적 매치 선택). 스크립트나 이전 공고가 없으면{available:false}로 부드럽게 실패합니다.tests/jd-similarity-reuse-route.test.mjs. 스위트: 2594.
[1.202.0] — 2026-08-15
추가 — #/portals에서 회사의 ATS 채용 보드를 찾아 바로 추적을 시작하세요.
추가됨
- #/portals에서 회사 이름을 입력하면 앱이 Greenhouse, Ashby, Lever에서 공개 채용 보드를 탐색하여 — LLM 0, 브라우저 없음 — 존재하고 현재 채용 ≥1건인 보드를 보여줍니다. 한 번 클릭하면 선택한 보드를 스캐너가 감시하는 회사에 추가합니다. 탐색은 읽기 전용이며,
portals.yml쓰기는 추가를 눌렀을 때만 일어납니다.
참고
- 새
server/lib/discover-ats.mjs(고정 호스트·문자셋 검증 슬러그를 DNS 고정safeGet으로 탐색, 요청당 ≤12회) +POST /api/portals/discover(읽기 전용) 및POST /api/portals/track(명시적 쓰기:withFileLock+ 텍스트 스플라이스 + 재파싱 가드 + 원자적 교체; 알려진 ATS 호스트만, 멱등). 스캐너의 어댑터 레지스트리를 재사용해 보드 확인·채용 수 계산. i18n ×17.tests/discover-ats-resolver.test.mjs+tests/discover-ats-route.test.mjs. 스위트: 2588.
[1.201.0] — 2026-08-15
수정 — 지역화되거나 변형된 열 머리글을 쓰는 트래커가 더 이상 빈칸으로 표시되지 않습니다.
수정됨
data/applications.md가 비영어 또는 변형 머리글(스페인어empresa/puesto/estado/fecha/enlace, 또는position/stage/link)을 쓰면, 트래커가 잘못된 키로 읽어 회사 / 직무 / 상태 / 날짜 / 링크 열이 빈칸으로 나왔습니다. 이제 이런 머리글이 표준 필드명으로 접혀 트래커가 올바르게 표시됩니다. 영어 트래커는 이전과 똑같이 처리됩니다.
참고
parseApplications(server/lib/parsers.mjs)에 새HEADER_ALIASES맵과 정규화 접힘 추가; 알 수 없거나 이미 표준인 머리글은 그대로 통과합니다.tests/tracker-header-aliases.test.mjs. 스위트: 2563.
[1.200.0] — 2026-08-15
추가 — 트래커에서 ATS 채용공고의 “아직 열려 있나요?“를 한 번에 확인.
추가됨
- #/tracker에서 URL이 Greenhouse / Lever / Ashby / Workday / SmartRecruiters 공고인 지원 항목에 이제 “아직 열려 있나요?” 버튼이 표시됩니다. 한 번 누르면 ATS 자체의 공개 JSON을 조회하여 — 토큰 0, 브라우저 없음 — 열림 / 마감 / 알 수 없음을 보여주므로, 하나씩 열지 않고도 사라진 공고를 찾을 수 있습니다. 보수적 설계: 확실한 404/410만 마감으로 보고, 애매하면 알 수 없음으로 둡니다(잘못된 마감 없음).
참고
- 새
server/lib/liveness-core.mjs+liveness-api.mjs와 읽기 전용GET /api/liveness?url=(쓰기 없음, LLM 없음). SSRF 안전: URL은isValidJobUrl로 검사한 뒤 ATS API는 고정 호스트와 문자셋 검증 경로로 DNS 고정safeGet을 통해서만 접근합니다.tests/liveness-core.test.mjs+tests/liveness-route.test.mjs. 스위트: 2557.
[1.199.0] — 2026-08-15
수정 — 넓은 표가 잘리지 않고 가로로 스크롤됩니다.
수정됨
- Scan 페이지(및 다른 모든 표 — 트래커, 통계, 사용량, 대시보드)에서 창보다 넓은 표가 스크롤바 없이 잘려 오른쪽 열에 접근할 수 없었습니다. 이제 넓은 표는 필요할 때 가로 스크롤바를 표시하므로 어떤 너비에서도 모든 열에 접근할 수 있습니다.
참고
public/css/components.css의.table-wrap을overflow: hidden에서overflow-x: auto로 변경했습니다(기존.reports-scroll컨테이너와 동일). 둥근 테두리는 유지됩니다.tests/table-wrap-scroll.test.mjs. 스위트: 2540.
[1.198.0] — 2026-08-15
추가 — 스캔 재시도가 이제 지수 백오프, 지터를 사용하고 속도 제한의 Retry-After를 존중합니다.
추가
- 스캔 도중 채용 보드가 잠깐 속도를 제한하거나 오류(HTTP 429 / 5xx)를 낼 때, 재시도가 고정된 짧은 지연 대신 지수 백오프 + 지터로 대기합니다 — 바쁜 보드를 같은 간격으로 계속 두드리지 않고, 동시 재시도가 한꺼번에 다시 충돌하지 않습니다. 보드가 보낸
Retry-After는 존중됩니다(단, 제한됨 — 악의적인Retry-After: 86400이 전체 스캔을 멈추지 못하게). 영구 오류(404, 거부된 리디렉션)는 여전히 즉시 실패합니다 — 변경 없음.
참고
server/lib/http-json.mjs에 새parseRetryAfterMs()와 순수 함수computeRetryDelayMs();fetchJson은 이제 비정상 응답에서.retryAfter를 캡처하고fetchJsonWithRetry는 선택적maxDelayMs(기본 8000)를 받습니다.tests/http-json.test.mjs(+9). 스위트: 2536.
[1.197.0] — 2026-08-14
추가 — Getro VC 채용 보드를 careers_url 하나만으로 추적하고, 컬렉션 id는 자동으로 해석됩니다.
추가
- 추적 중인 Getro 보드(b2venture, Earlybird, Point Nine, …)에 더 이상 손으로 찾은 숫자
getro_collection이 필요하지 않습니다. 보드 자체의careers_url만 주면 첫 스캔 때 그 페이지에서 id가 자동으로 해석됩니다 — SSRF 안전 GET 한 번으로 페이지에 내장된 데이터에서 숫자network.id를 바로 읽습니다. 명시적getro_collection은 여전히 우선이며 가져오기를 통째로 건너뜁니다.
참고
server/lib/sources/getro.mjs에 새httpsCareersUrl(),extractCollectionId(), 비동기resolveCollectionId()추가. 보드 페이지는 DNS 고정·크기 제한된safeGet으로 가져오고, 해석된 id는 여전히assertGetroUrl에 의해 호스트가api.getro.com으로 고정됩니다. 이제 어댑터는 id가 없어도 httpscareers_url을 지닌provider: getro항목에 매칭됩니다.tests/sources-getro.test.mjs(+8). 스위트: 2527.
[1.196.0] — 2026-08-14
수정 (보안) — Workday 어댑터가 api 엔드포인트를 부분 문자열이 아닌 호스트명으로 검증합니다.
수정
portals.yml의 Workdayapi:값은 이제 호스트명이myworkdayjobs.com(또는.myworkdayjobs.com하위 도메인)일 때만 허용됩니다. 이전 검사는 부분 문자열 일치라서, 문자열을 포함하기만 한 URL — 예:https://example.com/?x=myworkdayjobs.com— 도 통과해 엔드포인트로 사용될 수 있었습니다. 실제 Workday 엔드포인트는 영향 없음. (CodeQL 보고, #443.)
참고
- 새
isWorkdayApi()가 URL을 파싱해 호스트를 확인 (server/lib/portals/adapters/workday.mjs).tests/workday-adapter-endpoint.test.mjs(+1). 스위트: 2522.
[1.195.0] — 2026-08-14
성능 (스캐너) — 큰 스캔 이력에서도 재게시 감지가 빠르게 유지됩니다.
성능
- 큰
scan-history.tsv에서 중복 공고 감지가 더 이상 O(N²)로 저하되지 않습니다. 회사별 제목 그룹화는 모든 쌍마다 전체roleFuzzyMatch를 수행하는 중첩 루프였는데, 이제 역색인입니다 — 한 번의 패스로 정확한 제목별로 버킷팅한 뒤, 판별 토큰(비기본)을 공유하는 서로 다른 버킷에 대해서만 퍼지 매칭합니다. 출력은 동일 — 같은 재게시 클러스터 — 200개 이상의 무작위 이력에 대해 이전 알고리즘과의 차분 테스트로 증명.
참고
server/lib/detect-reposts.mjs의groupRowsByTitle(차분 테스트를 위해 export).tests/detect-reposts-grouping.test.mjs(+2). 스위트: 2521.
[1.194.0] — 2026-08-14
수정 (스캐너) — 단일 세그먼트 URL의 Workday 채용 페이지가 이제 올바르게 스캔됩니다.
수정
- Workday 어댑터가 이제 경로가 단일 세그먼트인 채용 URL을 파싱합니다 — 예:
https://parsons.wd5.myworkdayjobs.com/Search,.../KBR_Careers,.../Careers. 이전에는 사이트가External로 떨어져 잘못된 CXS 엔드포인트를 호출했고, 프로브가 정상처럼 보이지만 아무것도 반환하지 않을 수 있었습니다. 이제 경로의 첫 번째 비어 있지 않은 세그먼트를 사이트로 사용합니다(en-US같은 로케일 접두사 제외); 문서화된/en-US/External케이스는 변하지 않습니다. (#255 보고.)
참고
server/lib/portals/adapters/workday.mjs의 구조적 경로 파싱.tests/workday-adapter-endpoint.test.mjs(+7). 스위트: 2519.
[1.193.0] — 2026-08-14
추가 (통계) — 챙길 만한 면접을 보여주는 “면접 후 무응답” 탭.
추가
#/stats의 면접 후 무응답 탭: 예의상 기간(기본 30일)을 넘겨 조용해진 면접을 진행 중 면접과 트래커를 합쳐 보여줍니다 — 각 면접이 얼마나 조용한지, 마지막 면접일, 이유와 함께. 부드러운 알림/마무리 목록이며 제안일 뿐, 불합격 주장이 아닙니다. 토큰 없음.
참고
- 새
GET /api/stats/rejection-latency릴레이(스크립트 부재 시{available:false}).tests/stats-rejection-latency-route.test.mjs(+2). i18n 키 +10 ×17;#/statshelp-hint 7→8 탭. 스위트: 2510.
[1.192.0] — 2026-08-14
추가 (cv-studio) — 실제로 없던 수치를 잡아내는 “CV 사실 확인” 게이트.
추가
#/cv-studio의 CV 사실 확인 카드: 맞춤 CV나 커버레터를 붙여넣어 모든 수치와 사실을 실제 CV·프로필·two-pager와 대조합니다. pass / warn / block 판정과 함께 지어낸 수치·근거 없는 사실·금지/주의 표현을 정확히 보여줍니다. LLM 없음, 아무것도 저장하지 않음.
참고
- 새
POST /api/cv-studio/verify-facts릴레이: 텍스트를 일회용 임시 파일에 써서verify-cv-facts.mjs를 실행하며, 스크립트가 block에서 1로 종료해도 JSON 판정을 신뢰합니다.tests/cv-studio-verify-facts-route.test.mjs(+4). i18n 키 +15 ×17. 스위트: 2508.
[1.191.0] — 2026-08-14
추가 (통계) — 먼저 배울 스킬을 순위화하는 “다음에 배울 것” 탭.
추가
#/stats의 다음에 배울 것 탭: 트래커 전체 스킬 격차 집계 — 저적합을 가장 자주 초래한 부족 스킬을 가중치(모든 평가 리포트의 5−적합점수)와 Critical / High / Medium 티어로, 그리고 이미 CV/프로필이 다루는 스킬까지. 읽기 전용, 제안일 뿐, 토큰 없음.
참고
- 새
GET /api/stats/upskill릴레이(데이터 부족 시{ error }필드; 스크립트 부재 시{available:false}).tests/stats-upskill-route.test.mjs(+3). i18n 키 +15 ×17. 스위트: 2504.
[1.190.0] — 2026-08-14
추가 (트래커) — 어떤 회사가 실제로 응답하는지 알려주는 “회사 이력” 패널.
추가
#/tracker의 회사 이력 카드: 회사를 선택하면 읽기 전용 근거를 보여줍니다 — 그 회사가 당신에게 얼마나 응답했는지(무응답 / 혼합 / 이전에 응답함), 같은 공고가 계속 재게시되는지 — 트래커·후속 연락·스캔 이력을 결합. 토큰 없음; 스캐너를 호출하지 않습니다.
참고
- 새
GET /api/stats/company-history[?company=]릴레이(스크립트 부재 시{available:false}).tests/stats-company-history-route.test.mjs(+3). i18n 키 +18 ×17. 스위트: 2501.
[1.189.0] — 2026-08-14
수정 (스캐너) — 로마 숫자로 쓴 연차 레벨이 비라틴 제목에서도 인식됩니다.
수정
skip_tiers뒤의 티어 분류기가 이제 모든 문자 체계에서 직무 단어 뒤의 로마 숫자 레벨 접미사(I / II / III / IV / V)를 읽습니다 — “Инженер III”, “エンジニア I”, “Ingénieur IV” — ASCII 단어 뒤에서만이 아닙니다. 이전에는 비라틴 단어 뒤의 레벨 숫자가 무시되어 공고가 mid로 처리되었고,skip_tiers: [senior]또는[entry]가 그런 공고를 놓쳤습니다.
참고
server/lib/classify-tier.mjs의 문자 체계 독립 lookbehind; 죽은 중복Sr.매처 제거.tests/classify-tier.test.mjs(+1). 스위트: 2498.
[1.188.0] — 2026-08-14
수정 (UI) — 기본 액션 버튼이 더 이상 페이지 부제목에 딱 붙지 않습니다.
수정
- 주간 인터뷰 다이제스트, 투자받은 회사, 포털, 커리어 플랜, 커리어 오리엔테이션 페이지의 기본 액션/컨트롤 행에 적절한 위쪽 여백이 생겨, 버튼이 부제목에 붙지 않고 아래에서 여유를 갖습니다.
참고
- 회귀 가드
tests/lead-row-top-margin.test.mjs(+5). 스위트: 2497.
[1.187.0] — 2026-08-14
수정됨 (스캐너) — skip_tiers 설정이 다시 작동합니다: 연차로 건너뛰라고 한 공고가 제거됩니다.
수정됨
portals.yml의skip_tiers:목록(예:skip_tiers: [intern, entry])이 이제 스캔에서 반영됩니다. 각 공고의 제목이 연차 티어(intern / entry / mid / senior)로 분류되어 목록에 있으면 제거됩니다. 이전에는 스캔이 제목 / 위치 / 콘텐츠 / 신뢰 필터는 실행했지만 티어 필터가 없어skip_tiers가 조용히 무시되었습니다. 레벨 단어가 없는 제목은 mid로 분류되며(따라서skip_tiers: [mid]는 평범한 공고도 대부분 제거), 분류기는 가장 왼쪽 레벨 단어를 읽습니다.
참고
- 새 순수 모듈
server/lib/classify-tier.mjs(classifyTier+buildTierFilter)를 EN·RU 스캐너 필터 체인에 연결.tests/classify-tier.test.mjs(+7). 전체: 2492.
[1.186.0] — 2026-08-14
추가됨 (CV 스튜디오) — “스킬 갭” 패널: 직무의 요구 스킬 중 CV가 명시/암시/누락한 것.
추가됨
- CV 스튜디오의 새 스킬 갭 패널. 저장된 채용 공고를 선택하면 각 요구 스킬을 CV에 명시됨, CV에 암시됨, 누락으로 분류합니다 — LLM 없는 단어 비교, 아무것도 저장하지 않음. 공고에 명확한 요구사항 섹션이 없으면 신뢰도 낮음 안내가 표시됩니다.
참고
- 새
GET /api/jds/:name/skill-gap(JD 이름은 경로 정화되고jds/아래에 존재함을 확인한 뒤 인자로 사용; 스크립트가 없으면{available:false}로 소프트 폴백). i18n 키 +13 ×17. 테스트:tests/jds-skill-gap-route.test.mjs(+4, 경로 순회 거부 포함). 전체: 2485.
[1.185.0] — 2026-08-14
추가됨 (통계) — “퍼널 및 속도” 탭: 당신의 퍼널이 시장과 어떻게 비교되고 단계 간 이동이 얼마나 빠른지.
추가됨
- 통계의 새 퍼널 및 속도 탭은 시장 벤치마크 범위 옆에 당신의 응답률과 인터뷰율(소규모 표본 및 선택 편향 주의 포함), 일반적 첫 응답 창을 넘긴 진행 중 지원의 대기 목록, 단계별 중앙값 일수(지원 → 응답 → 인터뷰 → 오퍼)를 보여줍니다 — 느린 행은 우측 검열되어 중앙값을 왜곡하지 않습니다. 읽기 전용, 토큰 0; 당신의 트래커만 읽습니다.
참고
- 새
GET /api/stats/funnel(스크립트가 없으면{available:false}로 소프트 폴백). i18n 키 +18 ×17. 테스트:tests/stats-funnel-route.test.mjs(+2). 전체: 2481.
[1.184.0] — 2026-08-14
수정됨 (UI) — 대시보드의 빠른 작업 타일이 이제 균일한 그리드로 정렬됩니다.
수정됨
- 대시보드(커맨드 센터)에서 3개짜리 타일 그룹이 4개짜리보다 넓게 렌더링되어 섹션의 오른쪽 가장자리가 들쭉날쭉했습니다. 이제 각 그룹이 동일한 너비의 열을 사용하며(넓은 화면에서 4열, 창이 좁아지면 3 / 2 / 1로 감소), 모든 타일 크기가 같고 오른쪽 가장자리가 정렬됩니다.
참고
- CSS만 변경(
.qa-grid:auto-fill대신 고정repeat(N, minmax(0,1fr))).tests/dashboard-grid-align.test.mjs로 보호(+2). 전체: 2479.
[1.183.0] — 2026-08-14
추가됨 (스캐너) — 더 똑똑한 중복 감지: 추적 링크로 다시 게시된 같은 채용이 더 이상 두 번 나타나지 않습니다.
추가됨
- 스캐너가 이제 정규 URL 키로 공고를 인식하므로, 추적 매개변수(
?utm_…,gclid등),http대https, 또는 끝 슬래시 /#조각으로 다시 게시된 같은 채용을 하나의 공고로 취급합니다 — 스캔 결과나 파이프라인에 중복 행이 없고, 이미 본 채용에 대한 낭비된 평가도 없습니다. 진짜로 다른 공고(gh_jid같은 유지된 기능적 id)는 여전히 별도로 계산됩니다.
참고
- 새
server/lib/url-key.mjs를 두 스캐너의 dedup과 파이프라인 작성기에 연결. 의도적으로 덜 정규화 — 서로 다른 두 공고를 절대 병합하지 않습니다. 테스트:tests/url-key.test.mjs(+5),tests/parsers.test.mjs(+1). 전체: 2477 (+6).
[1.182.0] — 2026-08-14
수정됨 (스캐너) — 급여 범위가 이제 모든 언어에서 동일하게 표시됩니다.
수정됨
- 스캔과 트래커 행의 급여 수치가 영어 단어 “from” / “up to”(비영어 UI에 번역되지 않고 노출됨) 대신 로케일 중립 기호 ≥, ≤(예:
≥ 120000 EUR,≤ 90000)를 사용합니다. 한쪽 범위만 보고하는 모든 보드(Getro, Remotli, Manfred, Agentic Jobs, JustJoin, Jobicy)에 적용됩니다. 양쪽 범위(100000–150000 USD)는 이미 중립적이었습니다.
참고
- 표시 전용 — 클라이언트 급여 필터는 접두어와 무관하게 숫자를 파싱하므로 필터링은 변하지 않습니다. 전체: 2471.
[1.181.0] — 2026-08-14
추가됨 (스캐너) — Getro 채용 보드가 이제 급여, 모든 위치, 원격 채용을 표시합니다.
추가됨
- Getro 스캐너(펀드 인재 네트워크 보드)가 이제 각 채용에 급여 정보(연봉 범위 + 통화)를 표시하고, 첫 번째만이 아닌 모든 위치를 나열하며, 원격 채용을 표시합니다. 스캔과 트래커의 Getro 공고가 이제 다른 보드와 동일한 급여 + 위치 정보를 담습니다.
참고
- 스캐너 전용; 새 의존성 없음, 라우트 / CSP / SSRF 변경 없음. 테스트:
tests/sources-getro.test.mjs(+5). 전체: 2470 (+5).
[1.180.0] — 2026-08-14
수정됨 (중간, 리포트) — #/reports 목록이 이제 테이블이며, Machine Summary 플레이스홀더가 가리던 실제 점수를 복구합니다.
수정됨
#/reports목록이 4카드 그리드가 아닌 테이블(리포트 · 날짜 · 신뢰도 · 점수)입니다. 긴 “점수 미감지” 칩이 제목 열을 거의 0으로 밀어붙였고, 카드 제목의overflow-wrap: anywhere가 리포트 이름을 한 글자씩 줄바꿈했습니다. 이제 각 필드가 자체 열을 가지며, 이름 셀은 단어 단위로 줄바꿈되고, 좁은 화면에서는 테이블이 가로로 스크롤됩니다(새.reports-scroll컨테이너). 새 i18n 키rep.colReport×17.- 본문의 실제 점수(
**Итоговый балл:** 1.8 / 5)가 더 이상 Machine Summary 플레이스홀더(score: —)에 가려지지 않습니다.## Machine Summary블록에 숫자가 아니거나 범위를 벗어난 점수가 있으면 파싱된 점수 자리를 차지해 볼드 값-형식 폴백을 막아, 본문에 실제X / 5가 있어도 리포트가 “점수 미감지”로 표시되었습니다. 이제parseReportHeader는 사용 가능한 숫자가 남지 않으면 본문의 값-형식을 복구합니다(4.5단계).
참고
- 클라이언트 + 파서만; 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 테스트:
tests/reports-table.test.mjs(+5),tests/report-header-locale.test.mjs(+2). 전체: 2465 (+7).
[1.179.0] — 2026-08-13
변경됨 (LOW, 스캐너) — 중복된 20개 HTML 엔티티 디코더를 공유 모듈로 통합 (패리티 후속, 워크리스트 종료).
변경됨
- 20개 스크래핑 스캔 소스가 각자
decodeEntities/decodeXmlEntities(+fromCodePoint헬퍼)를 갖고 있었고, 이 복사본들은 드리프트했습니다(셋은RangeError를 던질 수 있었고 v1.172.0에서 수정; 나머지는 NUL/C0를 허용하거나a2;를 잘못 파싱). 이제 모두 단일server/lib/html-entities.mjs(XML 1.0 Char 안전 디코더)를 거치며 ~237줄의 중복을 제거했습니다. 8개 RSS 계열 소스는 디코딩을 얻었고(이전엔 5개 엔티티만 처리), cryptocurrencyjobs의 의도적 이중 디코딩은 별칭으로 보존됩니다.hh는 자체 디코더 유지(—/–처리, 공유 6개 밖). 새 가드 테스트는 어떤 소스든 로컬 디코더를 다시 만들면 실패합니다.
참고
- 동작을 보존하는 리팩터링; 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 테스트:
tests/decoder-consolidation.test.mjs(+2). 전체: 2458 (+2).
[1.178.0] — 2026-08-13
수정됨 (LOW, 상위 패리티) — 오래된 상수 두 개를 상위와 일치하도록 갱신 (PARENT-SYNC GAP #4 + #5).
수정됨
- 브라우저 User-Agent (GAP #4) —
BROWSER_LIKE_USER_AGENT(workable/workday/oraclecloud/a16z/eightfold가 WAF/봇 게이트 통과에 사용)를 Chrome 131에서 151로 올려 상위user-agent.mjs와 일치시킴; 오래된 빌드는 차단되기 쉽습니다.Chrome major ≥ 151테스트로 보호. - 트래커 상태 FALLBACK (GAP #5) —
states.mjs의 최후 수단FALLBACK(라이브templates/states.yml을 읽을 수 없을 때만 사용 — 새 클론 / CI 격리 루트)에 상위의 터키어 상태 별칭(#2615) 추가: değerlendirildi, başvuruldu, yanıt verildi, mülakat, teklif, reddedildi, iptal edildi, uygun değil, kabul edildi/işe alındı. 프로덕션에서는 라이브 파일이 이미 제공했습니다.
참고
- 상수 두 개뿐; 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 테스트:
tests/http-json.test.mjs(+1) +tests/states.test.mjs(+1). 전체: 2456 (+2).
[1.177.0] — 2026-08-13
수정됨 (MEDIUM, 스캐너) — 검색 API를 세션 쿠키로 보호하는 테넌트에서 csod(Cornerstone)가 0건을 반환하던 문제 (parent #2769, PARENT-SYNC GAP #1).
수정됨
- 일부 Cornerstone 테넌트는 부트스트랩 채용 사이트 홈에서 세션 쿠키를 설정하고, 그 쿠키가 익명 베어러 토큰과 함께 돌아오지 않으면 검색 API에서
401 CSOD Unauthorized를 반환합니다.sources/csod.mjs는 이제 새fetchResponse헬퍼로 부트스트랩을 읽어Set-Cookie값으로Cookie헤더를 만들고(cookieHeaderFrom— name=value만, jar 시맨틱) 검색 POST에 다시 보냅니다. 동일 출처 전용(호스트 고정 +redirect:'error')이라 세션 쿠키가 제3자에게 갈 수 없으며, 쿠키를 설정하지 않는 테넌트는 이전과 동일하게 동작합니다.
참고
- 새
server/lib/http-json.mjs::fetchResponse(추가형; 기존 소스 영향 없음). 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 테스트:tests/sources-parity-v1118a.test.mjs(+1). 전체: 2454 (+1).
[1.176.0] — 2026-08-13
수정됨 (MEDIUM, 리포트) — RU 표에 없는 굵은 라벨 아래의 점수가 여전히 “Score not detected”로 표시됨 (FIND-5).
수정됨
- 두 RU 리포트가 점수를
**Итоговый балл:** 1.8 / 5/**Скор:** 1.8 / 5로 적었는데,REPORT_LABELS.ru가 열거하지 않는 굵은 라벨(“Оценка”/“Балл”만 앎)이라 점수가 파싱되지 않았습니다. 동의어 목록을 늘리는 대신parseReportHeader는 이제 값 형태로 폴백합니다: 임의의 굵은 라벨 아래 /5 루브릭 분수. 언어 독립적이고, 제목에 면역이며(**없음,/5값 없음),5/5/2026같은 날짜를 거부합니다(분모에 대한 부정 전방탐색).
참고
- 서버 파서만; 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 테스트:
tests/report-header-locale.test.mjs(+2). 전체: 2453 (+2).
[1.175.0] — 2026-08-13
수정됨 (LOW, 견고화) — FIND-3 SEO 설명에 대한 회귀 가드 + 널 안전 정당성 스트립 (AI 리뷰 후속).
수정됨
- SEO 설명 패리티 가드 — 각 언어의
meta.desc에서 하드코딩된 “~55”를 레지스트리 기반{adapters}플레이스홀더로 바꾼 v1.174.0 수정에는 테스트가 없어 다음 로케일 편집에서 조용히 회귀할 수 있었습니다. 새 CI 격리tests/site-meta-desc-parity.test.mjs는 17개site/src/i18n/*.json중 하나라도 플레이스홀더를 잃거나 개수를 다시 하드코딩하거나,Landing.astro가 세 개의 설명 메타에 이를 보간하지 않으면 실패합니다. - 널 안전 정당성 스트립 —
stripEmphasis는 널 입력에 대해 문자열 “undefined”가 아닌''를 반환합니다(필드는 문자열로 초기화되므로 심층 방어).
참고
- 테스트 + 파서의 한 줄 가드; 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 테스트:
tests/site-meta-desc-parity.test.mjs(+3). 전체: 2451 (+3).
[1.174.0] — 2026-08-13
수정됨 (HIGH, 리포트) — 현지화된 리포트가 “Score not detected”로 표시되고 SEO 설명이 오래됨.
수정됨
- 점수 파싱 (FIND-1) — H1에 점수 라벨 단어가 포함된 비영어 리포트(
# Оценка вакансии: <제목>)에서 그 제목을 점수로 오인하지 않습니다.parseReportHeader가 이제 현지화된 굵은 라벨(**Оценка:** 1.5 / 5)에 고정하고, 제목 줄을 건너뛰며, 라벨이 콜론에 인접하도록 요구합니다 — 그래서 “Score not detected”였던 RU 리포트가 실제 점수를 표시합니다. - 정당성 칩 (FIND-2) — 값에서 Markdown 강조가 제거되어 칩이 “** High Confidence”가 아닌 “High Confidence”로 표시됩니다.
- 점수 오버플로 — 상태 텍스트가 뒤따르는 점수 줄(“1.8, Status: Evaluated, …”)은 점수만으로 압축됩니다;
.score-pill에 줄바꿈 없음/오버플로 캡이 추가되고 제목 열이 줄어들 수 있어 색상 칩이 카드 밖으로 넘치지 않습니다. - SEO 설명 (FIND-3) — cvstart.org의 meta / OG / Twitter 설명(17개 언어 모두)이 “Scan ~55 job boards”로 고정된 반면 본문은 실제 레지스트리(“~75”)를 세었습니다. 이제 설명이 레지스트리 기반 개수를 삽입하여 다시 어긋나지 않습니다.
참고
- 서버 파서 + 클라이언트 렌더/CSS + 사이트 i18n; 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 테스트:
tests/report-header-locale.test.mjs(+4). 전체: 2448 (+4).
[1.173.0] — 2026-08-13
추가됨 (LOW, 설정) — Hermes가 감지되는 AI CLI 목록에 추가됨 (career-ops 동등성).
추가됨
#/config→ “AI CLI 도구” 탭이 이제 Hermes(Nous Research), 상위 프로젝트가 새로 지원하는 에이전트 런타임(바이너리hermes)을 탐지합니다.server/lib/routes/cli-detect.mjs의 고정 허용 목록이 10개에서 11개 도구로 늘어납니다; 탐지는 읽기 전용 PATH 스캔으로 유지됩니다(바이너리는 절대 실행되지 않음).
참고
- i18n / 라우트 / CSP / SSRF / 상위 쓰기 변경 없음; 목록은 고정 허용 목록이며 입력이 아닙니다. 전체: 2444 (cli-detect 카나리 10 → 11 갱신).
[1.172.0] — 2026-08-13
수정됨 (MEDIUM, 스캐너) — 잘못된 HTML 엔티티가 스캔 소스를 중단시킬 수 있었음 (career-ops #2150 동등성).
수정됨
oraclecloud,gem,dassault소스는 숫자 HTML 엔티티를String.fromCodePoint이전에 단순한Number.isFinite검사만으로 디코딩했습니다 —0x10FFFF를 넘는 참조(예: 잘못되었거나 악의적인 피드의�)는 잡히지 않은RangeError를 던져 해당 소스의 전체 파싱을 중단시켰습니다. 공유 모듈server/lib/html-entities.mjs(상위 프로젝트의_html-entities.mjs를 반영)가 이제 숫자 참조를 XML 1.0 §2.2 Char 집합으로 제한하여String.fromCodePoint가 절대 던지지 않으며, 16진수와 10진수를 별도로 매칭하여a2;가 더 이상 잘못 파싱되지 않습니다. 세 소스가 이를 가져옵니다.
참고
- 유효한 피드에는 변화 없음; JS / i18n / 라우트 / CSP / SSRF / 상위 쓰기 변경 없음. 소스에 남은 ~20개 디코더 사본 통합은
qa/PARENT-SYNC-WORKLIST-v1.26.0.md에 기록됨. - 테스트:
tests/html-entities.test.mjs(+7). 전체: 2444 (+7).
[1.171.0] — 2026-08-13
변경 (LOW, 디자인 시스템) — 타입 스케일 + z-index 레이어 토큰 (D-4, 첫 단계). 크기와 쌓임이 컴포넌트별 리터럴이었습니다.
변경됨
- z-index 레이어 —
--z-*토큰(--z-topbar…--z-skiplink)을 도입하고 모든 z-index 리터럴을 마이그레이션했습니다. 값이 보존되어 쌓임은 동일; 새 카나리가 새로운 매직 넘버를 금지합니다. - 타입 스케일 —
--font-size-*램프(xs 11…2xl 28, base = Inter 15px); 컴포넌트가 이미 쓰던 핵심 크기를 마이그레이션(시각 변화 없음). 램프 밖 값은 점진 마이그레이션(docs/UX-ROADMAP.md).
참고
- CSS 토큰만; 동작/JS/i18n/라우트/CSP/SSRF/쓰기 변경 없음. 픽셀 변화 없음.
tests/design-tokens-scale.test.mjs(+3). 총 2437 (+3).
[1.170.0] — 2026-08-13
추가 (LOW) — 긴 AI 생성에 정직한 ETA 힌트 (P4-ETA). 무거운 생성(커리어 플랜 ~40초, 오리엔테이션 / 마켓 / 네트워킹 ~30초, 투페이저 ~20초)이 기간 표시 없이 “생성 중…“만 보였습니다.
추가됨
- 각 긴 생성 버튼 옆에 이제
⏱ ~N초힌트가 표시됩니다(#/autoETA와 동일). 공유.eta-hint스타일 + 제네릭 키 2개(common.eta~{n}s,common.etaTitle).
참고
- 클라이언트만; 라우트/CSP/SSRF/쓰기 변경 없음. i18n 키 +2 ×17(스냅샷 1219 → 1221).
tests/generation-eta-hint.test.mjs(+2). 총 2434 (+2).
[1.169.0] — 2026-08-13
추가 (LOW) — 인라인 PDF 미리보기 (D-5). GET /api/output/pdfs/:name가 Content-Disposition: attachment를 강제해 #/cv의 “열기” 링크조차 표시 대신 다운로드했습니다.
추가됨
- **
?inline=1**은 동일한 정제 파일을Content-Disposition: inline으로 제공해 새 탭에서 👁 미리보기로 렌더링합니다. 기본값은 여전히 다운로드. 새 라우트 없음; 동일한 이름 가드 적용. #/cvPDF 목록의 첫 버튼이 👁 미리보기(다운로드 옆).cv.openPdf“열기” → “미리보기” ×17.
참고
- CSP/SSRF 변경 없음 — 동일한
sanitizePathName. 기존 i18n 키 1개 재작성 ×17(스냅샷 1219).tests/output-pdfs.test.mjs(+3). 총 2432 (+3).
[1.168.0] — 2026-08-13
수정 (LOW, a11y) — 체크박스 행이 이제 WCAG 2.5.8의 24×24 최소 타깃 크기를 충족합니다 (D-2). #/scan, #/config, #/evaluate, #/cv-studio의 체크박스/라디오 라벨이 ~22 px 밴드였습니다.
수정됨
- 범위가 지정된 규칙
label:has(> input[type="checkbox"/"radio"]) { min-height: 24px }가 ≥24 px 밴드를 보장합니다.min-height만 — 라벨은 이미 flex이므로 이동 없음;.apply-checklist(32 px)는 이미 준수.
참고
- CSS만; 동작/JS/i18n/라우트/CSP/SSRF/쓰기 변경 없음.
tests/checkbox-target-size.test.mjs(+1). 총 2429 (+1).
[1.167.0] — 2026-08-13
수정 (LOW, 디자인 시스템) — 상승 표면이 이제 헤어라인과 구분됩니다 (D-3). --panel-2 / --surface-elev1이 헤어라인 --line / --border와 동일한 --slate로 해석되어 구분이 없었습니다.
수정됨
- 전용 테마 인식 토큰
--elev(라이트#eef1f6/ 다크#1e232e, 두 테마 모두--slate와 구분)가 상승 표면을 뒷받침하며 헤어라인은--slate유지. 나머지 항목(D-2, D-4, D-5, P4-ETA)은docs/UX-ROADMAP.md에 백로그로 기재.
참고
- CSS 토큰만; 동작/JS/i18n/라우트/CSP/SSRF/쓰기 변경 없음.
tests/elevation-token.test.mjs(+2). 총 2428 (+2).
[1.166.0] — 2026-08-13
수정 (LOW) — 루브릭 용어가 이제 공식 문서와 일치합니다. career-ops.org/docs는 “다섯 차원과 종합 전체 점수”라고 하지만, 웹 UI·cvstart.org·위키는 모두 “6차원 루브릭”이라고 했습니다(5 + 1 = 6이지만 표현이 달랐습니다).
수정됨
- 문서 표현 — “다섯 차원과 종합 전체 점수” — 을 README ×17, cvstart.org 사이트 ×17, 도움말 가이드 ×17,
docs/career-ops-canonical.md, 위키(Home ×17 + Features)에 일관 적용.
참고
- 문서/마케팅 문구만; 코드/i18n 키/라우트/CSP/SSRF/쓰기 변경 없음.
tests/rubric-terminology.test.mjs(+2). 총 2426 (+2).
[1.165.0] — 2026-08-13
수정 (LOW) — “Two-pager” 용어가 이제 각 로케일 내에서 일관됩니다. 아랍어에서 사이드바는 라틴 “Two-pager”를 표시하는데 <h1>은 완전히 현지화되어 있었습니다 — RTL 내비게이션에서 유일한 라틴 문자열이었습니다.
수정됨
- 결정 적용: 로케일별로
nav.twoPager와twoPager.title이 용어를 일치시킵니다(둘 다 라틴 또는 둘 다 현지화). 아랍어만 갈라져 있었고, 이제 내비 라벨이 현지화(“الصفحتان”)되었습니다. 새 카나리가 다시 갈라지면 실패합니다.
참고
- 문구만; 라우트/CSP/SSRF/쓰기 변경 없음. i18n 값 1개(ar) 변경; 새 키 없음(스냅샷 1219).
tests/two-pager-term-consistency.test.mjs(+2). 총 2424 (+2).
[1.164.0] — 2026-08-13
수정 (LOW) — 상단 검색 플레이스홀더가 이제 어떤 로케일에서도 넘치지 않습니다. “Find a company, role or URL…“는 검색바가 줄어들면 잘렸고, “…or URL” 부분이 보이지 않았습니다.
수정됨
top.search(×17)가 이제 짧은 “검색 또는 URL 붙여넣기”(모든 로케일에서 ≤24자)로, 좁은 바에서도 맞고 URL 안내를 유지합니다.index.html폴백이 일치하며aria-label은 전체 설명을 유지합니다.
참고
- 문구만; 라우트/CSP/SSRF/쓰기 변경 없음. 기존 i18n 키 1개 재작성 ×17(신규 없음; 스냅샷 1219).
tests/search-placeholder-fit.test.mjs(+2). 총 2422 (+2).
[1.163.0] — 2026-08-13
수정 (LOW) — 인앱 “문서에 질문” 어시스턴트가 이제 리포트 PDF 내보내기를 다룹니다. #/reports/:slug에 📄 Generate PDF가 있는데도 가이드가 다루지 않는다고 답하곤 했습니다.
수정됨
- 17개 도움말 번들 모두 §10 Reports 아래에 “리포트를 PDF로 내보내기” H3 추가(버튼 위치, 파일이
output/*.pdf에 저장, Playwright 필요, 전송 전 검토). 어시스턴트 검색이 이제 Reports 섹션을 노출합니다.
참고
- 문서/도움말만; 코드/라우트/CSP/SSRF/쓰기 변경 없음. 도움말 게이트 112 → 113 H3(31 H2 유지).
tests/help-reports-pdf-section.test.mjs(+2). 총 2420 (+2).
[1.162.0] — 2026-08-13
수정 (MEDIUM) — 도움말 “?“가 이제 ≥24×24 포인터 대상입니다 (WCAG 2.5.8). .help-hint는 모든 제목에서 padding:0의 18×18 px로 최소 기준 미만이었습니다.
수정됨
.help-hint박스가 이제 24×24(측정 가능한 대상)이며 보이는 링은 18px 유지 — 가운데 정렬된::before로 그려져 글리프와<h1>기준선이 그대로입니다. hover/활성/포커스 상태는 링을 따라가고, 여백 6→3px로 간격 유지.
참고
- CSS만; JS/i18n/라우트/CSP/SSRF/쓰기 변경 없음.
tests/help-hint-target-size.test.mjs(+2). 총 2418 (+2).
[1.161.0] — 2026-08-13
수정 (MEDIUM) — #/reports가 빈 공간 대신 “점수 감지 안 됨” 칩을 표시합니다. v1.159.0 로케일 파서 이후에도 점수를 파싱할 수 없는 리포트는 빈 영역을 보여 실패와 구분되지 않았습니다.
수정됨
- 점수 셀이 분기합니다: 점수 있음 → 톤 필; 점수 없음 →
.score-muted칩(“점수 감지 안 됨”, ×17) + “리포트를 열어…” 툴팁. 카드는 키보드로 조작 가능한role="link"로 유지되고 날짜도 표시됩니다. - 기존 중립 토큰 재사용; 새 색상 없음.
참고
- 클라이언트만; 라우트/CSP/SSRF/쓰기 변경 없음. i18n 키 +2 ×17 (스냅샷 1217 → 1219). 총 2416 (+3).
[1.160.0] — 2026-08-13
수정 (HIGH) — 제공자 안내 문구가 더 이상 7개 제공자 약속과 모순되지 않습니다. #/config는 라이브 평가가 “Anthropic 또는 Gemini 키를 사용”하고 OpenAI 키는 “웹 UI에서 사용되지 않는다”고 했고, 대시보드는 “Anthropic-first scoring”이라 표시했습니다 — 7개 제공자 캐스케이드(v1.157.0) 이후로 거짓.
수정됨
config.providerModelNote(×17): 이제 ⚡ 라이브 평가가 일곱 제공자 키(Anthropic · Gemini · OpenAI · Qwen · OpenRouter · GitHub Models · Hermes) 중 아무거나로 헤드리스 실행된다고 명시. 거짓 OpenAI 문장 제거.dash.quick.evaluateSub(×17): 제공자 중립(“0–5 적합도 점수”).Keys: N / 5→N / 7.
참고
- 문구만; 라우트/CSP/SSRF/쓰기 변경 없음. 새 i18n 키 없음(스냅샷 1217). 총 2413 (+3).
[1.159.0] — 2026-08-13
수정 (HIGH) — 리포트 메타데이터가 더 이상 언어에 종속되지 않습니다. 영어가 아닌 로케일로 생성된 리포트는 #/reports에서 빈 메타데이터 영역(점수/날짜/정당성 없음)으로 표시되었습니다. parseReportHeader가 영어 굵은 라벨만 인식했기 때문입니다.
수정됨
parseReportHeader가 언어 불변## Machine SummaryYAML 블록(score:/legitimacy:/date:—auto-pipeline이 이미 읽는 소스)을 파싱합니다. 우선순위: 영어 라벨 → Machine Summary → 로케일 라벨(REPORT_LABELS, 17개 로케일). 영어 리포트는 바이트 단위로 동일합니다.- 숫자 파싱 관대화(
1.5/5,1,5/5,1.5 из 5,4.5 out of 5); 본문에 날짜가 없으면 파일 mtime으로 대체.
참고
- 읽기/파싱만; 라우트·CSP·SSRF·부모 쓰기 변경 없음. 새 i18n 키 없음. 총 2410 (+8).
[1.158.0] — 2026-08-12
수정 — 두 가지 표시상의 사소한 버그(탭 제목에 새어 나온 «?»와 랜딩의 잘못된 제공자 수). 표시 전용이며 동작·보안·데이터 흐름 변경 없음.
수정됨
- HelpHint의 «?»가 더 이상
document.title에 새어 나오지 않습니다. 라우터가 가공되지 않은h1.textContent에서 탭 제목을 만들어 «Vacancy search?»로 표시되었습니다. 이제router.js::focusNewView가 제목을 복제하고.help-hint를 제거한 뒤 텍스트를 읽습니다. 화면의 «?»는 그대로입니다. - cvstart.org가 «7» 대신 «17 AI providers»를 표시했습니다.
Features.astro의sub()가 카드별 치환 전에 모든{n}을 로케일 수(17)로 바꿨습니다. 이제{n}은 카드별로 해석됩니다(제공자 → 7, 언어 → 17).
참고
- 서버·라우트·CSP·SSRF·i18n 키 변경 없음. 총 2402개 테스트(+1).
[1.157.0] — 2026-08-12
수정됨 — 라이브 평가가 이제 Anthropic/Gemini뿐 아니라 설정된 어떤 제공자로도 실행됩니다. OPENROUTER_API_KEY만 설정한 사용자가 잘못 수동 모드로 강제되었습니다.
수정됨
- 근본 원인: 키 없는
LLM_PROVIDER핀(예:init의LLM_PROVIDER=claude)이 막다른 길로 갔습니다. 이제 설정된 제공자들 사이에서 auto 순서로 대체됩니다(selectActiveProvider+ 두 디스패치 캐스케이드). - 클라이언트 게이팅(
#/deep+ mode-page 뷰)이 이제 오래된 Anthropic/Gemini 프로브 대신window.ProviderStatus(/api/status/providers, 7개)를 사용; 문구 재작성(deep/eval × 17) + 대시보드 «라이브 평가» 배지 +config.llmProviderHint.
참고
- 보안 변화 없음. 스위트: 2401개 테스트(+5).
[1.156.0] — 2026-08-12
리팩터 — scan.js를 파일 크기 한도 아래로 분할(P-16) + CodeQL 수정. scan.js는 906줄이었고, 동작 보존 팩토리 두 개를 추출해 648줄로. P-15/P-16 뷰 분할 쌍을 완성.
변경됨
- 새
scan/runner.js(스캔 실행 엔진)와scan/filters.js(필터 상태 머신)를ctx/refs백으로 추출;scan.js가 둘을 연결.
수정됨
- CodeQL
js/useless-assignment-to-local(#428)config/tab-controller.js:let n = i;→let n;.
참고
- 순수 리팩터, 동작 변화 없음; 소스를 읽는 테스트 4개 재지정. 두 큰 뷰 모두 800 미만(P-15/P-16 완료). 스위트: 2396개 테스트.
[1.155.0] — 2026-08-12
리팩터 — config.js를 파일 크기 한도 아래로 분할(P-15). config.js는 1030줄(800줄 한도 초과)이었고, 동작을 보존하는 두 모듈을 추출해 783줄로 줄였습니다.
변경됨
- 새
config/field-specs.js(필드 데이터 + 모델 목록)와config/tab-controller.js(탭 바 팩토리) 추출;config.js가 이를 참조하며 렌더 로직은 그대로.
참고
- 순수 리팩터, 동작 변화 없음; 소스를 읽는 테스트 6개를 재지정.
scan.js(906)는 그대로 둠(이미 부분 분할됨; 코어가 너무 결합되어 깔끔한 기계적 분할이 어려움). 스위트: 2396개 테스트.
[1.154.0] — 2026-08-12
새 가이드 — “클라우드에서 전체 스택 실행.” career-ops에는 자체 클라우드/서버 안내가 없어 하나 추가했습니다: 부모 career-ops 파이프라인, 이 career-ops-ui 뷰어, AI 엔진(Claude Code를 통한 Claude 구독, 로컬 Hermes, 또는 API 키)을 항상 켜진 소형 서버에 올리는 단계별 레시피. 17개 언어의 도움말 §31, README 섹션, 위키 페이지로 제공됩니다.
추가됨
- 도움말 §31 “클라우드에서 전체 스택 실행”(× 17) — 세 부분, 프로비저닝 + 설치, 엔진 선택, 안전한 노출(HTTPS 리버스 프록시 + 인증 + CSP/SSRF/XSS/비밀-없음 불변식). 도움말 번들이 31 H2 / 112 H3로 확장.
- README — “클라우드에서 전체 스택 실행” 섹션(× 17) + 위키 Cloud-Deployment 페이지.
참고
- 문서 전용 — 라우트·서버·클라이언트 변경 없음, 새 i18n 키 없음. 도움말 4개 테스트가 31 H2 / 112 H3 계약으로 이동. 스위트: 2396개 테스트(변화 없음).
[1.153.0] — 2026-08-12
Jobvite 스캐너가 공개 XML 피드로 이전(부모 동기화). 부모가 Jobvite JSON API를 폐기(이제 0건 반환)했고, web-ui source가 같은 죽은 엔드포인트를 써서 추적 중인 Jobvite 회사가 조용히 빈 결과로 스캔되었습니다. 부모 픽스(#2623)를 이식: 이제 companyEId로 키를 매기는 공개 테넌트별 XML 피드를 읽습니다.
수정됨
- source가 폐기된 JSON API를 호출해 0건을 반환했습니다. 이제
https://app.jobvite.com/CompanyJobs/Xml.aspx?c={companyEId}를 호출하고 XML<result><job>…를 파싱합니다(CDATA + 엔터티,apply-url보다detail-url우선).
변경됨
companyEId해석: (1) 포털의company_eid:, (2) 명시적api:의c=파라미터, (3) 보드 페이지 디스커버리.fetchText(http-json.mjs)가 non-ok 오류에.location/.retryAfter를 부착(읽기 전용, 하위 호환).
참고
- 보안 — 두 호스트(
jobs.jobvite.com,app.jobvite.com)를assertJobviteUrl로 매 요청 고정: https 전용, 엄격한 허용목록, 리디렉션 미추적.companyEId는 오직?c=값이며, source 개수 변화 없음. - 스위트: 2396개 테스트(+4).
[1.152.0] — 2026-08-12
Hermes 제공자 — 배선 완료 + 문서 최신화. v1.151.0 Hermes 통합의 코드 리뷰에서 실제 결함 2건과 완성도 항목 4건을 발견해 모두 여기서 수정했으며, 앱 전체의 LLM 제공자 목록을 모든 문서 표면과 17개 언어에서 완전한 7개로 맞췄습니다.
수정됨
#/config에서 Hermes를 강제할 수 없었음 —LLM_PROVIDER드롭다운이 6개 제공자만 나열해HERMES_API_KEY는 설정해도 UI에서 Hermes를 강제할 수 없었습니다. 이제hermes가 8번째 옵션이며, 새 패리티 테스트가 드롭다운이LLM_PROVIDERS에서 다시 어긋나지 않도록 막습니다.- 짧은 자체 호스팅 키가 조용히 거부됨 —
isUsableKey의 20자 하한은 클라우드 키 기준이었고,hasHermesKey는 이제 완화된 8자 하한을 씁니다(Hermes 문서 예시가 19자).
변경됨
- 제공자 목록을 README(× 17), 인앱 도움말(× 17),
config.llmProviderHint사전(× 17),docs/sdd에서 완전한 7개로 정규화했고,hermesChatUrl이 경로 없는 호스트를 보완하며, 수동 폴백 문구가 Hermes를 명시합니다.
참고
- 보안 변화 없음 — 새 라우트·SSRF/CSP 변경 없음; health/doctor에
HERMES_API_KEY행이 추가됩니다. - 스위트: 2392개 테스트(+2).
[1.151.0] — 2026-08-12
Hermes가 이제 연결된 LLM 제공자입니다(Phase 5) — Phase 5 스코핑 스파이크에서 Nous Research의 Hermes가 OpenAI 호환 API Server(hermes gateway → POST /v1/chat/completions)를 제공함을 확인했고, 이제 career-ops-ui는 OpenAI/Qwen과 똑같이 로컬 Hermes를 통해 라이브 평가를 실행합니다. 앱 설정에서 HERMES_API_KEY를 지정하면 auto 순서에 합류합니다(마지막). 로드맵의 마지막 미해결 항목 — Phase 5, Shape A — 을 마무리합니다.
추가
- Hermes LLM 제공자(Shape A) — 공유
runOpenAICompatible클라이언트 위의runHermes(server/lib/openai.mjs), 두 캐스케이드(llm-dispatch.mjs+routes/llm.mjs)에 게이트, auto 순서 꼬리 +LLM_PROVIDER=hermes핀,/api/status/providers,llm-pricing.mjs. 사용자 설정 가능한 로컬 base URL(기본http://127.0.0.1:8642/v1)에 Bearer 인증으로 접근 — 사용자 제공 채용 URL이 아니라 설정된 제공자 엔드포인트(OpenRouter/Qwen처럼)이므로 SSRF 가드를 거치지 않습니다. #/config필드 —HERMES_API_KEY(비밀) +HERMES_BASE_URL+HERMES_MODEL(기본hermes-agent), 6개 새 i18n 키 × 17개 언어(스냅샷 1208 → 1214).
변경
- 스코핑 스파이크가 해결됨:
docs/integrations/HERMES.md, 앱 내 도움말 §30(× 17), README 예고(× 14),hermes-bridge스킬, 로드맵이 “계획됨 / 아직 미연동”에서 연동됨(Shape A) 으로 전환. Shape B(맞춤형 에이전트 런타임 relay)는 불필요했습니다.
참고
- 보안: 제공자 fetch는 설정된 엔드포인트로, 다른 OpenAI 호환 제공자와 동일한 범주 — 새 SSRF 표면 없음, CSP/새니타이저 변경 없음.
HERMES_API_KEY는SECRET_KEY(절대 노출되지 않음). - 테스트(CI 격리, 전송 스텁):
tests/hermes-provider.test.mjs(+5); v1.146.0의 “Hermes 분기 없음” 캐너리를 연결됨을 확인하도록 반전; 제공자 표면 테스트를 7개 제공자 순서로 업데이트. - 스위트: 2390개 테스트(+5).
[1.150.0] — 2026-08-12
일관된 빈 상태 (Phase 4 다듬기) — 모든 “아직 없음” 패널이 이제 일부 뷰가 마법의 40px로 인라인 재선언하는 대신, 하나의 공유 .empty 스타일로 렌더링됩니다. 작은 시각적 일관성 수정; #/activity, #/cv-studio, #/stats, #/usage의 빈 상태가 이제 나머지 전부와 일치합니다(토큰화된 48px 패딩 + 점선 테두리).
변경
- **
#/activity,#/cv-studio,#/stats,#/usage**가 빈 패널의 인라인style: { padding: '40px', textAlign: 'center', color: 'var(--foggy)' }를 제거했습니다 — 세 속성 모두 공유.empty클래스가 이미 제공합니다(--space-7= 48px, 가운데 정렬, 흐린 색, 점선 테두리). 그래서 이 넷이 나머지 ~25개.empty패널과 동일하게 렌더링됩니다. - 뷰별 정당한 재정의(
#/dashboardwidth:100%,#/pipelineborder:none)는 그대로 — 순수 중복 재선언만 제거.
참고
- 클라이언트 CSS 사용 정리뿐 — 라우트/서버/i18n 키/CSS 규칙 변경 없음(
.empty클래스 자체는 불변); 사전 스냅샷 1208. 브라우저 검증(#/usage빈 패널이 48px 패딩 + 점선 테두리로 계산, 콘솔 오류 0). - 새 캐너리
tests/empty-state-consistency.test.mjs가.empty를 유일한 진실 원천으로 유지합니다. Phase 5(Hermes 제공자)는 여전히 차단됨. - 스위트: 2385개 테스트(+2:
tests/empty-state-consistency.test.mjs).
[1.149.0] — 2026-08-12
포털을 설정으로 이동 (Phase 4) — #/portals가 이제 Sourcing 아래가 아니라 앱 설정 옆의 Setup 내비게이션 그룹에 있습니다. v1.144.0부터 이것은 소싱 액션이 아니라 설정 화면(추적 회사 활성화/비활성화 + ATS 상태 점검)이므로, 여기가 제자리입니다. 내비게이션만 바뀌며 페이지와 라우트는 그대로입니다.
변경
#/portals내비게이션 항목 → Setup 그룹 (public/index.html), 앱 설정 바로 뒤에 배치. Sourcing 그룹에서 제거(그룹은 Scan / Pipeline / Auto-pipeline / 자금 조달 기업을 유지).#/portals라우트, 뷰,nav.portals라벨은 그대로 — 사이드바 위치만 이동.
참고
- 내비게이션 마크업만 — 라우트/뷰/i18n 키/서버 변경 없음. 브라우저 검증(콘솔 오류 0);
tests/portals-nav-placement.test.mjs로 보호. - 스위트: 2383개 테스트(+2:
tests/portals-nav-placement.test.mjs).
[1.148.0] — 2026-08-12
더 깔끔한 스캔 필터 (Phase 4) — 필터 패널이 이제 정돈된 그리드 — #/scan 필터 패널이 폭이 제각각인 상자들의 들쭉날쭉한 flex-wrap에서 반응형 그리드로 바뀌었고, 적용 / 초기화 액션은 이제 오른쪽 정렬된 별도 행을 차지합니다. 필터도 동작도 그대로 — 읽기만 더 쉬워졌습니다. 디자인 다듬기(parent-sync 없음).
변경
#/scan필터 패널 → 반응형 그리드 —.scan-filters는 이제repeat(auto-fill, minmax(180px, 1fr))열과 균일한 간격을 가진display: grid라서, 라벨이 붙은 11개 필터가 어떤 폭에서도 들쭉날쭉 줄바꿈되지 않고 정돈된 열로 정렬됩니다.- 적용 / 초기화 액션이 전체 그리드를 가로질러 자체 행을 차지하며, 얇은 선으로 구분되고 오른쪽 정렬됩니다.
scan.js의 옛 숨김 라벨 트릭 + 내부 flex 래퍼를 제거했습니다.
참고
- CSS + 작은 DOM 정리뿐 — 모든 필터 id(
#scan-filter-*,#scan-apply)와SR.render()연결은 그대로여서 Playwright 흐름은 영향받지 않습니다. 새 i18n 키 없음. - 브라우저 검증 완료(콘솔 오류 0);
tests/scan-filters-grid.test.mjs로 보호. - 스위트: 2381개 테스트(+3:
tests/scan-filters-grid.test.mjs).
[1.147.0] — 2026-08-12
Hermes & Telegram — 앱 내 도움말 섹션 + cvstart.org 표면 (Phase 5b, 2부) — Hermes 문서 작업의 두 번째이자 마지막 부분입니다: 사용법이 이제 17개 언어 전체에서 앱 자체 도움말 가이드 안에 담기며, 앱 내 문서 어시스턴트가 이를 바탕으로 Hermes 질문에 답합니다. 여전히 문서 전용입니다 — Hermes LLM 제공자 경로는 계획됨 / 아직 미연동 상태로 유지됩니다(Phase 5).
추가
- 앱 내 도움말 §30 “Hermes & Telegram” × 17개 언어 — 새 가이드 섹션(Hermes란 무엇인가 + 두 가지 통합 형태; 클라우드 서버에서 실행; Hermes를 통한 Telegram + “노출하지 말아야 할 것” 규칙),
#/help에서 접근 가능.docs-assistant/DocsFab그라운딩이 자동으로 이를 가져옵니다(둘 다docs/help/<lang>.md를 읽음). - cvstart.org — Hermes 가이드 링크, GitHub 문서로 연결.
변경
- 도움말 번들 게이트 29 → 30 H2 / 105 → 108 H3로 상향(
canonical-docs-coverage,help-ui,help-ru-config-section); §30이 H3 3개를 추가.
참고
- 여전히 아무것도 Hermes를 호출하지 않습니다. 새 캐너리
tests/help-hermes-section.test.mjs가 모든 언어에 §30이 언어 독립 앵커(docs/integrations/HERMES.md,hermes-bridge,#/help,127.0.0.1, Telegram)와 함께 있는지 확인합니다. 제공자는 Phase 5 API 계약 스파이크에 여전히 막혀 있습니다. - 이로써 Phase 5b의 문서 + 스킬 산출물이 마무리됩니다; 제공자 통합(Phase 5)은 별개의 차단된 항목으로 남습니다.
- 스위트: 2378개 테스트(+2:
tests/help-hermes-section.test.mjs).
[1.146.0] — 2026-08-12
Hermes 에이전트 + Telegram — 통합 가이드 + 스킬(Phase 5b, 1부) — 클라우드 서버에서 career-ops-ui를 실행하고, 그 이벤트(완료된 스캔, 새 리포트, 긴급한 후속 조치)를 Nous Research의 Hermes 에이전트를 통해 Telegram으로 연결할 수 있습니다. 이번 릴리스는 설계 + 배포 문서와 hermes-bridge 스킬을 제공합니다. Hermes LLM 제공자 경로는 여전히 계획됨/아직 연결되지 않음 상태입니다(Phase 5 API 계약 스파이크에 막혀 있음). 의도적으로 코드보다 문서가 앞서 있습니다.
추가
docs/integrations/HERMES.md— 심층 가이드: 두 가지 통합 형태(OpenAI 호환 엔드포인트 vs. 에이전트 런타임), 클라우드 서버 배포(reverse proxy + HTTPS + systemd, 헤드리스 서버에서의 읽기 전용 parent 계약), Hermes를 통한 Telegram 연동, 그리고 위협 모델 “노출하지 말아야 할 것” 목록(CV / 연봉 / 리포트 본문 / 키를 채널에 노출하지 않음).- README의
## Hermes agent + Telegram예고 — 영어 README와 완전히 번역된 각 언어 README에 짧은 안내와 링크가 반영됨. - 가이드를 실행 가능하게 만드는
hermes-bridge스킬(.claude/skills/hermes-bridge/) — 전제 조건 + 범위 게이트 검사(Node ≥ 18, 키 존재 여부, SSRF에 안전한 경로를 통한 엔드포인트 접근성 확인), 비밀 정보를 디스크/로그에 절대 기록하지 않으며, Hermes 엔드포인트를 임의로 만들어내거나 제공자가 연결되었다고 주장하지 않음. docs/architecture/OVERVIEW.md의 통합 섹션이 가이드를 링크함.
참고
- 아직 Hermes를 호출하는 코드는 없습니다. 캐너리 테스트(
tests/hermes-docs.test.mjs)가 “계획됨/아직 연결되지 않음” 정직성 표시와llm-dispatch.mjs에 Hermes/Nous 분기가 없음을 확인합니다 — 따라서 나중에 제공자를 연결할 때는 같은 변경에서 문서 + 로드맵도 함께 업데이트해야 합니다. - v1.147.0으로 연기됨(Phase 5b, 2부): 앱 내 도움말의 “Hermes & Telegram” H2 × 17개 언어, 그리고 cvstart.org 마케팅 페이지.
- 스위트: 2376개 테스트(+4:
tests/hermes-docs.test.mjs).
[1.145.0] — 2026-08-12
통찰력 있는 통계(계속): 재구성 가능한 차트 — #/stats의 “목표 직무 추세” 탭에 차트 만들기 위젯이 생겼습니다: 지표 × 차원을 고르면 실시간으로 다시 그립니다. 사용자 UX 요청(부모 동기화 없음).
추가
- 지표 × 차원 재구성 차트 — 지표(채용 공고 / 중앙값 연봉 / 평균 연봉)와 차원(국가별 / 직무별)을 고르면 막대 차트가 즉시 다시 그려집니다. 연봉 지표는 통화 + 연/월 전환을 따르고, 채용 공고는 단순 개수입니다.
- 8개의 새 i18n 키 × 17개 로케일; 스냅샷 1200 → 1208.
참고
- 브라우저에서 확인(콘솔 오류 0). 스위트: 2372개 테스트(+2).
[1.144.0] — 2026-08-12
설정 및 필터(Phase 4, 1부): 추적 포털 활성화/비활성화 — 이제 #/portals에서 감시 대상 회사를 켜고 끌 수 있으며, 스캐너가 이를 반영합니다. 사용자 UX 요청(부모 동기화 없음).
추가
#/portals의 회사별 활성화/비활성화 버튼 — 한 번 클릭으로 포털을 끄면(EN 스캐너가 이미enabled: false회사를 건너뛰므로 비활성 포털은 이후 모든 스캔에서 빠집니다) 또는 다시 켤 수 있으며, 낙관적 토스트가 표시됩니다.POST /api/portals/toggle—portals.yml에서 회사의enabled플래그를 외과적으로(파싱 검증 포함) 바꾸는 명시적 사용자 쓰기(주석·순서·기타 필드 보존). 5개의 새 i18n 키 × 17개 로케일; 스냅샷 1195 → 1200.
참고
- 스캐너 변경은 없음 —
en-scanner.mjs가 이미enabled !== false로 필터링합니다. 스위트: 2370개 테스트(+3).
[1.143.0] — 2026-08-12
이해하기 쉽게(계속): 핵심 워크플로 화면의 ? 힌트 — 도움말 ?가 이제 아홉 개의 주요 작업 페이지를 모든 언어로 다룹니다. 사용자 제보 UX 개선(부모 동기화 없음).
추가
- 9개 화면 제목에
?도움말 힌트 추가 —#/scan,#/evaluate,#/cv-studio,#/tracker,#/config,#/deep,#/batch,#/auto,#/apply에 인라인?(HelpHint.title)가 붙어 현지화된 “무엇을/어떻게/무엇을 기대” 팝오버를 엽니다 — v1.139.0의 CSP 안전 컴포넌트와 동일합니다. - 9개의 새 i18n 키 × 17개 로케일(
help.hint.scan/…/apply); 스냅샷 1186 → 1195.
참고
- 브라우저에서 확인(콘솔 오류 0). 스위트: 2365개 테스트(+1).
[1.142.0] — 2026-08-12
정확성: “Unknown” 커리어 아키타입 제거 — #/orientation이 이제 가끔 “Unknown”으로 답하고 그것을 “밀어붙이라”고 권하는 대신, 항상 여덟 개의 명명된 커리어 벡터 중에서 순위를 매깁니다. 사용자 제보 수정(부모 동기화 없음).
수정됨
#/orientation— AI 프롬프트가 이제 집합 밖 아키타입을 금지합니다. 모델은 정확히 여덟 개의 명명된 벡터 중에서 상위 3개를 골라야 하며 “Unknown”/“N/A”/“데이터 부족”이라고 답하거나 새 라벨을 만들 수 없습니다. 이력서가 빈약하면 가장 가까운 셋을 낮은 신뢰도로 지목하고 부족한 근거를 밝힙니다.
참고
- 서버 프롬프트만 변경(
buildOrientationPrompt); i18n/스키마 변경 없음. 스위트: 2364개 테스트(+1).
[1.141.0] — 2026-08-12
통찰력 있는 통계(계속): 투자받은 회사 정보 보강 — #/funded가 더 시각적으로 바뀌었습니다: 회사 로고, 투자 금액 차트, 라운드 / 금액 / 발굴 점수 / 권장 조치가 담긴 카드. 사용자 제보 UX 개선(부모 동기화 없음).
변경됨
#/funded— 평면 표 → 카드 그리드. 최근 투자받은 각 회사가 이제 로고(회사명 기반, 실패 시 글자 아바타), 라운드 + 금액 칩, 부모 프로젝트의 발굴 점수와 권장 조치, 그리고 투자 뉴스 링크·날짜가 담긴 카드로 표시됩니다.- 투자 금액 시각화 — 공개된 금액 기준 상위 회사의 가로 막대 차트; “$120M”/“€1.5B” 같은 자유 텍스트 금액을 새
parseAmount로 크기로 변환합니다. 3개의 새 i18n 키 × 17개 로케일.
참고
- 여전히
GET /api/company-funded위의 읽기 전용입니다. 설명과 급여 범위는 투자 피드에 없습니다. 스위트: 2363개 테스트(+2).
[1.140.0] — 2026-08-12
통찰력 있는 통계: 더 풍부한 급여 수치 — #/stats의 “내 파이프라인” 급여 분석이 이제 (중앙값뿐 아니라) 평균, 연간 ⇄ 월간 전환, 국가별 최소 · 평균 · 중앙값 · 최대 표를 보여줍니다. Phase 3의 첫 조각. 사용자 제보 UX 개선(부모 동기화 없음).
추가
- 평균 급여 —
RoleStats.salaryStats가 이제minUsd/medianUsd/maxUsd와 함께avgUsd를 반환합니다. 중앙값은 이상치에 강하고 평균은 편향을 드러내므로, 둘이 함께 분포로 읽힙니다. - 급여 섹션의 연간 ⇄ 월간 전환과 국가별 최소 · 평균 · 중앙값 · 최대 표(통화·기간 선택기 연동). 8개의 새 i18n 키 × 17개 로케일.
참고
- 수치는 여전히 급여를 파싱할 수 있는 공고에서만 도출되며 USD로 정규화됩니다(참고용). 스위트: 2361개 테스트(+1).
[1.139.0] — 2026-08-12
이해하기 쉽게: ? 도움말 힌트 — 클릭하면 “무엇을 하는지 / 어떻게 작동하는지 / 무엇을 기대할지”를 사용자 언어로 설명하는 재사용 가능한 CSP 안전 ? 버튼. 사용자 제보 UX 개선(부모 동기화 없음).
추가
?도움말 힌트 팝오버(window.HelpHint) — 제목 옆의 둥근?를 누르면 테마를 따르고 RTL로 반전되는 가벼운 팝오버가 열려UI.md()로 현지화된 설명을 렌더링합니다. 접근성(role="tooltip",aria-expanded, Escape/바깥 클릭으로 닫기, 포커스 복원)과 CSP 안전성을 갖췄습니다.#/stats의 5개 탭과 8개 AI/분석 화면 제목(career-plan, 오리엔테이션, two-pager, 네트워킹, 모의 면접, 메모리, funded, 주간 요약)에?추가 — 14개의 새 i18n 키 × 17개 로케일.
참고
- 모든 화면에는 이미 한 줄 부제가 있었으며,
?는 필요할 때 더 깊은 설명을 더하고 빈 화면도 스스로 설명되게 합니다. 스위트: 2360개 테스트(+4).
[1.138.0] — 2026-08-12
인터페이스 언어로 생성 — 모든 AI 생성이 이제 UI에서 선택한 언어로 응답합니다. 아울러 리뷰에서 나온 테스트 강화 포함. 사용자 제보 UX 개선(부모 동기화 없음).
변경됨
- AI 생성이 이제 UI 언어를 따릅니다. 인터페이스가 러시아어·스페인어·일본어 등으로 설정되면 생성 결과가 항상 영어가 아니라 그 언어로 돌아옵니다. 출력 언어 지시가 모든 생성 엔드포인트 — 커리어 플랜, 오리엔테이션, 시장 리포트, 모의 면접, 네트워킹 플랜, 「문서에 묻기」, 메모리 노트 제안, two-pager 초안 — 에 전달됩니다. 코드와 식별자는 영어로 유지되며(예: two-pager YAML 키), 산문·제목·항목만 현지화됩니다.
수정됨
- CSS 색 역할 가드(
tests/css-role-tokens.test.mjs) — v1.137.0 다크모드 별칭 토큰이 역할을 뒤집지 않음을 검증하는 정적 카나리아: 텍스트 역할 토큰(--fg/--danger/--ok/…)은background로, 표면 역할 토큰(--card/--panel/--line/…)은 텍스트color로 절대 쓰이지 않음(모든 CSS 및 SPA 인라인 스타일). UI.md()XSS 로더 자가 점검 —api.js에서md()를 로드하는 테스트가 추출 직후md('<script>…')를 점검해 이스케이프가 없으면 오류를 던지므로, 향후 잘못된 슬라이스가 보안 스위트를 초록으로 만들지 않고 큰 소리로 실패합니다.#/career-plan스크롤 가드 — 생성 후scrollIntoView는 미리보기가 문서에 아직 연결돼 있을 때만 실행됩니다.
참고
docs/UX-ROADMAP.md갱신:?도움말 힌트 + 페이지 설명 + 빈 상태 작업은 이제 v1.139.0; Nous Research / Hermes 프로바이더(클라우드 서버 + 텔레그램 배포 가이드 및 Hermes 스킬 포함)는 Phase 5 / 5b로 추적됩니다.- 스위트: 2356개 테스트(+5).
[1.137.0] — 2026-08-11
가독성 및 렌더링 수정 — 다크 모드 대비, 차트 레이블, 그리고 커리어 플랜입니다. 사용자 제보 기반 UX 개선입니다(상위 동기화 없음).
수정
- 다크 모드에서 백색-바탕-백색 / 흑색-바탕-흑색 글자가 여러 화면에서 나타나는 문제 — 여러 뷰가 참조하던 CSS 커스텀 프로퍼티 15개(
--fg,--panel,--panel-2,--ok,--danger,--card, …)가 선언되어 있지 않아 하드코딩된 라이트/블랙 값으로 폴백되었습니다: 라이트 모드에서는 문제가 없었지만 다크 모드에서는 읽을 수 없었습니다(#/pipeline개요 칩,#/stats의 활성 탭,#/config의 “Active / Keys” + “✓ set”,#/two-pager의 섹션,#/mock-interview의 질문 버블, 오류 텍스트). 이제 실제 테마 인식 토큰에 별칭(alias)되어 테마를 자동으로 따라가며 — 자동화된 감사 도구로 검증된 전체 29개 뷰에서 WCAG-AA 대비 실패 0건을 달성했습니다;#/config의 활성 탭은 읽기 쉬운 색조(tint) 스타일로 변경되었습니다. 회귀 방지 가드(tests/dark-theme-tokens.test.mjs)가 이 별칭을 고정합니다. #/stats의 차트 레이블이 단어 중간에서 잘렸습니다(“Senior Backend Engineer” → “…Enginee”) — 이제 말줄임표로 표시되며, 전체 레이블은 마우스오버 시 툴팁으로 확인할 수 있습니다.#/career-plan이 생성된 플랜을 가공되지 않은 마크다운으로 표시했습니다 — 이제 서식이 적용된 읽기 쉬운 텍스트로 자동 렌더링됩니다(편집 가능한 마크다운은 텍스트 영역에 그대로 유지되며, Preview 토글로 전환할 수 있습니다).
비고
#/career-plan,#/two-pager,#/stats, 그리고 주간 인터뷰 다이제스트는 고장난 것이 아닙니다 — 플랜을 생성하거나 데이터가 있을 때까지 빈 상태(empty state)를 보여줍니다. 더 명확한 온페이지 안내와?도움말 힌트가 다음 단계로 계획되어 있습니다(docs/UX-ROADMAP.md).
[1.136.0] — 2026-08-11
상위 career-ops v1.26.x 패리티(v1.26.0 메인라인 이후) — 새로운 무인증 소스 1개와, web-ui 미러에 코드로 반영된 품질 및 견고성 포팅 물결입니다. 레지스트리는 이제 79개 소스 = 영문 74개 + 러시아어 5개입니다(ALL_ADAPTERS 74).
추가
eightfold(Eightfold AI, #2684) — 무인증https://<tenant>.eightfold.ai/api/apply/v2/jobsAPI를 통한 채용 게시판으로,*.eightfold.ai에 호스트가 고정됩니다(브랜드 처리된careers.<company>.comCNAME은 의도적으로 거부됩니다); 안전 상한과 함께 페이지네이션되며, 죽은 게시판은 예외를 던지고(dead-board-throw), URL 중복 제거를 수행합니다. 소스 + 어댑터 + CI 격리 스위트를 갖추었습니다;#/scan의 Source 필터와 랜딩에 표시됩니다.
수정
- 유니코드 인식 중복 제거 및 역할 키(#2569 / #2587 / #2667) — 새로운 공유
normalizeTextKey(NFKC, 모든 문자 체계의 문자/결합기호/숫자를 유지)가 ASCII 전용 키를 대체합니다:detect-reposts는 이제 폭/구두점 변형(“Acme, Inc.” ≡ “Acme Inc”)을 하나로 묶으면서도 서로 다른 비라틴 문자 고용주는 절대 합치지 않으며,role-matcher는 전각(full-width) 제목을 정규화하면서도 비라틴 문자 역할 토큰을 지우지 않고 유지합니다. fetchJsonWithRetry가 이제 거부된 리다이렉트를 재시도하지 않습니다(#2657) —redirect:'error'가드가 3xx를 만나는 것은 결정적(deterministic)이므로, 이제는 재시도 불가능한 것으로 처리되어 재시도 예산을 소모하는 대신 즉시 실패합니다.title_filter.positive의 AND 그룹(#2552) — positive 항목 내부의 공백으로 구분된+는 이제 순서와 무관하게 모든 용어가 제목에 나타나야 함을 요구합니다.oraclecloud가 번호가 붙은 테넌트 에펙스(apex)를 허용합니다oraclecloud1.com … oraclecloud99.com(#2683) — 범위가 제한된 패밀리이며(앞자리 0 없음, 숫자 2자리 이하), 와일드카드 에펙스는 절대 아닙니다.workable강화(#2675) — Cloudflare가 앞단에 있는 호스트에 대해 재시도, 브라우저와 유사한 헤더, 요청 직렬화를 적용했습니다.personio가 HTML 스크레이핑으로 대체됩니다 — XML 피드가 비활성화된 경우, 아무것도 반환하지 않는 대신입니다.states의 FALLBACK 별칭이 상위와 재동기화되었습니다(#2615).
비고
- 포팅되지 않음(web-ui에서 미러링하지 않거나 CLI 전용): reply-matcher(#2672), jd-similarity(#2661), jd-skill-gap(#2686), scan의 env-path(#2568) /
--flag=value(#2589) 파싱, 그리고 cover-letter / CV-template / doctor / ollama / generate-pdf 변경 사항. 웹의js-yaml/nanoidHIGH 권고는 web-ui v1.135.0에서 이미 패치되었습니다.
[1.135.0] — 2026-08-11
상위 career-ops v1.26.0 패리티 — 다섯 개의 새로운 무인증 스캔 소스와, web-ui가 이미 보유한 네 개 게시판에 대한 정확성 수정입니다. 레지스트리는 이제 78개 소스 = 영문 73개 + 러시아어 5개입니다(ALL_ADAPTERS 73).
추가
- 새로운 스캔 소스 5개(각각 소스 + 어댑터 + CI 격리 스위트를 갖추며,
#/scan의 Source 필터와 cvstart.org 랜딩에 표시됩니다):join(JOIN) —join.com/companies/<slug>에 내장된 Next.js__NEXT_DATA__에서 회사의 JOIN 게시판을 읽어옵니다(호스트 고정, 페이지 상한).getro(Getro) — 공개api.getro.comPOST API를 통한 VC “탤런트 네트워크” 포트폴리오 게시판으로, 최신순으로 페이지네이션됩니다; 각 채용 공고는 펀드가 아니라 포트폴리오 기업 소속으로 표시됩니다.consider(Consider) — 동일 출처 POST를 통한 getconsider.com VC 포트폴리오 게시판으로, 구성 기반 호스트가 구조적 SSRF 가드(공개 HTTPS 호스트만 허용)로 고정됩니다.joinup(JOINUP) — 스위스 게시판 joinup.ch로, SSR된 최신 페이지를 읽으며 스크레이퍼가 깨지면 안전하게 실패합니다(fail-closed).remotli(Remotli) — remotli.ch, 스위스 기업의 원격 채용 공고(CHF 급여)를 다루며, 교차 게시가 중복 제거되도록 고용주 자체의 ATS 지원 URL을 내보냅니다.
수정
- a16z Speedrun이 이제 일시적 문제로 게시판 전체를 중단하지 않습니다 — 페이지 가져오기가 이제 공유
fetchJsonWithRetry(일시적 429/5xx/타임아웃에 대해서만 제한된 재시도, 영구적 4xx는 재시도하지 않음)를 거치며, 페이지 예산이 50개 채용 공고 페이지에 맞게 재조정되었습니다. - arbeitsagentur가 v6 Jobsuche API(
/pc/v6/jobs)로 이전되었습니다 — 기존 v4 엔드포인트는 404를 반환하며, 응답 형태가 이름을 바꾸었고 이제 원격 필터링이 서버 측에서 좁혀집니다. - thehub이 v2
jobsandfeaturedAPI로 이전되었습니다; 행에는 게시일이 없으며 기간 필터에서 제외됩니다. - hackernews가 자유 텍스트 검색 대신
author_whoishiring계정 태그로 Algolia 조회를 필터링하여 매월 “Who is hiring?” 스레드를 안정적으로 찾습니다.
비고
- 포팅되지 않음(web-ui는 이미 안전하거나, 릴레이로 흡수되었거나, CLI 전용): 유니코드 역할 중복 제거 / 회사 매칭 키(web-ui의 재게시 그룹화는 이미 회사명을 단순 소문자로 키잉하므로 비라틴 문자 고용주가 서로 다른 채로 유지됩니다); 팔로업 거절 지연 신호 + company-funded 보완(읽기 전용으로 중계되며 페일소프트); scan의 환경변수로 재정의 가능한 경로와
--flag=value파싱(web-ui는 스캐너를 인프로세스로 실행함); User-Agent 통합 리팩터(web-ui는 이미 이를 중앙화함); 그리고 CLI 전용 항목들(신뢰할 수 없는 콘텐츠 목록, oferta/offer-prep, doctor, 커버레터/CV 템플릿 변경).
[1.134.1] — 2026-08-05
검증 강화 — 전체 프로젝트 감사에서 드러난 수정 사항입니다.
수정
successfactors가 이제 스캔 중간 실패 시에도 이미 수집한 채용 공고를 버리지 않습니다(v1.134.0의 죽은 게시판 예외 포팅에서 발생한 회귀) — 페이지네이션 루프에try/catch가 없어서, 1페이지가 성공한 뒤 2페이지 이상에서 실패하면 예외가 던져지면서 이미 수집한 결과가 모두 사라졌습니다. 게다가 그 실패가404(범위를 벗어난startrow)였다면,en-scanner가 살아 있는 테넌트를 며칠씩 죽은 것으로 격리해 버렸습니다. 이제phenom/radancy와 동일하게 동작합니다: 0페이지 실패는 여전히 예외를 던지지만(죽은 게시판), 이후 페이지의 실패는 부분 결과를 그대로 유지합니다.#/scan의 필터 칩이 이제 키보드로 조작 가능합니다(WCAG 2.1.1) — 패싯 칩(“지우기” 칩 포함)이 클릭 핸들러만 있는span이었고tabindex/role이 없어서, 키보드와 스크린리더 사용자는 이를 선택하거나 토글할 수 없었습니다. 이제role="button",tabindex="0",aria-pressed를 갖추었으며 Enter/Space로 활성화할 수 있습니다.- 하드코딩된 영어 문자열 3개가 이제 현지화되었습니다 —
#/scan의 신뢰 배지 툴팁,#/scan의 이전(relocation) 열 헤더,#/dashboard의 점수 열 헤더는 i18n 패리티 게이트가 감지할 수 없는 순수 리터럴(키가 아님)이었기 때문에, 영어가 아닌 모든 로케일에서 계속 영어로 남아 있었습니다. 이제scan.trustTip+scan.col.reloc(신규 키 2개)와 기존track.col.score의 재사용으로 대체되었으며, 소스 정적 가드가 이를 고정합니다.
[1.134.0] — 2026-08-05
상위 career-ops v1.25.0 패리티.
추가
- 새로운 스캔 소스: getManfred(
manfred) —www.getmanfred.com/api/v2/public/offers에서 제공되는, 급여가 공개된 스페인/EU 기술직 게시판 전체 피드입니다(무인증, 호스트 고정 + HTTPS 전용, 단일 요청으로 전체 카탈로그 수집). 소스 + 어댑터 + CI 격리 스위트(tests/sources-manfred.test.mjs)를 추가했습니다; 레지스트리는 이제 73개 소스 = 영문 68개 + 러시아어 5개입니다(ALL_ADAPTERS68).#/scanSource 필터와 cvstart.org 랜딩에 표시됩니다.
수정
- a16z Speedrun 피드가 조용히 50개로 잘리고 있었습니다(#2404) — 피드는 페이지당 50개로 상한이 있지만 소스는
PER_PAGE = 100을 요청하고 있었기 때문에, 1페이지 이후 페이지네이션이 멈췄습니다. 50으로 정정했습니다. - 죽은 게시판이 이제 “라이브지만 비어 있음”으로 읽히는 대신 예외를 던집니다(#2379) —
cryptocurrencyjobs,phenom,radancy,successfactors: 어떤 요청도 완료되지 않는 fetch 실패가 이제 예외를 던지므로(#/portals상태 확인과 스캔이 실제 실패를 기록합니다), 빈 목록으로 조용히 삼켜지지 않습니다; 최소 한 번 성공한 이후 스캔 도중 발생한 실패는 부분 결과를 그대로 유지합니다. - workable이 이제 공개 위젯 API를 사용합니다(#5ab8425) —
apply.workable.com/api/v1/widget/accounts/<slug>로 전환되어, 대형 계정의 전체 채용 목록을 한 번의 요청으로 반환하므로 대형 계정이 더 이상 잘리지 않습니다.
비고
- 포팅되지 않음(CLI 전용이거나 web-ui에서 미러링하지 않음): detect-reposts #2389 제목 버킷팅 성능 재작성; 유니코드 회사 키 수정(web-ui 자체 트래커 중복 제거는 이미 비라틴 문자에 안전함);
scan --since;cv-facts; CV 템플릿 / PDF 감사 패스;doctor; 모드 미신뢰 콘텐츠 지시문.
[1.133.1] — 2026-08-02
수정
#/funded(투자 유치 기업)가 이제 결과를 렌더링합니다 — 상위의company-funded.mjs가 전체 목록을 반환해도 테이블에 항상 “투자 유치 기업 없음”이 표시되던 두 가지 버그를 수정했습니다. (1) 뷰가 결과를res.candidates아래에서 읽고 있었지만, 상위는 실제로companies(각 항목은{ company, amount, round, funding: { sources: [{ source, url, observed_date }] } }형태) 아래에 결과를 내보냅니다 — 이제 클라이언트가 올바른 키를 읽고 실제 증거 구조에 맞춰 매핑합니다. (2) 결과 테이블이 셀을UI.el('tr', {}, …)에 가변 인자로 전달했지만,UI.el(tag, attrs, children)은children을 단일 노드 또는 배열로 받으므로 첫 번째 열(Company)만 렌더링되고 있었습니다 — 이제 셀은 배열로 전달됩니다. 실제 브라우저에서 검증: 4개 피드에 걸쳐 11개 기업이 Company / Funding signal / Source / Date 열과 정상 작동하는 증거 링크로 렌더링되며 콘솔 오류는 0건입니다. 결과가 없을 때도 이제 소스별 진단 정보가 함께 표시되어, 조용한 뉴스 날과 차단된 피드를 구분할 수 있습니다.tests/parity-routes-v1133.test.mjs의 회귀 가드: 가짜 상위 스크립트가 이제 실제companies출력 형태를 내보냅니다(원래 픽스처가 잘못된candidates형태를 그대로 반영하고 있었고, 이것이 버그가 그린 상태로 배포된 정확한 이유입니다). 또한funded.js가res.companies를 읽고(res.candidates는 절대 읽지 않음) 배열 children으로 테이블 행을 구성하는지 확인하는 소스-정적 카나리아를 추가했습니다(+1 → 2144).
[1.133.0] — 2026-08-01
추가
- 투자 유치 기업 발굴(
#/funded, 상위 패리티 #2117) — 상위 career-ops의company-funded.mjs를GET /api/company-funded로 중계하는 새로운 읽기 전용 뷰입니다: 공개적이고 호스트가 고정된 펀딩 피드(TechCrunch, PR Newswire, The Guardian, Hacker News)에서 발견한, 최근 투자를 유치한 기업들의 검토 우선 목록입니다. 이 릴레이는 스크립트를--json --dry-run으로 실행하고(stdout으로 JSON 출력, 파일 쓰기 없음), 사용자 입력을 결코--sources에 전달하지 않으며, 속도 제한이 적용되고, 사용자가 직접 실행해야 합니다(Discover 버튼 — 마운트 시 자동 실행 없음). 새 라우트 모듈server/lib/routes/funded.mjs+public/js/views/funded.js, Sourcing 아래. - 주간 인터뷰 다이제스트(
#/interview-digest, 상위 패리티 #2129/#2130) — 상위의 LLM을 사용하지 않는weekly-digest.mjs를GET /api/interview/weekly-digest로 중계하는 새로운 읽기 전용 뷰입니다: 이번 주 인터뷰한 회사와 라운드, 반복되는 역량, 최선 노력으로 파악한 공백을 기계적으로 집계합니다. 선택적?from=&to=범위는 둘 다 유효한YYYY-MM-DD일 때만 전달되며, 빈 범위도 유효한available:true다이제스트입니다.server/lib/routes/interview.mjs+public/js/views/interview-digest.js에 추가, Analytics 아래. - 두 릴레이 모두 상위 스크립트가 없을 때(CI, 독립 설치) 기존의 페일소프트
available:false계약을 따릅니다. 26개의 새 i18n 키 ×17개 로케일; CI 격리 스위트tests/parity-routes-v1133.test.mjs(+5 → 2143).
비고
- 상위 career-ops가 Next.js
web/앱의 Follow-up Tracker 페이지(#1422)와 백엔드 PDF 렌더링(#2182)을 갖춘 v1.24.0을 넘어 발전했습니다 — 포팅되지 않음: web-ui는 이미 자체 팔로업 릴레이와 PDF 러너를 갖추고 있으며, 근간이 되는followup-cadence.mjs하드닝은 셸아웃 릴레이를 통해 자동으로 반영됩니다.set-status.mjs/tracker-utils.mjs변경 사항은 CLI 내부용이며 미러링되지 않습니다.
[1.132.0] — 2026-07-31
변경
#/scan결과 렌더링 서브시스템을public/js/lib/scan-results.js로 추출(파일 크기 계약 부채 해소 —public/js/views/scan.js가 ~1254줄까지 늘어났습니다).renderResults,buildChipRow, 행/패싯 빌더, 옵션 페인터,FALLBACK_SOURCES레지스트리 미러를 포함한 서브시스템이 뷰가 제공하는 컨텍스트 객체를 클로저로 감싸는window.ScanResults.create(ctx)팩토리로 이동합니다. 동작 변화 없음 — 함수들은 그대로 옮겨졌고 클로저 변수만ctx.*로 재배선되었습니다.scan.js는 이제 ~906줄입니다(800줄 목표를 향한 두 번째 추출 단계가 예정되어 있습니다).- 새로운 인브라우저 회귀 게이트 —
tests/playwright-scan-filters.mjs가 고정된data/last-scan.json을 시드하고 모든#/scan필터를 구동하여 정확한 행 수를 검증하므로, 추출이 실제 브라우저 동작에 대해 검증됩니다. - README 배너 슬림화 — 버전별 “최신 릴리스” 서술이 길게 이어지던 배너를 한 줄 요약 + 전체 변경 로그(이 파일) 링크로 대체합니다.
[1.131.2] — 2026-07-31
변경
app.css를 세 개의 순서가 있는 스타일시트로 분할(파일 크기 계약 부채 해소 — 단일 파일이 ~1990줄까지 늘어나 800줄 하드 한도를 크게 초과했습니다). 이제app.css(~672줄 — 접근성, 디자인 토큰/테마, 사이드바, 메인, 버튼, 콘텐츠 셸),components.css(~595줄 — 카드, 그리드, 페이지네이터, 배지, 테이블, 폼, 로그/콘솔, 마크다운, 언어 전환기, 칩 필터, 연결 배너),overlays.css(~737줄 — 토스트, 알림 드로어, 모달, 기타/반응형,[dir="rtl"]미러, docs-fab, usage-hud)로 나뉘며, 각각 하드 한도 이내입니다.- 분할은 연속적이며 순서를 유지하므로, 캐스케이드는 분할 이전 파일과 바이트 단위로 완전히 동일합니다.
index.html은 세 파일을 순서대로<link>로 로드합니다. 동작·마크업·i18n 변경 없음. - CSS를 검사하는 테스트는 이제 공유 헬퍼
tests/helpers/css.mjs::loadAppCss()를 통해 연결된 결과를 읽습니다. 새tests/css-modularization.test.mjs가 분할을 고정합니다(파일 존재 확인 · 각 파일 ≤ 800줄 · index.html 링크 순서) → 스위트 2138. 브라우저에서 검증: 세 스타일시트 모두 파싱되고 규칙이 적용됩니다.
- 분할은 연속적이며 순서를 유지하므로, 캐스케이드는 분할 이전 파일과 바이트 단위로 완전히 동일합니다.
[1.131.1] — 2026-07-31
수정
- v1.130.0의 두 소스에 대한 어댑터 호스트 고정 일관성(코드 리뷰 후속 조치, 심층 방어; 유효한 입력에 대한 동작 변화 없음):
a16z-speedrun-talent어댑터가 이제buildEndpoint에서api:/a16z-speedrun-talent:오버라이드를 재검증하며(HTTPS + 정확한 호스트speedrun-talent-network.com), 검증에 실패하면 정규 피드로 폴백합니다 —cryptocurrencyjobs어댑터와의 패리티로, 호스트가 다른 값이 fetch 슬롯에 도달하는 일이 없습니다(이전에는 fetch 시점의assertSpeedrunUrl가드에만 의존했습니다). 정확 호스트 검사는 이제 가드와 어댑터가 공유하는 단일 exportSPEEDRUN_TALENT_HOST_RE로 통합되었습니다.cryptocurrencyjobs파서 —cleanUrl이 이제assertCryptocurrencyJobsUrl및 어댑터 오버라이드와 동일한 정확 일치 호스트 가드를 사용합니다(이전에는 서브도메인을 허용하는endsWith였습니다). 파서는 SSRF 가드보다 더 관대해질 수 없습니다:sub.cryptocurrencyjobs.co아이템 링크는 폐기됩니다.- +2 테스트 → 스위트 2135.
[1.131.0] — 2026-07-31
추가
#/trackerCRM 스테이지 탭 보드(상위 웹 앱의/pipeline뷰에서 이식). 트래커의 퍼널 칩 바 + 상태 드롭다운이 스테이지 탭 스트립으로 대체됩니다: All 탭과 정규 상태별 탭 하나씩 — Evaluated · Applied · Responded · Interview · Offer · Rejected · Discarded · SKIP · Hired — 각각 실시간 전체 이력 카운트를 표시하며, 카운트 0인 스테이지도 포함되어 전체 퍼널이 항상 보입니다(CRM 형태). 활성 탭이 필터를 구동하며, 다시 클릭하면 All로 초기화됩니다. 행은 점수 톤, 적법성, PDF·리포트 기능을 그대로 유지하고, 로고가 활성화된 경우 회사 셀에 브랜드 로고가 표시됩니다(기본값 꺼짐 → 추가 요청 없음).- 새 읽기 전용 라우트 **
GET /api/tracker/stages**가 정규 퍼널(순서대로 라벨) + 별칭 폴드 맵을 반환하며,server/lib/states.mjs(templates/states.yml, 내장 폴백 포함)에서 가져옵니다 — 클라이언트가 상태 화이트리스트를 하드코딩하지 않도록 합니다. 파라미터 없는 레거시GET /api/tracker응답은 변경되지 않습니다({ rows }만). - 새 순수·단위 테스트된 클라이언트 라이브러리 **
public/js/lib/tracker-stages.js**가 서버의 스테이지에 맞춰 행을 버킷화하며, 흩어진 마크다운 볼드와 지역화된 별칭(예:aplicado→Applied)을 허용합니다. 탭은 접근성을 갖춥니다(role tablist/tab, aria-selected, ≥44px 히트 영역, 각 탭의 접근 가능한 이름에 카운트 포함). 새 i18n 키 없음. 스위트 2133.
- 새 읽기 전용 라우트 **
[1.130.0] — 2026-07-31
추가
- 상위 career-ops v1.24.0에서 이식된 2개의 새 스캔 소스(인프로세스, 새 의존성 없음; 둘 다
#/scanSource 필터와 cvstart.org 랜딩에 표시됩니다):- a16z Speedrun(
a16z-speedrun-talent, #2231) — a16z Speedrun talent-network 보드 전체 JSON 피드.speedrun-talent-network.com에 호스트 고정, HTTPS 전용, 페이지 상한이 있는 0-인덱스 페이지네이션, 회사별q/설정 스레딩, fail-soft. - Cryptocurrency Jobs(
cryptocurrencyjobs) — Web3 채용 보드cryptocurrencyjobs.co, 공개 RSS 2.0 피드로 수집(무인증). 2-패스 XML 엔티티 디코드, 원격 전용 공고, 제목 끝의"… at <Company>"에서 고용주 파싱. - 레지스트리 총합은 이제 72개 소스 = 영문 67개 + 러시아어 5개입니다(
ALL_ADAPTERS= 영문 포털 어댑터 67개).
- a16z Speedrun(
수정
echojobs— 하이브리드 근무 형태가 원격과 구분되어 유지(상위 #2258 미러링). 대소문자 구분 없는hybrid마커가 이제"<City> · Hybrid"(도시가 없으면 단순Hybrid)와workplaceType: 'Hybrid'를 생성하며, 더 이상Remote로 뭉뚱그려지지 않습니다.radancy— 레거시 TalentBrew 마크업 + JSON 결과 프래그먼트 전송(상위 a3e6df9 미러링), 주입 가능한opts.fetchJson으로 게이트됩니다.
비고
- 미포팅 — CLI 전용 상위 기능. career-ops v1.24.0의 방대한 CLI/모드 표면은 web-ui에 들어오지 않습니다. web-ui는 모드 호스트가 아니라 뷰어 + 씬(thin) 라이트스루입니다: 컴플라이언스/관할권 테이블, 연락처 전화번호부 + vCard, 인터뷰 트랜스크립트 디브리프/콜 플랫폼 감지, 원장 set-status, 결과 기록, 투패스 트리아지, jd-similarity, 버전 관리되는 지원-CV 아티팩트 스키마, doctor의 Playwright-MCP 감지,
portals/fix-slugs.mjs가 여기 해당합니다. 상위의scan.mjs에 있는 스캔 오케스트레이션 변경 사항 — Interamt.de Playwright 스캐너, iCIMS 리버스-ATS 전체 스윕, 국가 자격 원격 필터, DNS 조회 페이싱, StepStonerltr중복 제거, 스캔 기록의 정규화된 회사 컬럼 — 도 적용되지 않습니다: web-ui는 EN/RU 스캐너를 인프로세스로 실행하며scan.mjs로 셸아웃하지 않습니다. - 이미 반영됨.
role-matcher악센트 폴딩 수정(#2209)은 v1.127.0에서 이미 이식되어, 여기서는 변경 사항이 없습니다.
[1.129.1] — 2026-07-29
수정
- v1.128/v1.129 웹 포트에 대한 AI 리뷰 후속 조치(모두 권고, 원천 수정):
job-facets.js레벨 우선순위(명시적 수식어가 관리 단어보다 우선 —Senior Engineering Manager→senior, 이전lead);states.mjs폴백이 더 이상 고정되지 않음(성공 읽기는 메모이즈, 폴백은 캐시 없이 반환 — 부팅 시 일시적으로 부재한 부모는 다음 호출에 재읽기) + 존재하지만 손상된 파일에console.warn;score-tone.js— 점수 없는 행은 중립(muted), 빨강 아님;domainFromName()은/api/logo전에 비-ASCII 슬러그 건너뜀; +tests/states.test.mjs격리 가드. +4 테스트 → 2073.
[1.129.0] — 2026-07-29
추가
#/scan레벨 패싯 + 게시일 컬럼 — v1.128.0의job-facets.js라이브러리가 이제 스캔 UI에 연결되었습니다(이전엔 로직만). 새 레벨 드롭다운이 각 공고 제목을 lead/staff/senior/mid/junior/intern으로 분류(JobFacets.seniorityFromTitle)하고 결과에 실제로 있는 항목으로 자동 채웁니다(Country 패싯처럼); 레벨 단어가 없는 제목은 항상 통과. 저장된 검색·초기화·적용에 유지됩니다. 결과 테이블에 레벨 배지 컬럼과 무토큰 게시일 컬럼(오늘/N일,JobFacets.daysSince)이 추가됩니다. i18n 키 12개 ×17, +3 테스트 → 2069.
[1.128.0] — 2026-07-29
추가
- 상위 프로젝트 자체 웹앱(
../web/, Next.js)에서 4개 솔루션 포팅 — 바닐라 JS/ESM로 재구현, 새 의존성 없음: (1)server/lib/states.mjs가templates/states.yml을 트래커 상태 어휘의 단일 소스로 실시간 읽음(CI 폴백) — 매 릴리스 수동 화이트리스트 재동기화 제거; POST는 별칭(스페인어/레거시)을 정규 라벨로 접고, GET 퍼널은 정규 상태로 버킷; (2) ATS 호스트 행의 회사 로고 —domainFromName()(~90 브랜드→도메인); (3)score-tone.js— 4단계 점수 톤(≥4.2/3.8/3.0 + 문자 폴백); (4)job-facets.js— seniority/source/days 파셋. +21 테스트.
비고
- 미포팅(개념만): 상위의 에이전트 액션 레이어(
actions/registry.ts+api/assistant/route.ts) —docs-fab가 코파일럿이 될 때의 청사진. 새 소스 없음(레지스트리 70), i18n/help 변경 없음.
[1.127.0] — 2026-07-29
추가
- 3개의 새 스캔 소스(career-ops v1.23.0 패리티) — 레지스트리가 이제 **70개 어댑터(65 EN + 5 RU)**를 제공합니다: Flowxtra(무인증 전역 애그리게이터), VDAB(플랑드르 공공고용서비스 키워드 API), iCIMS(
careers-<tenant>.icims.com포털,jibeapply와 별개). 또한 Cursor가 CLI 로스터에 복귀(parent #2115):cli-detect가 이제cursor를 감지(10개 도구)하고 help/README/config ×17에 로스터가 복원됩니다.
수정
- agenticjobs가 HTML 스크래핑에서 REST API로 전환(#2167); Greenhouse는
location.name이 근무 형태뿐일 때/offices에서 도시를 복구(#2104); role-matcher 패리티(#1933/#2164/#2009: MTS 접두사,product베이스라인, 악센트 폴딩, 서브-베이스라인 불일치).
비고
- 미포팅. v1.23.0 대부분은 web-ui가 쓰지 않는 CLI/대시보드 표면(batch-tailor, discover-ats, NL/PT 모드, PDF 테마, Go 대시보드, updater/doctor); 릴레이 스크립트는 변경 불필요. 상위 VERSION → 1.23.0.
[1.126.1] — 2026-07-25
수정
- v1.126.0 재동기화가 놓친 CLI 로스터 드리프트 두 곳 — (1)
#/config의 API keys 탭 인트로(config.providerModelNote, i18n ×17)가 7개 CLI만 나열 — 이제 Antigravity와 Grok Build가 OpenCode 뒤에 삽입됨; (2) 도움말 가이드(×17)의 두 번째 비교표 행과 CI 빌드 사이트 help가 여전히Inside Claude Code / Codex / Cursor / Gemini CLI(구식 Cursor 포함)였음 — 이제 전체 로스터. 둘 다 v1.126.0 스윕 패턴이 다루지 못한 슬래시/가운뎃점 구분자를 사용. i18n 스냅샷 재생성; 스위트는 1969 유지.
[1.126.0] — 2026-07-25
추가
- AI CLI 도구 탭이 career-ops의 8개 일급 CLI를 모두 감지 —
#/config로스터를 상위의docs/SUPPORTED_CLIS.md와 동기화:server/lib/routes/cli-detect.mjs에 Grok Build CLI(grok)와 Kimi CLI(kimi)가 추가되고, Antigravity는 이제 정규 바이너리agy를 먼저 탐색합니다. 읽기 전용 PATH 스캔은 이제 9개 도구를 보고하며, 여전히 찾은 바이너리를 실행하지 않습니다.
변경
- career-ops.org/docs와 문서 재동기화 — 모든 문서 표면을 상위의 라이브 페이지(31개 전부 읽음)와 대조했습니다. 정규 AI 어시스턴트 로스터(help ×17 + README ×17)는 이제 8개 일급 CLI — Claude Code, Codex, OpenCode, Antigravity CLI, Grok Build CLI, Qwen Code, Kimi, GitHub Copilot CLI — 와 Gemini CLI(레거시 래퍼)를 나열합니다. 도움말 번들은 29 H2 / 105 H3 구조를 유지합니다.
[1.125.4] — 2026-07-23
변경
- site 의존성 (dependabot #151–#153) —
site/의sharp0.34.5→0.35.3,svgo4.0.1→4.0.2,fast-uri(dev) 3.1.3→3.1.4; Astro 빌드 정상, SPA/서버 영향 없음.
비고
- 상위 프로젝트 패리티 스윕(career-ops
37d17ec..254764a, v1.22.0 이후) — 포팅할 것 없음:set-status의 잘못된 행 가드(#2108)는 CLI 전용(web-ui 트래커 행은 UI에서 명시적으로 선택되며 어떤 라우트도set-status.mjs를 호출하지 않음), 로컬라이즈 모드의 Risk Summary(#2109)는 web-ui가 읽지 않는modes/<lang>/파일(최상위modes/*.md만 읽음),update-system매니페스트 검증(#2111)은 업데이터 전용, 나머지는 상위 문서(터키어 README, SIGNATURES ×4, SCRIPTS.md, es 악센트). 상위 VERSION은 1.22.0 유지 —parentVersion변화 없음.
[1.125.3] — 2026-07-23
수정
- 덴마크어·힌디어 LLM 프롬프트가 영어로 응답하던 문제 (사용자 제보) —
server/lib/prompts.mjs의LOCALE_NAMES와 다섯 개의SCAFFOLD_STRINGS블록이da·hi로 확장되지 않아resolveLocale()이en으로 폴백했고, 딥 리서치(라이브·수동), 모드 실행, 평가, 인터뷰, 네트워킹, CV Studio 등 모든 AI 프롬프트에서# Output language지시문이 빠졌습니다. 이제 두 로케일 모두 정식 지원됩니다: 로케일 지시문 + 현지화된 스캐폴딩.tests/locale-scaffold.test.mjs의 회귀 게이트는 하드코딩된 12개 대신 정식 17개 로케일 목록을 순회하고, 새 구조 패리티 게이트는 영어로 폴백하는 스캐폴드 키를 실패시킵니다 — 앞으로prompts.mjs를 놓친 로케일은 출시될 수 없습니다 (+12 테스트, 스위트 총 1969개).
[1.125.2] — 2026-07-22
수정
- Gemini 딥 리서치: HTTP 502(
MALFORMED_FUNCTION_CALL) (#145, @Alien10140 기여) — 라이브/api/deep프롬프트가 모델에게 “Use WebFetch / WebSearch”와 파일 저장을 지시했지만, 헤드리스 API 제공자에는 도구 채널이 없어 Gemini가 텍스트 대신 함수 호출로 응답했고, 이것이 빈 HTTP 502로 나타났습니다.buildDeepPrompt와bundleProjectContext에headless플래그가 추가되어, 라이브 실행(Anthropic/Gemini/폴백 캐스케이드)은 인라인된 컨텍스트만으로 브리프를 작성하는 도구 없는 프롬프트를 받고, Claude Code용 복사-붙여넣기 프롬프트는 도구 지시를 유지합니다.tests/critical-fixes.test.mjs에 테스트 +1.
변경
- 지원 종료된
gemini-2.0-flash를 넘어 Gemini 기본 모델 상향 (#144, @Alien10140 기여) — 설정 드롭다운,gemini.mjs의 서버 폴백(힌트와 조용히 어긋나 있던 값), OpenRouter 폴백 체인,config.geminiModelHint×17, 도움말 가이드 ×17이 모두gemini-3.6-flash를 가리킵니다. 새 드리프트 게이트tests/gemini-default-model.test.mjs(+5 테스트)가 모든 표면을 같은 리터럴로 고정합니다 — 스위트는 이제 1957 테스트입니다.
[1.125.1] — 2026-07-21
수정
- SuccessFactors: 멀티 브랜드 RMK 테넌트가 브랜드 경로 유지 (상위 #2099, v1.22.0 이후) — 하나의 공유 RMK 인스턴스에서 여러 인수 브랜드를 운영하는 지주회사는 경로 세그먼트로 브랜드를 구분합니다(
careers.nemetschek.com/Bluebeam/vs…/Vectorworks/). 어댑터는 이전에 설정된 URL을 origin으로 축소해버려, 상위 브랜드의 채용 공고만 조용히 스캔하고 있었습니다. 이제 엔드포인트는 브랜드 접두사를 보존하고 끝에 붙은/search/또는/tile-search-results/세그먼트만 제거하므로, 어떤 경우에도 스캔이 중복되지 않습니다. 단일 도메인 테넌트는 바이트 단위로 변경되지 않습니다. 새로 export된resolveTenantBase헬퍼 +tests/sources-successfactors.test.mjs에 이식된 테스트 블록 1개.
[1.125.0] — 2026-07-21
추가
- cvstart.org: “채용 공고 소스” 랜딩 섹션 — 스크린샷과 비교 목록 사이에 새로운 섹션이 추가되어, 스캔 소스 67개 전체를 클릭 가능한 칩(영어권 게시판/ATS 62개 + 별도 소제목으로 묶인 러시아어 게시판 5개)으로 나열하며, 각 칩은 해당 소스의 공개 사이트로 연결됩니다. 이 목록은 빌드 시점에 실제 어댑터 레지스트리(
sync-assets.mjs→facts.sources)에서 동기화되므로 앱과 절대 어긋날 수 없습니다.Sources.astro의 큐레이션된 링크 맵은 새로운tests/site-sources.test.mjs로 검증됩니다. 헤더 내비게이션에는 Sources 앵커가 추가되었고, 사이트 i18n 키 4개 × 17개 언어가 새로 생겼습니다. 또한 랜딩 페이지 JSON-LD의inLanguage목록에서 그동안 빠져 있던hi도 함께 수정했습니다.
[1.124.0] — 2026-07-21
추가
- 스캔 소스 5개 (상위 v1.22.0 패리티, #1808/#1572/#2024/#2055) — Welcome to the Jungle(보드 전체 JSON API), Agentic Engineering Jobs(에이전틱/AI 엔지니어링 보드), Jobvite(무인증 테넌트별 ATS), Gem(테넌트별 ATS), Alibaba Group(채용 JSON API, Meituan/Tencent 패턴)입니다. 각각 호스트 고정되고 CI 격리된 소스 + 어댑터 쌍이며, 레지스트리는 이제 **어댑터 67개(EN 62개 + RU 5개)**를 제공합니다.
#/scanSource 드롭다운 폴백과 드리프트 게이트가 갱신되었고, 다섯 개의 새로운 테스트 스위트tests/sources-{wttj,agenticjobs,jobvite,gem,alibaba}.test.mjs가 추가되었습니다.
수정
- Arbeitsagentur:
homeofficetyp이VOLLSTAENDIG일 때만 전국 원격으로 판정 (상위 #1981) —homeoffice=nv_true쿼리는 하이브리드 직무도 함께 반환하므로, 이제 원격 판정 단계가 작은 배치 단위로 각 결과를 채용 상세 엔드포인트에 대조 확인하며 실패 시 안전하게 닫힙니다(조회 오류가 나면 채용의 실제 도시를 유지하여 위치 필터가 계속 적용되도록 합니다). - SmartRecruiters: 공개 채용 URL이
/postings/없이 생성되던 문제 수정 (상위 #2047) — 공개 사이트에서 해당 경로 세그먼트가 빠지는 테넌트의 경우, 링크가 404 대신 공개 채용 공고 페이지로 정상 연결됩니다.
비고
- 상위 v1.22.0은 웹 UI가 셸아웃하지 않거나 이미 커버하고 있는 CLI 측 변경 사항도 함께 배포했습니다: zh-CN CV 템플릿 + PDF 타이포그래피,
/expand모드, 프로바이더 프롬프트 캐시 조정(Gemini/OpenAI/Ollama), 스텝별 토큰 분석(웹 UI는 자체 사용량 미터를 보유), 트래커 writer-lock 직렬화(웹 UI는 v1.21부터withFileLock을 통해 쓰기를 라우팅), 스캔visa_filter+ 절대 게시일 CLI 플래그(웹 UI는 자체 “게시 기간” 나이 필터를 보유), seen-sources 중복 제거 시딩(웹 UI 스캐너는 자체 scan-history 중복 제거를 유지)입니다.
[1.123.0] — 2026-07-17
추가
- Oracle Recruiting Cloud 스캔 소스 (상위 v1.21.0 패리티, #1929) — Oracle Fusion/ORC 채용 사이트(JPMorgan Chase, Oracle, BNY Mellon, American Express, Honeywell 등)의 무인증
recruitingCEJobRequisitionsREST API입니다:*.fa[.<region>][.ocs].oraclecloud.com에 호스트 고정되며, 사이트 번호는 추적 중인 각 회사의careers_url에서 추출되고, 오프셋 페이지네이션에는 하드 페이지 상한이 적용되며, WAF를 우회하는 브라우저형 헤더를 사용합니다. 레지스트리는 이제 **어댑터 62개(영어 57개 + 러시아어 5개)**를 제공합니다.#/scanSource 드롭다운 폴백과 드리프트 게이트가 갱신되었고, 새로운 CI 격리 테스트 스위트tests/sources-oraclecloud.test.mjs가 추가되었습니다.
수정
- 리포스트 감지기: 기본 제목이 전문화 접미사가 붙은 유사 공고와 구분되도록 수정 (상위 #1922) — “Senior Analytics Engineer”가 더 이상 “Senior Analytics Engineer, People Analytics”와 하나로 묶이지 않습니다: 한 제목의 토큰이 다른 제목 토큰의 진부분집합이고 추가된 토큰이 (기본 단어가 아닌) 실제 전문화 표현일 경우, 두 공고는 별개로 게시 가능한 채용 건으로 취급됩니다. 리포스트 표기(“(Repost)”, “relisted”)는 이제 의미 없는 메타 노이즈로 불용어 처리됩니다.
tests/detect-reposts.test.mjs에 검증 2건이 추가되었습니다.
비고
- 상위 v1.21.0은 웹 UI가 셸아웃하지 않거나 이미 커버하고 있는 CLI 측 변경 사항도 함께 배포했습니다: 반복 지원 회사 재지원 경고(웹 UI는 v1.84.0부터 재지원 쿨다운을 보유), 커버레터
--format/--report플래그, 인터뷰 red-flag/패널 인텔/노쇼 이메일 프롬프트 모드, 스캔 신뢰 신호 및 포털 상태 영속화(웹 UI는trust-validator와 함께 자체 인프로세스 스캐너와 Portals health 페이지를 운영), 그리고 통계/급여 격차 확장(읽기 전용·페일소프트 방식으로 중계).
[1.122.0] — 2026-07-16
추가
- 힌디어(हिन्दी) — 17번째 언어 — 전체 UI 사전(약 1,110개 키), 인앱 도움말 가이드 전체(29개 H2 / 105개 H3 패리티),
README.hi.md, 새CHANGELOG.hi.md(de/it/tr 선례를 따라 v1.122.0부터 시작), cvstart.org 랜딩 + 방법론/라이선스/체인지로그/도움말 페이지, 언어 전환기(🇮🇳), 브라우저 언어 자동 감지, 그리고 현지화된 대시보드 스크린샷까지 모두 추가되었습니다. 이제 모든 ×16 패리티 게이트가 ×17로 실행됩니다: i18n 사전 패리티 + 스냅샷, 도움말 H2/H3 게이트, CHANGELOG 패리티, 사이트check-i18n, 그리고 Playwright 로케일 스윕까지.
[1.121.0] — 2026-07-16
추가
- cvstart.org: 방법론, 라이선스, 체인지로그 페이지 — 랜딩이 기존 Compare 블록 옆에 16개 언어 전체로 세 개의 새 섹션을 추가했습니다: /methodology/(6차원 0.0–5.0 채점 루브릭, 4.0 지원 임계값, 그리고 절대 하지 않는 규칙들 — career-ops.org/methodology를 현지화하여 요약), /license/(NOTICE.md 포인터가 포함된 정식 MIT 전문), /changelog/(이 파일을 저장소의 번역된 16개 CHANGELOG로부터 로케일별로 렌더링). 새 헤더 Methodology 항목과 푸터 리소스 링크가 추가되었고,
sync-assets.mjs가 이제 빌드 시점에 CHANGELOG ×16과 LICENSE를 사이트로 동기화하므로 페이지가 저장소와 어긋날 일이 없습니다. - 문서 전반의 방법론 링크 — README(16개 전체), 인앱 도움말 가이드 §1 정식 레퍼런스 목록(16개 전체), 위키가 이제 기존 career-ops.org/docs 가이드와 나란히 career-ops.org/methodology를 링크합니다(FAQ와 용어집도 포함).
변경
- README 릴리스 배너와 배지를 새로 고쳤습니다(테스트 1850개, 릴리스 v1.121.0) — 배너가 여전히 v1.119.5를 알리고 있었습니다.
[1.120.0] — 2026-07-16
추가
- CareerOps 선언문 (부모 v1.20.0 패리티) — 상위 프로젝트가 CareerOps 선언문(
MANIFESTO.md· career-ops.org/manifesto)을 출시하고, 이제 자신의 README·업데이터·Go 대시보드에서 이를 안내합니다. 웹 UI도 이를 뒤따릅니다: 사이드바 푸터의 새 링크가 선언문 페이지를 열고(16개 로케일 전체에 새 i18n 키footer.manifesto추가), 인앱 도움말 가이드는 16개 언어 전체에서 §29 «CareerOps 선언문»이 추가되었으며, README는 선언문이 무엇이고 어떻게 서명하는지 설명하고, cvstart.org 랜딩 푸터도 이를 링크합니다.
비고
- 부모 v1.20.0은
upskill타깃 모드의 기존 스킬 억제 문제도 수정했고,scan --json의 표준 출력이 계속 파싱 가능하도록 dotenv를 조용히 처리했으며, 역할 제목이 항목과 분리되지 않도록 HTML CV 템플릿을 수정했습니다 — 모두 웹 UI가 셸을 통해 호출하지 않는 CLI 측 동작이므로, 웹 UI 코드 변경은 필요하지 않았습니다.
[1.119.5] — 2026-07-13
수정
- 랜딩의 언어 버튼이 더 이상 줄바꿈되지 않습니다 — v1.119.2의 국기 추가로 헤더 전환기 레이블(예: «🇷🇺 Русский»)이 좁은 데스크톱 폭에서 최대 세 줄로 깨질 수 있었습니다. 전환기 레이블과 드롭다운의 모든 옵션에
whitespace-nowrap을 적용해 국기 + 언어명이 항상 한 줄에 유지됩니다. 푸터의 언어 목록은 고정 2열 그리드에서 한 줄 항목이 감싸며 배치되는 행으로 바뀌어 «🇧🇷 Português (Brasil)»도 더 이상 이름 중간에서 갈라지지 않습니다.
[1.119.4] — 2026-07-13
변경
- LICENSE에 작성자 명시 — 저작권 줄이 이제 Sergei Emelianov (Fighter90) https://sergey-cv.com and career-ops-ui contributors입니다(MIT 정식 본문은 그대로). 새 NOTICE.md가 라이선스를 상세히 설명합니다: 저작권 보유자, MIT 허가가 정확히 무엇을 포함하는지(코드, 문서, 번역, 랜딩, 위키), 무엇을 포함하지 않는지(런타임의 사용자 데이터, 부모 프로젝트, 채용 보드 콘텐츠, 상표), 서드파티 구성 요소 표(express/js-yaml — MIT; Astro/Tailwind — MIT; Figtree·JetBrains Mono 글꼴 — SIL OFL 1.1; sharp — Apache-2.0), 선택적 저작자 표시 문구.
[1.119.3] — 2026-07-13
추가
- SECURITY.md — CONTRIBUTING이 가리키던 보안 정책이 이제 실제로 존재합니다: 지원 버전, 비공개 신고 절차(저장소에 GitHub private vulnerability reporting 활성화 — Security 탭 → «Report a vulnerability»), localhost 바인딩 단일 사용자 앱의 위협 모델(범위 내: 적대적 공고를 통한 XSS / SSRF / 경로 순회 / 시크릿 유출 / CSP 약화; 범위 외: 자신의 localhost에 대한 DoS 및 부모 프로젝트 문제), 리뷰어를 위한 하드닝 기준선.
[1.119.2] — 2026-07-13
추가
- CONTRIBUTING.md — 랜딩과 README가 줄곧 링크해 온 기여자 가이드가 이제 실제로 존재합니다: 설치, 프로젝트 맵, 엄격한 보안/노빌드 규칙, 테스트 계층, 스캔 소스 추가를 위한 «두 레지스트리» 워크스루, ×16 i18n 계약, 커밋/PR 규약, 릴리스 프로세스.
- 랜딩의 언어 국기 — cvstart.org의 언어 전환기, 푸터의 언어 그리드, «당신의 언어로 읽기» 배너가 이제 각 로케일의 국기를 엔도님 옆에 표시합니다(앱의 언어
<select>와 같은 지역 지시자 세트; 국기 글리프가 없는 곳에서는 지역 문자로 대체). - 랜딩 푸터 수정 — 저장소에서 기능이 비활성화되어 죽어 있던 Discussions 링크가 이제 프로젝트 위키를 가리키며, 푸터에 작성자 크레딧이 추가되었습니다: Sergei Emelianov(sergey-cv.com · LinkedIn).
[1.119.1] — 2026-07-13
수정
#/scan소스 필터가 레지스트리를 따라잡았습니다 — Source 드롭다운 뒤의 정적FALLBACK_SOURCES목록(GET /api/scan/sources에 접근할 수 없을 때만 사용)이 v1.87.0 이후 조용히 뒤처져 있었습니다: 오프라인 폴백에 20개 프로바이더(Amazon, Avature, SAP SuccessFactors, Get on Board, Dassault Systèmes, beesite, HigherEdJobs, JibeApply (iCIMS), softgarden, Cornerstone, Phenom, Radancy, Deutsche Bahn, EchoJobs, TKMS, Heckler & Koch, Rheinmetall, LaraJobs, 그리고 새 Meituan / Tencent)가 빠져 있었습니다. 전체 61개와 동기화했고, 클라이언트 목록이 서버 레지스트리와 어긋나면 CI를 실패시키는 드리프트 테스트(값과 레이블 모두)로 보호됩니다. +1 테스트(1845).
[1.119.0] — 2026-07-13
부모 career-ops v1.19.0 패리티 + cvstart.org 랜딩 새 단장.
추가
- 2개의 새 스캔 프로바이더 — Meituan(
zhaopin.meituan.com)과 Tencent(careers.tencent.com): 중국 테크 보드의 인증 불필요 공개 JSON API. 호스트로 자동 감지되거나 명시적provider:로 선택되며, 키워드별 서버 사이드 검색·페이지네이션·URL 중복 제거를 지원 — 이제 61개 어댑터(영문 56 + 러시아어 5). +20개 테스트(1844). - 랜딩의 기여자 블록 — cvstart.org가 코드를 기여한 모든 사람의 아바타를 표시합니다(빌드 시 GitHub
/contributorsAPI, 봇 필터링). 16개 언어 모두 현지화되었고 전체 기여자 그래프 링크가 포함됩니다. - 랜딩의 실시간 GitHub 스타 카운터 — 헤더 배지가 이제 방문할 때마다 클라이언트에서 GitHub API로 갱신됩니다(빌드 시 스냅샷은 폴백). 주간 예약 Pages 재빌드가 스냅샷과 기여자 목록을 신선하게 유지하며, CI의 API 호출은 토큰 인증을 사용합니다.
수정
- Workday CXS 요청에 브라우저형 헤더 적용(부모 #1813) — Cloudflare 뒤의 테넌트(실사례: geico)는 일반적인 UA/
accept-language/origin/referer가 없는 요청에 500을 반환합니다. 페처가 이제 CXS URL 자체에서 origin과 사이트 슬러그를 도출합니다. Glints 요청에도 같은 브라우저 UA + origin/referer가 추가되었으며, 둘 다http-json.mjs의 공유 상수BROWSER_LIKE_USER_AGENT에서 가져옵니다.
[1.118.4] — 2026-07-10
수정
- 러시아 IP에서 hh.ru 스캔이 0건을 반환하던 문제(지역 서브도메인 링크) — 러시아 가정용 IP에서 hh.ru는 검색을 지역 서브도메인(
sochi.hh.ru,spb.hh.ru등)으로 302 리디렉션하고 채용 링크를 해당 서브도메인으로 반환합니다. 파서는 제목 링크를 고정 호스트https://hh.ru/vacancy/에서 찾았고 지역 링크와는 하나도 일치하지 않아, 완전히 작동하는 스캔이 조용히 0건을 기록했습니다. 이제 임의의*.hh.ru호스트를 허용하고(adsrv.hh.ru/click?…광고는 계속 제외 —/vacancy/<id>경로가 없음), 각 결과 URL을https://hh.ru/vacancy/<id>로 정규화합니다. 실시간 검증: 이전에 0건이던sochi.hh.ru페이지에서 17개의 실제 채용이 파싱됩니다. +1 테스트(1824).
[1.118.3] — 2026-07-10
수정
- hh.ru가 오류 없이 0건을 반환하던 문제(VPN 확인 중간 페이지) — hh.ru는 이제 VPN/프록시로 판단한 네트워크(데이터센터 IP)를
/vpncheeck중간 페이지(“VPN мешает работе сайта”)로 302 리디렉션하며, 이 페이지는 채용 카드가 하나도 없는 HTTP 200으로 응답하므로 스캔이 아무 오류 없이 0건을 보고했습니다. 스캐너는 이제 응답의 최종 URL로 리디렉션을 감지하고, 남은 실행 동안 hh.ru를 비활성화하며, 정직한 힌트를 출력합니다: 트래픽이 실제로 가정용 IP로 나가야 합니다 — 브라우저 토글을 꺼도 시스템 전역 VPN/프록시는 계속 켜져 있을 수 있습니다. +1 테스트(1823).
[1.118.2] — 2026-07-10
유지보수
- 랜딩 후속 작업(#118) —
site/README.md를 Astro 7(#116의 보안 업그레이드)에 맞게 정리하고, 미사용 import를 제거했으며, 랜딩 빌드 스크립트용 실행 가능한 가드 4개 추가: i18n 패리티 게이트가 깨진 사전에서 실제로 실패함을 증명하고sync-assets는site/밖에 절대 쓰지 않습니다 — 스위트 1822. CodeQL 경고 2건 해결(1건 소스 수정, 1건 의도된 빌드 동작으로 기각).
[1.118.1] — 2026-07-10
수정
- 러시아 밖에서의 hh.ru 스캔 — hh.ru가 이제 공개 검색 페이지에서 러시아 외 IP에 HTTP 451(지역 법적 차단)을 반환합니다. 스캐너는 451을 403과 동일하게 처리합니다: 첫 차단 후 남은 실행 동안 hh.ru를 비활성화하고 러시아 IP/VPN 출구를 안내하는 정직한 로그를 남겨, 남은 쿼리와 다른 RU 소스를 낭비하지 않습니다. 도움말 §7을 16개 언어로 갱신. 테스트 +1 (1818).
[1.118.0] — 2026-07-09
부모 career-ops v1.18.0 패리티 팩.
추가
- 9개의 새 스캔 프로바이더 — Cornerstone OnDemand (
csod), Phenom (phenom), Radancy (radancy), Deutsche Bahn (deutschebahn), EchoJobs (echojobs), TKMS (tkms), Heckler & Koch (hecklerkoch), Rheinmetall (rheinmetall), LaraJobs (larajobs) — 이제 어댑터 54개. Lever 어댑터는 EU 테넌시 보드(jobs.eu.lever.co)도 감지합니다. - 트래커
Hired상태 (부모states.yml패리티): 수락한 오퍼는 고유한 정식 상태, 축하 배지,#/tracker의 취업 성공 배너를 갖습니다. 퍼널과 전환 차트는 모든 단계를 통과한 것으로 계산합니다. #/stats의 누적 통계 탭 — 부모의stats.mjs(누적 트래커 집계, 누적 퍼널 비율, 스캐너 합계, 포털 커버리지)와salary-gap.mjs의 보상 관찰(희망 vs 공고 제시 vs 실제, 지원 건별)을 읽기 전용으로 중계합니다. 새 라우트GET /api/stats/lifetime및GET /api/stats/salary-gap— 제로 토큰 셸아웃, 부모 프로젝트가 없으면{available:false}로 안전하게 동작합니다.- 16개 로케일 전체에 28개의 새 i18n 키; 도움말 가이드 §14/§26을 모든 언어로 갱신.
테스트
- 유닛 테스트 +38개(프로바이더 패리티 스위트 3개 + 릴레이/상태 라우트) — 총 1817개.
[1.117.2] — 2026-07-06
패리티 셸-아웃의 빈 트래커 수정. 트래커에 아직 지원이 없으면 부모 스크립트가 코드 1과 구조화된 {error} JSON으로 종료합니다. 팔로업 보드와 거절 패턴 탭이 이를 “script-error”로 표시했습니다. 이제 두 라우트 모두 이를 건강한 빈 상태(available:true, empty:true)로 전달해 UI가 정직한 “아직 없음” 메시지를 보여줍니다. 실제 부모로 라이브 검증됨.
신규: 없음.
[1.117.1] — 2026-07-06
v1.117.0 하드닝 (CodeQL 분류). 세 개의 셸-아웃 엔드포인트(GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns)에 공유 per-IP 리미터를 적용했습니다(요청마다 자식 프로세스 생성; 루프백에서는 no-op). CV에 추가의 URL 텍스트 추출은 태그를 고정점까지 제거한 뒤 남은 </>를 모두 삭제합니다 — LLM 프롬프트 텍스트에 대한 증명 가능한 완전 정화. 유효 입력의 동작 변화 없음.
신규: 없음.
[1.117.0] — 2026-07-06
부모 패리티 팩 — 부모 career-ops의 여섯 가지 기능을 UI로. (1) #/followup의 팔로업 케이던스 보드: followup-cadence.mjs의 지원별 긴급도(🔴/🟠/🟡/🔵) + 팔로업 날짜 시드 버튼(followup-seed.mjs --backfill). (2) 거절 패턴: 통계의 네 번째 탭이 analyze-patterns.mjs(읽기 전용)를 실행 — 결과 분포, 추천, ATS 벤더별 진전율. (3) CV에 추가: CV Studio 카드가 URL 또는 붙여넣은 텍스트를 그 출처에만 근거한 ATS 불릿으로 변환(제안만, 쓰기 없음; URL 페치는 SSRF 보호). (4) 새 스캔 프로바이더 4개 — beesite, HigherEdJobs(RSS), JibeApply(iCIMS), softgarden — 레지스트리는 이제 50개 어댑터(45 EN + 5 RU), 모두 Scan 드롭다운에 표시. (5) Apply 체크리스트에 탈락 요건 사전 스캔 단계. (6) reconcile 러너(/api/run/reconcile). 셸-아웃 라우트는 부모 스크립트가 없으면 정직하게 “사용 불가”를 표시.
- 새 라우트 모듈
server/lib/routes/followup.mjs(31번째) + 새 라우트 + 소스/어댑터 8파일. 테스트: 6 + 7 추가; 스위트 1737 → 1750. 새 i18n 키 41개 ×16. 도움말 §13/§17/§24/§26 ×16 확장.
신규: GET /api/followup, POST /api/followup/seed, GET /api/stats/patterns, POST /api/cv-studio/add-entry, POST /api/run/reconcile.
[1.116.0] — 2026-07-06
사용량 미터 재작업 + 첫 엔드투엔드 위젯 테스트. AI 사용량 미터(v1.114.0)를 제대로 고정했습니다: 이제 왼쪽 사이드바 하단에 고정되고(사이드바 전체 너비, 동일한 표면) 자신의 높이만큼 사이드바 하단에 여백을 두어 메뉴가 절대 가려지지 않습니다 — 내비게이션과 버전 푸터는 항상 그 위로 스크롤됩니다. 실시간으로 갱신되며(15초 간격 + 탭 포커스 + 라우트 변경), 각 창 행은 항상 100%인 “비율” 대신 실제 <토큰> · <예상 비용>(막대는 30일 창 기준)을 표시합니다. 또한 CV 임포터의 지속적인 typeof 방벽이 반복되는 CodeQL 타입 혼동 오탐을 근본에서 닫고, 새 Playwright 엔드투엔드 테스트가 두 상주 위젯을 실제 브라우저에서 검증합니다.
public/js/lib/usage-hud.js+app.css,server/lib/cv-import.mjs. 테스트:tests/playwright-widgets.mjs(2 E2E) +tests/usage-hud.test.mjs(10). 도움말 §6 ×16 확장.
신규: 없음.
[1.115.0] — 2026-07-06
디자인 다듬기(보수적, 코럴 브랜드 유지). 공유 디자인 시스템에 대한 가벼운 정제 패스 — 구조 변경 없음, 팔레트 변경 없음. 대시보드 지표 카드가 이제 호버 시 살짝 떠오르며 코럴 테두리를 띱니다(빠른 작업 타일과 동일). 콘텐츠 카드도 약간 떠오릅니다. primary / dark / danger 버튼은 정지 그림자와 부드러운 호버 리프트로 입체감을 얻습니다. 큰 숫자는 tabular-nums로 정렬됩니다. 상호작용 컨트롤은 2px 키보드 링 뒤에 부드러운 코럴 포커스 헤일로를 받습니다. 모든 모션은 prefers-reduced-motion을 존중하며, 헤일로는 컨트롤로 한정됩니다 — 전역 *:focus-visible은 아닙니다.
- CSS 전용(
public/css/app.css); 마크업·i18n·라우트·CSP 변경 없음. 테스트:tests/design-polish-v1115.test.mjs(5). Playwright로 라이브 검증.
신규: 없음.
[1.114.0] — 2026-07-06
사이드바의 AI 사용량 및 비용 미터(왼쪽 아래). 컴팩트한 사용량 섹션이 이제 모든 페이지의 사이드바 하단에 표시됩니다(사이드바가 없으면 왼쪽 아래 고정 카드, RTL에서는 오른쪽 아래). 24시간 / 7일 / 30일 창별 LLM 토큰 사용량을 <토큰> · <비율%>로(전체 기간 대비 비율) 녹색 미터 바와 함께 보여주고, 24시간 예상 비용 푸터를 더합니다. 데이터는 data/llm-usage.jsonl의 읽기 전용 GET /api/usage 집계(로컬 전용)로 #/usage 페이지와 동일한 소스입니다. 비용은 추정치이며 수동 모드 실행은 무료이고 집계되지 않습니다. 접을 수 있음 — 헤더로 토글하며 상태가 유지됩니다.
- 새 클라이언트 위젯
public/js/lib/usage-hud.js를index.html에서 로드, 버전 푸터 위 사이드바에 마운트(고정 코너 폴백). CSP 안전; 테마 대응 + RTL 미러. 새 서버 라우트 없음. 테스트:tests/usage-hud.test.mjs(8). 새 i18n 키 3개 ×16.
신규: 없음.
[1.113.0] — 2026-07-06
모든 페이지에 떠 있는 “도움말에 질문하기” 어시스턴트. 그라데이션 로봇 채팅 버튼이 이제 모든 페이지의 오른쪽 아래 모서리(RTL에서는 왼쪽 아래)에 떠 있습니다. 클릭하면 당신의 언어로 된 인앱 도움말 가이드에서만 근거를 두고 사용법 질문에 답하는 소형 채팅이 열립니다 — #/docs-assistant 페이지와 동일한 엔드포인트(POST /api/docs-assistant/ask)이므로 CV·프로필·트래커를 읽지 않습니다. LLM 키가 있으면 실시간, 없으면 바로 실행 가능한 프롬프트. 헤더에는 로봇 아바타 + 온라인 상태; 시작 칩으로 자주 묻는 질문을 채우고; Esc 또는 바깥 클릭으로 닫히며; #/docs-assistant 페이지에서는 숨겨집니다.
- 새 클라이언트 위젯
public/js/lib/docs-fab.js를index.html에서 전역 마운트; CSP 안전;app.css에 테마 대응 + RTL 미러 스타일. 새 서버 라우트 없음. 테스트:tests/docs-fab.test.mjs(8). 새 i18n 키 6개 ×16. 도움말 §1 현 위치 확장.
신규: 없음.
[1.112.0] — 2026-07-06
문서 및 QA 통합. 사용자 표시 코드 변경 없음. SDD 규약 문서(docs/sdd/CONVENTIONS.md)를 현재 30개 라우트 모듈(이전 24개)과 현재 테스트 기준선으로 갱신했고, 전체 프로젝트 확정 QA 프롬프트(qa/QA-REGRESSION-PROMPT.md)를 통합했습니다 — 릴리스 절차 최신화(v1.111, parentVersion 1.17.0, 릴리스 트리거 배포), §14 추가 표 수정(Scan 제외를 v1.109.0으로 재라벨) 및 v1.111 CodeQL 정리 추가 — 그래서 모든 기능에 대한 단일 회귀 프롬프트로 단독 성립합니다. 초과 업로드 분기에 대한 커버리지 테스트 1개 추가.
신규: 없음.
[1.111.0] — 2026-07-06
보안 — CodeQL 백로그 정리. 남은 정적 분석 지적을 무시(dismiss)하는 대신 근본 원인에서 닫는 심층 방어 강화 3건. stripDangerousMarkdown는 이제 잘린 위험 태그 여는 부분(<script/<iframe/… 로 끝나는 페이로드)의 <를 이스케이프하여, 출력에 살아 있는 위험 태그가 존재할 수 없음을 보장합니다. CV 가져오기는 업로드된 버퍼 크기를 명시적 Number() 강제 변환으로 읽습니다 — 타입 혼동 방벽. 모드 역할 줄은 저장된 함수 대신 String.replace로 보간되는 템플릿 문자열이 되어 동적 디스패치 호출을 완전히 제거했습니다. 사용자 표시 동작 변화 없음.
server/lib/security.mjs,server/lib/cv-import.mjs,server/lib/prompts.mjs. 테스트:tests/security-hardening-v1111.test.mjs(7) + v1108 가드 테스트 갱신. i18n/도움말/라우트 변경 없음.
신규: 없음.
[1.110.0] — 2026-07-06
문서 및 QA 갱신(모든 언어). 코드 변경 없음. 전체 프로젝트 QA 프롬프트가 v1.109.0으로 갱신되며 v1.98→v1.109을 다루는 새 §14가 추가되고, UX 감사 및 디자인 내보내기 프롬프트가 현재 화면 목록을 반영합니다. v1.100–v1.109에서 추가된 모든 도움말 단락이 이제 16개 언어 전부로 번역되었습니다.
신규: 없음.
[1.109.0] — 2026-07-06
Scan 제외 필터 + 파이프라인 개요(웹 레이아웃 패리티). #/scan에서 검색 상자가 이제 쉼표를 OR로 취급하고(“찾을 역할”), 새 제외 필드는 회사/역할/위치에 쉼표로 구분된 단어(예: senior, staff)가 포함된 행을 숨깁니다. 둘 다 저장된 검색에 기억됩니다. #/pipeline에는 컴팩트 개요 스트립이 파이프라인을 한눈에 보여줍니다 — 받은함 N, 추적 N, 트래커의 Applied / Responded / Interview / Offer 수, 각 칩은 #/tracker로 연결됩니다.
- 클라이언트 전용(새 라우트/쓰기 없음).
public/js/views/scan.js+public/js/views/pipeline.js. 테스트:tests/scan-pipeline-ui-v1109.test.mjs(2). 새 i18n 키 4개 ×16. 도움말 §7 + §8 현 위치 확장.
신규: 없음.
[1.108.0] — 2026-07-06
보안 강화(CodeQL 분류 2차). 경미한 문제 세 건 추가 수정: 모드 프롬프트 빌더가 로케일 역할 줄을 **자체 키 + typeof === function**으로 해석해 조작된 로케일이 프로토타입 메서드로 디스패치하지 못하게 하고(unvalidated-dynamic-method-call); PDF 파일명 슬러그를 정규식 이전에 200자로 제한해 전부-하이픈 입력이 백트래킹하지 못하게 하며(다항 ReDoS); 문서 가져오기가 배열 filename(반복 헤더)을 문자열로 강제 변환합니다(type-confusion). 유효 입력에 대한 동작 변화 없음.
server/lib/prompts.mjs,server/lib/routes/runners.mjs,server/lib/cv-import.mjs+tests/security-hardening-v1108.test.mjs(3). v1.106–v1.108에 걸쳐 정적 분석 백로그가 167에서 ~14로 줄었고, 실제 보안 관련 문제는 모두 수정, 나머지(가드/새니타이즈된 오탐 + 노트 수준 린트)는 근거와 함께 기각.
신규: 없음.
[1.107.0] — 2026-07-06
새니타이저 강화(저장 시 XSS 심층 방어). stripDangerousMarkdown — 저장된 이력서/공고 마크다운의 위험한 HTML을 무력화해 렌더 시 이스케이프하는 클라이언트를 우회하는 소비자도 안전하게 함 — 이 이제 태그 제거를 고정점까지(변화가 없을 때까지 반복) 실행해 제거가 페이로드를 재구성하는 경우(예: <scr<script></script>ipt>)를 잡아내고, 뒤에 쓰레기가 붙은 script/style 등의 종료 태그(</script foo>)와 닫히지 않은 실행 오프너(<script …>)를 제거합니다. 유효한 마크다운의 동작은 변하지 않고 — 더 많이 제거할 뿐입니다.
server/lib/security.mjs: 고정점 루프(8회 제한) +[^>]*>종료 태그 패턴 + 미닫힘 오프너 제거.tests/cv-xss-bypasses.test.mjs에 회귀 케이스 +3. 권위 있는 XSS 경계는 여전히 출력 이스케이프(UI.md)이며, 이는 저장 보증을 강화하고 해당 CodeQL 발견을 해소합니다.
신규: 없음.
[1.106.0] — 2026-07-06
보안 강화(CodeQL 분류). 정적 분석 백로그를 훑어 실제(경미하지만) 문제 세 건을 수정했습니다: 라우트 렌더 오류 경로가 이제 오류 메시지를 DOM에 도달하기 전에 이스케이프하며(서버 오류가 사용자 입력을 반영할 수 있으므로 신뢰할 수 없는 것으로 취급 — XSS 경계), 프로필/설정의 속성 쓰기가 __proto__ / constructor / prototype 키를 거부합니다(만약을 위한 프로토타입 오염 방지 — 키는 고정 필드 스펙에서 오며 원시 요청 입력이 아님). 나머지 경고의 대부분은 스캐너의 정당한 data/* 읽기/쓰기와 이미 자체 제한기를 가진 라우트에 대한 오탐이며 근거와 함께 기각했습니다.
public/js/router.js는innerHTML이전에UI.escapeHtml로err.message를 이스케이프;server/lib/routes/content.mjs와server/lib/routes/config.mjs는 프로토타입 키를 방어합니다. 유효 입력에 대한 동작 변화 없음. 테스트:tests/security-hardening-v1106.test.mjs(3). 새 i18n 키 없음.
신규: 없음.
[1.105.0] — 2026-07-06
AI 사용량 및 비용 페이지. 새 AI 사용량 페이지(사이드바, 상태 옆)가 실시간 AI 생성 — 평가·리포트·채팅 — 에 쓴 토큰을 최근 24시간·7일·30일·전체 기간에 걸쳐 제공자별로 보여주며 예상 USD 비용을 함께 표시합니다. 모든 실시간 호출은 작은 {provider, in, out} 레코드를 data/llm-usage.jsonl에 추가합니다(어디로도 전송되지 않음). 키 없는 실행(수동 모드)은 비용이 없고 기록되지 않습니다.
- 새 라우트 모듈(30번째)
server/lib/routes/usage.mjs—GET /api/usage(읽기 전용 집계) +server/lib/llm-usage.mjs(recordUsage가 Anthropic/OpenAI/Gemini 사용량 형태를 정규화해 best-effort로 추가;readUsage/aggregate가 24h/7d/30d/전체 창 × 제공자로 집계) +server/lib/llm-pricing.mjs(편집 가능한 제공자별$/1M토큰 가격표 — 토큰은 정확하고 달러는 대략적 정가로 요금제에 맞게 수정 가능; 청구 안 됨). 기록은 디스패치 지점(runActiveProvider+routes/llm.mjs)에 연결됩니다. - 새 뷰
public/js/views/usage.js(#/usage, 기간 탭). 테스트:tests/usage-routes.test.mjs. 새 i18n 키 17개 ×16(usage.*+nav.usage). 도움말 §6 현 위치 확장.
신규: server/lib/routes/usage.mjs; server/lib/llm-usage.mjs; server/lib/llm-pricing.mjs; public/js/views/usage.js.
[1.104.0] — 2026-07-06
스캔 표의 회사 로고(개인정보 보호형). 앱 설정의 새 모양 토글 — 스캔 표에 회사 로고 표시(기본 꺼짐) — 이 #/scan에서 회사 이름 옆에 로고를 그립니다. 로고는 회사 자체 도메인에서 가져온 파비콘이며 서버에서 프록시됩니다(GET /api/logo). 따라서 어떤 제3자 로고 서비스도 당신이 어떤 고용주를 보는지 알지 못합니다. 공용 채용 보드(Greenhouse, Lever, Ashby 등)의 공고는 보드 아이콘 대신 색상 글자 배지를 표시하고, 로드에 실패한 로고도 같은 배지로 대체됩니다.
- 새 라우트 모듈(29번째)
server/lib/routes/logos.mjs—GET /api/logo?domain=. 도메인을 검증하고(스킴/경로/루프백 없음) SSRF 안전safeGet(새binary모드가 원시 바이트 + content-type 반환; DNS 고정·리다이렉트 검증·크기 제한 불변)로/favicon.ico를 가져오며, HTML 오류 페이지를 이미지로 제공하지 않도록 이미지 매직 스니핑을 하고, 적중 및 실패를 메모리 LRU에 캐시하며, 디스크에 아무것도 쓰지 않습니다. - 새 클라이언트 라이브러리
public/js/lib/company-logo.js(window.CompanyLogo): localStorage 플래그로 기본 꺼짐; 공용 ATS 호스트는 건너뛰고 결정적 글자 아바타 사용; CSP 안전img.onerror대체. 테스트:tests/logo-routes.test.mjs. 새 i18n 키 5개 ×16(appear.*). 도움말 §2 현 위치 확장.
신규: server/lib/routes/logos.mjs; public/js/lib/company-logo.js.
[1.103.0] — 2026-07-06
설정: “AI CLI 도구” — 무엇이 설치되어 있는지. career-ops는 Claude Code 기반이지만 개방형 스킬 표준의 모든 에이전트 CLI와 작동합니다. 앱 설정(#/config)의 새 AI CLI 도구 탭은 Claude Code, Codex, Gemini CLI, OpenCode, GitHub Copilot CLI, Qwen, Antigravity 중 서버를 실행하는 머신에 무엇이 설치되어 있고 경로가 어디인지 보여줍니다. 이는 읽기 전용 PATH 스캔입니다: 각 바이너리 존재 여부만 확인하고 절대 실행하지 않으며(--version 없음, 실행 없음), 아무것도 쓰지 않고 사용자 데이터를 건드리지 않습니다.
- 새 라우트 모듈(28번째)
server/lib/routes/cli-detect.mjs—GET /api/cli-detect. 감지는 고정된 7개 허용목록에서process.env.PATH를 통해 바이너리 경로를 확인합니다(Windows.cmd/.exe/.bat심; POSIX 실행 비트). PATH의 악성 파일은 이 라우트로 절대 실행될 수 없습니다. public/js/views/config.js의 새 “AI CLI 도구” 탭(지연 로드,#/config?tab=cli로 딥링크). 테스트:tests/cli-detect-routes.test.mjs. 새 i18n 키 8개 ×16(cli.*+config.tabCli). 도움말 §2 현 위치 확장.
신규: server/lib/routes/cli-detect.mjs.
[1.102.0] — 2026-07-05
“문서에 질문” — 앱 내 도움말 가이드에 근거한 채팅. 새 문서에 질문 💬 페이지(사이드바, 도움말 아래): “채용 포털은 어떻게 스캔하나요?” 같은 질문을 입력하면 당신의 언어로 된 앱 도움말 가이드에서만 답을 얻습니다 — 사용한 섹션을 보여주고 당신의 이력서·프로필·구직 활동은 절대 읽지 않습니다. 당신이 아니라 앱 사용법에 관한 것입니다. LLM 키가 있으면 실시간 답변, 없으면 관련 도움말 섹션이 이미 채워진 실행 가능한 프롬프트를 제공합니다.
- 새 라우트 모듈(27번째)
server/lib/routes/docs-assistant.mjs—POST /api/docs-assistant/ask. 의존성 없는 검색: 당신 언어의 도움말 문서를##섹션으로 나누고 질문과의 키워드 겹침으로 점수화 — 상위 섹션을 인라인하고 모델은 그로부터 답하거나 가이드가 다루지 않는다고 말합니다(기능/라우트 지어내기 없음). 공유 제공자 캐스케이드, 수동 대체, 속도 제한, 쓰기 없음, 사용자 데이터 읽지 않음. - 새 뷰
public/js/views/docs-assistant.js. 테스트:tests/docs-assistant-routes.test.mjs. 새 i18n 키 14개 ×16 (docs.*+nav.docsAssistant). 도움말 §1 현 위치 확장.
신규: server/lib/routes/docs-assistant.mjs; public/js/views/docs-assistant.js.
[1.101.0] — 2026-07-05
CV Studio: 특정 채용공고에 맞춰 이력서를 조정하고 커버레터를 작성 — 리크루터 체크리스트로 게이트. #/cv-studio에 새 채용공고에 맞춤 조정 카드: 채용공고(선택적으로 목표 직무/헤드라인)를 붙여넣으면 CV Studio가 그 공고에 맞춘 이력서와 어울리는 커버레터를 만들고, 넘겨주기 전에 둘 다 체크리스트 게이트에 통과시킵니다 — error는 차단(결과를 보기 전에 수정됨), warn은 권고. 이 메커니즘은 커리어 코칭 실무를 일반 규칙으로 증류한 것입니다 — 리크루터는 몇 초 안에 읽으므로 관련 경력은 위로, 헤드라인은 공고의 역할과 일치, 성과에는 구체적 수치, 커버레터는 “요구사항 ↔ 당신의 부합 사실” 하나의 브릿지를 담은 짧은 티저. 오직 당신의 이력서·프로필·투페이저에만 근거하며 절대 지어내지 않습니다 — 회사·역할·이력을 하드코딩하지 않습니다.
- 새 엔드포인트
POST /api/cv-studio/tailor(기존 cv-studio 모듈 확장 — 27번째 모듈 아님):buildTailorPrompt+ 일반TAILOR_INSTRUCTIONS게이트,bundleProjectContext기반, 공유 제공자 캐스케이드, 키 없으면 수동 프롬프트, 속도 제한, 쓰기 없음. 결과는 공유report-export.js바로 Markdown / PDF / DOCX 내보내기. - 테스트:
tests/cv-studio-routes.test.mjs에 +3. 새 i18n 키 10개 ×16(cvs.tailor*). 일반 참조docs/prompts/resume-cover.md. 도움말 §24 현 위치 확장.
신규: docs/prompts/resume-cover.md.
[1.100.0] — 2026-07-05
투페이저: 이력서 기반 AI 자동 채우기 + 미리보기 + PDF/DOCX/Markdown 내보내기. 투페이저(#/two-pager)는 다음 역할에서 진짜로 원하는 것을 담지만, 지금까지는 각 필드를 손으로 작성하거나 프롬프트를 다른 도구에 복사해야 했습니다. 이제 ✨ AI 채우기 도우미가 설정된 제공자로 실시간 실행됩니다 — 오직 이력서 + 프로필만 읽어(bundleProjectContext, 무엇도 지어내지 않음) 모든 필드(나는 누구 / 좋아하는 것 / 필수 조건 / 싫어하는 것 / 절대 불가 / 타협 불가 / 목표 환경)를 작성하고 폼을 채워 검토·수정·저장하도록 합니다. API 키가 없으면 예전처럼 프롬프트 복사 모달로 대체됩니다. 새 👁 미리보기 및 내보내기 버튼은 투페이저를 서식 있는 문서로 렌더링하고 .md 다운로드 / PDF로 저장 / DOCX로 저장 / 복사 바를 제공합니다.
- 의존성 없는
.docx내보내기. 새server/lib/docx.mjs가 최소하지만 유효한 Office Open XML.docx(네 개의 OOXML 파트를 DEFLATE ZIP으로, 항목별 CRC-32)를 생성합니다 — 새 런타임 의존성 없음(deps는express+js-yaml유지). 새 라우트POST /api/export/docx(server/lib/routes/export.mjs, 26번째 라우트 모듈; 무상태, 200 KB 제한, 쓰기·LLM·URL fetch 없음). 공유public/js/lib/report-export.js에 연결되어 시장 리포트, 커리어 플랜, 커리어 오리엔테이션 리포트에도 DOCX 내보내기가 추가됩니다. - 실시간 자동 채우기는 공유 제공자 캐스케이드(
runActiveProvider/providerAvailable)를 사용하며, 반환된 YAML은 파싱되어 제한된 투페이저 형태(parseYamlFields+normalizeTwoPager)로 강제 변환됩니다 — 알 수 없는 키 제거, 배열/문자열 제한. 수동 모드 유지. - 테스트:
tests/export-routes.test.mjs. 새 i18n 키 4개 ×16 (export.saveDocx,twoPager.preview,twoPager.aiFilling,twoPager.aiFilled).
신규: server/lib/docx.mjs; server/lib/routes/export.mjs.
[1.99.0] — 2026-07-05
포털 상태 페이지 (#/portals). 스캐너는 portals.yml의 회사 집합을 지켜봅니다. ATS 슬러그가 조용히 깨지면 그 고용주는 이후 모든 스캔에서 소리 없이 사라집니다. 새 Portals 페이지는 감시 중인 모든 회사를 나열하고, Check portal health를 누르면 DNS 고정 safeGet(SSRF 안전)로 각 careers_url을 탐침해 죽은 것(404 = 조용히 제외)을 표시합니다 — 읽기 전용. 또한 리뷰에 따라 v1.98.0 버그 리포터를 강화했습니다: 오류 링 버퍼가 이제 네트워크 fetch 실패를 포착하고, 스크러버가 라벨 없는 공급자 키를 가립니다.
신규: server/lib/routes/portals.mjs; public/js/views/portals.js.
[1.98.0] — 2026-07-05
인앱 버그 리포터 (상위 프로젝트 web-v0.2.0 웹과의 패리티). 알림 서랍의 🐞 Report a bug 버튼이 개인정보 하한선을 지키는 진단 스냅샷을 수집합니다 — 버전, 현재 화면, 브라우저, /api/health 점검 요약, 그리고 새 클라이언트 측 링 버퍼에서 가져온 최근 20개 오류 — 여기에 결정적 중복 제거 지문(co-web-<base36>)을 더하고, 정확한 Markdown을 검토하게 한 뒤 미리 채워진 GitHub 이슈를 엽니다. 자동으로 제출되는 것은 없으며, CV·프로필·답변·채용 URL·키를 절대 담지 않습니다. 새 라이브러리 logbuf.js + bug-report.js; 11개 i18n 키 ×16; tests/bug-report.test.mjs.
신규: public/js/lib/logbuf.js; public/js/lib/bug-report.js.
[1.97.1] — 2026-07-05
리뷰 기반 강화 및 문서 패리티(v1.97.0 후속). AI 리뷰 로그를 훑어보니 실제 수정 사항이 드러났습니다:
fit-score.js(스캔◎적합도 배지).salaryFloor()는 더 이상 연 단위 미만의 급여를 엉뚱한 연봉 하한으로 승격하지 않습니다 — “at least 500 EUR/day”, “$80/hr”, “6000 monthly” 는 이제 500k/80k 딜브레이커 대신null을 반환합니다. 국가 매칭이 이제 완전 단어 단위(\b…\b)여서 “Germany” 가 더 이상 형용사 “German”(또는 “Nigerian” 안의 “Nigeria”)에 매칭되어 잘못된 필수-조건-타지역 위반을 발생시키지 않습니다.tests/fit-score.test.mjs에 테스트 +3.- 문서 패리티. 이제 모든 현지화 README 가 16개 로케일 을 일관되게 표기합니다 — Help 행 개수/목록(×13)과 현지화 섹션 본문 + “모든 N개 파일에 키 추가” 노트(×8)가 여전히 v1.85 이전 개수(8/9)에 머물러 있었습니다. 인앱 도움말 §17 의 어댑터 개수는 16개 번들 전체에서 어댑터 46개 — 영어 41개 + 러시아어 5개 로 수정되었습니다.
적합도 배지 휴리스틱 외에 동작 변경 없음; 새 라우트, 키, i18n 추가 없음.
[1.97.0] — 2026-07-05
Dassault Systèmes 스캐너 소스 + 3방면 품질 정비.
- 새 스캔 소스 — Dassault Systèmes (상위 career-ops 패리티, #1498).
server/lib/sources/dassault.mjs+server/lib/portals/adapters/dassault.mjs는 상위 프로젝트의 제로 토큰 Exalead “카드 검색” 프로바이더(3ds.com/careers/jobs뒤의 공개 피드)를 그대로 옮깁니다. 단일 글로벌 엔드포인트이므로 프로바이더로 선택(provider: dassault)하거나3ds.com호스트에서 자동 감지되며,redirect:'error'와 함께www.3ds.com으로 SSRF 호스트 고정됩니다. XML 은 DOM 없이(<Hit>별<Meta>맵) 파싱되고, 도시/국가는 현지화된 카테고리 문자열에서 추출되며, 공고는 그 공개 URL 이*.3ds.com에 있을 때만 유지됩니다. 이제 레지스트리는 어댑터 46개(EN 41 + RU 5)를 제공합니다;ALL_ADAPTERS개수, 정렬된 id 및/api/scan/sourcesEN 집합 어서션이 40 → 41 로 상향되었습니다. 스위트tests/sources-dassault.test.mjs(10개 케이스). - 상위 프로젝트 견고성 수정 이식. Avature 파서는 이제 두 가지 라이브 테넌트 마크업 변형(위치 인덱스 접미사가 붙은
article--result+ 클래스 없는 JobDetail 제목 앵커, #1541)을 허용합니다; Get on Board 는0/음수published_at을 방어합니다(더 이상 잘못된 1970 날짜 없음); SuccessFactors 는 마지막 페이지를 상한 처리하여MAX_JOBS를 초과할 수 없게 합니다(#1528). - 서버 감사 수정.
safe-fetch는 더 이상 상한 초과 응답에서 멈추지 않습니다 — 크기 상한 경로가 이제 파괴된 스트림이 결코 발생시키지 않는'end'이벤트를 기다리는 대신 프로미스를 직접 해결합니다(대용량 페이지/api/pipeline/preview+ 자동 파이프라인 페치 수정). SSEstream.*활동 로깅이 다시 도달 가능합니다(/api/stream/검사가 포괄적인 “GET 건너뛰기” 가드 위로 이동됨). - SPA 감사 수정.
#/stats탭 스위처가 비동기 렌더 경쟁을 방어합니다 — 느린 탭의 결과가 사용자가 이미 전환한 더 새로운 탭을 더 이상 덮어쓸 수 없습니다. mock interview 와 네트워킹 삭제 확인 대화상자는 이제 적절한 제목 + 본문을 전달합니다(더 이상 본문이 빈 대화상자 없음). - 번역 수정. 번역되지 않은 사전 값 수정 — 우크라이나어
config.modes*(Adaptive Framing / Exit Narrative / Location Policy), 러시아어eval.jdLbl(“Job Description”), 이탈리아어dash.quick.contactoSub(“referral” → “segnalazione”) — 그리고 ru/uk/ja/ko/zh-CN/zh-TW CHANGELOG 에서 영어 16개 로케일 상용구를 현지화했습니다.
신규: server/lib/sources/dassault.mjs; server/lib/portals/adapters/dassault.mjs.
[1.96.0] — 2026-07-04
커리어 방향성 (Epic 27). 새로운 #/orientation 페이지는 “어떤 방향이 실제로 나에게 맞을까?“라는 질문에 답합니다 — 직업 적성 검사에서 얻을 법한 통찰이지만, 설문지가 아니라 당신 자신의 CV와 프로필에서 추론합니다. 프로필 생성을 클릭하면 모델은 당신의 가장 잘 맞는 커리어 벡터(여덟 가지 원형 — 기능주의자, 관리자, 소통가, 전문가, 분석가, 혁신가, 매니저, 기업가 — 중 어느 것이 맞는지, 근거와 함께), 커리어 유형 성향, 추천 직무, CV에 연결된 직업적 강점, 업무 스타일 경향, 그리고 개발 권장 사항을 반환합니다. 이는 당신의 CV가 어떻게 읽히는지에 대한 AI의 성찰이며 — 심리 검사가 아닙니다: 성취를 지어내지 않으며 수치 점수를 마치 측정된 것처럼 보고하지 않습니다. Markdown이나 PDF로 내보내세요; 디스크에는 아무것도 기록되지 않습니다.
- 새 라우트
server/lib/routes/orientation.mjs(24번째 라우트 모듈) —POST /api/orientation/generate는 공유 프로바이더 캐스케이드를 통해 CV+프로필+투페이저+메모리로부터 프로필 프롬프트를 구성하며, 복사·붙여넣기 수동 대체와 파일 쓰기 없음을 제공합니다. - Growth 내비게이션 그룹 아래에서 Markdown/PDF/복사를 위해
report-export.js를 재사용합니다. - 테스트:
tests/orientation-routes.test.mjs(성찰 프레이밍 / 조작된 점수 없음, CV/프로필 시드 수동 모드). 새 i18n 키 7개 ×16 로케일, 도움말 §28 ×16.
신규: #/orientation; server/lib/routes/orientation.mjs.
[1.95.0] — 2026-07-04
커리어 플랜 (Epic 26). 새로운 #/career-plan 페이지는 당신의 CV와 프로필을 구체적이고 개인화된 성장 계획으로 바꿉니다. 기간(6/12/24개월)과 선택적 포커스를 고르면, 모델은 — 당신의 CV, 프로필, two-pager, 메모를 읽고 — 출발점 스냅샷, 강점/성장 SWOT, SMART / OKR / WOOP로 된 목표, 대안 경로, 하드/소프트 스킬 계획, 월별 로드맵, 진행 추적 방법, 함정, 그리고 지원 방안을 작성합니다. 당신의 자료가 실제로 보여주는 것에서부터 앞을 계획하며, 당신의 이력에 관한 사실을 결코 지어내지 않습니다. 인라인으로 편집하고, 사용자 레이어(config/career-plan.md)에 저장하며, Markdown이나 PDF로 내보낼 수 있습니다.
- 새로운 라우트
server/lib/routes/career-plan.mjs(23번째 라우트 모듈) —GET/PUT /api/career-plan(config/career-plan.md에 쓰기) +POST /api/career-plan/generate(공유 제공자 캐스케이드, 수동 대체, 날조 없음).PATHS.careerPlan. - 공유 헬퍼
report-export.js(v1.94.0)를 Markdown/PDF/복사에 재사용하고, 새로운 성장 내비게이션 그룹을 추가. - 테스트:
tests/career-plan-routes.test.mjs(경계, GET/PUT 왕복, 기간을 반영한 CV/프로필로 시드된 프롬프트). 16개 로케일 전체에 20개의 새 i18n 키, 도움말 §27 16개 전체.
신규: #/career-plan; server/lib/routes/career-plan.mjs; PATHS.careerPlan.
[1.94.0] — 2026-07-04
통계, 재구성 (Epic 25). #/stats 페이지는 이제 실제 그래프와 훨씬 더 많은 데이터를 갖춘 세 개의 탭으로 된 통계 섹션입니다. 새로운 시장 리포트 탭은 당신이 선택한 지역과 통화로 당신의 타깃 직무에 대한 급여 및 노동 시장 분석을 모델에게 요청합니다 — 경영진 요약, P10/P25/P75/P90 백분위가 딸린 등급별 급여, 상위 고용주, 수요가 많은 스킬 테이블, 복리후생 빈도, 사무실/하이브리드/원격 비율, 12–24개월 추세, 그리고 협상 가이드. 모든 수치는 스크레이핑한 데이터가 아니라 모델 지식에 기반한 방향성 추정치로 표기됩니다. 새로운 내 파이프라인 탭은 당신 자신의 트래커를 차트로 보여줍니다: 점수 분포, 상태 퍼널, 상위 회사와 직무, 시간에 따른 지원, 그리고 전환율. 원래의 타깃 직무 뷰(국가별 공고/급여 + 저장된 스냅샷 추세)는 세 번째 탭으로 이동하며, 이제 통화 선택기와 직무별 공고 개요를 갖춥니다.
- 어떤 리포트든 내보내기 — 공유 헬퍼
report-export.js를 통해 Markdown이나 PDF로 내보내거나 복사합니다(Markdown blob 다운로드; PDF는 기존 인라인 PDF 러너를 통해). - 새로운 라우트
server/lib/routes/market.mjs(22번째 라우트 모듈) —POST /api/stats/market은 당신의 CV/프로필(그래서 당신의 타깃 직무를 알고 있음), 지역, 통화로부터 시장 분석 프롬프트를 구성하고, 공유 제공자 캐스케이드를 통해 실행하며, 키가 없으면 복사-붙여넣기용 프롬프트로 대체합니다. 디스크 쓰기 없음. - 테스트:
tests/market-routes.test.mjs(지역/통화 경계, 정직성 표기 프롬프트, CV/프로필로 시드된 수동 모드). 16개 로케일 전체에 36개의 새 i18n 키, 도움말 §26 16개 전체.
신규: 탭으로 재구성된 #/stats; server/lib/routes/market.mjs; public/js/lib/report-export.js.
[1.93.0] — 2026-07-04
메모리 레이어 (Epic 24). 새로운 #/memory 페이지는 어시스턴트가 모든 작업에서 염두에 두는 짧고 편집 가능한 “나에 대해 이걸 기억해 줘” 메모를 담습니다:
- 하나의 메모, 어디에서나 —
bundleProjectContext에 인라인으로 삽입되므로, 이 메모는 모든 제공자에 걸친 모든 AI 요청(평가, mock interview, 네트워킹, CV Studio)에 자동으로 도달합니다. 한 번만 작성하면 모든 것을 이끕니다. - 사실이 아니라 조향 — 당신의 선호와 일하는 방식(어조, 형식, deal-breakers, 주기)을 담을 뿐, 당신의 경력에 대한 새로운 사실 주장은 절대 담지 않습니다 — 그런 것들은 여전히 당신의 CV, 프로필, two-pager에만 존재합니다. 사용자 계층의
config/memory.md에 저장되며, 업데이트로 덮어써지지 않습니다. - 당신의 데이터에서 제안 —
POST /api/memory/suggest는 당신 자신의 지원 트래커에서 행동 패턴을 캐내어 검토하고 편집할 불릿을 초안으로 작성합니다. 트래커를 읽을 뿐, 사실을 지어내지 않으며 라이브 호출도 하지 않습니다.
신규: server/lib/routes/memory.mjs(21번째 라우트 모듈 — GET/PUT /api/memory + POST /api/memory/suggest), public/js/views/memory.js, PATHS.memory, 그리고 bundleProjectContext에 추가된 config/memory.md 블록. 16개 로케일 전체에 11개의 새 i18n 키. 테스트: tests/memory-routes.test.mjs.
[1.92.0] — 2026-07-04
CV Studio (Epic 21). 새로운 #/cv-studio 페이지는 당신의 CV에 정직하고 대부분 로컬에서 동작하는 세 가지 도구를 제공합니다:
- 이력서 진단 — 항목별 설명이 딸린 결정론적 0–100 점수(정량화된 임팩트, 약한 동사, 유행어, 길이, 핵심 섹션, 연락처 정보). 순수 클라이언트 측(
window.CvDiagnostics) — LLM 없음, 지어낸 것 없음, 모든 발견 사항이 설명되어 당신이 무엇을 바꿀지 결정합니다. - 프라이버시 마스크 — CV를 샘플이나 스크린샷으로 공유하기 전에 개인정보(이메일, 전화번호, 링크/핸들, 도로명 주소, 그리고 선택적으로 이름 → 이니셜)를 가립니다. 전적으로 브라우저에서 실행되며(
window.CvPrivacy), 정확히 무엇을 가렸는지 보고하고 원본은 절대 저장하지 않습니다. - 사람처럼 만들기 / 보이스 매칭 — 딱딱한 한 줄이나 문단을 붙여넣으면 당신의 목소리로 다시 쓰며, 서버 측에서
voice-dna.md와writing-samples/에 근거합니다. 엄격한 가드레일: 재배열, 다듬기, 재-보이싱은 가능하지만, 텍스트에 이미 없는 사실·지표·성과는 절대 새로 넣지 않습니다. 공유 제공자 캐스케이드를 통해 라이브로 실행되거나, 키 없이 복사-붙여넣기용 프롬프트를 돌려줍니다.
신규: server/lib/routes/cv-studio.mjs(20번째 라우트 모듈 — POST /api/cv-studio/humanize), public/js/views/cv-studio.js, public/js/lib/cv-diagnostics.js, public/js/lib/cv-privacy.js, PATHS.voiceDna + PATHS.writingSamplesDir. 16개 로케일 전체에 29개의 새 i18n 키. 테스트: tests/cv-diagnostics.test.mjs, tests/cv-studio-routes.test.mjs. (템플릿 갤러리, Word 내보내기, 채용공고 PDF 아카이브는 CV Studio 후속 작업으로 추적됩니다.)
[1.91.0] — 2026-07-04
네트워킹 및 심층 기업 리서치 (Epic 16). 새로운 #/networking 페이지는 기업 하나를 면접을 따내기 위한 실행 가능한 계획으로 바꿔주며, 당신의 CV·프로필·two-pager에 근거합니다:
- 기업 도시에 — 기업이 무엇을 하는지, 인용할 만한 최근 신호, 그리고 당신의 실제 이력에서 끌어낸 “내가 적합한 이유” 훅을 담은 간결한 브리프.
- 누구에게 연락할지 — 3–5명의 타깃 페르소나(채용 담당 매니저, 사내 리크루터, 팀의 시니어 IC, 따뜻한/동문 인맥)와 각자를 찾을 수 있는 구체적인 LinkedIn 검색 문자열. 실제 이름은 절대 지어내지 않습니다.
- 가장 따뜻한 소개 경로 — 당신의 이력에 가장 현실적인 따뜻한 진입 경로(공통 고용주/학교/커뮤니티, 2촌 경로, 또는 신호가 강한 콜드 DM)와 그 이유.
- 아웃리치 초안 — 상위 페르소나를 위한 짧고 구체적인 메시지로, 당신의 실제 증거 포인트에 근거합니다.
- 라이브 또는 수동 — 아무 키로든 공유 제공자 캐스케이드를 통해 라이브로 실행되거나, 복사-붙여넣기용 프롬프트를 돌려줍니다(정직한 폴백, 지어낸 것 없음). 계획 저장은 완성된 계획을 사용자 계층(
networking/net-{company}-{role}-{date}.md)에 보존하며, 페이지는 저장된 계획을 나열·열기·삭제합니다.
신규: server/lib/routes/networking.mjs(19번째 라우트 모듈), public/js/views/networking.js, PATHS.networkingDir. v1.90.0의 server/lib/llm-dispatch.mjs 캐스케이드를 재사용합니다. 16개 로케일 전체에 24개의 새 i18n 키. 테스트: tests/networking-routes.test.mjs.
[1.90.0] — 2026-07-04
Mock Interview 2.0 (Epic 15). 새로운 #/mock-interview 페이지가 이력서, 프로필, two-pager, 스토리 뱅크를 턴 바이 턴 면접 리허설로 바꿔 줍니다:
- 대화형 연습 — 목표 직무(+ 선택적으로 회사 / JD)를 설정하면 면접관이 초점 있는 질문으로 시작합니다. 보내는 답변마다 구조화된 응답을 받습니다: Feedback(강점 + STAR+R 격차), Score(
N/5), 그리고 직전 답변에서 가장 약한 부분을 파고드는 Next question. 서버 측에서 실제 자료에 근거하며 — 없는 경험을 지어내지 않습니다. - 스토리 뱅크 인식 —
interview-prep/story-bank.md가 프롬프트에 인라인으로 포함되므로(cv.md와 동일한 신뢰 수준), 피드백이 당신의 가장 좋은 스토리를 가리켜 줄 수 있습니다. - 라이브 또는 수동 — 제공자 키가 있으면 해당 턴이 공유 제공자 캐스케이드(Anthropic → Gemini → OpenAI → Qwen → OpenRouter → GitHub Models)를 통해 라이브로 실행됩니다. 키가 없으면 바로 복사·붙여넣기할 수 있는 프롬프트를 받습니다(정직한 fallback, 지어낸 답변 없음).
- 저장된 세션 — Save transcript 를 클릭하면 완료된 면접이 사용자 레이어(
interview-prep/mock-{company}-{role}-{date}.md)에 보존됩니다. 페이지에서 저장된 세션을 나열, 열기, 삭제할 수 있습니다.
신규: server/lib/routes/interview.mjs(18번째 라우트 모듈), public/js/views/mock-interview.js, server/lib/llm-dispatch.mjs(공유 제공자 캐스케이드), PATHS.storyBank, bundleProjectContext({ extraFiles }). 16개 로케일 전체에 30개의 새 i18n 키. 테스트: tests/interview-routes.test.mjs.
[1.89.0] — 2026-07-04
후보자-시장 적합성 — two-pager (Epic 14). 새로운 #/two-pager 페이지에서 다음 직무에 대해 당신이 실제로 원하는 것을 담을 수 있습니다. Never Search Alone 의 “Mnookin two-pager” 를 본떴습니다:
- 가이드 빌더 — 1인칭 “나는 누구인가” 서사, “목표 환경” 노트, 그리고 다섯 개의 칩 목록 편집기: loves, must-haves, hates, deal-breakers, non-negotiables. 상위 프로젝트의 사용자 레이어(
config/two-pager.yml)에PUT /api/two-pager로 저장됩니다 — 시스템 업데이트로 덮어써지지 않습니다. - AI 채우기 도우미(
POST /api/two-pager/draft) — 당신의 CV + 프로필이 인라인된, 바로 실행 가능한 Mnookin 프롬프트를 만들어 줍니다. 아무 LLM 에서 실행하고 결과를 다시 붙여넣으면 됩니다. 오직 당신 자신의 자료만 사용하며, 지어내는 것은 없습니다. - 원하는-것과의-적합도 배지 —
#/scan의 각 채용공고에 이제◎ N적합도 점수(클라이언트 측,window.FitScore경유)가 표시되어, 공고의 근무 형태, 국가, 최저 급여, 재배치를 당신의 two-pager 와 대조합니다. 설계상 정직합니다: 공고가 대조 가능한 신호를 주지 않으면 배지가 표시되지 않습니다(지어낸 숫자는 절대 없음). deal-breaker 위반은 가벼운 불호보다 더 무겁게 반영됩니다. - 모든 평가에 반영 — 저장된 two-pager 는
bundleProjectContext에 인라인되므로, 이후의 모든 LLM 평가가 당신이 밝힌 선호와 CV-대-JD 매칭을 함께 반영합니다.
신규: server/lib/routes/two-pager.mjs, public/js/views/two-pager.js, public/js/lib/fit-score.js, PATHS.twoPager. 16개 로케일 전체에 27개의 새 i18n 키. 테스트: tests/two-pager-routes.test.mjs, tests/fit-score.test.mjs.
[1.88.0] — 2026-07-04
이슈 #29 마무리 — 스캔 i18n 공백 + API 위생.
- 마지막까지 하드코딩되어 있던 스캔 문자열 현지화 (로드맵 v1.69.4): 소스 요약 알약(
N개 신규 / M개 일치),N개 신규 채용공고토스트,reloc배지가 이제t()를 통과합니다 — 16개 로케일 전체에 4개의 새 키(scan.pillNew,scan.pillMatching,scan.newOffers,scan.relocBadge). 비영어 사용자는 핵심 스캔 흐름에서 더 이상 떠도는 영어를 보지 않습니다. X-Powered-By헤더 비활성화 (로드맵 v1.69.5):createApp()내app.disable('x-powered-by')— 서버가 더 이상 Express 를 광고하지 않습니다. (해당 에픽의 나머지는 이미 출시됨:parentVersion이 release-please 주석을 제거하고, 라이트 모드 테마 토글, 라우트 변경 시 모달 닫힘, 보고서의 “Score”(rep.score) 현지화.)
테스트: tests/scan-i18n-gaps.test.mjs + tests/security-headers.test.mjs 의 X-Powered-By 부재 어서션.
[1.87.0] — 2026-07-04
인증 불필요 스캔 공급자 4종 추가 (상위 career-ops v1.16.0 동등성). 스캐너 레지스트리가 41 → 45개 어댑터(40 EN + 5 RU)로 늘어납니다 — 모두 공개, 무인증, 호스트 고정, redirect:'error'(SSRF 안전)이며 각각 CI 격리 테스트를 포함합니다:
- Get on Board (
getonbrd) — 포털 전체 공개 JSON:API(LATAM/원격 기술), 공급자 선택, 페이지네이션.server/lib/sources/getonbrd.mjs. - Amazon (
amazon) —amazon.jobs공개 검색 JSON, 호스트 감지 또는provider: amazon, 오프셋 페이지네이션.server/lib/sources/amazon.mjs. - Avature (
avature) — 테넌트별*.avature.netATS, HTML 파싱, 호스트 감지 또는provider: avature.server/lib/sources/avature.mjs. - SAP SuccessFactors (
successfactors) — 테넌트별 RMK 타일 목록(*.successfactors.eu/.com,jobs2web.com), HTML 파싱.server/lib/sources/successfactors.mjs.
각 항목은 sources/<slug>.mjs(자동 발견 meta → #/scan 드롭다운) 및 ALL_ADAPTERS 내의 portals/adapters/<slug>.mjs(두 레지스트리 규칙) + tests/sources-<slug>.test.mjs 를 제공합니다. ALL_ADAPTERS 개수와 정렬된 id 및 /api/scan/sources EN 세트 어서션이 36→40 으로 올랐고, GET /api/scan/sources 는 이제 45개를 나열합니다.
[1.86.0] — 2026-07-03
목표 직무별 통계 (#/stats) — 당신의 목표 직무에 대한 시장 채용공고 및 급여 통계. 새 분석 페이지가 프로필의 목표 직무(config/profile.yml → 하드코딩 아님)와 최신 스캔의 공고를 읽어, 직무 및 국가별로 다음을 표시합니다:
- 국가별 채용공고 수와 국가별 급여 중앙값(USD) — 스캐너가 이미 수집하는 희소한 데이터로부터 클라이언트 측에서 집계됩니다(
public/js/lib/role-stats.js,window.Countries재사용). 임의 통화의 급여는 명시적으로 근사치인 FX 표를 통해 USD로 정규화되며, 표본 크기 주의 사항이 함께 표시됩니다 — 결코 조작되지 않습니다. - 직무 및 국가 필터와 직접 작성한 인라인 SVG 막대·추세 차트(새 의존성 없음, CSP 안전 —
addEventListener전용). - 스냅샷 저장(
POST /api/stats/snapshot)은 현재 집계를data/role-stats.jsonl에 저장하며, 추세 차트(GET /api/stats/trend)는 시간 경과에 따른 채용공고 수를 추적합니다 — “다이내믹스” 뷰. 정직한 하이브리드: 스냅샷은 로컬 스캔 데이터에서 나오며 요청 시 새로 고쳐집니다. - 16개 로케일 전체에 완전 현지화(신규 i18n 키 26개).
신규: server/lib/routes/stats.mjs(16번째 라우트 모듈), public/js/lib/role-stats.js, public/js/views/stats.js, PATHS.roleStats; 테스트 role-stats.test.mjs(7) + stats-routes.test.mjs(5).
[1.85.0] — 2026-07-03
독일어, 이탈리아어, 터키어 로케일 (상위 career-ops v1.16.0 로케일 패리티). 이제 UI가 16개 언어로 제공됩니다 — 기존 13개에 de 🇩🇪, it 🇮🇹, tr 🇹🇷 가 추가되었습니다.
- 전체 UI 번역 —
public/js/lib/locales/i18n-dict.{de,it,tr}.js에서 730개 i18n 키 모두 번역; 언어 전환기에 Deutsch / Italiano / Türkçe 가 표시되고 브라우저 언어 자동 감지가de/it/tr를 인식합니다(public/js/lib/i18n.js). - 인앱 도움말 가이드 —
docs/help/{de,it,tr}.md번역(전체 19 H2 / 75 H3 구조),GET /api/help/:lang로 제공됩니다. - 문서 —
README.{de,it,tr}.md및CHANGELOG.{de,it,tr}.md추가; CHANGELOG 로케일 패리티 게이트가 이제 15개 비 EN 로케일을 커버합니다. - 프롬프트 스캐폴딩 —
server/lib/prompts.mjs(LOCALE_NAMES+SCAFFOLD_STRINGS)가 세 개의 새 로케일에 맞게 현지화되어, LLM 출력이 UI 언어를 따릅니다.
모든 패리티 게이트(i18n-locale-files, i18n-coverage, check-changelog-parity, lang-switcher-rtl)가 16개 로케일 세트로 확장되었습니다.
[1.84.0] — 2026-06-30
재적용 쿨다운 + pipeline.md 내 보상 정보 (상위 career-ops v1.15.0 패리티). 두 가지 스캐너 업그레이드:
- 재적용 쿨다운 (#1201): EN 스캔이 최근 지원한 회사의 역할을 건너뛰어 결과가 새로운 채용 공고에만 집중됩니다.
config/profile.yml의re_apply_windows:아래에서 회사별 기간을 설정할 수 있습니다(last_apply_date,same_role_days,applied_to: [roles], 선택적cross_role_bucket); 회사 매칭은 구두점 비민감 + 단어 경계 방식입니다(server/lib/cooldown.mjs). 키가 없으면 비활성화; 스캔 로그에Cooldown skipped: N이 표시됩니다. - pipeline.md 내 보상 정보 (#1017): 스캔된 채용 공고가 이제 선택적 후행 열로 급여를
data/pipeline.md에 저장합니다(url | <salary>). URL은 중복 제거 키로 유지되고(| comp열은 읽을 때 제거됨), 셀은 정제되며(행/열 주입 없음, 수식 선두 중화), 일반 URL 파이프라인은 하위 호환성을 유지합니다.
tests/cooldown.test.mjs 및 pipeline 보상 테스트 포함. 소스 수는 41개 유지 — 두 기능 모두 스캔 로직 업그레이드이며, 새 보드가 아닙니다.
[1.83.0] — 2026-06-30
재게시 / 유령 공고 감지기 (상위 career-ops v1.15.0 패리티). #/scan에 새로운 🔁 재게시 / 유령 공고 패널이 추가되어, 90일 롤링 윈도우 내에서 다른 URL로 재게재된 회사+직무 클러스터를 표시합니다 — 오래된 채용 파이프라인과 유령 공고의 신호입니다. 퍼지 직무 제목 매처(server/lib/role-matcher.mjs)와 data/scan-history.tsv에 대한 읽기 전용 감지기(server/lib/detect-reposts.mjs)를 기반으로 하며, GET /api/scan/reposts를 통해 노출됩니다. 또한: /api/health의 parentVersion이 이제 순수 semver만 반환합니다(릴리스-플리즈 # x-release-please-version 주석이 제거됨). tests/detect-reposts.test.mjs 포함. 소스 수는 41개 유지 — 재게시는 분석 기능으로 새 보드가 아닙니다.
[1.82.0] — 2026-06-30
NoDesk 스캔 소스 (상위 career-ops v1.15.0 패리티). 보드 전체 NoDesk 원격 채용 RSS 피드가 이제 1급 스캔 소스입니다 — provider: nodesk 항목을 추가하면 #/scan의 Source 드롭다운에 표시됩니다(총 41개 어댑터: EN 36 + RU 5). 호스트는 nodesk.co로 고정되고 redirect:'error'(SSRF 방지); 제목은 Role at Company로 분리됩니다(NoDesk에는 위치 태그가 없어 위치는 빈 채로 유지); 모든 항목은 원격. CI 격리 tests/sources-nodesk.test.mjs 스위트 포함; 전체 유닛 테스트 스위트 1523개 통과.
[1.81.0] — 2026-06-29
상위 career-ops 패리티 — 13개의 새로운 채용 보드 스캔 소스. Fighter90/career-ops main의 최신 프로바이더 배치를 인프로세스 스캐너에 이식합니다. 보드 전체 공개 API(프로바이더 선택): Arbeitnow, Himalayas, Jobicy, Landing.jobs, 4 Day Week, The Muse, The Hub, Jobspresso(RSS), 그리고 Hacker News “Who is hiring?”(Algolia 2단계). 폴란드 보드(호스트 또는 provider: 감지): JustJoin.it 및 NoFluffJobs(POST 검색). 테넌트별 ATS(careers_url에서 자동 감지): Pinpoint(<slug>.pinpointhq.com/postings.json) 및 Rippling(ats.rippling.com/<slug> → api.rippling.com 보드). 모든 소스는 redirect:'error'로 호스트 고정(SSRF 방지)되며 #/scan Source 드롭다운에서 선택 가능합니다 — 레지스트리는 이제 40개 스캐너 어댑터(EN 35 + RU 5)를 제공합니다. 소스별 CI 격리 테스트 스위트 13개 추가; 전체 유닛 테스트 스위트 1513개 통과.
[1.80.0] — 2026-06-28
스캔 5가지 개선(job-crawler 아이디어 재구현). (1) Teamtailor 소스 — <slug>.teamtailor.com 테넌트 채용 사이트를 공개 /jobs.rss 피드로, careers_url에서 자동 감지(호스트 고정 + redirect:'error'); 레지스트리는 이제 27개 어댑터. (2) 소스 격리 — 영구 404/410을 반환한 소스를 data/scan-quarantine.json에 기록하고 이후 스캔에서 건너뜀(자가 치유: 14일 후 재시도). (3) 소스당 최대 — #/scan의 선택 필드로 보드당 채용 수 제한(기본 ∞). (4) 게시 기간 — 클라이언트 측 기간 필터(24시간 / 7일 / 30일). (5) 저장된 검색 + ★ 즐겨찾기 — 필터 세트를 이름 지어 재사용하고 채용을 별표; localStorage에 방어적 검증과 함께 저장(손상된 캐시는 깔끔히 초기화); 결과 캐시는 스캔 전에 초기화되고 실시간으로 다시 채워집니다.
[1.79.0] — 2026-06-28
WeWorkRemotely 스캔 소스 (상위 career-ops v1.14.0 패리티). We Work Remotely의 보드 전체 원격 채용 RSS 피드가 이제 1급 스캔 소스입니다 — provider: weworkremotely 항목을 추가하면 #/scan의 Source 드롭다운에 표시됩니다(총 26개 어댑터). 호스트는 weworkremotely.com으로 고정되고 redirect:'error'(SSRF 방지); 제목은 Company: Role로 분리됩니다. 또한 title_filter 키워드는 길이 검사 전에 트림됩니다(parent #1261).
[1.78.2] — 2026-06-27
i18n·UX 보강 (v1.78.1 후속 수정). 로고의 접근성 이름이 13개 언어 전체로 현지화되었습니다(nav.logoHome). 이미 #/scan에 있는 상태에서의 전역 검색 Enter는 강제 리렌더링하여 미리 채운 검색어를 잃지 않습니다(동일 경로 가드). health.title가 폴란드어(Kondycja)와 덴마크어(Systemtilstand)로 번역되었습니다(이전엔 영어). 테스트 1235 → 1238.
[1.78.1] — 2026-06-27
Scan UX 수정. #/scan 결과 테이블이 이제 스캔 중 실시간으로, 그리고 완료 후 한 번 더 자동 새로고침됩니다(수동 새로고침 불필요). 상단바 전역 검색은 Enter 힌트를 표시하고, URL이 아닌 검색어에서는 검색창이 채워진 채로 #/scan으로 이동합니다(이전엔 #/tracker). 로고는 이제 대시보드(홈)로 연결됩니다.
[1.78.0] — 2026-06-27
Scan 페이지에 지역 필터 — 국가별(국기 포함)로 채용 결과 필터링. #/scan의 새 국가 드롭다운은 결과에서 감지된 각 국가(국기 이모지 + 개수)를 나열하여 특정 국가에 연결된 공고만 남길 수 있습니다. Remote/Hybrid/Onsite 근무 형태 필터와 함께 사용해 국가 기반·원격 모두 검색할 수 있습니다. 새 countries.js 헬퍼가 공고의 자유 텍스트 위치(국가명·별칭·주요 도시 ~100개)를 ISO 국가 + 국기로 매핑하며, 감지는 보수적이라 추측하지 않습니다.
[1.77.0] — 2026-06-27
덴마크어(Dansk)를 13번째 인터페이스 언어로 추가. UI, 인앱 도움말 가이드(19 H2 / 75 H3), README, CHANGELOG의 완전한 덴마크어 번역. 덴마크어가 국기 언어 선택기에 추가되었고, i18n 체계(어셈블러, 감사, 패리티 게이트, 스냅샷)가 이제 13개 로케일을 포함합니다.
[1.76.0] — 2026-06-26
상위 career-ops v1.13.0 패리티 — 신규 소스 6종, 스캐너 강화, 결과 테이블 상한 제거.
추가
- 테넌트별 ATS 소스 6종 — BambooHR, Breezy HR, Comeet, Personio, Recruitee, SolidJobs.
careers_url호스트로 자동 감지(Comeet은 전체api:필요). 각 호스트는 앵커 정규식 +redirect:'error'로 고정(SSRF 방지).#/scan의 Source 드롭다운에서 선택 가능 — 레지스트리는 이제 25개 어댑터(EN 20 + RU 5). Personio XML 피드용fetchText헬퍼 추가. trust_filter— 선택적 신뢰 점수(0–100, 레벨 high/medium/low, 플래그). 주석만.high미만 행은#/scan에 언어 중립 ⚠ 배지 표시. 채용공고는 절대 제거하지 않음.- Arbeitsagentur
remoteMatch+remoteMaxPages— 설정 기반 원격 감지:title,filter(서버측homeoffice=nv_true+ 페이지네이션),off.
변경
- 스캔 결과 상한 없음. 표시 상한
MAX_STORED_RESULTS(2000) 제거 — 일치하는 모든 공고를 저장하고#/scan테이블이 페이지로 표시(200/페이지). - 제목 필터 견고성 — 짧은 약어(COO, SDR…)는 단어 경계로 매칭; 잘못된
title_filter설정이 스캔을 중단시키지 않음. ATS·지역 스캐너 모두.
테스트
- +32 케이스(1190 → 1222):
sources-ats-providers,title-filter,arbeitsagentur-remote,trust-validator, 재작성된scan-result-cap“상한 없음” 가드.
[1.75.2] — 2026-06-19
docs: v1.75.0 스캐너 집계기에 대한 12개 로케일 전체의 완전한 문서 패리티. 코드 변경 없음 — v1.75.0에 들어온 일곱 개 소스에 맞춰 사용자 대상 문서를 정렬합니다:
- 도움말 가이드(12개 로케일). §5에
content_filter블록(설명/스니펫 키워드 게이팅,location_filter의 형제)과 집계기 노트가 추가됩니다. §7은 원클릭 스캔 스윕과 전체 Source 드롭다운 열거에 일곱 개의 새 소스를 나열합니다. §17의 어댑터 수가 오래된 “11 adapters”에서 “19 adapters — 14 English + 5 Russian”으로 수정됩니다.##/###헤딩이 추가되지 않았으므로 게이팅된 19 H2 / 75 H3 구조는 변경되지 않습니다. - README(9개 전체 로케일). 스캔 소스 아래에 새 “Aggregator boards (v1.75.0)” 항목이 추가되고, 릴리스 배지가 v1.75.2로 갱신됩니다. (축약된 pl/uk/ar README에는 소스별 목록이 없으며 그 부분은 의도적으로 그대로 둡니다.)
- 참조 문서.
docs/portals-examples.md에 일곱 개 모두에 대한 정확한provider:/<provider>:설정 블록을 담은 복사-붙여넣기용 “Aggregator boards” 섹션이 추가됩니다.docs/PROJECT.md는 19 adapters로 갱신됩니다.docs/sdd/CONVENTIONS.md는 두 레지스트리 구분(드롭다운용sources/registry.mjs대 페칭용portals/registry.mjs),opts.company로 전달되는provider:기반 집계기 선택, 스캔 쓰기 새니타이저(scan-sanitize.mjs), 그리고 v1.75.1 테스트 수(1190)를 문서화합니다. - QA.
qa/QA-REGRESSION-PROMPT-v1.75.2-FULL.md추가 — v1.75.x 스캔 집계기 사이클에 맞춰 갱신된 전체 표면 릴리스 게이트 드라이버.
[1.75.1] — 2026-06-19
fix(scan): v1.75.0의 구성 기반 소스에 대한 견고성 다듬기. 릴리스 후 검토에서 나온 세 가지 작은 강화 수정입니다(정상 스캔에서는 동작 변화 없음):
- Abort 인지 페이지네이션 지연. Glints(300 ms) 및 Jobstreet/SEEK(200 ms) 페이지 간 예의 대기는 이제
server/lib/http-json.mjs의 새delay(ms, signal)헬퍼를 통해 스캔의AbortSignal이 발생하면 즉시 해제됩니다. 따라서 연결이 끊긴 클라이언트가 페이지네이션 중인 스캔을 추가 대기 시간 동안 붙잡아 둘 수 없습니다. - 설명적인 비JSON 오류.
fetchJson은 이제 비JSON2xx본문(예: 상태 200으로 제공되는 HTML 유지보수 페이지)을 노출되는SyntaxError대신non-JSON 2xx response from <url>로 래핑합니다. 따라서 스캐너의 소스별 오류 로그가 오작동하는 엔드포인트를 명시합니다. - 더 강력한 스캔 쓰기 정규화.
normalizeScanScalar은 이제\r \n \t에 더해 수직 탭, 폼 피드, 그리고 유니코드 줄/문단 구분자(\v \f U+2028 U+2029)까지 축약합니다 — 엄격한 상위 집합이므로, 스프레드시트나 뷰어가 인식할 수 있는 어떤 레코드/줄 구분자도scan-history.tsv에 살아남지 않습니다.
[1.75.0] — 2026-06-19
feat(scan): 상위 career-ops v1.12.0 패리티 이식 — 신규 채용 소스 7종, 콘텐츠 필터링, 보안/품질 수정. web-ui는 자체 인프로세스 스캐너를 실행하며(상위의 scan.mjs로 셸 아웃하지 않음) 따라서 상위 v1.12.0의 프로바이더 및 스캔 변경 사항은 자동으로 전파되지 않습니다 — 이번 릴리스는 적용 가능한 변경 사항을 web-ui의 어댑터 계약에 맞춰 재구현합니다.
- 신규 스캐너 소스 7종. 보드 전반 원격 집계기 3종 — RemoteOK, Remotive, Working Nomads — 은 자동 탐색되는
server/lib/sources/*.mjs패턴에 들어맞습니다(provider: remoteok/remotive/workingnomads로 선택). 설정 기반 지역 집계기 4종 — IBM careers, Arbeitsagentur(독일 연방고용청), Glints(동남아시아), Jobstreet / SEEK — 은 항목별<provider>:설정 블록을 읽으며, en-scanner는 이제 해결된 회사 항목을 각 페처까지 전달하여 페처가 이를 읽을 수 있게 합니다. 일곱 개 모두#/scan소스 드롭다운에 자동으로 표시됩니다. content_filter(상위 #974). 게시물을 설명/스니펫 텍스트로 게이팅하는 선택적portals.yml블록(positive/negative키워드 목록) —location_filter시맨틱을 따르며, 설명이 없는 게시물은 항상 통과합니다. EN 및 RU 스캐너 모두에 연결되었습니다.- 스캔 쓰기 강화 (상위 #1098). 외부 피드 메타데이터는 이제
data/scan-history.tsv및data/pipeline.md에 기록되기 전에 정제됩니다: 제어 문자가 축약되고(회사/직함의 줄바꿈이 더 이상 TSV 행을 주입할 수 없음) 선행= + - @는 스프레드시트 수식 주입에 대비해 무력화됩니다. - Ashby
secondaryLocations(상위 #1073). Ashby 소스는 이제 각 보조 위치의 지역 레이블과 우편addressLocality/addressCountry를 위치 문자열에 접어 넣으므로(중복 제거), 기본 레이블이 예컨대 “Canada”로 표시되는 EU 자격 직무가location_filter에 노출됩니다. - 평가 보고서 형태 검증 (상위 #819).
/api/evaluate의 인프로세스 프로바이더(Anthropic / OpenAI / Qwen / OpenRouter / GitHub Models)는 이제 형식이 잘못된 A–G /SCORE_SUMMARY보고서를 치명적이지 않은warnings배열로 플래그합니다; Gemini 평가 경로는 이미 상위gemini-eval.mjs로부터 해당 가드를 상속합니다. - docs: Antigravity CLI를 12개 README 전반의 지원 어시스턴트 목록에 추가(Gemini 프로바이더에 매핑).
상위의 git pull에서 무상으로 상속됨(web-ui가 이들로 셸 아웃함): 일본어 CJK PDF 폰트 폴백(#1053), ATS 안전 PDF 폰트(#1074), LaTeX CJK 가드(#1054), tracker/merge/followup/dashboard 수정, 그리고 modes/zh 중국어 모드(web-ui는 모드를 동적으로 나열함).
[1.74.3] — 2026-06-18
docs(parent-source): 부모 career-ops 저장소를 Fighter90/career-ops 포크로 변경. web-ui는 이제 실제 소스로 쓰이는 모든 곳에서 메인테이너의 포크를 부모 프로젝트로 참조합니다: bin/setup.sh 설치 스크립트의 CAREER_OPS_REPO 클론 기본값, 12개 README의 모든 git clone / “위에 올라가는” / 온보딩 링크, 그리고 에이전트 문서(CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, docs/). 작성자 santifer 크레딧(및 비공식 UI 고지)은 변경되지 않았으며 — 소스/클론 URL만 이동했습니다. tests/sh-files.test.mjs는 이제 설치 스크립트가 포크를 클론하는지 검증합니다.
[1.74.2] — 2026-06-17
fix(health): #/health 및 /api/status/providers에서 GITHUB_MODELS_API_KEY를 선택적 점검 항목으로 노출. v1.74.0의 GitHub Models 프로바이더는 #/config에서 설정할 수 있었지만 Health 페이지에 행이 없었고 keysConfigured 프로바이더 표면에서 누락되어 있었습니다. 선택적 점검 항목(다른 다섯 개 라이브 평가 프로바이더와 동일한 “set / unset (manual mode)” 문구)과 github(+ 해당 GITHUB_MODELS_MODEL)를 /api/status/providers에 추가하여, 활성 프로바이더 라우팅과 Health 페이지가 이제 여섯 개 모두를 반영합니다. tests/api.test.mjs 헬스 행 테스트를 여섯 개 프로바이더 모두로 확장했습니다.
[1.74.1] — 2026-06-17
docs + test: README “AI 어시스턴트 설치” 섹션; Gemini 커넥터의 전체 브랜치 커버리지. README에 설치/로그인 표 추가 — Claude Code / Gemini CLI / Codex / Qwen Code / OpenCode / GitHub Copilot CLI 설치 링크 + 각각의 #/config 공급자 매핑 + “계속하기 전에 로그인” (career-ops.org/docs 빠른 시작 반영; web-ui가 CLI 없이도 사용 가능한 독립형 대안임을 명확히 설명). 새로운 tests/gemini-connector.test.mjs (8개 케이스)는 runGemini의 모든 브랜치를 다룸 — 키 없음, 성공, API 오류, 빈/차단된 완료, 잘못된 형식의 본문, 타임아웃, 네트워크 오류, hasGeminiKey — server/lib/gemini.mjs를 구문 100%로 끌어올림. 전체 커버리지: 96% 라인 / 88% 브랜치 / 96% 함수. 스위트 1126 → 1134.
[1.74.0] — 2026-06-17
feat(llm): GitHub Models (Copilot)을 6번째 프로바이더로 추가 + 6개 어시스턴트 표준 정렬. career-ops.org/docs에는 여섯 가지 AI 코딩 어시스턴트가 나열되어 있습니다 — Claude Code, Gemini CLI, Codex, Qwen Code, OpenCode, GitHub Copilot CLI. 이제 web-ui는 여섯 가지 모두를 지원합니다: 다섯 개는 기존 라이브 프로바이더(Anthropic / Gemini / OpenAI / Qwen / OpenRouter)에 매핑되고, GitHub Copilot CLI는 전용 GitHub Models 커넥터 — runGitHubModels (OpenAI-compatible; models 스코프를 가진 GitHub PAT)를 갖게 됩니다. 이는 #/config에서 (GITHUB_MODELS_API_KEY + GITHUB_MODELS_MODEL)로 설정하고 LLM_PROVIDER=github를 통해 선택할 수 있으며 auto 순서에서 6번째입니다. 도움말 번들과 README는 이제 표준 여섯 가지를 나열하고(Qwen CLI→Qwen Code로 이름 변경; Gemini CLI + GitHub Copilot CLI 추가), README에는 모든 기능이 상위 프로젝트로 추적될 수 있도록 전체 모드 참조 및 포털 어댑터 링크 테이블이 career-ops.org/docs에 추가됩니다. tests/llm-provider-context.test.mjs는 여섯 프로바이더 모두로 페치 경계 매트릭스를 확장하고(cv.md + profile.yml 인라인 + 아티팩트 반환), 새로운 GITHUB_MODELS_* 키가 12개의 모든 로케일 사전에 추가됩니다. 스위트 1125 → 1126.
[1.73.0] — 2026-06-17
feat(llm): 범용 Gemini 커넥터 + 모든 공급자에 걸쳐 검증된 CV/프로필 컨텍스트. server/lib/gemini.mjs (runGemini)를 추가했습니다 — Anthropic / OpenAI 호환 클라이언트와 동일한 {markdown, usage, error} 형태를 반환하는 외부 의존성 없는 Gemini generateContent 클라이언트입니다. 수정 사항: /api/mode/:slug와 /api/deep이 이전에는 채용 공고 전용인 gemini-eval.mjs를 통해 프롬프트를 라우팅하여, Gemini Run live가 요청된 결과물(자기소개서, 아웃리치, 브리프) 대신 평가를 반환했습니다. 이제 bundleProjectContext와 함께 runGemini를 호출하므로 cv.md + config/profile.yml이 다른 모든 공급자와 동일하게 Gemini에도 인라인으로 삽입되어 편지와 브리프가 상세하고 개인화됩니다. 새로운 tests/llm-provider-context.test.mjs는 각 공급자의 HTTP 경계를 목킹하고 다섯 개 공급자(Anthropic / Gemini / OpenAI / Qwen / OpenRouter) 모두 cv.md + profile.yml을 인라인으로 삽입하고 결과물을 반환함을 검증합니다(mode + deep + evaluate 매트릭스, 9개 케이스). /api/evaluate는 채용 공고 전용 gemini-eval.mjs를 유지합니다. Suite 1116 → 1125.
[1.72.0] — 2026-06-17
feat(modes): Run live 가 이제 최종 결과물을 직접 반환합니다 (단일 실행 출력 계약). 상위 modes/<slug>.md 템플릿은 Claude Code 대화형 세션을 위해 작성되었으며, 일부(cover, contacto 등)는 결과를 생성하기 전에 확인 질문을 위해 일시 중지합니다. 이로 인해 웹 UI의 Run live 가 결과물 대신 설문지를 출력했습니다. buildModePrompt 는 이제 모든 모드를 비대화형 출력 계약으로 감쌉니다: 분석(JD 분석, 회사 노트, ATS 키워드, 프로필↔JD 간극, 톤/방향 선택)을 자동으로 수행하고, 템플릿이 보통 물어볼 내용에 대해 cv.md / config/profile.yml 에서 합리적인 기본값을 선택하며, 최종 결과물만 출력합니다 — 모드별 «output ONLY {the cover letter / outreach message / …}» 알림으로 마무리됩니다. 이제 #/cover 에서 Run live 를 클릭하면 커버 레터 자체가 반환됩니다. 동일한 수정이 모든 일반 모드(cover, contacto, interview-prep, project, training, followup, patterns)의 12개 로케일 전체에 적용됩니다(결과물은 로케일 지시어를 통해 UI 언어로 작성됩니다). Suite 1103 → 1116.
[1.71.2] — 2026-06-17
docs(i18n): 문서 일관성 검토 결과를 게시합니다. 각 README의 “Translations of this guide” 블록에 이제 11개의 형제 언어가 모두 나열되며(이전에는 일부가 English/Français를 생략하거나 자기 자신에 링크), 섹션 구분 앞의 빈 줄이 복원되었습니다. 전체 QA 회귀 프롬프트가 현재 버전으로 이름이 변경되었고, 문서(CLAUDE.md, CONVENTIONS, LOCALIZATION, PROJECT-CONTEXT)가 현재 버전 및 테스트 수(1103)와 동기화되었습니다. 코드 또는 동작 변경 없음 — 문서만 변경되었으므로 도움말/UI 번역 및 1.70.0–1.71.1의 모든 기능은 변경되지 않습니다.
[1.71.1] — 2026-06-17
fix(i18n): 앱 내 도움말 가이드가 이제 12개 언어 모두에서 완전히 번역되었습니다. docs/help/{pl,uk,ar}.md를 추가하여 (각 파일은 검증된 19 H2 / 75 H3 구조를 포함) #/help가 영어로 폴백하는 대신 폴란드어, 우크라이나어, 아랍어 네이티브 번들을 제공합니다 — GET /api/help/{pl,uk,ar}가 이제 해당 로케일을 반환합니다. 모든 도움말 게이트(help-ui, help.test, help-ru-config-section, canonical-docs-coverage)에 연결되었습니다. 또한 12개 언어 번역 목록을 모두 완성했습니다: README의 «Translations of this guide» 블록(9개 README), 로컬화된 CHANGELOG의 «Translations:» 헤더(8개 파일), 그리고 오래된 문서 수치를 업데이트했습니다. Suite 1100 → 1103.
[1.71.0] — 2026-06-16
feat(cover): #/cover에서 바로 자기소개서 PDF를 생성합니다. v1.70.0에 추가된 cover 모드는 편지 텍스트를 생성하며, 이제 결과 화면에 Generate PDF 버튼이 제공되어 공유 인라인 markdown→PDF 파이프라인(POST /api/stream/pdf/inline → generate-pdf.mjs)을 통해 렌더링됩니다. 이는 interview-prep이 사용하는 것과 동일한 경로입니다. 이제 SPA를 벗어나지 않고도 편지를 작성하고 PDF를 제출할 수 있습니다.
test/docs: v1.70.0 검토 강화. cover 모드(허용 목록 + 프롬프트 조립), 국기 <select> 스위처 + 아랍어 RTL(dirFor/<html dir>), 모든 로케일의 top.langLabel, 자기소개서 PDF 연결, prompts.mjs 로케일 지시문 + fr/pl/uk/ar 스캐폴딩에 대한 CI-격리 커버리지를 추가했습니다. docs/sdd/CONVENTIONS.md 및 전체 프로젝트 QA 회귀 프롬프트에서 오래된 «8개 전체» → 12개 로케일 참조를 업데이트했습니다.
[1.70.0] — 2026-06-16
feat(i18n): 세 가지 새로운 UI 언어 — 폴란드어(pl), 우크라이나어(uk), 아랍어(ar, 완전한 RTL 지원 포함) — 를 추가하여 SPA를 12개 로케일로 확장하고, 상위 career-ops README의 모든 언어와 일치시킵니다. 각 새 로케일은 697개 키로 구성된 완전한 사전(public/js/lib/locales/i18n-dict.{pl,uk,ar}.js)을 제공하며, 기존의 패리티 / 커버리지 / 라틴 누락 방지 / 개인 데이터 없음 테스트 스위트로 검증됩니다. 아랍어는 진정한 오른쪽에서 왼쪽 지원을 추가합니다: i18n.js는 RTL 로케일에 대해 <html dir="rtl">을 설정하고, app.css의 범위가 지정된 [dir="rtl"] 블록이 크롬(사이드바, 알림 드로어, 마크다운 테이블/블록 인용, 인라인 간격)을 미러링합니다 — LTR 로케일은 바이트 단위로 변경되지 않습니다. 새로운 top.langLabel 키(×12)가 스크린 리더를 위해 선택기 이름을 지정합니다.
feat(ui): 플래그 아이콘 <select> 언어 전환기가 래핑 버튼 행을 대체합니다. 12개 로케일에서 이전 .lang-btn 행은 사이드바에서 세 줄로 줄 바꿈되었습니다. 네이티브 <select>(각 옵션 앞에 국기 이모지)는 깔끔하게 확장되고, 기본적으로 키보드 및 스크린 리더 친화적이며, CSP에 안전합니다(addEventListener를 통한 변경 핸들러, 인라인 JS 없음). 플랫폼에 국기 글리프가 없는 경우 국기는 지역 문자로 대체되므로 언어 레이블이 항상 핵심 식별자입니다.
feat(cover): 상위 프로젝트의 자기소개서 모드(career-ops v1.10.0 + v1.11.0 인사말)를 SPA로 이식합니다. 지원 탐색 그룹 아래에 새로운 #/cover 페이지를 추가하고, 범용 모드 러너 위에 구축합니다: 채용 공고 설명 + 회사/역할 + 선택적 인사말 → cv.md / modes/_profile.md에서 생성된 맞춤형 편지. 서버 MODE_ALLOWLIST에 cover를 추가하고 cover.* i18n 블록(×12 로케일)을 추가했습니다.
chore(compat): 상위 career-ops v1.11.0을 추적합니다. 읽기/쓰기 계약이 그대로 유지됨을 확인했습니다 — data/applications.md는 마크다운 진실 소스로 유지되며(v1.11.0 SQLite 트래커 인덱스는 파생 캐시), 트래커 열은 여전히 헤더 매핑됩니다. parentVersion은 이제 1.11.0을 보고합니다.
fix(i18n): 프랑스어(v1.61.0에 추가)가 server/lib/prompts.mjs의 LOCALE_NAMES 및 SCAFFOLD_STRINGS에 누락된 잠재적 결함을 수정합니다 — 프랑스어 LLM 호출이 자동으로 영어 출력 및 영어 스캐폴딩으로 대체되었습니다. fr/pl/uk/ar가 이제 모두 프롬프트 로케일 경로에 연결되었습니다.
알려진 후속 작업: 앱 내 도움말 가이드(
docs/help/)는 pl/uk/ar의 경우 영어로 대체됩니다(UI 크롬 자체는 완전히 현지화됨); 상위 프로젝트의 대화형 인터뷰 온보딩, 역방향 ATS 검색, 새로운 스캔 제공업체는 아직 SPA에 노출되지 않았습니다.
[1.69.2] — 2026-06-12
fix(test): npm test가 실제 config/profile.yml과 data/scan-history.tsv를 덮어쓰던 테스트 격리 누수를 수정합니다. tests/critical-fixes.test.mjs가 파일 상단에서 prompts.mjs(→ paths.mjs)를 가져왔기 때문에, before()가 CAREER_OPS_ROOT를 임시 디렉터리로 설정하기 전에 PROJECT_ROOT가 실제 부모로 해석되어 PUT /api/profile가 매 실행마다 “Acceptance Test” 픽스처를 실제 프로필에 기록했습니다. 수정: prompts.mjs를 before() 안에서 동적 import()로 로드. 새 tests/test-root-isolation.test.mjs(2개 케이스)가 전체 스위트를 이 패턴으로부터 보호합니다. 프로덕션 코드 변경 없음. 스위트 1084 → 1086.
[1.69.1] — 2026-06-12
fix(scan): #/scan 가 대규모 지역 스윕을 조용히 잘라내지 않습니다. 리전별 표시 집합이 500개로 고정되어 있었습니다(실제 RU 스캔에서 일치 1352개 중 500개만 표시, 852개 숨김 — “2000개 스캔, 약 600개 표시” 증상). 두 스캐너 모두 공유 및 환경 변수로 재정의 가능한 상수 MAX_STORED_RESULTS(기본 2000, SCAN_MAX_RESULTS로 재정의)를 사용합니다. 표시 전용 — pipeline.md / scan-history.tsv 추가는 이미 잘리지 않은 집합을 사용했습니다. fix(health/ui): #/health 점검 카드가 더 이상 넘치지 않습니다. 긴 이름/값이 Fix → 버튼 및 상태 배지와 충돌했으나, 이제 .health-check-row로 축소·줄바꿈됩니다. 새 테스트 scan-result-cap + health-card-overflow. 스위트 1079 → 1084.
[1.69.0] — 2026-06-12
feat(scan): 스캐너 어댑터 자동 발견 (P-14) — server/lib/sources/ 에 .mjs 파일을 두기만 하면 새로운 소스가 등록됩니다. v1.69 이전에는 server/lib/sources/registry.mjs 의 소스 목록이 손으로 유지되는 정적 배열이어서, 어댑터를 추가하려면 <id>.mjs 와 registry.mjs 를 모두 수정해야 했습니다. 로드맵 항목 P-14 (docs/ROADMAP.md) 의 남은 절반을 마무리합니다. 이제 server/lib/sources/ 의 모든 *.mjs 가 모듈 부팅 시 동적으로 로드되며, 각 어댑터는 자기 기술적인 블록 export const meta = { value, label, region, configKey? } 로 자신의 정체성을 선언합니다. 동봉된 12개의 어댑터(ashby / greenhouse / lever / rss / smartrecruiters / workable / workday + geekjob / getmatch / habr / hh / trudvsem) 에 meta 가 추가되었고, registry.mjs 는 readdirSync + 동적 import() 를 top-level await 로 해석합니다 (Node 18+ ESM 표준). 공개 API (SOURCES, SOURCES_BY_REGION, RU_CONFIG_KEYS, getRegionalSources) 는 변경되지 않습니다 — 기존 임포트는 그대로 동작합니다. 잘못된 meta 는 거부되고 파일마다 console.warn 가 한 번씩 기록됩니다. tests/sources-registry-discovery.test.mjs 에 14개의 케이스를 추가했습니다. 스위트 1065 → 1079.
[1.68.2] — 2026-06-07
fix(bin): npx / npm link로 실행하는 CLI 동사가 깨져 있었음 — 이제 bin 경로를 심볼릭 링크를 따라 해석함. npm과 npx는 career-ops-ui를 node_modules/.bin/ 아래의 심볼릭 링크로 노출하는데, 기존 dirname "${BASH_SOURCE[0]}"가 패키지 루트가 아니라 .bin을 가리켜서 npx career-ops-ui init이 node node_modules/scripts/init.mjs를 실행하다 MODULE_NOT_FOUND로 죽었습니다(로컬 npm install 실행은 영향을 받지 않아 버그가 숨겨졌습니다). 이제 bin/career-ops-ui.sh와 bin/start.sh는 심볼릭 링크 체인을 따라 SCRIPT_DIR를 정규화하므로(readlink 루프 + cd -P) 모든 동사가 저장소에서, npm link로, 그리고 npx로 동작합니다. .bin 스타일 심볼릭 링크로 동사를 실행하는 회귀 방지 테스트를 tests/sh-files.test.mjs에 추가했습니다. 스위트 1065/1065.
[1.68.1] — 2026-05-29
fix(scan): 소스별 fetch 타임아웃 10s → 60s로 상향. v1.67.1의 10s fail-fast는 시간이 조금 더 필요할 뿐인 “느리지만 살아있는” Ashby 보드까지 끊어냈습니다. 기본값을 1분으로 올려 그런 보드가 응답하도록 합니다. 트레이드오프: 정말로 죽거나 멈춘 소스는 이제 60s 내내 동시성 슬롯을 점유하며(최악의 경우 스캔이 느려짐), 만성적으로 멈추는 곳(Perplexity, Supabase, Resend 등)은 여전히 타임아웃될 가능성이 큽니다 — 제대로 고치려면 소스별 / Ashby 동시성 축소가 필요합니다. SCAN_FETCH_TIMEOUT_MS로 재정의 가능. 스위트 1063/1063.
[1.68.0] — 2026-05-29
feat(scan): 결과 필터 패널 개편 — 라벨 필드, 적용 버튼, 사무실 근무 옵션, 그리고 실제로 동작하는 급여 필터. #/scan의 모든 필터가 라벨 필드가 되었습니다(플레이스홀더가 아니라 컨트롤 위에 라벨): 검색 · 근무 형태 · 급여 최소 · 급여 최대 · 소스 · 범위. 명시적 적용 버튼(및 초기화, 각 필드에서 Enter)이 필터를 재실행하고, 페이지의 힌트가 사용법을 설명합니다. 이제 급여 범위가 실제로 필터링합니다 — 최소/최대를 설정하면 급여가 범위를 벗어난 공고 및 급여가 없는 공고가 제거됩니다(범위 겹침, 통화 무시). 근무 형태 필터에 원격 / 하이브리드 / 재배치와 함께 사무실 근무 옵션 추가. 신규 i18n 키 ×9, salaryInRange 엄격화, 스위트 1063/1063.
[1.67.1] — 2026-05-29
fix(scan): 소스별 fetch 타임아웃 30s → 10s로 단축(fail-fast). v1.67.0의 30s 상향은 느린 Ashby 보드의 절반 정도만 회복했고, 나머지(Perplexity, Supabase, Resend, DeepL, Ramp 등)는 마감과 무관하게 멈추므로 긴 타임아웃은 죽은 슬롯을 기다리며 전체 스캔을 지연시킬 뿐이었습니다. 10s는 만성 행에 즉시 실패하고 스캔 반응성을 유지합니다. SCAN_FETCH_TIMEOUT_MS로 재정의 가능. 스위트 1060/1060.
[1.67.0] — 2026-05-29
feat(scan): #/scan에 급여 범위(최소/최대) 필터 추가 및 소스별 fetch 타임아웃 연장. 결과 테이블에 텍스트·원격 필터 옆으로 숫자 입력 두 개(급여 최소 / 최대)가 추가됩니다. 각 행의 자유 텍스트 급여(от 100 000 до 200 000 ₽, 120000-150000 USD, $120K–$150K 등)를 숫자 범위로 파싱해 범위 겹침 방식으로 매칭합니다. 급여가 없는 행은 유지하므로 필터가 목록을 비우지 않고 좁힙니다(통화는 구분하지 않음 — 환율 변환 없음). 또한 소스별 스캔 fetch 타임아웃을 15s → 30s로 상향(SCAN_FETCH_TIMEOUT_MS로 재정의 가능). Ashby의 includeCompensation 페이로드는 ×8 동시성에서 흔히 15s를 초과해 매 스캔마다 ~30개 Ashby 보드가 타임아웃되었습니다. 신규 window.Skills.parseSalaryRange/salaryInRange + i18n ×9, 테스트 13개 추가, 스위트 1060/1060.
[1.66.0] — 2026-05-28
feat(scan): RU 소스가 첫 페이지뿐 아니라 모든 결과 페이지를 순회합니다. hh.ru, Habr Career, Trudvsem은 쿼리당 처음 ~50건만 가져왔지만, 이제 끝까지 페이지를 따라갑니다 — hh.ru/Habr는 &page=N, Trudvsem은 offset/meta.total — 페이지 간 중복 제거 및 새 항목이 없으면(또는 50페이지 안전 상한에서) 중단. “Backend разработчик” 같은 쿼리가 한 페이지가 아니라 전체 결과를 반환합니다(예: hh.ru PHP 17 → 3페이지에 55건 이상; Trudvsem 72건 전부). 페이지별 요청은 기존 타임아웃 + AbortSignal 유지. 테스트 4개 추가; 스위트 1045/1045.
[1.65.0] — 2026-05-28
feat(scan): hh.ru를 JSON API 대신 공개 웹사이트에서 스크랩 — 어떤 IP에서도 프록시 없이 작동. api.hh.ru가 IP나 User-Agent와 무관하게 모든 프로그램 클라이언트에 403 forbidden을 반환하기 시작했습니다(엣지 안티봇 차단). 반면 웹사이트(hh.ru/search/vacancy)는 브라우저류 클라이언트에 전체 결과를 제공하므로, 어댑터가 이제 그 HTML을 파싱합니다(Habr Career처럼). 1.64.0의 HH_PROXY 변수와 undici 의존성을 제거 — 프록시·키·User-Agent 설정이 필요 없습니다. HTML 파서용으로 테스트 재작성; 스위트 1041/1041.
[1.64.0] — 2026-05-27
feat(scan): HH_PROXY로 hh.ru 요청을 러시아 프록시로 라우팅. hh.ru는 API를 User-Agent가 아니라 IP로 차단합니다 — 따라서 HH_USER_AGENT만으로는 러시아 외 출구 노드의 403을 해제할 수 없었습니다. HH_PROXY에 러시아 HTTP/HTTPS 프록시 URL(예: http://user:pass@ru-host:port)을 지정하면 hh.ru 요청만 그 프록시를 통과하고 나머지 소스는 직접 연결을 유지합니다. undici의 ProxyAgent 기반(런타임 의존성 추가); HH_PROXY가 없으면 dispatcher는 전혀 추가되지 않습니다. 테스트 3개 추가; 스위트 1041/1041.
[1.63.2] — 2026-05-27
feat(scan): #/scan 콘솔에 실시간 % 진행률 + 소스별 상세. 진행 표시줄이 확정형이 되었습니다 — 스캐너가 진행 이벤트(EN: 회사별, RU: 쿼리별)를 SSE로 보내고, “Scanning… NN%” 라벨과 함께 막대가 채워집니다(애니메이션 줄무늬는 첫 이벤트까지만). 각 소스의 첫 실패(타임아웃 / 403 / 네트워크)는 콘솔에 상세히 기록되고 이후 반복은 억제됩니다. 신규 테스트 1개, 스위트 1040/1040.
[1.63.1] — 2026-05-27
style(scan): #/scan 진행 표시줄을 더 눈에 띄게. 진행 표시기에 보이는 “Scanning…” 라벨을 추가하고 막대를 8px(기존 4px)로 키워 스캔 중 분명히 보이게 했습니다. 동작 변경 없음.
[1.63.0] — 2026-05-27
feat(scan): 요청별 타임아웃 + #/scan 진행 표시줄. 소스 요청에 마감 시간이 없어, 멈춘 업스트림(예: 차단된 IP에서의 api.hh.ru)이 스캔 전체를 멈출 수 있었습니다. 새 server/lib/fetch-timeout.mjs 가 스캐너의 fetchImpl 을 감싸(makeTimeoutFetch, 기본 15초, SCAN_FETCH_TIMEOUT_MS 로 변경) 각 요청에 엄격한 마감을 부여합니다. 타임아웃된 소스는 비치명적 오류로 기록되고 스캔은 계속됩니다. #/scan 은 스캔 중 진행 표시줄을 보여줍니다(9개 로케일 scan.progress). 신규 테스트 7개, 스위트 1039/1039.
[1.62.3] — 2026-05-27
docs: 설치 명확화(career-ops-ui 는 career-ops/web-ui/ 안에서 실행)+ init 문제 해결, 9개 로케일 전체. 설치 섹션을 Option 1(원커맨드 curl)/ Option 2(기존 career-ops 프로젝트 안에 web-ui 로 UI 클론)+ CLI 명령어 + 공급자 설정 + Troubleshooting init 블록으로 재작성. 중첩 구조 안내를 /help §1 Setup 에도 추가하고, README 하이라이트에서 v1.62.* 라인 전체를 요약. 문서만 변경, 코드 변경 없음.
[1.62.2] — 2026-05-27
fix(help): #/help 필터가 이제 전문 검색입니다(RSS 같은 H3 하위 섹션도 검색됨). 도움말 페이지의 검색/TOC 필터는 이전에 H2 섹션 제목만 일치시켰기 때문에 v1.62.x의 RSS 문서(§5 Portals & sources 아래 H3)를 찾을 수 없었습니다. 이제 각 섹션의 본문이 필터에 인덱싱되어 예를 들어 “RSS”를 검색하면 §5가 표시됩니다. 클라이언트 측만 변경, API 변경 없음.
[1.62.1] — 2026-05-27
feat(scan): 소스 필터에 RSS 추가 + RSS 위치 수정. #/scan 소스 필터 드롭다운에 이제 RSS 가 표시됩니다(server/lib/sources/registry.mjs 와 SPA 폴백 목록에 추가). 따라서 RSS 보드(LaraJobs, WeWorkRemotely 등) 결과도 다른 ATS 소스처럼 필터링됩니다. RSS 어댑터는 더 이상 피드의 <category> 태그를 location 에 매핑하지 않습니다 — 위치가 아닌 태그가 location_filter 로 하여금 원격 직무를 잘못 제외하게 만들었기 때문입니다. 이제 location 은 비어 있어 피드가 위치 필터를 통과합니다. 스캔 버튼 툴팁/레이블과 소스 목록 i18n 문자열을 9개 로케일 전체에서 업데이트(Workable / SmartRecruiters / Workday / RSS). i18n 스냅샷과 소스 엔드포인트 테스트(EN 6 → 7) 업데이트.
[1.62.0] — 2026-05-27
feat(scan): 비-ATS 채용 보드를 위한 범용 RSS 어댑터. 새 rss 어댑터(server/lib/portals/adapters/rss.mjs + server/lib/sources/rss.mjs)를 통해 스캐너가 모든 RSS 피드(LaraJobs, WeWorkRemotely, RemoteOK, golangprojects 등 Greenhouse/Ashby/Lever 외 보드)에서 채용 공고를 가져올 수 있습니다. 새 의존성 없음: 피드 파싱은 정규식 기반이며 CDATA와 HTML 엔티티를 지원합니다(제목/회사명 태그 제거, astral 코드포인트 안전 디코딩). portals.yml의 provider: rss / rss: / feed_url:로 회사별 활성화되어 ATS에 이미 매칭된 회사를 가로채지 않습니다. ALL_ADAPTERS가 6 → 7로 증가. 신규 테스트 29개; 9개 README 로케일에 문서화.
[1.61.1] — 2026-05-22
fix(i18n): 테마 토글의 title + aria-label을 9개 로케일 전체에서 현지화 (MINOR-001). 다크/라이트 테마 버튼(#theme-toggle)이 index.html에 title="Toggle theme"과 aria-label="Toggle theme"을 하드코딩하여 — 모든 로케일에서 툴팁과 스크린리더 텍스트가 번역되지 않았습니다. 새 top.themeToggle 키 + applyI18n()의 data-i18n-title 핸들러(v1.58.15 검색 aria-label 수정과 동일 패턴)가 부팅 시와 언어 전환 시마다 두 속성을 현지화합니다. tests/playwright-theme-toggle-i18n.mjs(9개 로케일 + 런타임 전환)와 정적 가드 2개로 잠금. v1.61.0 프랑스어 사인오프의 유일한 LOW 항목. (MINOR-001)
[1.61.0] — 2026-05-22
feat(i18n): 프랑스어를 9번째 UI 언어로 추가. 새 로케일별 사전 public/js/lib/locales/i18n-dict.fr.js(window.__I18N_DICT_FR)는 영어와 668개 키 완전 동등성을 가지며, 새 도움말 번들 docs/help/fr.md(19 H2 / 73 H3, en과 정확한 구조 동등성). fr는 언어 전환기와 브라우저 자동 감지(i18n.js), 어셈블러(i18n-dict.js), index.html(어셈블러 앞의 <script> 태그), 테스트 스냅샷, 모든 테스트 로케일 목록에 등록됨. 초기 번역 표는 PR #9(커뮤니티 기여)에서 가져옴. 로직 변경 없음: t()와 모든 뷰는 그대로. 단위 테스트 1001 / 1001; Playwright 로케일 스윕은 9개 서브테스트로 확장. (FR-LOCALE)
[1.60.0] — 2026-05-22
refactor(i18n): 8개 언어 통합 파일을 로케일별 파일로 분리 (I18N-SPLIT). 번역 사전이 하나의 public/js/lib/i18n-dict.js에 있었으나, 이제 public/js/lib/locales/ 아래 언어당 한 파일과 공용 i18n-dict.aliases.js로 나뉘어 번역가가 한 언어만 독립적으로 편집할 수 있습니다(i18next / OpenWA 방식). i18n-dict.js는 동일한 window.__I18N_DICT를 다시 조립하는 조립기가 되어 t()와 모든 화면은 그대로입니다. <script src>로 동기 로드 — 빌드·fetch 없음. 스냅샷으로 무손실 마이그레이션 검증(678개 키). 도구와 ~25개 테스트를 분리 인식하도록 수정; 신규 tests/i18n-locale-files.test.mjs, tests/playwright-locale-sweep.mjs(모든 페이지 × 8개 로케일, 실제 Chromium). 994 → 1000 단위 · 62 → 70 Playwright. 동작 변화 없음. (I18N-SPLIT)
[1.59.13] — 2026-05-21
fix(i18n): @alias 로 진짜 중복 키 통합 + 개인정보 최종 정리. 메인테이너 실명을 테스트 픽스처/QA 리포트에서 제거(→ Jane Doe), LICENSE/package.json 을 Fighter90 핸들로 변경. @alias 로 8개 로케일 동일한 10개 키 통합. nav.config/config.title 은 스페인어에서 달라 통합 안 함. 991 → 994 테스트. (I18N-CL3)
[1.59.12] — 2026-05-21
fix(i18n): i18n-dict.js 정리 — fr 로케일 이전 (I18N-CL1, I18N-CL2, I18N-CL4). training.coursePh 의 개인 데이터를 일반 플레이스홀더로 교체, followup.lastPh 를 고정 날짜에서 형식 힌트로 변경, npm run audit:i18n 추가. 중복 값 그룹은 의도적(서로 다른 UI 역할) — 사전 헤더 참고. (I18N-CL1, I18N-CL2, I18N-CL4)
[1.59.11] — 2026-05-21
fix(test): v1.59.11 — e2e-comprehensive 스위트가 23/23 통과 (이전 11/23). Playwright 의 page.goto 가 해시만 바뀌는 경우 no-op 이라서 발생한 문제. 새 goRoute(hash) 헬퍼가 about:blank 를 거쳐 실제 내비게이션을 강제합니다. (e2e-harness-r1)
[1.59.10] — 2026-05-21
fix(api): NEW-F1-sub-r1 (v1.59.10) — 원시 .. 가드를 모든 /api 라우트 등록 위로 끌어올렸습니다. v1.59.8 버전은 app.all 뒤에 있어 발화되지 않았습니다. Express 정규화 이전에 실행됩니다. (NEW-F1-sub-r1)
[1.59.9] — 2026-05-21
fix(ux): UX-A5-r4 (v1.59.9) — Help TOC 스크롤 스파이에 data-toc-spy="active" 디버그 마커 + 동작 기반 락 테스트. 6번째 사이클. 동기 초기 페인트 + 이중 rAF 재계산 + resize 리스너 + hashchange 정리. (UX-A5-r4)
[1.59.8] — 2026-05-21
fix(ux+api): v1.59.8 — UX-A5-r3 + NEW-F1-sub (HIGH + LOW 묶음). FINAL-REGRESSION-v1.59.7 권고에 따른 doctrine 예외. UX-A5-r3: #/help 가 IntersectionObserver 를 scroll 리스너 + rAF 쓰로틀링으로 교체. NEW-F1-sub: /api/* 의 가공되지 않은 .. 를 404 JSON 으로 거부. (UX-A5-r3 · NEW-F1-sub)
[1.59.7] — 2026-05-20
fix(api): NEW-D3-cache (v1.59.7) — GET /api/cv 가 Cache-Control: no-store 전송. CV 는 사용자 핵심 아티팩트이므로 항상 재검증. (NEW-D3-cache)
[1.59.6] — 2026-05-20
feat(a11y): NEW-D2-motion (v1.59.6) — prefers-reduced-motion: reduce 존중. 새 @media 블록이 애니메이션·트랜지션·scroll-behavior 를 비활성화. (NEW-D2-motion)
[1.59.5] — 2026-05-20
fix(api): NEW-F1 (v1.59.5) — 알 수 없는 /api/* 가 모든 HTTP 동사에서 JSON 404 반환. app.get → app.all. (NEW-F1)
[1.59.4] — 2026-05-20
fix(ui): NEW-OR1 (v1.59.4) — #/config Active/Keys 칩 경쟁 조건 제거. atomic replaceChildren + 인-플라이트 토큰 + last-good 캐시. (NEW-OR1)
[1.59.3] — 2026-05-20
fix(ux): UX-A5-r2 (v1.59.3) — #/help 스크롤 스파이 강화. rootMargin 가시 영역을 10 %에서 25 %로 확대 + 마운트 시 초기 상태 계산 추가. (UX-A5-r2)
[1.59.2] — 2026-05-20
fix(ui): v1.59.2 — Active/Keys 칩: 카운트 정확화, 공급자 이름 대문자화, 겹침 제거. (post-v1.59.1 hotfix)
[1.59.1] — 2026-05-20
fix(test): v1.59.1 — NEW-D1 가드가 UX-A11 다듬어진 ES 복사를 허용합니다. (v1.59.1)
[1.59.0] — 2026-05-20
feat(ui): UX-A14 (v1.59.0) — 모바일 (≤ 420 px) 감사 패스. 새 @media (max-width: 420px) 블록에 5 가지 수정. (UX-A14)
[1.58.65] — 2026-05-20
test(ui): UX-A2 (v1.58.65) — Modes 구조화된 필드 폼 리그레션 락 테스트. v1.54.3 구현을 회귀로부터 보호합니다. (UX-A2)
[1.58.64] — 2026-05-20
fix(i18n): UX-A11 (v1.58.64) — es/pt-BR 사본 다듬기. 영어 차용어를 모국어 동등 표현으로 교체. (UX-A11)
[1.58.63] — 2026-05-20
fix(ui): UX-A15 (v1.58.63) — Dashboard Pipeline 타일에 시각적 주요 강조. Pipeline 타일은 이제 강조 테두리, 더 큰 아이콘, 굵은 라벨로 눈에 띄게 표시됩니다. (UX-A15)
[1.58.62] — 2026-05-20
feat(ui): UX-A9 (v1.58.62) — API keys 탭 상단의 sticky 요약 칩. #/config → API keys 탭 상단에 활성 공급자와 설정된 키 수를 표시하는 sticky 칩이 추가되었습니다. (UX-A9)
[1.58.61] — 2026-05-20
docs(readme): UX-A8 (v1.58.61) — 8 개 README 모두에 첫 실행 정리 섹션 추가. 첫 스캔 전 두 개의 QA 픽스처 URL 을 정리하는 make clean-test-fixtures 단계를 문서화했습니다. (UX-A8)
[1.58.60] — 2026-05-20
feat(ui): UX-A12 (v1.58.60) — 알림 드로어에 모두 지우기 + 항목별 닫기. 알림 패널에 전체 지우기 버튼과 항목별 × 버튼이 추가되었습니다. (UX-A12)
[1.58.59] — 2026-05-20
feat(ui): UX-A13 (v1.58.59) — #/health 실패 행에 실행 가능한 “Fix →” CTA. FAIL/OPTIONAL 행은 이제 해당 설정 탭으로 바로 이동하는 고스트 버튼을 표시합니다. (UX-A13)
[1.58.58] — 2026-05-20
fix(ux): UX-A10 (v1.58.58) — #/cv 의 저장되지 않은 편집 손실 방지. 브라우저 종료(beforeunload)와 SPA 내부 이동(hashchange) 시 더티 버퍼가 있으면 현지화된 확인을 표시합니다. (UX-A10)
[1.58.57] — 2026-05-20
test(ui): UX-A7 (v1.58.57) — cost-line 자동 갱신 계약에 대한 리그레션 락 테스트. providers-changed 이벤트의 디스패치/구독/모든 어드바이저 뷰 호출을 정적으로 보장합니다. (UX-A7)
[1.58.56] — 2026-05-20
fix(a11y): UX-A4 (v1.58.56) — .lang-btn 이 WCAG 2.5.8 최소 터치 타깃 크기를 충족. 이전에 언어 버튼은 높이 23–25 px 로 24×24 px 기준 미달이었으나, 이제 min-height: 28px + min-width: 28px 로 WCAG 2.2 AA 를 충족합니다. (UX-A4)
[1.58.55] — 2026-05-20
feat(ui): UX-A3 (v1.58.55) — Dashboard 활성 공급자 칩. #/dashboard 히어로에 현재 활성 LLM 공급자가 표시됩니다(⚡ Live evals: Anthropic claude-sonnet-4-6 또는 📋 Manual prompt mode). #/config 에서 LLM_PROVIDER 변경 또는 탭 포커스 복원 시 자동 갱신. (UX-A3)
[1.58.54] — 2026-05-20
fix(ux): UX-A1 (v1.58.54) — Deep 브리프 구조 방어 경고. 저장된 브리프에 표준 6개 섹션(Company snapshot / Engineering culture / Recent news / Glassdoor / Interview process / Negotiation leverage) 중 3개 미만이 포함되면 public/js/views/deep.js가 비차단 경고와 레퍼런스 링크를 앞에 표시합니다. UI 가드레일이며, 프롬프트 단계 수정은 상위 프로젝트에서 진행됩니다. (UX-A1)
[1.58.53] — 2026-05-20
fix(ux): UX-A6 — 모든 saved-card 가 단일 renderSavedCard() 헬퍼를 통과. 모든 렌더 경로에서 <span>+<time> 구조 보장. 948 → 949 유닛. (UX-A6)
[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.
[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/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 순서 감사는 대기 중. |
[1.16.0] — 2026-05-13
Auto-pipeline 마무리 + 어댑터 폴리시 + i18n long-tail. v1.15.0 REVIEW 의 11개 후속 작업을 모두 종료합니다: 서버 사이드 SSE auto-pipeline, POST /api/reports 프리미티브, Cmd+K 단축키, SmartRecruiters 페이지네이션, Workday CAPTCHA 폴백, CI 스크린샷 드리프트 게이트, 스캔 소스 필터 UX, 과거 CHANGELOG 번역 (v1.13.0/v1.12.0 × 6 로케일), 비영어 README 확장, paste-ready trending-companies importer.
✨ 기능
feat(auto-pipeline): server-side SSE orchestrator(#1, #2, #3, #8) — v1.15 의 클라이언트 사이드 chained-fetch 오케스트레이터는 사라졌습니다.POST /api/auto-pipeline은 이제 curl 가능한 SSE 엔드포인트로, validate → fetch JD → evaluate → save report → tracker 를 서버 사이드에서 실시간 단계 이벤트와 함께 수행합니다. 느린 Anthropic 호출 (30–90 초) 은 일반 spinner 대신running이벤트를 emit 합니다. 실패는step+message와 함께error를 emit 합니다. 오케스트레이터는 또한 report markdown 을 부모의reports/<slug>.md에 영속화합니다 (v1.15 에서 손실되었음).feat(reports): POST /api/reports primitive—server/lib/routes/reports.mjs의 신규 writer 엔드포인트. 경로 탐색 가드를 포함한 slug 정화 (sanitization) (선행 점 제거, 내부...축약). 1 MB 캡 (413).overwrite:true가 아니면 기존 파일에 대해 409.stripDangerousMarkdownXSS 패스를 통한 원자적 쓰기. activity.reports.save 로그. 테스트: 9 케이스.feat(app): Cmd+K paste URL → auto-pipeline— 글로벌 검색에 URL 을 붙여넣고 Enter 를 누르면 이제autoStart=true와 함께 AutoPipeline 모달이 열립니다. Shift+Enter 는 레거시 “파이프라인에만 추가” 경로를 보존합니다. 정규 career-ops.org Quick Start §7 의 “paste URL → done” UX 입니다.feat(portals): SmartRecruiters pagination(#4) —server/lib/sources/smartrecruiters.mjs가?limit=100&offset=N을 통해totalFound에 도달하거나 빈 페이지가 반환되거나 30 페이지 / 3000 잡 안전 캡이 발동될 때까지 페이지를 순회합니다. 호출자가 공급한 limit/offset 을 제거하여 커서가 서버 소유가 되도록 합니다. 큰 보드 (Procter & Gamble, Amazon 류) 가 더 이상 100+ 공고의 뒷부분을 잃지 않습니다. 테스트: 6 케이스.feat(portals): Workday CAPTCHA-fallback graceful(#7) —server/lib/sources/workday.mjs가 더 이상 4xx / 비-JSON / 네트워크 오류에 throw 하지 않습니다.[]를 반환하고 신규 exportlastWorkdayFallback스냅샷에 주석합니다. 스캐너 타임라인은 다음 테넌트로 계속됩니다. 호출자는strict:true로 v1.14 throw 동작에 다시 옵트인할 수 있습니다. 테스트: 7 케이스.
🛠️ 도구 + CI
ci(workflows): dashboard-screenshots drift gate(#5) — 신규.github/workflows/dashboard-screenshots.yml.public/css/app.css/public/js/views/dashboard.js/public/js/lib/i18n.js/public/index.html를 건드리는 PR 에서 workflow 는 /tmp scaffold 에 대해 web-ui 서버를 기동하고, Playwright + chromium 으로 8개 hero PNG 를 재생성하며, 커밋된 것과 결과가 어긋나면 빌드를 실패시킵니다. 실패 시 재생성된 PNG 를 CI 아티팩트로 업로드합니다.feat(scripts): import-trending-companies.mjs(#11) —docs/portals-examples.md의 13개 trending 회사를 실제 boards-API 로 검증하고 사용자의 부모portals.yml::tracked_companies에 붙여넣을 수 있는 YAML 을 출력합니다. slug 가 404 인 후보에는enabled: false가 찍힙니다. 6개 ATS 모두 라이브 프로브 (Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday).npm run import:trending으로 실행.feat(scripts): npm run capture:dashboards—scripts/capture-dashboard-screenshots.mjs를 top-level 스크립트로 노출합니다 (이전에는images/README.md에만 문서화됨).
🎨 UX
fix(scan): consolidated source-filter dropdown(#6) —#/scan소스 드롭다운이 v1.14 어댑터 레지스트리에서 재구성되었습니다: 6개 ATS + hh.ru + Habr Career, 알파벳순, 지역 prefix 없음.runEnScan/runRuScan이 이제 폐기된/api/stream/scan-{en,ru}별칭 대신 통합된/api/stream/scan?source={ats,regional}엔드포인트를 호출합니다 (Sunset 헤더는 v1.16 까지 라이브 유지).
📚 i18n long-tail
docs(i18n): translate v1.13.0 + v1.12.0 CHANGELOG in 6 locales(#9) —CHANGELOG.{es,pt-BR,ko-KR,ja,zh-CN,zh-TW}.md에 이전에 RU 본문이었던 항목들이 이제 실제 로케일에 있습니다. 각 비영어/비러시아어 CHANGELOG 에는 pre-v1.12 항목이 프로젝트 관례에 따라 RU 로 유지된다는 i18n 노트도 포함됩니다 (정규 텍스트는CHANGELOG.md에 있음).docs: expand non-EN READMEs with v1.16.0 highlights section(#10) — 6개 비영어 README (es / pt-BR / ko-KR / ja / ru / zh-CN / zh-TW) 가 ~35 줄의 새 섹션을 받습니다: auto-pipeline 원클릭 흐름 + curl 예제, SmartRecruiters 페이지네이션, Workday 폴백, 스캔 소스 필터 UX, importer 스크립트, CI 스크린샷 워크플로. RU README 도 확장되었습니다.
🧪 테스트
- 신규
tests/reports-write.test.mjs(9 케이스) — happy path, slug 정화 (경로 탐색 가드 포함), 409 충돌, overwrite 플래그, XSS strip, 필드 누락 시 400, >1 MB 시 413, GET/POST 라운드트립. - 신규
tests/auto-pipeline.test.mjs(5 케이스) — SSE 프레이밍, 잘못된 URL 게이트, SSRF/loopback 게이트, LLM 키 없음 오류 경로,text/event-streamContent-Type 헤더. - 신규
tests/smartrecruiters-pagination.test.mjs(6 케이스) — 단일 페이지, 3 페이지, 빈 페이지 조기 종료, 하드 캡 준수, 쿼리 strip, 503 throws. - 신규
tests/workday-fallback.test.mjs(7 케이스) — happy path, 403/429 graceful, 비-JSON 본문, 네트워크 오류, 4xx 및 네트워크 오류 양쪽의 strict 옵트인. - 합계: 427 / 427 유닛 (이전 400; +27 순증). 0 실패. 28/28 Playwright + 23/23 comprehensive E2E + 20/20 smoke E2E 가 v1.15.0 기준선에서 모두 통과.
Out of scope (v1.17+)
| 항목 | 비고 |
|---|---|
| 정규 A-F 프롬프트의 부모 커밋 | upstream 의 santifer/career-ops::modes/oferta.md 재작성이 여전히 대기 중 (CLAUDE.md 하드 룰 #1). |
| pre-v1.12 CHANGELOG 항목 번역 (v1.11.x, v1.10.x) | 관례 유지: RU 본문. 백포트는 ~1800 줄 번역 작업; 연기. |
| 비영어 README 의 전체 패리티 (EN 처럼 585줄) | v1.16 이 로케일당 ~35 줄 추가; 전체 패리티는 별도 작업입니다. |
Workday 폴백 주석을 읽어 🔒 chip 을 렌더링하는 서버 사이드 runEnScan |
lastWorkdayFallback export 가 연결되어 있습니다; SPA 의 Active Companies 카드는 v1.17+ 에서 이를 소비합니다. |
검증
npm test # 427 / 427
npm run test:e2e:full # 23 / 23
npm run import:trending --check-only # probe 13 trending boards
# Auto-pipeline curl smoke:
curl -N -X POST http://127.0.0.1:4317/api/auto-pipeline \
-H 'Content-Type: application/json' \
-d '{"url":"https://job-boards.greenhouse.io/anthropic/jobs/4567"}'
# POST /api/reports round-trip:
curl -X POST http://127.0.0.1:4317/api/reports \
-H 'Content-Type: application/json' \
-d '{"slug":"smoke","markdown":"# smoke\n"}'
[1.15.0] — 2026-05-13
문서 적합성 (Doc-conformance) 릴리스. 적합성 감사 (qa/conformance-vs-docs/00-CONFORMANCE-REPORT.md) 에서 여전히 열려 있던 10개 결과 중 9개와 현지화된 hero 이미지를 종료합니다. CLI 가 약속하는 동일한 파이프라인이 모든 로케일에서 브라우저를 통해 end-to-end 로 동작하도록 UI 를 정규 career-ops.org/docs 워크플로에 일치시킵니다.
✨ 기능
feat(auto-pipeline): PR-C — 1-click "paste URL → report + PDF + tracker row"(G-007) — 정규 career-ops.org 약속을 충족합니다. v1.15 이전에 사용자는 /#/pipeline → /#/evaluate → /#/cv → /#/tracker 에 걸쳐 5번 수동 클릭했습니다. 이제 /#/dashboard 의 단일 ✨ 버튼이 체인합니다: validate URL → fetch JD (SSRF-safe) → evaluate against CV → generate PDF → add tracker row. 단계별 모달 타임라인을 단계당 [✓]/[…]/[✗] 로 렌더링합니다. JD 첫 줄에서 휴리스틱한 회사/역할 추출. 평가 markdown 에서 정규표현식으로 점수 + 정당성 추출. 신규 파일:public/js/lib/auto-pipeline.js. 신규 i18n 키 19개 × 8 로케일.feat(modes): PR-D — modes/_profile.md editor as #/config → Modes tab(G-008) — Quick Start §Step-5 의 정규 “Career framing” 파일이 이전에는 UI 사용자에게 보이지 않았습니다. 이제 /#/config 의 신규 “Modes” 탭과 /#/profile 의 발견 가능한 카드로 노출됩니다. 신규 엔드포인트: 256 KB 캡,stripDangerousMarkdownXSS 패스, 첫 읽기 시_profile.template.md로부터 scaffold 하는GET/PUT /api/modes/_profile. 신규 i18n 키 9개 × 8 로케일.feat(profile): PR-E — accept canonical schema; add location + headline(G-009) —/api/profile이 이제 레거시 (candidate:{...}) 와 정규 (최상위full_name,narrative.headline,target_roles.primary,compensation.target_range) 스키마를 모두 수용합니다. 둘 다 있을 때는 레거시가 우선하므로 기존 YAML 이 동일하게 렌더링됩니다. 신규summarizeProfile()헬퍼가 통합된 형태를 반환합니다./#/profile이narrative.headline을 새 카드로 노출합니다. 신규 i18n 키 2개 × 8 로케일.feat(tracker): PR-B — Legitimacy column on #/tracker(G-006) — career-ops.org/docs 의 정규 파이프라인 출력 표와의 패리티를 복원합니다. Status 와 PDF 사이에 Legitimacy 열을 추가하고 badge-ok/warn/bad 틴팅 (statusClass 패턴을 미러링). Graceful degrade — Legitimacy 열이 없는 v1.15 이전 행은—를 표시합니다. 신규 i18n 키 1개 × 8 로케일.fix(routing): PR-H — dedupe sidebar; route #/batch to v1.13.0 TSV SPA(G-011) — 이 수정 이전에는 /#/batch 가 사이드바에 두 번 등록되어 있었고 둘 다 레거시 mode-prompt builder 로 갔습니다. v1.13.0 TSV SPA (8 KB, 4 엔드포인트) 는 접근 불가능했습니다. 중복 사이드바 항목을 제거; mode slugbatch→batch-prompt로 이름을 바꾸고 deprecation 배너 추가. 정규 /#/batch 는 이제 TSV SPA 입니다.
📚 문서
docs(evaluate): PR-A — realign Block A-F with canonical career-ops.org rubric(G-005) — career-ops.org 문서는 A–F 를 사용합니다 (Strategy/Personalization/STAR stories 가 C/E/F). 우리는 의미가 옮겨진 A–G 를 emit 했습니다 (Risks/Verdict/Legitimacy). v1.15 는 8개 help 번들의 §9 를 정규 A–F 로 보여주도록 갱신하고 “v1.15 이전은 A–G 를 사용; 하위 호환을 위해 그대로 렌더링” 콜아웃을 추가합니다.eval.subtitlei18n 키 × 8 로케일도 재정렬되었습니다. 점수 + 정당성이 이제 보고서 헤더 필드로 문서화됩니다. ⚠ 부모 커밋이 여전히 필요합니다:santifer/career-ops::modes/oferta.md가 upstream 에서 재작성되어 정규 A–F 를 emit 해야 합니다.docs: PR-F — seniority_boost + search_queries in help §5 across 8 locales + scaffold(G-010) — 8개 번들의 Help §5 가 이제 세 번째 title-filter 키 (seniority_boost) 를 문서화하고 AI 기반 Option B 스캔만을 구동한다는 점을 명확히 하는 번역된 1단락 도입과 함께search_queries예제 블록을 갖습니다.bin/setup.shportals.yml scaffold 가 기본으로seniority_boost: ["Senior", "Staff", "Lead"]를 시드합니다. H2 패리티 유지: 16 × 8 로케일.docs: PR-I — localized hero images per README locale— 8개 README 각각이 이제 (scripts/capture-dashboard-screenshots.mjs를 통해 Playwright + chromium 으로 생성된) 로케일별images/dashboard-<locale>.png(HiDPI 1440×900) 을 갖습니다. 옛 공유public/images/screen_vacancy_found.png는 삭제되었습니다. 비영어 독자가 첫 진입에서 자신의 언어로 라벨링된 UI 를 봅니다.
🧹 이월 정리
PR-G — G-001scan.noResultsi18n 번들: “EN or RU scan” 리터럴을 포함한 8개 문자열을 로케일 깔끔한 문구로 교체.PR-G — G-002📄 Generate PDF 버튼이 이제 #/interview-prep 결과 패널에 등장합니다 (deep.js 패턴 미러링).PR-G — G-003README.cn.md→README.zh-CN.md(정규 로케일 태그); 형제 파일들과 tests/canonical-docs-coverage.test.mjs 에서 참조 일괄 수정.PR-G — G-004/api/stream/scan-en+scan-ru가 이제 RFC 8594 Sunset + Deprecation + Link 헤더를 emit 합니다 (sunset 2026-10-01). v1.16.0 에서 제거 예정.
🧪 테스트
- 신규
tests/profile-canonical-schema.test.mjs(6 케이스) — 정규 YAML, 레거시 YAML, 혼합 (레거시 우선), 정규 전용 수용, 둘 다 아닌 형태 거부, 보상 범위 파싱. - 신규
tests/modes-profile-crud.test.mjs(8 케이스) — 비어 있을 때 내장 scaffold, template-takeover, persisted-wins, 쓰기 happy-path, 정화, 비문자열 시 400, >256 KB 시 413, 범용 /api/modes/:name 도 여전히 동작. - 테스트 픽스처의 격리 회귀 수정: 테스트가 이제
before/after + dynamic-import패턴을 사용 (tests/batch-endpoints.test.mjs와 일치) 하여 더 이상 사용자의 실제 부모config/profile.yml을 변경하지 않습니다. 사용자 주의: v1.15.0-RC 빌드에서 업그레이드한 후config/profile.yml이 테스트 placeholder 처럼 보인다면 백업에서 복원하십시오 — 회귀는 dev 브랜치에만 존재했습니다. - 합계: 400 / 400 유닛 (이전 386; +14 순증). 0 실패. 20/20 smoke E2E + 23/23 comprehensive E2E + 28/28 Playwright 가 v1.14.0 기준선에서 모두 통과.
Out of scope (v1.16+ 후속)
| 항목 | 비고 |
|---|---|
| 정규 A–F 프롬프트의 부모 커밋 | santifer/career-ops::modes/oferta.md 가 upstream 에서 재작성되어야 합니다. CLAUDE.md 하드 룰 #1 이 부모 파일 편집을 금지합니다. web-ui 쪽은 이미 완료되었습니다 (graceful degrade — v1.15 이전 A–G 보고서는 변경 없이 렌더링됨). |
서버 사이드 POST /api/auto-pipeline SSE |
클라이언트 사이드 오케스트레이터가 UX 승리를 출하했습니다. 서버 사이드 엔드포인트는 retry-from-step-N + curl 가능한 CI 를 가능하게 합니다. |
POST /api/reports 프리미티브 |
Auto-pipeline 이 현재 report markdown 을 인라인으로 보여주지만 부모 reports/ 에 영속화하지 않습니다. PDF + tracker 행이 영구 아티팩트입니다. |
| Cmd+K paste-URL → run auto-pipeline | v1.16+ 로 연기. |
검증
npm test # 400 / 400
npm run test:e2e:full # 23 / 23
curl -sf http://127.0.0.1:4317/api/health | jq '.checks | length' # → 18
curl -sI http://127.0.0.1:4317/api/stream/scan-en | grep -i sunset # G-004 visible
curl -sf http://127.0.0.1:4317/api/modes/_profile | jq '.scaffolded' # G-008 wired
ls images/dashboard-*.png | wc -l # 8 (PR-I)
grep -c 'href="#/batch"' public/index.html # 1 (PR-H dedupe)
[1.14.0] — 2026-05-13
v1.13.0 의 레지스트리 위에 3개의 신규 ATS 어댑터가 도착하면서 지원 ATS 가 3 → 6 으로 늘었습니다 (Greenhouse / Ashby / Lever + Workable / SmartRecruiters / Workday-beta). 17개 파일에 걸친 사용자 대상 문서를 한 번에 “3 ATSes” 에서 “6 ATSes” 로 일괄 갱신했습니다 (42개 문구 업그레이드) — README × 8 로케일, help 번들 × 8 로케일, PROJECT.md. 부모 portals.yml 을 위한 13개 trending 회사의 paste-ready YAML 블록을 docs/portals-examples.md 에 추가합니다.
✨ 기능
feat(portals): 3 new ATS adapters — Workable, SmartRecruiters, Workday-beta— 레지스트리가 이제 6개 ATS 를 해석합니다 (이전 3). 신규 파일:server/lib/portals/adapters/{workable,smartrecruiters,workday}.mjs(각각 신규 sources 를 감싸는 얇은 균일 계약 래퍼) 와server/lib/sources/{workable,smartrecruiters,workday}.mjs(raw HTTP +source: <id>가 포함된 정규{ id, title, company, url, location, isRemote, … }형태로의 응답 정규화).- Workable:
apply.workable.com/<slug>와 레거시<subdomain>.workable.com을 감지. 엔드포인트:https://apply.workable.com/api/v3/accounts/<slug>/jobs?details=true. - SmartRecruiters:
jobs.smartrecruiters.com/<slug>와careers.smartrecruiters.com/<slug>를 감지. 엔드포인트:https://api.smartrecruiters.com/v1/companies/<slug>/postings. - Workday (beta):
<tenant>.wd<N>.myworkdayjobs.com/<lang>/<site>를 감지. 엔드포인트:/wday/cxs/<tenant>/<site>/jobs로 POST. careers_url 이 site 를 생략하면site=External기본. 일부 테넌트가 CXS 를 CAPTCHA 뒤에 두기 때문에 beta — 그럴 경우 부모의/career-ops scan(Playwright 구동) 으로 폴백합니다.
- Workable:
📚 문서
docs(portals-examples): trending boards block—docs/portals-examples.md에 v1.14.0 섹션이 추가되어tracked_companies용 paste-ready YAML 로 13개 trending 회사를 나열합니다: Greenhouse-hosted (Stripe, GitLab, HashiCorp, Cloudflare, Datadog, Hugging Face) 와 Ashby-hosted (Notion, Linear, PostHog, Replicate, Modal Labs, Fly.io, Render). 각 항목은enabled: false로 표시되어 사용자가 활성화 전에 slug 가 응답하는지 검증하도록 합니다. 더하여 각 패턴을 감지하는 URL 패턴이 포함된 Workable / SmartRecruiters / Workday 예제 블록.docs(framing): 42 ATS-phrase upgrades across 17 user-facing docs— 사용자 대상 문서의 “Greenhouse / Ashby / Lever” 가 등장하는 모든 곳이 이제 “Greenhouse / Ashby / Lever / Workable / SmartRecruiters / Workday” 로 표시됩니다. 영향: README × 8 로케일 (EN/ES/PT-BR/RU/JA/KO/CN/TW), help 번들 × 8 로케일, PROJECT.md. 과거 CHANGELOG 항목과 버그 수정 처방 문서 (qa/fixes/F-014,qa/FIX-PROMPT) 는 의도적으로 손대지 않았습니다 — 과거 또는 이미 올바른 상태를 기술합니다.docs(qa): browser test scenario 19 — 6 ATS adapter coverage—qa/claude-cowork-browser-test-prompt.md에 Scenario 19 가 추가되었습니다:ALL_ADAPTERS.length === 6불변성, 6개 어댑터 모두에 대한resolveAdapter()URL 감지 sweep,#/scan의 Active Companies 카드 soft-check,docs/portals-examples.md의 ATS 별 블록 구조 검사.
🧪 테스트
tests/adapter-registry.test.mjs가 3개의 신규 어댑터에 대한 7개 신규 테스트로 확장되었습니다 (Workable apply-URL 패턴, Workable 레거시 서브도메인 패턴, SmartRecruiters jobs.* + careers.* 패턴, 명시적 site 를 가진 Workday tenant.wd5.*, “External” 로의 Workday 기본 site 폴백,ALL_ADAPTERS.length === 6불변성,detectApi()레거시 형태 호환성).- 합계: 386 / 386 유닛 (이전 379; +7 순증). 0 실패.
검증
npm test # 386 / 386
node -e "import('./server/lib/portals/registry.mjs').then(m => console.log(m.ALL_ADAPTERS.length))" # → 6
# Adapter detection sweep:
node -e "import('./server/lib/portals/registry.mjs').then(m => {
console.log(m.resolveAdapter({ careers_url: 'https://apply.workable.com/foo/' }).adapter.id); // → workable
console.log(m.resolveAdapter({ careers_url: 'https://jobs.smartrecruiters.com/Bar' }).adapter.id); // → smartrecruiters
console.log(m.resolveAdapter({ careers_url: 'https://baz.wd5.myworkdayjobs.com/en-US' }).adapter.id); // → workday
})"
Out of scope (연기된 후속)
| 항목 | 비고 |
|---|---|
| 13개 trending Greenhouse/Ashby 회사를 위한 회사별 어댑터 레코드 | docs/portals-examples.md v1.14.0 블록이 사용자 paste 가능한 YAML 로 이들을 나열합니다; slug 검증 + 부모 portals.yml 로의 일괄 추가는 별도 단계입니다. |
| Workday CAPTCHA 폴백 자동화 | Workday 어댑터는 CXS 피드가 게이팅되면 throw 합니다; 계획된 폴백은 부모의 /career-ops scan (Playwright) 에 위임합니다. SPA 의 “scan” UX 에 이를 연결하는 것은 v1.15+ 입니다. |
[1.13.0] — 2026-05-13
큰 슬라이스. v1.12.0 이후 백로그의 연기된 4개 항목을 한 릴리스로 모두 종료합니다: PR-4 (완전한 multer 파이프라인), 어댑터 레지스트리 (아키텍처적 F-018 후속), Batch evaluate SPA 페이지, 로케일 인식 mode-template scaffolding. 더하여 세션 중간에 발견된 다크 테마 테이블 수정.
✨ 기능
feat(cv): multer-based multipart upload (PR-4 full)—/api/cv/import가 이제 원본 octet-stream 계약 (Content-Type: application/octet-stream+X-Filename) 과 multer 로 적절히 파싱되는multipart/form-data양쪽을 수용합니다. v1.10.2 의 415-거부는 임시방편이었습니다; v1.13.0 이 진짜 수정입니다. 외부 클라이언트 (curl-F, Postman 기본, 모든 HTTP 클라이언트) 가 매끄럽게 동작합니다. 두 경로 모두 동일한importDocumentToMarkdown변환기 +stripDangerousMarkdownXSS 패스를 거칩니다. 신규 의존성:multer ^2.1.1.feat(portals): adapter registry— Greenhouse / Ashby / Lever fetcher 를 균일 계약 (id,label,matches,buildEndpoint,fetch) 으로server/lib/portals/adapters/*.mjs에 추출했습니다. 신규server/lib/portals/registry.mjs::resolveAdapter()가 단일 디스패치 표면입니다.en-scanner.mjs::detectApi()+FETCHERS가 이제 레지스트리에 위임합니다; 레거시 반환 형태는 보존됩니다. 새 ATS 추가 =adapters/아래 파일 하나를 떨어뜨리고ALL_ADAPTERS에 추가 — 스캐너 변경이 필요 없습니다.feat(batch): #/batch evaluate page— 신규 SPA 뷰 + 4개 엔드포인트 (GET /api/batch,PUT /api/batch,GET /api/stream/batch,POST /api/batch/merge).batch/batch-input.tsv용 TSV 편집기, parallel/min-score/dry-run/retry 컨트롤,bash batch/batch-runner.sh의 라이브 SSE 로그, 실행 후batch/tracker-additions/목록과 원클릭node merge-tracker.mjs. Decision 그룹 아래의 사이드바 링크. 신규 i18n 키 21개 × 8 로케일.feat(prompts): locale-aware mode scaffolding—buildModePrompt+buildEvaluationPrompt가 이제 부모의 영어 mode-template 본문을 8개 로케일에서 현지화된 scaffolding 텍스트 (역할 줄, “Read these files first”, “User-supplied context”) 로 감쌉니다. 부모의modes/<slug>.md본문은 영어로 유지됩니다 (CLAUDE.md 하드 룰 #1 에 따라 읽기 전용); career-ops-ui 의 주변 scaffolding 이 번역됩니다.
🎨 UX 수정
fix(theme): dark-mode table hover + tab-btn— 하드코딩된#fafafa/#fff/#f7f7f7가var(--beach)/var(--paper)/var(--slate)토큰으로 교체되어 다크 팔레트 교체가 실제로 테이블 행과 탭 버튼에 도달합니다. boosted 스캔 행을 위한.row-boosted액센트 strip 을 추가했고 두 테마 모두에서 동작합니다.
🧪 테스트
- 신규
tests/adapter-registry.test.mjs(7 케이스) — 균일 계약, ATS 별 URL 감지, 명시적api:필드 우선, 일치 없을 시 null, 레거시detectApi()형태 보존. - 신규
tests/batch-endpoints.test.mjs(5 케이스) — 빈 픽스처, TSV 라운드트립, URL 없음 거부, 1 MB 캡, 러너 누락 오류 프레임. - 신규
tests/locale-scaffold.test.mjs(6 케이스) — en/ru/ja/ko 의 scaffold 문자열,buildModePrompt/buildEvaluationPrompt통합, 영어 하위 호환. tests/cv-upload-multipart-reject.test.mjs재작성 — 기존의 “multipart 는 415 를 반환” 계약이 이제 “multipart 가 multer 로 파싱됨” 계약입니다; cv.md 에 부작용 없음 불변성은 보존됩니다.- 합계: 379 / 379 유닛 (이전 360; +19 순증). 0 실패.
- 커버리지: 95.46 % 라인 / 84.06 % 브랜치.
- 20/20 smoke E2E · 23/23 comprehensive E2E · 28/28 Playwright.
Out of scope (연기된 후속 작업)
| 항목 | 비고 |
|---|---|
| 14개 신규 포털 어댑터 (Workable / SmartRecruiters / Workday / GitLab / HashiCorp / Cloudflare / Datadog / Stripe / Notion / Linear / Posthog / Hugging Face / Replicate / Modal Labs / Fly.io / Render) | 어댑터 레지스트리가 자리잡았습니다 — 신규 어댑터 추가는 이제 각각 파일 하나입니다. 14개 ATS 에 대한 포털별 리서치 + URL 패턴 + 엔드포인트 정규화는 별도 단계입니다. |
부모의 modes/<slug>.md 본문 번역 |
부모 파일은 CLAUDE.md 하드 룰 #1 에 따라 읽기 전용입니다. v1.13.0 의 로케일 인식 scaffolding 이 80 % 를 해결해주며; 전체 본문 번역은 santifer/career-ops 로의 upstream PR 이 필요합니다. |
문서
docs/reviews/REVIEW-2026-05-13-v1.13.0.md— 세션 컨텍스트 + 어댑터 레지스트리 계약 + batch 흐름.- 모든 8개 README: 배지 갱신 (tests 360 → 379, release v1.12.0 → v1.13.0).
- 모든 8개 CHANGELOG 가 이 항목을 받습니다.
[1.12.0] — 2026-05-13
버그 수정 + UX + 브랜딩 패스. v1.11.1 이후 정직한 백로그에서 8개 항목을 종료합니다 (테스트 갭 #9–12, 콘솔 오류 #8, portals-dead 드리프트 #4, seniority_boost surface #6, F-018 엔드포인트 통합). 다크/라이트 테마 토글을 추가하고 모든 문서, 패키지 메타데이터, GitHub 저장소 설명에서 “Airbnb-styled” 브랜딩을 제거합니다.
✨ 기능
feat(theme): dark/light toggle (v1.12.0)— top bar 의 신규 테마 버튼. light ↔ dark 순환;localStorage.theme에 영속; 페이지 로드 시 pre-paint 부트스트랩 (public/js/lib/theme-bootstrap.js) 으로 복원되어 사용자가 잘못된 색상 스킴의 플래시를 보지 않습니다. 첫 방문자에게는prefers-color-scheme을 존중.public/css/app.css의[data-theme="dark"]아래 전체 다크 팔레트 — 모든 컴포넌트가 CSS 사용자 속성을 읽으므로 교체가 한곳에서 중앙 집중됩니다.- **
feat(scan): /api/stream/scan?source=ats|regional|both(F-018 LITE)** — 단일 통합 SSE 엔트리포인트. SPA 가 이제 두 단계 (ATS 먼저, 그다음 regional) 를 순차 구동하는 하나의 이벤트 스트림을 엽니다. 두 개의 별도 스트림을 체인하는 대신입니다. 레거시/api/stream/scan-en+/api/stream/scan-ru는 폐기된 별칭으로 활성 유지됩니다. runners-table/api/stream/scan이 네임스페이스를 비우기 위해/api/stream/scan-parent로 이름이 변경되었습니다; 부모가 spawn 하는scan.mjs` 폴백은 보존됩니다. feat(scan): seniority_boost surface (canonical docs §3)—en-scanner.mjs와ru-scanner.mjs모두 이제portals.yml::title_filter.seniority_boost를 읽고 매칭되는 잡에_boosted: true+_boostedBy: <keyword>를 찍습니다. SPA 는 boosted 행을#/scan결과 상단으로 정렬하고 title 어트리뷰트에 매칭 키워드를 담은⬆ boosted배지를 렌더링합니다. 신규 i18n 키 2개 (scan.boosted,scan.boostedBy) 가 8개 로케일에서 현지화됩니다.
🐛 버그 수정
fix(ui): null-safe error message reads in 4 places (#8)—app.js(top bar doctor 버튼 + global-search pipeline add),views/tracker.js(112번 줄),views/apply.js(21번 줄),views/evaluate.js(32번 줄) 가 모두 이제(err && err.message) || '<fallback>'를 읽습니다. 이전에는 Error 페이로드 없는 Promise rejection 이 e2e teardown 동안 page-error 스트림에서 “Cannot read properties of undefined (reading ‘message’)” 를 던졌습니다.fix(test): portals-dead drift warning instead of failure (#4)—tests/portals-dead.test.mjs::FIX-C3이 우리가 dead 로 표시한 slug 를 부모의templates/portals.example.yml이 다시 활성화시키는 방향으로 표류하면 실패했습니다. v1.12.0 은 그 단언을 stderr 경고로 변환하여 CI 가 부모 드리프트에서 녹색으로 통과합니다; 릴리스 결정은 수동으로 유지됩니다. slug 목록KNOWN_DEAD는 의도의 문서로서 보존됩니다.
📝 브랜딩 / 문서
docs(brand): strip 'Airbnb' references from every doc (8 locales)— README.md, README.es.md, README.pt-BR.md, README.ko-KR.md, README.ja.md, README.ru.md, README.cn.md, README.zh-TW.md, CLAUDE.md, docs/architecture/FRONTEND.md, package.json, GitHub 저장소 설명이 모두 “Airbnb-styled” / “Airbnb-inspired” 표현에서 “Clean, docs-style” 로 이동했습니다. CSS 파일은 디자인 토큰 이름을 유지했습니다 (내부 식별자이며 외부 결합 없음) 만, 설명 주석은 재작성되었습니다.
🧪 테스트
- 신규
tests/canonical-docs-coverage.test.mjs(5 케이스) 가 테스트 갭 #9–12 를 닫습니다: 모든 help 번들이 5개 정규 career-ops.org 가이드를 참조; 로케일당 16-H2 패리티 계약; 모든 README 가 정규 첫 페이지 + ≥ 3 서브 가이드를 참조;#/reports뷰 소스가 점수 임계값 카드 scaffold 를 포함; i18n 번들이 모든 v1.11.x 신규 키를 8개 로케일 모두에서 포함. - 신규
tests/scan-consolidated.test.mjs(6 케이스) 가 F-018 LITE 를 다룹니다:?source=ats|regional|both가 올바르게 디스패치; 알 수 없는 소스가 오류 프레임을 emit; 레거시/api/stream/scan-en+/api/stream/scan-ru가 폐기된 별칭으로 여전히 동작. - 합계: 360 / 360 유닛 (이전 349; +11 신규). 0 실패. 커버리지: 95.62 % 라인 / 84.37 % 브랜치 (94.59 에서 상승).
- 20 / 20 smoke E2E · 23 / 23 comprehensive E2E · 28 / 28 Playwright.
📋 내부
docs/reviews/REVIEW-2026-05-13-v1.12.0.md— 세션 컨텍스트, 연기 목록 요약, career-ops.org 콘텐츠 동기화를 위한 갱신 절차.- 모든 8개 CHANGELOG 가 이 항목을 받습니다.
- GitHub 저장소 설명이 새 브랜딩에 맞게 갱신되었습니다.
Out of scope (연기, v1.11.1 에서 변경 없음)
| 항목 | 이유 |
|---|---|
| Batch evaluate SPA 페이지 | 정규 문서당 CLI 전용 흐름; SPA 등가물은 신규 뷰 + ≥3 엔드포인트 + 픽스처가 필요합니다. 2–3일 단계. |
전체 어댑터 레지스트리 (8개 server/lib/portals/adapters/*.mjs + 14 신규 포털 + FE 재작성) |
이 릴리스의 F-018 LITE 가 API 표면을 통합합니다; 전체 아키텍처 리팩터는 남아 있습니다. |
| 전체 multer 파이프라인 (PR-4) | v1.10.2 가 415 envelope 로 데이터 손상 구멍을 닫았습니다; 전체 multipart 파서 + ConversionError envelope 는 자체 단계입니다. |
| Mode-template 번역 | 부모 프로젝트와의 조정이 필요합니다. |
이전 릴리스 (v1.11.x 및 v1.10.x)
v1.11.0 / v1.11.1 / v1.10.0–v1.10.3 의 상세 항목은 🇬🇧 영어 CHANGELOG 에 있습니다. 요약:
- v1.11.1 — 2026-05-13 · 폴리시 슬라이스:
#/apply의 Playwright 힌트, 통일된 태그라인, 대시보드 점수 임계값 카드. 349/349 테스트. - v1.11.0 — 2026-05-13 · 8개 help 번들과 8개 README 에 career-ops.org/docs 통합. 신규
docs/career-ops-canonical.md. Mode/Archetype/Pipeline/Tracker/Report/Scan history 개념 문서화. 348/349 테스트. - v1.10.3 — 2026-05-12 · 버그 수정 슬라이스: v1.10.2 회귀 실행의 11개 QA 결과 중 7개 종료.
- v1.10.2 — 2026-05-12 · CV multipart 415 거부 (v1.13.0 multer 까지의 임시 패치); PDF 생성 수정.
- v1.10.1 — 2026-05-09 · v1.10.0 릴리스 QA 회귀 실행의 중요 패치.
- v1.10.0 — 2026-05-08 ·
#/profile편집기 + CV 업로드 UX (pandoc/pdftotext/passthrough), 8개 로케일 × 16개 H2 help 패리티, 언어 전환기.
[1.9.1] — 2026-05-08
프로덕션 준비도 패스. 표적화된 버그 수정 4건 (BF-1..BF-4), Playwright smoke 가 5에서 12 테스트로 확장되어 tracker / pipeline / reports / evaluate / config / cv 저장 라운드트립을 다룹니다. 모두 CI 에서 통과.
🐛 버그 수정
fix(tracker): escape pipes + collapse newlines in every cell, not just notes (BF-1)—"Acme | Co"같은 회사 이름이 이전에는 markdown 표 레이아웃을 깨뜨렸습니다 (파서가 셀을 둘로 분할). 셀 정화 (sanitization) 가 이제 company / role / reportSlug / notes 에 균일하게 적용됩니다;parsers.mjs::parseMarkdownTable의 동반 수정이 GFM 준수\|이스케이프 지원을 추가하여 라운드트립이 무손실입니다.fix(config): wrap updateEnvFile in try/catch (BF-2)—POST /api/config가 이전에는 permission-denied / 읽기 전용 파일시스템에서 처리되지 않은 rejection 을 일으켰습니다. 이제 깔끔한 500{ error: 'failed to write parent .env', details: [...] }를 반환합니다.fix(llm): soft cap on assembled prompt size for Anthropic SDK calls (BF-3 + BF-4)—/api/evaluate,/api/deep,/api/mode/:slug의 Anthropic 분기가 이제bundleProjectContext + prompt가 200 KB (≈50K 토큰) 를 초과하면 413 으로 중단합니다. API 가 컨텍스트 크기 불평을 하도록 두는 대신 다초 라운드트립 + 토큰을 절약합니다. 캡은 어떤 현재 모델 상한보다도 충분히 낮습니다 (Sonnet 4.6 = 1M 컨텍스트).
🧪 Playwright smoke — 커버리지 확장
5 → 12 테스트. 신규 케이스:
tracker view renders empty + accepts API-seeded row— 회사 이름에 리터럴 파이프를 가진 행을 시드하고 라운드트립이 이를 보존하는지 단언하여 BF-1 을 행사.pipeline add-URL form populates the queue+ 잘못된 URL 거부 sweep (loopback,javascript:, 빈 문자열).reports view handles empty state— non-crash 단언.evaluate view returns a manual prompt without API key— 폴백 체인 검증.config GET returns known keys masked— 비밀이/api/config를 통해 절대 누설되지 않음.cv.md PUT round-trips with sanitization— XSS 류 (script 태그,javascript:스킴) 가 end-to-end 로 strip 됨.pipeline preview proxy strips scripts— 잘못된 URL 거부 경로.
📦 동작 변경 (API 계약 변경 없음)
- Tracker 쓰기가 이제 파이프가 포함된 company / role 이름에 대해 무손실입니다. 원시 파이프를 가진 기존 행은 다음 읽기에서 올바르게 파싱되기 시작합니다.
/api/{evaluate,deep,mode/:slug}가 이제 비합리적으로 큰 프롬프트 (200 KB+) 에서 502/타임아웃 대신 413 을 반환합니다.
🧪 테스트
- 284 유닛 테스트 (수 변경 없음; 파서 갱신 후에도 기존 테스트가 모두 녹색).
- 12 Playwright 브라우저 smoke 테스트 (이전 5).
[1.9.0] — 2026-05-08
v1.8.0 백로그의 P-6 → P-10 이 한 번들로 출하되었습니다. 헤드라인: server/index.mjs 가 이제 130-LOC 오케스트레이터입니다 (762 에서 감소, 합계 1230 → 130 = -89%); 모든 라우트 토픽이 자체 모듈을 갖습니다. /api/evaluate 의 Anthropic 패리티, 멀티 CLI shim, 확장된 i18n 패리티 테스트, CI 에 연결된 Playwright 브라우저 smoke.
🏗️ P-6 — 서버 split-by-concern (phase 2)
P-2 의 연속. 나머지 9개 라우트 토픽을 server/index.mjs 에서 server/lib/routes/<topic>.mjs 모듈로 추출. index.mjs 는 이제 순수 오케스트레이터입니다: 미들웨어 (보안 헤더 + activity 로그 + static), 12개 register<Topic>Routes(app) 호출, SPA catch-all.
server/lib/routes/activity.mjs—/api/activity.server/lib/routes/config.mjs—/api/configGET/POST (부모 .env 라운드트립).server/lib/routes/health.mjs—/api/health+/api/dashboard.server/lib/routes/help.mjs—/api/help/:lang.server/lib/routes/jds.mjs—jds/*.txt의 전체 CRUD.server/lib/routes/llm.mjs— 모든 LLM 결합 엔드포인트 (evaluate, deep, mode, apply-helper, interview-prep).server/lib/routes/pipeline.mjs— 명명된 상수 (timeout / max-redirects / max-body) 가 포함된 SSRF-안전 preview proxy 를 포함한/api/pipeline*.server/lib/routes/reports.mjs—/api/reports*.server/lib/routes/tracker.mjs—/api/trackerGET + dedup 인식 POST.
동작 변경 없음. 모든 단계에서 283/283 유닛 테스트가 녹색을 유지했습니다. 오케스트레이터의 import surface 가 47줄에서 22줄로 떨어졌습니다.
🔌 P-7 — /api/evaluate 의 Anthropic 패리티
/api/evaluate 는 이전에 Gemini 또는 manual 이었습니다. v1.9.0 은 Anthropic 분기를 추가합니다 (두 키가 모두 있을 때 선호). /api/deep 와 /api/mode/:slug 가 이미 사용하는 라우팅 규칙을 미러링합니다. 모델이 cv / profile / mode 템플릿을 인라인으로 갖도록 bundleProjectContext({ modeSlugs: ['_shared', 'oferta'] }) 를 통해 라우팅됩니다 (REVIEW-A1).
신규 엔드포인트: POST /api/evaluate/test-anthropic — ANTHROPIC_API_KEY 용 smoke 검사, 기존 Gemini smoke 를 미러링. 작은 프롬프트 (≤256 출력 토큰) 를 보내므로 사실상 비용이 없습니다; 200자 샘플을 반환합니다.
폴백 체인은 이제: Anthropic → Gemini → manual.
🌐 P-8 — Help-center i18n 패리티 (감사 + 테스트 강화)
모든 docs/help/<lang>.md 의 구조 패리티를 감사했습니다. 8개 로케일이 이미 동일한 14개 정규 h2 섹션을 다룹니다. 테스트 업그레이드:
tests/help-ui.test.mjs::every help doc covers the same 14 sections가 en + ru 만 확인하고 있었습니다. 이제 8개 로케일 전체 (en, es, pt-BR, ko-KR, ja, ru, zh-CN, zh-TW) 를 순회하며 각각에 대해 섹션 수를 단언합니다.- 신규 테스트:
tests/help-ui.test.mjs::every help locale has substantive content— 각 비영어 로케일이en.md바이트 길이의 최소 30% 인지 단언하여 로케일 스텁을 방어합니다. 컴팩트 번역은 자연히 40-50% 에 도달합니다; 스텁은 한 자리 % 일 것입니다.
결과: 구조 패리티가 이제 CI 강제됩니다.
🤖 P-9 — CI 매트릭스의 Playwright 브라우저 smoke
tests/playwright-smoke.mjs (v1.8.0 에서 옵트인으로 추가됨) 가 이제 CI 워크플로의 일부입니다. 기존 e2e 잡이 이미 Playwright + Chromium 을 설치합니다; 신규 스텝 (npm run test:e2e:browser) 하나가 comprehensive Node E2E 직후에 5개 브라우저 smoke 테스트를 실행합니다.
CI 의 순서: 유닛 (Node 18/20/22 매트릭스) → smoke node E2E → comprehensive node E2E → Playwright 브라우저 smoke → 실패 시 스크린샷 아티팩트 업로드.
🌍 P-10 — 멀티 CLI 호환성
부모 career-ops v1.7.0 이 멀티 CLI / Open Agent Skill 표준 지원을 도입했습니다. UI 서브 프로젝트는 정규 CLAUDE.md 를 가리키는 얇은 shim 으로 동일한 관례를 따릅니다:
web-ui/AGENTS.md— Codex / Aider / 범용 CLI 엔트리포인트.web-ui/GEMINI.md— Gemini CLI 엔트리포인트.
두 shim 모두 하드 룰과 빠른 참조를 재진술하지만 전체 프로젝트 수준 지침은 CLAUDE.md 에 위임하므로 비-Claude CLI 도 Claude Code 세션과 동일한 오리엔테이션에 도착합니다. 배포된 UI 자체는 런타임에서 CLI 무관입니다.
🧪 테스트
- 284 유닛 테스트 (이전 283): +1 신규 help-locale 패리티 테스트.
- 5 Playwright 브라우저 smoke 테스트 — 이제 옵트인이 아니라 CI 의 일부.
- 커버리지 유지.
🔧 변경된 파일
+ server/lib/routes/activity.mjs + server/lib/routes/config.mjs
+ server/lib/routes/health.mjs + server/lib/routes/help.mjs
+ server/lib/routes/jds.mjs + server/lib/routes/llm.mjs
+ server/lib/routes/pipeline.mjs + server/lib/routes/reports.mjs
+ server/lib/routes/tracker.mjs
+ AGENTS.md + GEMINI.md
~ server/index.mjs (762 → 130 LOC, -83%)
~ .github/workflows/ci.yml (Playwright smoke step)
~ tests/help-ui.test.mjs (all-8-locales section parity + content-floor)
~ docs/{ROADMAP,architecture/{OVERVIEW,SERVER}}.md
~ docs/sdd/CONVENTIONS.md
~ CLAUDE.md
~ package.json (1.8.0 → 1.9.0)
📦 신규 REST 엔드포인트
| Method | Path | 용도 |
|---|---|---|
POST |
/api/evaluate/test-anthropic |
ANTHROPIC_API_KEY 용 smoke 검사 (P-7). /api/evaluate/test-gemini 를 미러링. |
🤖 신규 CLI 엔트리포인트
| 파일 | CLI | 비고 |
|---|---|---|
AGENTS.md |
Codex / Aider / 범용 | 전체 지침을 위해 CLAUDE.md 를 가리킵니다. |
GEMINI.md |
Gemini CLI | 세션 시작 시 Gemini 가 자동 로드합니다. |
[1.8.0] — 2026-05-08
강화, 리팩터, SDD 부트스트랩. 고위험 정확성/보안 수정 3건 (A1, A2, A3), 중간 4건 (B1–B4), 정리 6건, 부모 career-ops v1.7.0 표면 감사, 서버 split-by-concern (P-2 phase 1), Playwright 브라우저 smoke 하니스, docs/ 와 .claude/ 아래 전체 SDD 기반.
🔥 고위험 수정
fix(deep): inline cv/profile/mode files for Anthropic SDK calls (REVIEW-A1)—/api/deep와/api/mode/:slug가 이전에는 모델에게 “이 파일들을 먼저 읽어라” 라고 했지만 Anthropic SDK 에는 파일시스템이 없습니다. 출력이 비어 있었습니다. 신규bundleProjectContext({ modeSlugs })가cv.md,config/profile.yml,modes/_shared.md, 그리고 mode 템플릿을 읽고 각각을 16 KB 에서 자른 뒤 프롬프트 앞에<project_context>블록을 추가합니다. 라이브 검증:claude-sonnet-4-6의 deep-research 호출에서 26 KB 의 근거 있는 markdown 응답.fix(runner): SIGKILL escalation after SIGTERM grace period (REVIEW-A2)—runNodeScript와streamNodeScript가 이전에는 타임아웃 / 클라이언트 연결 끊김에서SIGTERM만 보냈습니다. syscall (DNS, 블록된 소켓) 에서 멈춘 자식 프로세스가 이를 무시하여 Node 의 GC 가 회수할 때까지 SSE 연결이 매달렸습니다. 이제 각 경로가 5초 워치독을 무장하여SIGKILL로 에스컬레이션합니다. Promise 가 항상 해결됩니다.fix(runner): max-runtime cap on streaming endpoints (REVIEW-A3)— 모든 SSE 스크립트 러너 (/api/stream/{scan,liveness,pdf}) 가 이제 하드 30분 한도를 갖습니다. 만료 시:event: error { message: 'maximum runtime exceeded' }emit, A2 워치독으로 자식 프로세스 종료, 응답 종료.
🛡️ 중간 위험 수정
fix(preview): per-hop redirect validation in /api/pipeline/preview (REVIEW-B1)—redirect: 'follow'에서 수동 redirect 워킹으로 전환. 각Location헤더가isValidJobUrl로 재검증됩니다; 3홉으로 캡. 적대적 보드가 더 이상 loopback / 사설 IP /file://로 우리를 튕길 수 없습니다. 거부 경로를 다루는 신규 테스트 4개.refactor(keys): hasGeminiKey helper unifies LLM-key checks (REVIEW-B2)— 라우트 핸들러의 직접process.env.GEMINI_API_KEY읽기가lib/anthropic.mjs의hasGeminiKey()로 교체되었습니다. 일관성과 더 쉬운 모킹을 위해hasAnthropicKey()형태를 미러링합니다.feat(scanners): thread AbortSignal through hh.ru, Habr, Greenhouse, Ashby, Lever (REVIEW-B3)— SSE 클라이언트가 스캔 중간에 연결을 끊으면, 진행 중인 HTTP fetch 가 이제 모든 쿼리를 완료까지 실행하고 이벤트를 드롭하는 대신 abort 됩니다.runRuScan과runEnScan이opts.signal을 수용합니다;/api/stream/scan-{ru,en}의 SSE 핸들러가AbortController를 생성하고res.close에서 abort 합니다.test(anthropic): log-guard test prevents future API-key leaks via console (REVIEW-B4)—runAnthropichappy + error 경로 동안 모든console.{log,info,warn,error,debug}호출을 캡처하여 0 출력과 canary 키 문자열이 절대 등장하지 않음을 단언합니다. 미래의console.log(opts)회귀에 대한 심층 방어.
🧹 저위험 폴리시
fix(parsers): defense-in-depth URL gate inside addPipelineUrl (REVIEW-C4)— 라우트 수준isValidJobUrl을 보완하는 파서 수준의 비-http(s) 값 거부. 더 엄격한 규칙을 원하는 호출자를 위한 옵션opts.validate.docs(readme): badge "tests-88 passed" → "tests-277 passed" (REVIEW-C3)— 자릿수가 틀렸습니다.test(i18n): missing-keys diff grouped by locale (REVIEW-C6)—tests/i18n-coverage.test.mjs가 갭을 찾으면 출력이 이제 혼합된 줄 대신[ru] (3): foo, bar, baz입니다.docs(review): C1 closed as resolved-on-inspection— 정화 (sanitizer) 정규표현식이 이미\x00-\x0816진 형태였습니다; 리뷰 항목은 도구 렌더링 아티팩트였습니다.
🏗️ P-2 phase 1 — 서버 split-by-concern
server/index.mjs 가 1230 LOC 로 800줄 상한을 한참 넘었습니다. 동작 변경 없이 집중된 모듈로 분할했습니다. 모든 단계에서 283 유닛 테스트가 녹색을 유지했습니다.
server/lib/security.mjs—isValidJobUrl,stripDangerousMarkdown,sanitizeJobDescription,isPubliclyExposed. 외부 컨슈머와의 하위 호환을 위해index.mjs에서 재내보냅니다.server/lib/prompts.mjs—bundleProjectContext,buildEvaluationPrompt,buildDeepPrompt,buildModePrompt,buildApplyChecklist.server/lib/store.mjs—safeReadApps,safeReadPipeline,safeListReports,checkProfileCustomized,ensureRussianPortalsDefaults.server/lib/routes/scan.mjs—/api/stream/scan-{ru,en},/api/scan-ru/config,/api/scan-results용registerScanRoutes(app).server/lib/routes/runners.mjs— 버퍼링된/api/run/*표, 스트리밍/api/stream/{scan,liveness,pdf}, 생성된 PDF 목록/다운로드용registerRunnerRoutes(app).server/lib/routes/content.mjs— CV / Profile / Portals / Modes 용registerContentRoutes(app).
index.mjs 는 이제 762 LOC 입니다 (-38%, 800 캡 아래). Phase 2 는 tracker, pipeline, reports, jds, llm (evaluate/deep/mode), health 를 라우트 모듈로 추출할 것입니다. 오케스트레이터의 목표 <500 LOC.
🔍 부모 career-ops v1.7.0 감사
사용자가 부모 프로젝트를 v1.7.0 으로 갱신했습니다. 모든 소비 표면을 감사했습니다 — UI 는 완전히 호환됩니다. 주목할 발견은 docs/architecture/DATA-FLOWS.md 에 문서화되어 있습니다:
- 모드 카탈로그가 7 → 19 파일로 성장했습니다. UI 의
MODE_ALLOWLIST는 의도적으로 7개만 surface 합니다 (나머지는 Claude-Code 전용). 의도적인 좁은 범위를 설명하는 주석이 추가되었습니다. portals.yml스키마 확인:tracked_companies(96 항목, 87 활성화, 71 개가 API 보유). EN 스캐너가 이를 올바르게 읽습니다; 레거시companies키도 여전히 지원됩니다.- 오늘 소비되지 않는 신규 부모 표면:
dashboard/(Go 프로그램),update-system.mjs,generate-latex.mjs,analyze-patterns.mjs,liveness-core.mjs,followup-cadence.mjs,test-all.mjs, 현지화된 mode 서브디렉토리 (de/fr/ja/pt/ru). - 라이브
/api/dashboard,/api/health,/api/modes,/api/portals,/api/profile,/api/cv,/api/jds,/api/reports,/api/tracker,/api/pipeline,/api/evaluate,/api/deep,/api/stream/scan-en이 모두 녹색으로 검증되었습니다.
🤖 SDD / GSD 부트스트랩
career-ops-ui 는 이제 GSD 파이프라인 (superpowers@claude-plugins-official 의 gsd-* 스킬) 과 정렬된 전체 Spec-Driven Development 기반을 갖습니다.
CLAUDE.md(루트) — 프로젝트 수준 에이전트 시스템 프롬프트: 스택, GSD 파이프라인, 하드 룰 (부모 계약, 보안 영역,--no-verify금지), 관례, 부모 프로젝트 경계..aiignore— AI 에이전트용 제외 목록: vendored, 바이너리, 부모 사용자 데이터,.planning/,.env, 로케일 중복..claude/agents/— 세 개의 프로젝트별 서브에이전트 정의:web-ui-route-reviewer.md— 신규 라우트를 SSRF, CSP, 정화 (sanitizer), 부모 쓰기 계약, 관례, 테스트에 대해 게이팅합니다.spa-view-reviewer.md— CSP 안전 DOM, i18n, 라우터 등록, 접근성.test-isolation-reviewer.md— 테스트가 CI 격리되어 있는지 검증합니다 (부모 프로젝트 가정 없음, 라이브 네트워크 없음, 포트 충돌 없음).
.claude/commands/— 슬래시 커맨드 스텁:/sdd-status,/codebase-tour.docs/트리 — 모두 영어로:PROJECT.md— what/why/for-whom, 범위, 제약, 성공 기준.ROADMAP.md— 현재 마일스톤 + 완료 이력 + 백로그.sdd/SDD-GUIDE.md—gsd-*스킬에 매핑된 discuss → spec → plan → execute → verify → review 파이프라인.sdd/CONVENTIONS.md— 모듈 시스템, 명명, 라우트, 정화 (sanitizer), 클라이언트 패턴, i18n, 오류, 로깅, 테스트, 커밋, 브랜치, CSS.architecture/OVERVIEW.md— 최상위 다이어그램, 계층, 부팅 시퀀스, 불변성, “…일 때 먼저 볼 곳” 치트시트.architecture/SERVER.md—server/lib/*.mjs의 파일별 맵 (P-2 분할에 맞게 갱신).architecture/FRONTEND.md— SPA 구조, 뷰 인벤토리, 전역, “뷰 추가 방법”.architecture/API.md— 모든/api/*라우트의 전체 인벤토리.architecture/DATA-FLOWS.md— 명시적 사용자 액션 계약을 가진 모든 부모 프로젝트 읽기/쓰기.reviews/REVIEW-2026-05-07.md— 이 변경 로그의 수정을 낳은 정적 리뷰.
🔒 보안 & 저장소 위생
chore(.gitignore): comprehensive defense-in-depth patterns— env 변종, IDE 폴더, GSD 스크래치 (.planning/), 사용자별 에이전트 설정 (.claude/settings.local.json,.claude/cache/,.claude/state/,.claude/memory/), Playwright 아티팩트 (playwright-report/,test-results/,.playwright/,trace.zip), heap/CPU 프로파일, 미출하 도구의 락 파일, 확장된 macOS Finder 노이즈, 범용 비밀 패턴 (secrets.json,credentials.json,*.pem,*.key) 를 다룹니다.
🧪 테스트
- 283 유닛 테스트 (이전 277): +11 신규 (B1 redirect 거부용 4개,
hasGeminiKey용 1개,runAnthropic로그 가드용 1개). - 5 Playwright 브라우저 smoke 테스트 (신규,
npm run test:e2e:browser를 통한 옵트인): 대시보드 렌더 + 버전 푸터, 대시보드 → 스캔 → 파이프라인 → cv 내비게이션, 언어 전환 영속, 404 뷰, health 페이지 렌더. Playwright 를 부모의node_modules를 통해 해결합니다 — 신규 의존성 없음. - 커버리지가 ~93% 라인 / ~83% 브랜치로 유지됩니다.
📝 신규 / 갱신된 package.json 스크립트
| 스크립트 | 용도 |
|---|---|
npm run test:e2e:browser |
in-process 서버에 대해 Playwright smoke 하니스 실행 (5 테스트). |
🔧 변경된 파일
+ CLAUDE.md + .aiignore
+ docs/PROJECT.md + docs/ROADMAP.md
+ docs/sdd/SDD-GUIDE.md + docs/sdd/CONVENTIONS.md
+ docs/architecture/OVERVIEW.md + docs/architecture/SERVER.md
+ docs/architecture/FRONTEND.md + docs/architecture/API.md
+ docs/architecture/DATA-FLOWS.md + docs/reviews/REVIEW-2026-05-07.md
+ .claude/agents/web-ui-route-reviewer.md + .claude/agents/spa-view-reviewer.md
+ .claude/agents/test-isolation-reviewer.md
+ .claude/commands/sdd-status.md + .claude/commands/codebase-tour.md
+ server/lib/security.mjs + server/lib/prompts.mjs
+ server/lib/store.mjs
+ server/lib/routes/scan.mjs + server/lib/routes/runners.mjs
+ server/lib/routes/content.mjs
+ tests/playwright-smoke.mjs
~ .gitignore ~ README.md (badge fix)
~ package.json (1.7.2 → 1.8.0)
~ server/index.mjs (1230 → 762 LOC)
~ server/lib/runner.mjs (SIGKILL escalation, max-runtime cap)
~ server/lib/anthropic.mjs (hasGeminiKey)
~ server/lib/parsers.mjs (URL gate in addPipelineUrl)
~ server/lib/ru-scanner.mjs ~ server/lib/en-scanner.mjs
~ server/lib/sources/{hh,habr,greenhouse,ashby,lever}.mjs (signal threading)
~ tests/anthropic.test.mjs ~ tests/i18n-coverage.test.mjs
~ tests/pipeline-preview.test.mjs
[1.7.2] — 2026-05-04
Help 센터, in-UI App settings, 모바일 사이드바, 단일 Scan 버튼, 모든 프롬프트 빌더의 “Show result” 단축.
✨ 신규 기능
feat(help): in-app user guide(/#/help) — 신규 사이드바 항목에서 접근 가능한 장문 Markdown 문서. 모든 페이지를 단계별로 다룹니다: 빠른 시작, CV 편집기, Profile, 스캔 필터, Pipeline preview, Evaluate, Deep research, Apply, Tracker, Reports, 모든 7개 모드, Activity log, Health, 설정 힌트.<h2>헤딩에서 자동 구축되는 sticky 목차, 동기적 DOM 빌드 (경쟁 상태 없음). 지원되는 8개 로케일 모두에 현지화.feat(config): in-UI App settings page(/#/config) — 브라우저에서ANTHROPIC_API_KEY,ANTHROPIC_MODEL,GEMINI_API_KEY,GEMINI_MODEL,HH_USER_AGENT,PORT,HOST를 편집합니다. 부모 프로젝트의.env파일에 쓰므로 career-ops Node 스크립트와 web-ui 의 dotenv 로더가 같은 소스를 가져갑니다. 비밀 키는 읽기 시 마스킹됩니다 (앞/뒤 4자). 모델 필드는 큐레이션된 목록의 드롭다운입니다 (claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 / gemini-2.0-flash / etc.). 빈 값은 키를 삭제합니다. 값이 즉시 실행 중인 process.env 에 적용됩니다 — 대부분의 설정은 재시작 없음.feat(modes): "⚡ Show result" button alongside "Copy prompt"— manual 모드에서 프롬프트가 생성될 때 사용자가 LLM 결과를 얻기 위해 입력을 재입력하지 않아도 됩니다. 신규 버튼이 동일한 폼을run: true로 재제출하고, 키가 구성되지 않았을 때는 명확한 toast (Set ANTHROPIC_API_KEY or GEMINI_API_KEY in .env first) 로 폴스루합니다./#/deep,/#/project,/#/training,/#/followup,/#/batch,/#/contacto,/#/interview-prep,/#/patterns에서 동작합니다.
🐛 UX + UI 수정
fix(scan): single Scan button replaces three (Scan all + EN + RU)— 압도적인 선택이고, 99% 경우 동일한 기본값입니다. 통합된🌐 Scan버튼이 모든 활성화된 소스를 실행합니다. Help 문서가 8개 로케일에서 갱신되었습니다.fix(ui): mobile sidebar drawer— 뷰포트 <900px 가 이제 top bar 에 햄버거 버튼 (☰) 을 받습니다;body.sidebar-open이 사이드바를 슬라이드 인 시키는 CSS transform 을 토글합니다. backdrop 어둡게 + 아무 곳이나 클릭하면 닫힘. 앵커 클릭 + hashchange 가 자동 닫기를 일으켜 사용자가 drawer 가 접힌 상태로 새 페이지에 도착합니다. 더 큰 뷰포트는 영향이 없습니다.fix(server): footer version reflects web-ui, not the parent VERSION—/api/health가 이제 web-ui 자체의package.json을 읽습니다. 푸터가 더 이상 부모의 버전 파일에서 오래된1.6.0을 누설하지 않습니다. 부모의 VERSION 은 여전히parentVersion으로 별도 노출됩니다.
📦 신규 REST 엔드포인트
| Method | Path | 용도 |
|---|---|---|
GET |
/api/help/:lang |
요청된 로케일의 Markdown 사용자 가이드를 반환, en.md 로 폴백. 경로 탐색 안전. |
GET |
/api/config |
모든 알려진 env 키의 현재 값 반환; 비밀 마스킹됨. |
POST |
/api/config |
주어진 키를 부모 프로젝트의 .env 에 쓰고, 각 값을 검증하며, process.env 에 라이브 적용. |
🌐 i18n
nav.help,nav.config,help.*,config.*,deep.showResult,deep.needKey,scan.btnRun에 걸쳐 30+ 신규 키. 8개 로케일 모두 채워졌습니다.
🧪 테스트
tests/help.test.mjs(12 케이스) — 모든 지원 로케일이 실질적인 markdown 을 반환, EN 이 모든 페이지 slug 를 spot-check, 알 수 없는 lang → EN 폴백, 경로 탐색 정화, 모든 로케일이cv.md/profile.yml/.env를 참조.tests/help-ui.test.mjs(9 케이스) — 뷰 파일 등록, 사이드바 항목, 모든 로케일에 i18n 키 존재, 모든 로케일용 docs 파일 존재, EN/RU help 가 14개 정규 섹션 보유, 모든 #/foo 라우트 커버됨, deep + mode-page 의 Show-result 배선.tests/env-config.test.mjs(18 케이스) —parseEnv,maskSecret,validateConfig,updateEnvFile(부트스트랩, 주석을 보존하는 in-place 재작성, 빈 값 삭제, 필요시 따옴표) 의 순수 함수 테스트.tests/config-endpoint.test.mjs(8 케이스) — GET 이 비밀 마스킹 / env 경로 반환; POST 가 부모 .env 에 쓰기; 라이브 process.env 적용; 빈 값이 unsets; 알 수 없는 키 + 잘못된 형식의 Anthropic 키를 400 으로 거부.
📊 통계
- 테스트: 233 → 277 (+44, 4개 신규 테스트 파일).
- E2E: 20 smoke + 23 comprehensive = 43 Playwright 단계, 모두 녹색.
- 커버리지: 93.5% 라인 / 82.6% 브랜치 / 93.7% 함수 (변경 없음 — 신규 코드는 완전히 테스트됨).
[1.7.1] — 2026-05-04
v1.7.0 이후 작업을 쌓는 패치 릴리스: 파이프라인 preview 패널, Anthropic API 통합, 스크롤 가능 사이드바, dotenv 로더, 동적 Active-companies 목록, CI 워크플로 강화.
✨ 파이프라인 Preview 패널
/#/pipeline개편 — 왼쪽 목록 + 오른쪽 preview 패널. 모든 URL 을 클릭하여 서버 사이드 프록시 스냅샷 (GET /api/pipeline/preview가 script/style/태그를 strip 하고 8 KB 에서 캡,isValidJobUrl로 검증) 을 가져옵니다. 라이브 필터 입력, “In queue” 카운터, ⚡ “Evaluate first” 헤더 버튼. 모든 행의 인라인 ▶/✕ 와 preview 패널의 전체 Evaluate / Open in tab / Delete.data-url+.pipeline-row+.pipeline-row-delete클래스로 안정된 테스트 셀렉터.tests/pipeline-preview.test.mjs의 8개 신규 테스트 (목 fetch, upstream 바인딩 불필요).
✨ Anthropic API 통합 — 모든 곳에서 “Run live”
server/lib/anthropic.mjs— Anthropic Messages API 용 제로 의존성 클라이언트 (claude-sonnet-4-6 기본,ANTHROPIC_MODEL로 재정의).ANTHROPIC_API_KEY가 설정되면 모든 모드 페이지 (/#/deep,/#/project,/#/training,/#/batch,/#/contacto,/#/interview-prep,/#/patterns) 가 주요 액션으로 “⚡ Run live (Anthropic)” 버튼을 렌더링합니다 — 클릭 시 프롬프트를 실행하고 Claude Code 로 핸드오프하는 대신 Markdown 을 브라우저로 다시 렌더링합니다. Gemini 키만 설정된 경우 Gemini 가 폴백으로 유지됩니다. 키가 전혀 없어도 manual 모드는 여전히 동작합니다.tests/anthropic.test.mjs의 8개 신규 테스트.
🐛 CI / 파이프라인 수정
fix(api): tighten pipeline URL validator(FIX-M7) — 이제 loopback 호스트네임, 길이 <10 또는 >2000, URL 내 공백도 거부합니다.fix(server): actually load .env so HH_USER_AGENT / GEMINI_API_KEY hints work—server/lib/dotenv.mjs(35줄 제로 의존성 로더) 를 추가하여server/index.mjs상단에 배선했습니다. 스캐너 코드의 런타임 힌트가 마침내 무언가를 합니다. 6개 신규 테스트.fix(ui): scrollable sidebar— 6개 그룹의 18개 내비 항목이 짧은 뷰포트를 오버플로우했습니다..sidebar가 이제 thin 사용자 정의 스타일 스크롤바와 함께overflow-y: auto를 갖습니다.fix(ui): make HH_USER_AGENT banner dismissible— 그러다 과한 것이라고 판단하여/scan에서 완전히 제거했습니다. Health 페이지 검사가 여전히 이를 noted 합니다.fix(scan): Active companies list is now collapsible + filterable + grouped— 평면 87 태그는 압도적이었습니다. 이제 “▸ Active companies 87/71” 토글이 정렬된 목록을 펼칩니다 (✓ API 지원 먼저, ○ 웹 검색 두 번째) 더하기 검색 필터.fix(test): isolate api.test.mjs + en-scanner.test.mjs from parent project— 둘 다 이제 tmp 프로젝트 루트를 띄우므로 web-ui 옆에 부모를 체크아웃하지 않고도 CI 가 동작합니다.fix(workflow): publish-package version-match only on release events— main 에서의workflow_dispatch가 더 이상 tag/version 검사를 실패시키지 않습니다.fix(e2e): stable selector for pipeline row delete— 앵커 래퍼 복원 +data-url어트리뷰트 추가로 e2e 스위트의 셀렉터가 안정됩니다.
📦 신규 REST 엔드포인트
| Method | Path | 용도 |
|---|---|---|
GET |
/api/pipeline/preview?url=… |
서버 사이드 프록시: URL 의 가시 텍스트 스냅샷 반환 (script/style 제거, 8 KB 캡), isValidJobUrl 로 게이팅. |
📊 이 배치 후 통계
- 테스트: 225 → 233 (v1.7.0 위에 +8).
- 테스트 파일: 25 → 26.
- E2E: 20 + 23 = 43 Playwright 단계, 모두 녹색.
[1.7.0] — 2026-05-03
QA r5 가 구동한 35커밋 강화 + UX + 기능 완성 패스. 보안 레이어 3개 (XSS 정화, CSP, 입력 검증) 가 도착했고, 누락된 CRUD 엔드포인트가 채워졌고, 부모 프로젝트 부트스트랩이 이제 완전히 자동화되었으며, UI 는 신규 페이지 9개 를 얻었습니다 — Activity, 재설계된 Deep Research, 더하여 부모의 modes/ 의 100% 를 다루는 7개의 사이드바 그룹화된 모드 (project / training / followup / batch / outreach / interview-prep / patterns). 파이프라인이 서버 사이드 preview 패널을 얻었습니다. Anthropic API 통합이 모든 모드에서 “Run live” 를 원클릭으로 만듭니다. 테스트 커버리지가 73 에서 225 로, 25개 테스트 파일 에 걸쳐 갔고, 23개 comprehensive Playwright e2e 단계 도 있습니다. GitHub Actions 가 CI / AI 리뷰 / Release / Publish-Package 워크플로를 출하합니다.
🔒 보안
fix(cv): sanitize CV markdown to block stored XSS in preview(FIX-C10) —PUT /api/cv가 이제cv.md를 쓰기 전에<script>,<iframe>,<object>,<embed>,<style>,<form>,<svg>,on*=이벤트 핸들러,javascript:/vbscript:/data:text/htmlURI 를 strip 합니다. 본문 1 MB 캡 (오버플로 시 413). 클라이언트 사이드UI.md()는 어떤 markdown 변환도 실행되기 전에 모든 바이트를 escape 하도록 재작성되어 원시 HTML 이 절대innerHTML에 도달할 수 없습니다. 링크href어트리뷰트는 안전한 스킴 (http/https/mailto/tel/상대 +data:image만) 의 allowlist 에 대해 검증됩니다. strip 헬퍼와 HTTP 라운드트립에 걸친 신규 테스트 17개.fix(server): add CSP and baseline security headers(FIX-L2) — 모든 응답이 이제X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: same-origin을 운반합니다. 서버가 loopback 너머에 바인딩되면 (HOST≠127.0.0.1/::1/localhost), 엄격한Content-Security-Policy가 위에 레이어링됩니다:default-src 'self',script-src 'self'(nounsafe-inline), Google Fonts whitelisted,connect-src 'self'가 XSS 유출을 차단.index.html과router.js의 인라인onclick핸들러가 엄격한 CSP 를 손상시키지 않도록addEventListener로 이동되었습니다. 5개의 다른HOST값에 걸쳐 CSP 를 게이팅하는 신규 테스트 8개.fix(api): tighten pipeline URL validator(FIX-M7) —POST /api/pipeline가 이전에는"not-a-url"을 수용하고 영속화했습니다. 이제isValidJobUrl()이 빈 문자열, <10 또는 >2000 자 입력, 공백 포함 URL, 비-http(s)스킴, loopback 호스트네임 (localhost/127.0.0.1/::1) 을 거부합니다. FIX-M3 + FIX-M6 (잘못된 입력에서 400 반환, 더하여 성공 시deduped플래그) 도 포함합니다.fix(server): actually load .env so HH_USER_AGENT / GEMINI_API_KEY hints work— 이전에는 런타임이 사용자에게 “.env 에 HH_USER_AGENT 를 설정하라” 고 했지만 서버가 그 파일을 읽지 않았으므로 지시를 따라도 아무것도 하지 않았습니다. 35줄의 제로 의존성 dotenv 로더 (server/lib/dotenv.mjs) 를 추가하여server/index.mjs상단에 배선했습니다. 커맨드 라인에 설정된 process-env 값은 여전히 이깁니다. 부모의.env.example이 이제 실제 Chrome User-Agent 예제가 포함된 문서화된HH_USER_AGENT블록을 포함합니다. 6개 신규 테스트.fix(api): sanitize JD before prompt assembly(FIX-M5) —POST /api/evaluate가 Gemini 를 호출하거나 프롬프트를 다시 echo 하기 전에 ANSI 이스케이프, 제어 바이트, 인라인<script>태그를 strip 하고 공백을 trim 합니다. 50 KB 길이 캡. 50자 최소는 정화된 텍스트에 대해 실행되므로 충분히 길어 보이지만 대부분 이스케이프로 구성된 프롬프트 주입 시도가 400 으로 fail-fast 됩니다.fix(health): mask Node version + project root when HOST!=loopback(FIX-M1) —/api/health가 LAN 노출 배포에서 더 이상 호스트를 fingerprint 하지 않습니다. Loopback 응답은 로컬 진단을 위해 값을 유지합니다.
✨ 신규 기능
feat: 7 new sidebar modes + grouped sidebar(FIX-C8) — 부모의modes/디렉토리의 100% 를 UI 갭 없이 다룹니다. 신규 라우트:#/project(포트폴리오 프로젝트 어드바이저),#/training(코스/자격 평가),#/followup(지원별 cadence),#/batch(병렬 URL 프로세서),#/contacto(LinkedIn 아웃리치 드래프터),#/interview-prep(단계별 준비),#/patterns(거절 패턴 분석기). 일곱 모두 단일 config 구동 뷰 팩토리 (public/js/views/mode-page.js) 와 단일 범용 엔드포인트POST /api/mode/:slug를 공유합니다 — 미래에 새 모드를 추가하는 것은 하나의 config 행 + 하나의 i18n 블록입니다. 사이드바가 6개 그룹으로 재구성됨: Sourcing / Decision / Application / Networking / Analytics / Setup. 총 18개 내비 항목.tests/modes-endpoints.test.mjs의 신규 테스트 12개.fix: bootstrap parent deps + russian_portals defaults(FIX-C4 + C9 + C12 + H2) —bin/start.sh가 이제 신규 클론에서 부모의node_modules(js-yaml, playwright, jsdom) 와npx playwright install chromium을 설치하므로/api/stream/scan,/pdf,/liveness가 즉시 end-to-end 로 동작합니다.createApp()이 모든 부팅에서portals.yml을 프로브합니다 —russian_portals:블록이 없으면 주석과 함께 문서화된 기본을 추가합니다. 멱등성: 두 번째 부팅은 no-op. 3개 신규 테스트.fix: disable 9 dead portal slugs in template + health-check script(FIX-C3) —templates/portals.example.yml이 이제 Ada / Factorial / Tinybird / Weights & Biases / Travelperk / Clarity AI / Forto / Vinted / Runway 를enabled: false로 표시하여 출하합니다 (각 항목에 인라인 이유 주석). 신규 설치는 96 대신 87 개의 활성 회사를 스캔합니다. 신규web-ui/scripts/portals-health-check.mjs가 활성화된 모든careers_url을 HEAD 프로브하고 제안된 패치 목록과 함께 DEAD 항목을 보고합니다 (--json을 통한 JSON 출력). 3개 신규 테스트.feat(activity): user-action log + Activity sidebar page— 모든 상태 변경 API 요청이data/activity.jsonl에 캡처됩니다 (타임스탬프, 액션 동사, 대상, 성공 플래그, 선택적 세부). 액션 접두어 chip 필터 (pipeline / cv / jd / evaluate / scan / stream / script), 액션 ✓/✗ 배지, 새로고침 버튼이 있는 신규 사이드바 항목 Activity. 5 MB 에서 자동 회전. 미들웨어, 읽기 필터, 손상된 라인 허용,GET /api/activity자체에 대한 재귀 가드를 다루는 10개 신규 테스트.feat(deep): view Deep Research in browser + saved-results archive— Deep Research 페이지가 이제 (a){ run: true }와GEMINI_API_KEY가 설정되면 Gemini 를 통해 프롬프트를 라이브 실행하고 출력을interview-prep/{slug}.md에 영속화합니다; (b) 저장된 모든 deep-research 파일을 상대 타임스탬프와 함께 클릭 가능한 카드로 나열합니다; (c) 결과를 Markdown 으로 렌더링하며 결과별 📋 Copy / ⬇ Download .md / ↗ Open in tab 액션을 제공합니다. 신규 REST 표면:GET /api/interview-prep,GET /api/interview-prep/:name,DELETE /api/interview-prep/:name. 7개 신규 테스트.feat(cv): generate + download PDF in browser, with PDF archive— CV 페이지의 신규 📄 Generate PDF 버튼이 모달 콘솔에서/api/stream/pdf를 스트리밍합니다.ERR_MODULE_NOT_FOUND/playwright오류 시 복사-붙여넣기 가능한 부트스트랩 커맨드를 노출합니다. 신규 “Generated PDFs” 섹션이 각 성공 실행 후 자동 로드되어 모든output/*.pdf를 ↗ Open 과 ⬇ Download 버튼과 함께 나열합니다. 신규 REST 표면:GET /api/output/pdfs,GET /api/output/pdfs/:name. 6개 신규 테스트.feat(api): POST /api/tracker — append rows from the UI(FIX-H8) — 브라우저에서 정규 행을data/applications.md에 추가합니다. company + role 을 검증하고,templates/states.yml에 대해 상태를 정규화하며, 0 패딩된#을 자동 증분하고, company+role 로 중복 제거 (대소문자 무시), 노트의 파이프를 이스케이프하여 markdown 표가 깨지지 않도록 합니다. 파일이 비어 있을 때 표를 부트스트랩합니다. 6개 신규 테스트.feat(api): DELETE /api/jds/:name(FIX-H4) — shell out 없이 저장된 JD 를 제거합니다. 어떤 파일시스템 접촉 전에 경로 탐색 문자가 strip 되며; 파라미터는.txt로 끝나야 합니다.../../etc/passwd거부 포함 5개 신규 테스트.feat(api): POST /api/evaluate/test-gemini(FIX-H7) —gemini-eval.mjs를 통해 50자 더미 JD 를 실행하여 사용자가 실제 평가를 기다리지 않고도 API 키가 동작하는지 확인할 수 있는 smoke-test 엔드포인트.{ ok, code, sampleLength, sample }반환.
🐛 버그 수정
fix(router): catch-all 404 view + i18n coverage guard(FIX-C7) — 알 수 없는 해시 라우트가 이전에는 조용히 대시보드로 폴백되어 오타와 깨진 북마크를 가렸습니다. 이제#/totally-random-xyz가 잘못된 경로를 인용하고 대시보드로 링크하는 전용 404 페이지를 렌더링합니다. 404 뷰는 라우터 IIFE 자체 안에 등록되어 어떤 사용자 라우트와도 충돌할 수 없습니다. 신규tests/i18n-coverage.test.mjs가vm.Context안에서 stubwindow와 함께i18n.js를 실행하고 privateDICT를 노출하여 173+ 키 × 8 로케일 각각이 채워지고 비어 있지 않음을 단언합니다. 4개 신규 라우터 테스트.fix(router): alias #/profile → settings(FIX-C2) — 내부 라우트 이름은settings(nav.settings가 “Profile” 을 렌더링) 이지만 외부 링크와 근육 기억은#/profile로 갑니다. 이제 두 주소가 같은 뷰에 도달하고 사이드바 내비 항목이 어느 쪽이든 강조됩니다. 2개 신규 테스트.fix(health): unify Health/Doctor + flag template profiles(FIX-C6 + FIX-H6) — Health 와 Doctor 가 두 개의 다른 진실 소스였습니다. 이제/api/health가 Doctor 가 보고하는 모든 것 (parent-deps, Playwright, dirs, profile-customized,HH_USER_AGENT) 을 노출합니다.Profile customized검사가 placeholder 이름 (Jane Smith,Alex Doe,John Doe,Your Name,Test User) 과 명시적 YAML 파싱 오류를 감지합니다. 4개 신규 테스트.fix(scan): warn on query↔negative collisions in RU config(FIX-H3) —portals.yml이 Senior PHP 를 타깃하는 쿼리와 함께title_filter.negative에"PHP"를 출하하면 모든 매치가 필터링되어 사용자가 결과 0 을 봅니다.loadConfig()가 이제warnings배열을 계산합니다;runRuScan()이 스캔이 시작되기 전에 각 경고를 SSE stderr 라인으로 emit 합니다. 출하 기본이 PHP-친화적으로 유지됨을 검증하는 2개 테스트.fix(scan): warn when HH_USER_AGENT is unset(FIX-H1) —/scan페이지가/api/health를 프로브하고HH_USER_AGENT가 비어 있으면 액션 행 위에 노란 경고 카드를 표시하여 사용자가 RU 스캔을 클릭하기 전에 hh.ru 403 을 알도록 합니다.fix(api): warn when POST /api/jds slug had unsafe chars stripped(FIX-M2) — 위험한 문자를 strip 하는 slug 정규화가 이제warning필드를 반환합니다; 순수 대소문자/공백 정리는 조용히 유지됩니다. 정화 후 빈 결과는 400 을 반환합니다.fix(ui): clear global search on route change + button spinners(FIX-M4 + FIX-L1) — 글로벌 검색 입력이hashchange에서 클리어됩니다 (활성 입력 가드 포함). 신규UI.withSpinner(button, fn)헬퍼가 로딩 상태, ARIA, 더블 클릭 방지를 모든 비동기 버튼 클릭에 배선합니다. 이미 Doctor / Verify / sync-check / Save CV / Normalize / Dedup / Merge 버튼에 적용되었습니다.fix(ui): make sidebar scrollable so 18 nav items always reach the footer— FIX-C8 의 그룹화된 사이드바가 짧은 뷰포트를 오버플로우했습니다; 하단 항목 (Activity / Health) 이 잘렸습니다..sidebar가 이제 thin 사용자 정의 스타일 스크롤바와 함께overflow-y: auto를 갖습니다 (WebKit + Firefox). 푸터는 기존margin-top: auto를 통해 핀 유지됩니다.fix(ui): empty modal-title placeholder(FIX-H9) —index.html의 하드코딩된 영어"Title"문자열이 사라졌으며, 모달이 열리는 동안 보였던 짧은 경쟁 창을 닫습니다.
🌐 i18n
- 173+ 번역 키 × 8개 지원 로케일 (
en,es,pt-BR,ko,ja,ru,zh-CN,zh-TW). 모든 로케일에 추가된 신규 키: 404 페이지, activity 로그, deep research, PDF 흐름, 보안 경고, tracker 변경, apply 이름 변경. 커버리지가 이제tests/i18n-coverage.test.mjs로 강제됩니다 — 모든 키가 모든 지원 로케일에서 비어 있지 않은 값을 가져야 하며 그렇지 않으면 CI 실패합니다.
⚙️ DevOps
- 테스트 수: 73 → 201 (23개 테스트 파일에 걸쳐 +128 테스트). 유일하게 남은 실패 테스트 (
runEnScan: dry-run end-to-end across multiple sources) 는 Greenhouse/Ashby/Lever 라이브 API 응답에 의존하는 기존 flake 입니다. - Comprehensive Playwright e2e (
tests/e2e-comprehensive.mjs, 23 단계): 전체 사용자 여정을 걷습니다 — CV 저장 → preview → PDF 생성 → 모든 7개 신규 모드 → tracker 필터 → activity 로그 → 404 → 모달 ESC → 사이드바 스크롤 → Ctrl-K 포커스 → 검색 클리어 → profile alias → 언어 영속. - GitHub Actions (
.github/workflows/):ci.yml— Node 18/20/22 매트릭스의 유닛 + 통합 테스트, 더하여 i18n 커버리지 게이트 (모든 키 × 8 로케일이 비어 있지 않아야 함), 더하여 모든 PR 의 전체 Playwright e2e.ai-review.yml— 모든 PR 에 대한 Claude Code AI 리뷰. 메인테이너가 머지 권한을 유지합니다; Claude 는 제안만 합니다.skip-ai-review레이블로 건너뛰기.release.yml—v*.*.*태그가 푸시되면 GitHub Release 자동 게시; 릴리스 노트는CHANGELOG.md에서 슬라이스되므로 8개 언어 변형이 모두 정규 소스로 유지됩니다.
- CSP 친화적 UI: 모든 인라인
onclick핸들러가index.html과router.js에서 제거되었습니다. 엄격한script-src 'self'정책이 어떤 기능도 깨뜨리지 않고 이제 강제 가능합니다.
📦 신규 REST 엔드포인트
| Method | Path | 용도 |
|---|---|---|
GET |
/api/activity |
사용자 액션 이벤트 목록, 최신 우선 |
GET |
/api/interview-prep |
저장된 Deep Research 파일 목록 |
GET |
/api/interview-prep/:name |
단일 Deep Research 파일 읽기 |
DELETE |
/api/interview-prep/:name |
Deep Research 파일 제거 |
GET |
/api/output/pdfs |
생성된 PDF 목록 |
GET |
/api/output/pdfs/:name |
첨부로 PDF 스트리밍 |
POST |
/api/tracker |
applications.md 에 행 추가 |
DELETE |
/api/jds/:name |
저장된 JD 제거 |
POST |
/api/evaluate/test-gemini |
Gemini API 키 smoke 테스트 |
POST |
/api/mode/:slug |
7개 신규 모드 (project / training / followup / batch / contacto / interview-prep / patterns) 의 범용 프롬프트 빌더 |
[1.6.0] — 2026-05-02
웹 UI 의 초기 공개 릴리스. 이 기준선의 기능 인벤토리는 README.md 를 참조하십시오.