resumate 09. 이력서 문서 모델 v2 설계와 빌더 도입
이력서가 markdown 텍스트 한 뭉치뿐이라는 게 문제였다. 기능을 여섯 개 쌓고 나서야 알았다.
하루 만에 기능 여섯 개를 쌓았다
8월 7일 하루에 서로 관련 없는 기능들이 줄줄이 쌓였다.
점수 계산기는 AI에게 이력서 총점을 물으면 같은 이력서에도 7점과 7.5점이 번갈아 나오던 것을 고쳤다. 총평은 회차마다 흔들리므로 셀 수 있는 것만 세기로 했다.
- 필수 섹션 4점: 연락처·경력·기술·학력
- 성과의 구체성 4점: 수치가 든 본문 비율
- 간결성 2점: 지나치게 긴 문장 비율
기간·연도는 성과 수치 집계에서 뺐다. 이력서 어느 항목에나 붙는 값이라 세면 모든 이력서가 만점이 되기 때문이다.
첨삭본 PDF 렌더링은 pdf-lib(자바스크립트에서 PDF 페이지에 좌표를 지정해 글자·도형을 직접 그리는 라이브러리)로 좌표를 찍어 그리던 방식에서 HTML 템플릿을 WKWebView(macOS에 내장된 웹뷰 엔진)로 인쇄하는 방식으로 바꿨다. 좌표 방식은 margin collapsing 같은 브라우저 규칙을 손으로 다시 구현해야 해서 섹션마다 여백이 어긋났는데, 같은 CSS를 실제 브라우저 엔진으로 인쇄하면 그 오차가 사라진다. html2pdf.swift가 WKWebView 인쇄 경로를 맡고, Chromium 같은 무거운 렌더러를 따로 싣지 않는다.
이 김에 원본 PDF를 첨삭할 때 글자를 흰 사각형으로 덮는 대신 mupdf redaction으로 실제 제거하게 바꿨다. 덮기만 하면 텍스트가 파일에 그대로 남아 ATS(채용 쪽에서 이력서 텍스트를 자동으로 읽어 걸러내는 소프트웨어)가 읽어버린다.
보관함에는 업로드 시각을 붙였다. 날짜만 보이면 같은 이력서를 하루에 여러 번 손봤을 때 어느 게 최신인지 구분이 안 된다. 연도는 올해가 아닐 때만 붙게 했다.
이력서 빌더는 원문을 AI로 섹션·필드에 분류해 가져온 뒤 구조 그대로 직접 편집할 수 있는 새 화면이다.
Codex(OpenAI의 코딩 에이전트) 제공자는 Claude CLI와 같은 계약으로 Codex app-server를 붙여 두 제공자를 전환할 수 있게 했다. 로그인 상태 확인과 읽기 전용 샌드박스 적용이 포함됐다.
토큰 사용량 집계와 디자인 토큰 정리(design-system.ts + tailwind 설정 통합)까지 각각 따로 올라갔다.
통합 커밋 하나를 쪼갤 수 없었다
이 여섯 기능을 실제 화면과 store에 연결하면서 화면 곳곳을 한꺼번에 건드렸는데, 더 잘게 나누지 못했다. 한 파일 안에 여러 기능의 변경이 겹쳐 있었기 때문이다. src-tauri/src/lib.rs에는 html2pdf 경로 전달과 find_free_port 제거가 함께, MaterialsView에는 업로드 시각 표기와 디자인 토큰 전환이 함께 들어 있었다. 삭제도 분리할 수 없었다. CharacterBlob 제거와 three.js 의존성 제거가 갈리면 중간 상태가 타입체크를 통과하지 못한다.
기능 단위로 잘게 나눠 만들어도, 그걸 화면에 실제로 붙이는 순간 파일 하나에 여러 기능이 겹친다. 여기서 캐릭터별 대화를 분리하고, 이력서 선택(마스터/첨부)을 구분하고, 첨삭 프롬프트를 "설명 문장만 수정"으로 다시 쓰고, ChatView·Terminal·CharacterBlob·ToolConfirmDialog·드래그 훅·confirmStore·Tauri ipc/rpc 모듈·sidecar/tools.ts를 한꺼번에 걷어냈다.
이력서가 한 장뿐이라 공고별 이력서를 만들 수 없었다
기능을 다 붙이고 나서야 더 근본적인 문제가 드러났다. 공용 이력서 하나로 공고별 이력서를 뽑아내려면 문서를 여러 벌 갖고 문장별 근거를 기억해야 한다. 지금까지 이력서는 markdown 텍스트 한 뭉치였다. 공고에 맞춰 순서를 바꾸거나 문장을 다르게 쓴 버전을 여러 개 두려면, 그 문서들이 서로 무엇을 근거로 갈렸는지 구조적으로 기억할 방법이 필요했다.
같은 날 저녁에 문서 모델 v2 설계를 새로 썼다.
- 구조체(ResumeDoc)를 정본으로 삼고 markdown·PDF는 그로부터 파생시킨다
- description 필드를 meta(기간·소속 같은 한 줄)와 paragraph(본문)로 쪼개 PDF 내보내기 블록과 1:1로 맞춘다
- 공고별 문서는 원본의 복사본 + 출처 표시로 둔다. 원본이 바뀐다고 이미 제출한 문서가 저절로 바뀌면 안 된다
- resume_docs 전용 테이블을 두고, 마스터 문서는 부분 유니크 인덱스(조건에 맞는 행에만 걸리는 유니크 제약)로 하나만 존재하게 DB가 보장한다
flowchart LR
D["ResumeDoc (정본)"] --> A["resumeDocToMarkdown"]
A --> M["markdown"]
D --> H["HTML 템플릿"]
H --> P["PDF"]
D --> F["description 필드"]
F --> Me["meta (기간·소속)"]
F --> Pa["paragraph (본문)"]
바로 이어서 이 설계의 코어와 DB 접근 함수를 구현했다. 필드 타입은 description을 meta/paragraph로 나눈 만큼 4개에서 5개로 늘었다. upgradeLegacyDocument는 옛 markdown 문서를 새 구조로 전환하는데, 제목 바로 다음에 오는 기간 줄만 meta로 본다. 이 추측은 전환 시점에 한 번만 돌고, 이후로는 사용자가 지정한 필드 타입이 정본이 된다. resumeDocToMarkdown 어댑터를 둬서 기존 markdown 소비자 쪽 코드는 건드리지 않았다.
resume_docs 접근 함수 자체는 Tauri SQL 플러그인(Rust 쪽에서 SQLite를 열고 프론트엔드에 쿼리 API를 노출하는 Tauri 공식 플러그인)에 의존해 Tauri 런타임 없이는 실제 SQL 동작을 검증할 수 없다. 그래서 판단 로직을 전부 순수 함수로 분리해 32개 테스트로 덮었다. 마이그레이션은 localStorage에 있던 임시본을 지우지 않는다. 문제가 생기면 되돌릴 자리를 남겨둔 것이다.
검토 없이 커밋한 체크포인트
8월 8일은 다른 작업 세션의 변경을 체크포인트로 남기는 것부터 시작했다. 타입체크와 테스트 104개가 통과하는 것만 확인했고, 내용은 검토하지 않았다. 여러 작업 세션이 병행되던 중이라 다음 세션이 이어받을 지점을 먼저 만들어둔 것이다. 내용은 문서 모델 v2 위에서 빌더 편집 UI를 다듬은 것이었다.
reflowResumeFieldText: PDF나 AI가 화면 폭에 맞춰 넣은 줄바꿈을 다시 문장 단위로 복구markdownBodyToFields: 연속된 설명 줄을 한 문단 필드로 합침- 빌더 편집 UI, PawsView, 인증 배지, 토큰 사용량 배지 정리
useCloseOnNavigation훅 추가
기간 줄이 본문을 삼켰다
바로 이 줄바꿈 복구 로직에서 버그가 나왔다. 연속된 설명 줄을 한 문단으로 합치는 로직이, 항목 제목 바로 뒤에 오는 기간 줄까지 함께 삼켜버렸다. 그 결과 문서 모델 v2가 필드 전체를 meta로 잘못 보고, 본문 내용까지 작은 회색 글씨(메타 스타일)로 렌더링하게 됐다.
같은 입력이 고치기 전후로 어떤 필드로 갈리는지 나란히 놓고 봤다.
before meta :: 2024.01 ~ 현재 | 프론트엔드 제품을 개선했습니다.
after meta :: 2024.01 ~ 현재 | 프론트엔드
paragraph :: 제품을 개선했습니다.고친 방법은 제목 뒤 첫 줄이 기간 꼴이면 문단 버퍼에 넣지 않고 따로 떼어내는 것이다. 줄바꿈 복구는 그대로 두 번째 줄부터 적용된다.
기간 줄 판단 함수(looksLikeMetaLine)는 resumeDocument.ts 한 곳에만 두고 v2 쪽에서 가져다 쓰게 했다. 같은 판단을 두 곳에 복제해두면 한쪽만 고쳐졌을 때 필드가 조용히 갈라진다는 걸 이미 다른 자리들에서 겪은 터였다.
기능을 먼저 쌓고 나서야 데이터 모델의 구멍이 보였다. 검토를 미룬 대가도 하루 만에 돌아왔다. 다만 판단 로직을 한 곳에 모아둔 덕에 고치는 범위는 좁았다.
하루 끝에 설계 문서에 진행 상황과 남은 작업을 적어뒀다. 다음 세션이 바로 이어받게 하려고 끝난 작업과 빌더 UI 전환 절차를 같이 남겼다.
빌더 UI를 문서 모델 v2 위로 완전히 전환하는 일은 아직 남았다. 스펙 문서에 전환 절차만 적어둔 상태다. 공고별 문서 생성과 출처 표시 흐름도 설계만 있고 구현은 시작하지 않았다.