개발 이력 — 결정과 버린 대안을 그림과 함께 남긴다
H-26 · 2026-09-03 · H-25 에서 진단한 세 가지를 고쳤다
- 제기: 지시자 — "세 가지 다 고쳐라."
① 한 겹 더 싸여 온 답을 벗긴다 (진짜 원인)
engine/vision.py 에 _unwrap() 을 두고 _check() 앞에 세웠다. 배열·객체 자리에 JSON 문자열이 오면 풀고, {"pages": {"pages": [...]}} 처럼 한 겹 더 싸여 있으면 같은 이름 안쪽을 꺼낸다. 이름이 달라도 키가 하나뿐이고 그 값이 배열이면 꺼낸다.
풀리지 않는 문자열은 손대지 않는다 — 그건 _check 가 잡아야 할 진짜 오류다. 다섯 가지 모양으로 시험했다.
| 넣은 것 | 결과 |
|---|---|
| 정상 배열 | 그대로 통과 |
통째로 문자열 "{\"pages\":[...]}" | 풀어서 통과 (10개) |
배열만 문자열 "[...]" | 풀어서 통과 (10개) |
다른 이름으로 싸임 "{\"items\":[...]}" | 풀어서 통과 (10개) |
| JSON 아닌 문자열 | 여전히 실패 (의도한 대로) |
vision.ask() 를 지나는 모든 단계(분류·머리·품목·패킹)에 한 번에 걸린다.
② 화면 시각을 한국 시간으로
build_docs.py 의 when() 두 곳(작업대·질의응답)이 UTC 를 그대로 찍고 있었다. Intl.DateTimeFormat 으로 Asia/Seoul 고정 — 보는 사람 PC 시계가 어디로 맞춰져 있든 한국 시간으로 보인다. 타임존 표시가 없는 값은 UTC 로 보고(서버가 UTC 로 쓴다), 날짜로 못 읽는 값은 예전처럼 그대로 보여 준다.
③ 오류 문구가 서류 장수로 읽히던 것
판독 실패(sdk, 10장) 의 10장은 서류 장수가 아니라 한 번에 보내는 묶음 크기였는데, 27쪽 서류가 10쪽으로 읽혔다. ask(what=...) 를 더해 단계 이름을 받게 하고 문구를 바꿨다.
전 판독 실패(sdk, 10장): $.pages: array 여야 하는데 str 후 판독 실패(sdk, 쪽 분류 1~10쪽/27쪽 · 그림 10장 묶음): ...
호출부 넷에 이름을 달았다 — 쪽 분류 N~M쪽/전체쪽, 인보이스 머리, 인보이스 품목, 패킹 상세.
확인 — 아침에 두 번 죽은 그 서류로
[판독] 답이 문자열로 와서 풀었다 (pages, 557자) ← 같은 실패가 또 났고, 이번엔 풀었다 분류: 27쪽 I i P p I i P p I i P p I i P p I i i P p p p I i P p 건 나누기: 6건 → 6건 모두 시트까지. 엑셀 20260903_005.xlsx
시각도 확인했다 — 장부 2026-09-03T01:05:28Z 가 화면에 2026-09-03 10:05:28 로 나온다.
- 남은 것: #5(
20260831028)의 노란 칸 72개는 이번 건과 무관하다. 규격T/C B N W JSS S10T에 HS 규칙이 없어서다(Q-16 답 대기).
H-25 · 2026-09-03 · 실제 업무 서류가 판독 전에 죽은 원인 (진단만, 고치지 않음)
- 제기: 지시자 — "오늘 진행한 검수에 이런 오류가 있었던 것 같은데, 원인을 파악해서 보고해라, 해결은 내가 시키면 하고."
- 증상:
2026.09.03-kpf.pdf업로드 두 건이 같은 오류로 실패.
engine.vision.VisionError: 판독 실패(sdk, 10장): $.pages: array 여야 하는데 str
- 먼저 바로잡을 것 두 가지
- "10장" 은 서류 장수가 아니라 분류 묶음 크기(
split.BATCH)다. 실제 서류는 27쪽이다. 오류 문구가 사람을 헷갈리게 한다. - 화면의 접수 시각
01:04는 UTC 다. 실제로는 오늘 오전 10:04 (KST) 였다. 지시자도 "저 시간은 신뢰할 수 없다" 고 지적했다. 사이트가 UTC 를 그대로 찍고 있다. - 원인 — 모델이 답을 한 겹 더 싸서 준다: tool 인자를
{"pages": [...]}로 줘야 하는데, 가끔 답 전체를 JSON 문자열로 만들어pages안에 넣는다.
정상 : {"pages": [{"n":1,...}, ...]}
실패 : {"pages": "{\"pages\":[{\"n\":1,...}]}"} ← 문자열 한 겹- 빈도 — 이 서류에서 6번 중 3번(50%). 4건·8건 샘플에서는 안 났다. 서류마다 다르게 나온다.
- 잘린 게 아니다:
stop_reason=tool_use, 출력 445토큰 /max_tokens=16000. 응답은 온전하다. - 버려지는 답이 맞는 답이다: 그 문자열을
json.loads하면 10쪽 분류가 신뢰도 0.99 로 완전하게 들어 있다. 엔진이 정답을 받아 놓고 스키마 검사에서 버린다. - 재시도가 못 살리는 이유:
ask(retries=1)이라 두 번 부르는데 한 번당 실패 확률이 50% 면 둘 다 실패할 확률이 25% 다. 게다가 재시도의strict는 "반드시 record 도구를 호출하라" 를 덧붙일 뿐이라 이 실패 방식과 무관하다. - 범위:
vision.ask()를 쓰는 모든 호출(분류·머리·품목·패킹)이 같은 위험에 노출돼 있다. 이번엔 첫 단계인 분류에서 터져 눈에 띄었을 뿐이다. - 고칠 자리(제안, 아직 안 함):
vision._check()가 array/object 자리에 str 이 오면json.loads로 한 번 풀어 보고, 풀린 것이{"pages": ...}처럼 한 겹 더 싸여 있으면 벗겨 쓰게 한다. 다섯 줄이면 이번 건은 통과했을 것이다.
H-24 · 2026-09-03 · 검증용 두 번째 판독기 — Tesseract 를 재 보고 버렸다
- 제기: 지시자 — "다른 파싱 툴을 연결해서 검증을 하는 방안을 해보면 좋겠는데 비용이 발생하지 않고 성능이 괜찮은 것으로 추천해라."
- 전제: 우리 서류는 글자층 없는 팩스 스캔이다. PDF 텍스트 추출 도구(pdfplumber·PyPDF 등)는 아무것도 못 읽는다. 후보는 OCR 뿐이다.
- 재 봤다 — Tesseract 5.3.4 (무료, 설치 15MB). 방법: AI 가 이미 읽고 검산까지 통과한 건을 정답지로 놓고, 같은 쪽 그림을 OCR 한 원문에 그 값이 실제로 나타나는지만 봤다(구조 파싱을 시키지 않았다. OCR 은 표에 약하지만 "이 숫자가 종이에 있었나" 는 비교적 맞히기 때문).
| 결과 | |
|---|---|
| 3건 · 36개 항목 대조 | 23개 일치 (63%) |
| 못 찾은 것 | 대부분 수량(7560, 9698, 6296, 29860 …)과 소계 |
| 속도 | 건당 4쪽에 15~22초 |
- 해상도를 올리면 나아지나 — 아니다. 같은 건에 200/300/400dpi × psm 6/4/11 로 아홉 번 돌렸다.
200dpi psm6 7/11 (63%) ← 제일 나음 300dpi psm6 6/11 (54%) 400dpi psm6 4/11 (36%)
올릴수록 나빠진다. 팩스 스캔의 잡티까지 같이 커지기 때문이다. 우리가 준 그림이 나쁜 게 아니라 원본이 그런 것이다.
- 결론: 버린다. 37% 가 헛경보면 검증기가 아니라 소음이다. 노란 칸이 지금보다 늘어나 사람이 더 보게 만든다.
- 대신 갈 길: ① 이미 있는 산술 검산(수량×단가=금액, 합=소계, 소계+운임=총계, 인보이스↔패킹 대조)이 숫자 오류를 이미 잡고 있다 — 4건·8건 샘플에서 실패 0건. ② 산술로 못 잡는 칸(인보이스 번호·부품번호·규격)은 다른 모델로 한 번 더 읽어 서로 대조하는 쪽이 맞다. 구조 대 구조 비교라 신호가 깨끗하고, 캐시가 있어 같은 서류를 다시 읽어도 값이 안 든다.
- 서버에 tesseract 는 깔린 채로 뒀다(15MB). 쓰지 않는다.
H-23 · 2026-09-03 · 엑셀 미리보기를 겹쳐 뜨는 창으로
- 제기: 지시자 — "고객사 엑셀 미리보기 버튼 눌렀을 때 스크롤이 내려가면서 화면 중심이 이동하는게 불편하다."
- 무엇: 미리보기가 페이지 아래에 펼쳐지고
scrollIntoView로 그리로 스크롤을 옮기던 것을, 화면 위에 겹쳐 뜨는 창으로 바꿨다. 페이지는 그대로 있고 닫으면 보던 자리로 바로 돌아온다. - 보임 제어는
.show클래스 하나에만 둔다 —hidden속성에 기대면 CSS 가 이긴다(H-15 에서 겪음). - 닫는 길 셋: [닫기] ·
Esc· 바깥 누르기. 표 안을 누른 것과 헷갈리지 않게 바깥 클릭은e.target===this로 가린다. - 뒤 배경이 같이 스크롤되지 않게 열 때
body.style.overflow='hidden', 닫을 때 되돌린다. - 창은 고정 높이(
min(86vh,880px))이고 표만 안에서 스크롤된다. - 걸린 것 — 창이 화면 밖으로 209px 삐져나갔다: 바탕을
display:grid; place-items:center로 두었는데, 안쪽 표가 아주 넓어서 격자 칸 자체가 화면보다 커졌고 가운데 정렬이 그 넓은 칸을 기준으로 잡혔다.grid-template-columns:minmax(0,1fr)로 칸을 화면 폭에 묶어 해결했다. 같은 CSS 를 쓰는 겹침 팝업(.modalwrap)은 내용이 좁아(520px) 이 문제가 안 드러나 있었다. - 확인: 열 때
window.scrollY0 → 0 (페이지 안 움직임). 닫기 세 가지 모두getComputedStyle().display === 'none'이 되고 배경 잠금도 풀린다. 표 안을 눌렀을 때는grid로 열려 있다. 좌우 여백 23px 로 대칭.
H-22 · 2026-09-03 · 작업 하나 = 엑셀 하나. 아카이브를 없애고 내려받기로
- 제기: 지시자 — "지금 니가 갖고 있는 프로젝트 대원칙에서 뭔가 좀 이상한 문제가 있는 것 같다. 1회 사이클에서 엑셀파일이 나오면 그걸로 끝 … 엑셀파일이 계속 업데이트 되어야 하는게 아니다." 이어서 "서류창고는 네이버클라우드로 정해졌잖아", "이 아카이브라는 표현이 필요없겠다 … 내려받기 버턴을 만들어서 로컬 PC에 내려받을 수 있게".
① 엑셀은 쌓이는 장부가 아니다
그동안 워크북 하나(KPF_쿠시업로드.xlsx)에 시트를 계속 붙였다. 틀린 전제였다. 올리고 → 나누고 → 읽고 → 엑셀 하나 나오면 그 사이클은 끝이고, 다음에 올린 파일은 또 다른 엑셀 하나를 낳는다.
| 전 | 후 | |
|---|---|---|
| 엑셀 | 워크북 하나에 계속 붙임 | 작업 하나 = 워크북 하나 |
| 이름 | KPF_쿠시업로드.xlsx 고정 | YYYYMMDD_NNN.xlsx (20260903_001.xlsx) |
| 자리 | 엑셀/ | 엑셀/YYYYMMDD/ |
| 새 작업 | 워크북을 엑셀/이전/ 으로 물림 | 필요 없다. 새 작업은 새 파일을 연다 |
| 다시 변환(replace) | 옛 시트를 지우고 다시 붙임 | 같은 이름으로 파일째 새로 만든다 |
| 따로 추가(add) | 시트에 _2 를 닮 | 다음 차수 엑셀을 하나 더 만든다 |
이 전제가 틀렸던 탓에 생긴 군더더기가 같이 사라졌다 — drop_sheet 로 옛 시트 찾아 지우기, sheet_name 물려받기, 시트 이름 _2 붙이기, /workbook/reset. 시트가 42장까지 쌓였던 H-12 도 애초에 이 전제 때문이었다.
- 코드:
engine/excel.py에next_book()(그날의 다음 차수),engine/server.py에
open_book()·book_of()·job_file(). /sheet* 는 ?book=20260903_001.xlsx 를 받는다 ("현재본" 이 없으므로 어느 권인지 늘 말해야 한다). /books 로 만들어진 엑셀 목록을 준다. src/index.js 는 그 질의문자열을 엔진까지 넘기고, newJob 에서 /workbook/reset 호출을 뺐다.
- 확인: 4건을 변환 →
엑셀 20260903_001.xlsx (작업 하나 = 워크북 하나)→ 시트 4장
(서식 견본 포함 5장). 첫 시트가 4줄 = 품목 3 + 운임 1 (H-19 그대로).
② 아카이브를 없애고 내려받기를 넣었다
쪼개진 파일은 만들어지는 즉시 건별/ 에 저장되고 클라우드에도 올라간다. 아카이브는 그것을 보관/ 에 또 한 번 복사하는 것이라 아무것도 더 지켜 주지 못했다. 사람이 정작 원한 것은 "내 PC 로 가져가기" 였다.
- 건 줄은 [내려받기](그 PDF 하나), 접수 줄은 [전체 내려받기](그 접수의 건별 PDF 를
zip 하나로). 엔진에 GET /zip/{upload_id}, 사이트에 /api/zip/{upload_id} 를 냈다. 한글 파일명이라 Content-Disposition 은 RFC 5987 형식으로 준다.
POST /api/archive와 작업대의 아카이브 단추를 걷어냈다. 이미보관/에 있는 파일은
그대로 둔다.
- 확인:
/api/zip/…→HTTP 200 · application/zip · 233,457바이트.
③ 서류 창고 표시를 네이버 클라우드로
작업대 3단계가 아직 "회사 공유폴더" 라고 적혀 있었고, 창고 칸은 "경로가 아직 없습니다 · 질의응답 Q-07에 경로를 적어 주세요" 를 띄우고 있었다. 창고는 H-17 에서 네이버 클라우드로 정해졌으므로 맞지 않는 안내였다. /api/health 가 cloud {on, bucket} 을 돌려주게 하고, 그 자리에 버킷 이름과 무엇이 쌓이는지를 적었다. Q-07(회사 공유폴더 경로)은 이로써 무의미해졌다.
- 뒤이어 고친 것 두 가지 (지시자 — "파일이름을 누르니 이렇게 나온다. 파일이 열리면 좋겟는데"):
- 파일명의
#때문에 404 였다. 건별 파일명이KPF_20260827009_SUPPLY_#3.pdf인데,#은 URL 에서 조각(fragment) 구분자다. 주소를 인코딩하지 않고 그대로 넣으면 브라우저가 거기서 자르고 서버는…_SUPPLY_까지만 받는다.fileHref()가 경로 조각마다encodeURIComponent하도록 고쳤다(%233). 고친 뒤200 · application/pdf · inline— 브라우저에서 그대로 열린다. - 엑셀 이름이 옛 워크북을 가리켰다.
/api/uploads는 최신이 먼저 오는데live[live.length-1]로 마지막을 집어 가장 오래된 작업의KPF_쿠시업로드.xlsx를 가리켰다.live[0]로 고치고, 새 작업으로 내린(done) 것과 지운 것은 빼도록 했다.
H-21 · 2026-09-03 · 판독용 이미지도 클라우드 창고에 올린다 (H-17 결정 뒤집음)
- 제기: 지시자 — "근데 판독용 이미지는 저장창고인 네이버클라우드 저장소에 저장하면 되는거 아니야?" 이어서 "일단 클라우드에 올리는 걸로 하자".
- 원래 결정(H-17): 이미지는 안 올렸다. 파생물이라 원본 PDF 에서 몇 초면 다시 만들 수 있고, 용량의 90% 를 차지하기 때문이다(실측: 8건 한 번 처리에 11MB 중 이미지가 9.9MB). 판독 캐시 열쇠가 이미지 바이트라서 다시 구워도 AI 비용이 안 든다는 것도 근거였다.
- 왜 뒤집었나: H-20 에서 엔진이 네이버 서버로 가면서 디스크가 10GB 로 줄었다(여유 4.7GB, 그중 이미지가 11MB로 창고의 45%). 클라우드에 사본이 있어야 로컬에서 마음 놓고 지울 수 있다. 앞으로 확인 화면(
/review.html)에서 쪽 그림을 볼 때도 쓴다. - 무엇: 세 곳을 같이 고쳤다. 한쪽만 고치면 사이트가 클라우드에 있는 파일을 못 찾고 엔진으로 헛걸음한다.
| 파일 | 고친 것 | |---|---| | engine/cloud.py | KEEP 에 이미지 추가, SKIP_DIRS 에서 제거. 이제 안 올리는 것은 판독/_cache/ 뿐이다 | | engine/server.py | run_pipeline 끝에서 cloud.put_many(work_dir). 건마다 올리지 않고 한 번에 — 분류용 띠와 건별 그림이 다 만들어진 뒤라야 빠짐이 없다 | | src/ncloud.js | inCloud() 에 이미지 추가 (엔진의 KEEP 과 짝) |
- 기존 것 밀어넣기:
/cloud/mirror로 이미 있던 이미지를 올렸다 — 107장 올림 · 209개 건너뜀 · 실패 0. - 확인: 사이트로 이미지 한 장을 받아
HTTP 200 · Content-Type: image/png · 205,849바이트 · x-kpf-source: cloud. 클라우드에서 직접 온다. - 아직 아닌 것: 올리는 것만으로는 서버 디스크가 안 줄어든다. 사본이 하나 더 생겼을 뿐 로컬 파일은 그대로다. 디스크를 비우려면 오래된 이미지를 로컬에서 지우는 규칙이 따로 있어야 한다 — 이제 클라우드에 사본이 있으니 안전하게 지울 수 있다.
H-20 · 2026-09-03 · 엔진을 이 PC에서 네이버 클라우드 서버로 옮겼다
- 제기: 지시자 — "엔진 PC가 뭐고? 소스코드는 깃에 다 있고, 지금 DB를 네이버클라우드로 한다는건데?" 이어서 다른 프로젝트(
voucher_review)가 PC 없이 도는 이유를 확인해 달라 → "네이버를 이용하는게 좋겠네" → "일단 한 번 해보자". - 알아낸 것:
voucher_review도 "엔진 PC" 가 없는 게 아니라 G2SysNet 서버가 그 자리에 있었다(AI_AGENT_DEPLOYMENT_GUIDE.md: 깃 푸시 → 자동 빌드·배포, DB는 G2SysNet PostgreSQL, 파일은 서버 로컬). 우리도 서버 한 대만 있으면 되는 것이었다. - 판단 — 왜 Supabase/Vercel 로 안 갔나: 지시자가 제안한 구조(Vercel + Supabase + Python Worker)는 맞지만, Supabase Edge Functions 도 Deno 라 Python 을 못 돌린다. Cloudflare Workers 와 같은 한계다. 갈아엎어도 "Python 돌릴 자리" 문제는 그대로 남고 사이트만 며칠 다시 쓰게 된다. 그래서 급한 것(엔진 자리)부터 풀었다.
- 왜 네이버: 지시자가 네이버를 고른 이유는 국내 보관이 아니라 "한국어 메뉴와 직관적인 표현"이었다. 다만 실제로 이득이 큰 곳은 콘솔 언어가 아니라 원화 결제·세금계산서(매달 겪는 일)였고, 창고(
kpf-dreamcustoms)와 같은 리전이라 서버↔창고가 내부망으로 붙는 이점도 있다.
서버
| 이름 | kpf-engine (Instance ID 144917022) |
| 사양 | s2-g3a vCPU 2 / 메모리 8GB / SSD 10GB · KVM(3세대) |
| OS | Ubuntu 24.04.1 LTS · Python 3.12.3 · 로케일 en_US.UTF-8(한글 파일명 정상) |
| 위치 | 한국 KR-1 (창고와 같은 리전) |
| 공인 IP | 49.50.128.41 |
| 망 | VPC kpf-vpc (10.0.0.0/16) · Subnet kpf-subnet (10.0.1.0/24, Public) |
| 방화벽 | ACG kpf-vpc-default-acg — TCP 22 를 지시자 PC 한 곳(/32)에만 연다. 엔진 포트(8790)는 안 연다(터널이 밖으로 나가서 붙는 방식이라 필요 없다) |
| 요금 | 월 88,000원 + 공인 IP 4,032원 (VAT 별도) |
서버 안 구조
/opt/kpf/app 코드 (engine/ + 샘플/케이피에프 엑셀업로드.xlsx)
/opt/kpf/venv 파이썬 꾸러미
/opt/kpf/창고 원본 / 건별 / 이미지 / 판독 / 엑셀 / 보관
/opt/kpf/kpf.env 키 4개 (chmod 600) — ANTHROPIC_API_KEY · NCLOUD_ACCESS_KEY
· NCLOUD_SECRET_KEY · ENGINE_TOKEN- 자동 시작:
kpf-engine.service와cloudflared.service를 systemd 에 등록했다. 재부팅해도 알아서 뜬다 — 8장에 있던 "상시 가동" 항목이 여기서 같이 풀렸다. - 터널: 같은 터널(
bc6677fc-…)을 PC 에서 서버로 옮겼다. 자격증명 파일만 옮기고 DNS 는 그대로다(engine.dreamcustomsservice.com). PC 것을 먼저 끄고 서버 것을 켰다 — 같은 터널을 두 곳에서 돌리면 요청이 둘로 갈린다. - 창고 이사: PC 의
C:\KPF창고에서 이미지·휴지통을 뺀 13MB(원본·건별·엑셀·판독·보관)를 그대로 옮겼다. 판독 캐시(판독/_cache)도 같이 옮겨서 같은 서류를 다시 읽을 때 AI 비용이 안 든다.
걸린 것 네 가지
- Micro 타입은 아예 생성이 안 된다. 무료 자격이 있는데도(콘솔이 "2026년 09월에 결제수단 등록 → 2027년 09월 말일까지 무료"라고 확인해 줬다)
400 Bad Request로 거부된다. 화면은 "오류가 발생하였습니다"로만 뭉개서 보여줘 원인을 알 수 없었는데, 개발자도구 Network 탭에서 실제 응답을 꺼내errorCode 10008 · originCode 1151036 · requestId 29dcdca9-08b3-482e-a75e-021e180c3339를 얻었다. 공인 IP·이미지(ubuntu/rocky)·요금제(월/시간)를 바꿔도 전부 동일하게 실패했고, 같은 설정에서 Standard 는 한 번에 성공했다. → 고객문의 대기. 풀리면 월 88,000원이 0원이 된다. - 400 이라는 것 자체가 단서였다. 401/403 이면 권한·결제, 500 이면 네이버 장애인데 400 은 "우리가 보낸 값" 이다. H-17 에서 배운 대로 남의 설정을 탓하기 전에 요청을 봤다.
Pillow을 빠뜨렸다.render.py가 함수 안에서from PIL import Image를 하는 늦은 import 라 코드를 훑을 때 놓쳤고, 첫 변환이ModuleNotFoundError로 죽었다. 같은 식의 늦은 import 가boto3·anthropic·uvicorn에도 있다. →engine/requirements.txt를 만들었다. 눈으로 훑지 말고 이 파일을 쓴다.- systemd ExecStart 는 공백 있는 인자에 따옴표가 필요하다.
--template 샘플/케이피에프 엑셀업로드.xlsx를 그냥 쓰면 systemd 가 공백에서 쪼개 인자가 둘이 된다. --token을 환경변수로도 받게 고쳤다. 명령줄에 토큰을 쓰면ps목록에 그대로 보인다.--bucket이 이미 쓰던 방식(os.environ.get)에 맞췄고, systemd 는EnvironmentFile로 넘긴다.
확인
- 밖에서
https://engine.dreamcustomsservice.com/health→{"root":"/opt/kpf/창고", "cloud":{"on":true,"bucket":"kpf-dreamcustoms"}}. PC 엔진을 끈 상태에서 응답한다. - 사이트에서 8건 PDF 변환 → 37쪽 분류 → 8건 정확히 나눔 → 판독·엑셀까지.
- 한글 폴더(
원본·건별…)와 한글 파일명(KPF_20260827007_EARNEST_#1.pdf) 모두 정상.
남은 것
- 디스크가 10GB뿐이다(여유 4.7GB). 판독용 이미지가 용량의 90%라 쌓이면 찬다. 오래된 이미지를 지우는 규칙이 필요하다 — 이미지는 원본에서 다시 만들 수 있다.
- 깃 푸시 → 자동 배포는 아직 없다. 지금은 손으로 올린다.
H-19 · 2026-09-03 · 운임 줄을 표에 넣는다 (Q-03 반영)
- 제기: 지시자 — Q-03 답. "CIF Freight cost는 넣는다. 다만, 규격1에 "CIF Freight cost"를 넣고, 규격2, 규격3은 공란, 수량은 항상 1, 수량단위는 항상 ZZ, 단가와 금액에는 인보이스에 기재된 금액, 순중량은 0으로 하고, 나머지 칼럼은 위의 행과 동일하게 적용." 이어서 "처리해라."
- 무엇:
engine/excel.py에_freight_row()를 두고 품목 줄 아래에 운임 줄 한 줄을 붙인다. 그동안은 표에서 빼고 R1 메모에만 적었다.
| 열 | 값 | |---|---| | A~I (HS·원산지·FTA·포장수량·포장단위·인보이스넘버·세번품명·거래품명·발급협정코드) | 바로 윗줄과 똑같이 | | J 규격1 | CIF Freight cost | | K 규격2 · L 규격3 | 공란 | | M 수량 | 1 (항상) | | N 수량단위 | ZZ (항상) | | O 단가 · P 금액 | 인보이스에 적힌 운임 금액 | | Q 순중량 | 0 (항상) |
- 판단 — 노란 칠도 따라간다: A~I 는 값만이 아니라 노란 칠과 "왜 노란지" 설명까지 윗줄에서 그대로 복사한다. 윗줄에서 HS 를 못 정했으면 이 줄도 못 정한 것이기 때문이다. 규칙으로 정해진 M·N·Q 는 확인이 필요 없으니 칠하지 않는다.
- 없는 경우: 인보이스에 운임 줄이 없으면(
freight_amount가 없으면) 줄을 안 붙인다. 품목 줄이 하나도 없는데 운임만 있으면 A~I 를 노랗게 두고 "윗줄이 없어 못 채웠다"로 표시한다. - 확인: 4건 샘플로 실제로 붙여 봤다. #1(품목 3줄)·#2(품목 2줄)에 5행·4행으로 운임 줄이 붙었고
… | CIF Freight cost | | | 1 | ZZ | 2500 | 2500 | 0, A~I 는 윗줄과 같고 윗줄의 노란 칸(A·D·G·H)이 그대로 따라왔다. 운임이 없는 #4 는 줄이 안 늘었다. - 걸린 것 — 또
\n: 코드를 파이썬 문자열로 넣다가\n이 진짜 줄바꿈이 되어 f-문자열이 깨졌다. H-13 에서 같은 것을 겪었다. 이번엔 문법 검사로 바로 잡았다. - 질의응답: Q-03 반영 표시.
H-18 · 2026-09-03 · 클라우드 창고 2단계 — 엔진이 꺼져 있어도 파일이 열린다
- 제기: 지시자 — "위에 2단계 바로 진행해라."
- 무엇:
src/ncloud.js를 새로 두고 사이트의/files/*가 클라우드를 먼저 본다. 거기서 나오면 엔진을 아예 부르지 않는다. 클라우드에 없는 것(판독용 이미지 등)만 엔진으로 물러난다. 응답에x-kpf-source: cloud를 달아 어디서 왔는지 알아볼 수 있게 했다. - 발견 — 네이버는 presigned URL 을 거부한다: 처음에는 서명한 주소로 넘겨(302) 브라우저가 네이버에서 곧장 받게 하려 했다.
403 SignatureDoesNotMatch. 내 구현을 의심했는데 boto3 가 만든 presigned URL 도 똑같이 403 이었다. 반면 같은 키로 헤더를 서명한get_object는 멀쩡했다. 그래서 헤더 서명(AWS SigV4)으로 Worker 가 직접 받아 흘려보내는 방식으로 바꿨다. 파일이 Worker 를 한 번 거치지만 확실히 된다. - 이번에는 내 것과 남의 것을 같은 조건으로 비교해서 30분 만에 갈랐다. H-17 에서 계정 설정을 세 번 잘못 짚은 뒤에 배운 방법이다.
- 확인: 엔진을 실제로 껐다(
PID 30120종료,127.0.0.1:8790응답 없음 확인). 그 상태에서/files/원본/…4건.pdf가 HTTP 200 · 242,911바이트 ·%PDF-1.4·x-kpf-source: cloud. 엔진이 죽어 있어도 서류가 열린다. - 아직 아닌 것: 새 업로드와 변환은 여전히 엔진이 켜져 있어야 한다. 3단계(엔진이 일감을 스스로 가져가기)에서 푼다.
- 사이트 secrets 추가:
NCLOUD_ACCESS_KEY·NCLOUD_SECRET_KEY·NCLOUD_BUCKET.
H-17 · 2026-09-02 · 클라우드 창고 1단계 — 네이버 Object Storage 에 사본 (R-12 뒤집음)
- 제기: 지시자가 클라우드 창고로 방향을 바꾸고 네이버 클라우드를 택했다(Q-19). R-12 "R2 안 씀, 창고 = 회사 공유폴더" 를 뒤집는 결정이다.
- 왜 바꾸나: 창고가 엔진 PC 디스크뿐이라 그 PC 가 꺼지면 업로드도 [PDF 보기]도 멈추고, 고장나면 서류를 잃는다.
- 왜 네이버인가: 데이터가 국내에 남는다. 통관 서류에는 거래처·단가가 들어 있어 해외 보관이 고객사 계약에 걸릴 여지가 있다. R2 가 붙이기 쉽고 내려받기도 공짜지만(무료 10GB/월), 되돌리기 어려운 결정이라 국내를 택했다.
- 무엇:
engine/cloud.py. 버킷kpf-dreamcustoms· endpointhttps://kr.object.ncloudstorage.com· regionkr-standard. 키는 환경변수NCLOUD_ACCESS_KEY/NCLOUD_SECRET_KEY로만 받고 저장소에 넣지 않는다. 파일이 만들어지는 다섯 곳(접수·쪼개기·판독·엑셀·보관)에서 사본을 올린다.POST /cloud/mirror로 창고 전체를 맞출 수 있다. - 1단계는 사본이다: 원본은 여전히 엔진 PC 창고다. 올리기가 실패해도 판독은 끝난다 — 모든 실패를 삼키고 로그만 남긴다. 사이트가 클라우드에서 직접 꺼내는 것은 2단계다.
- 안 올리는 것:
이미지/(판독용 PNG)와_cache/. 다시 만들 수 있고 용량의 90% 다(실측: 8건 한 번에 11MB 중 이미지가 9.9MB). 빼니 접수당 약 1MB.
하루를 잡아먹은 오진 — 남겨 둔다
PutObject 만 403 AccessDenied. 버킷 만들기·목록·ACL 읽기는 전부 통했다. 권한 문제로 보였다.
내가 틀린 순서:
- 키가
ncp_iam_로 시작하는 것을 보고 서브계정 키라고 단정했다 → 서브계정은 하나도 없었다. 접두어만 보고 지레짐작했다. - 결제수단 미등록이라고 했다 → 카드가 등록돼 있었다.
- 이용 신청 미완료라고 했다 → "상품 이용 중" 이었다.
지시자가 화면을 두 장 보여주고서야 멈췄다. 진짜 원인은 내 코드였다:
boto3 1.36+ 는 올릴 때 AWS 전용 체크섬 헤더(x-amz-checksum-*)를 기본으로 붙인다. 네이버 Object Storage 는 그것을 거부하고 403 AccessDenied 를 돌려준다. 권한 메시지라서 권한을 뒤지게 만든다.
Config(request_checksum_calculation="when_required",
response_checksum_validation="when_required")놓친 단서: 처음부터 "본문이 있는 요청만 실패한다"가 보였다. 버킷 만들기(본문 없음) OK, 파일 넣기(본문 있음) 실패. 계정 설정이 아니라 요청 내용을 의심했어야 했다. 남의 설정을 탓하기 전에 내가 보내는 것부터 본다.
- 확인: 고친 뒤
/cloud/mirror로 183개 전부 올라감(실패 0), 10.1MB — 판독 80 · 엑셀 70 · 원본 13 · 건별 12 · 보관 8. 새 접수도 자동으로 올라가는 것 확인(23:50:57 접수 → 같은 시각 업로드).
H-16 · 2026-09-02 · 화면 정리: 이름 변경, 드롭존 축소, 신호등 색, 노란 칸 이유, 접기
- 제기: 지시자 — "파일올리기가 쓸데 없이 너무 크네.. 신호등은 노란색 파란색 이렇게 눈에 확 띄이게. 노란색으로 표시한 건 이유를 기재해주면 좋겠다. PDF를 여기에 던져넣으세요와 그 아래 두 줄 텍스트 빼라. 상단 KPF 통관서류 포털은 KPF 수출서류 변환 프로그램으로. 작업내역과 작업내역 로그는 내용을 숨길 수 있게 버튼 넣어라."
- 무엇:
- 이름:
KPF 통관서류 포털→KPF 수출서류 변환 프로그램(머리띠·제목·메타 4곳). - 히어로 제거: "PDF를 여기에 던져 넣으세요" 큰 제목과 아래 설명 두 줄을 뺐다.
- 드롭존 축소:
padding:clamp(44px,10vw,96px)에 글자 52px 짜리 세로 상자였다 — 화면 절반을 먹었다. 한 줄로 눕혀padding:18px, 글자 15px 로 낮췄다. - 신호등 색: 초록·회색이 흐려서 안 보였다. 끝 = 파랑 알약, 하는 중 = 노랑 알약(깜빡), 실패 = 빨강. 테두리·바탕·글자를 다 칠해 멀리서도 보이게 했다.
- 노란 칸 이유:
append_set이 이미 셀 메모(Comment)에 "왜 노란지"를 적고 있었다. 엔진GET /sheet/{name}이 그 메모를why로 함께 돌려주고, 미리보기 표에서 마우스를 올리면 뜨고 칸을 누르면 표 아래에 노란 띠로 적힌다. - 접기: 작업내역과 작업 내역 로그 머리에 [접기]/[펼치기]. 접은 상태는
localStorage에 기억해 다음에 열 때도 접혀 있다. - 확인: 배포 후 실제 사이트에서 — 이름 바뀜(탭 제목까지), 드롭존 한 줄, 신호등이 파란 알약(
rgb(231,237,255)), 접기block→none→block(글자도 접기↔펼치기), #4 시트의 노란 칸 72개 중 A2 를 눌러 "규격 대응표: T/C B N W JSS S10T M30x3.5Px80: 대응표에 없음 → 사람이 HS 코드를 채워야 한다" 가 뜨는 것 확인.
H-15 · 2026-09-02 · 팝업이 안 닫히던 것, 시트를 너무 많이 지운 것
두 가지 다 내(구현) 실수다. 둘 다 "확인했다"고 말했는데 확인이 틀렸다.
1) 팝업이 처음부터 떠 있고 닫히지도 않았다 (두 번 고침)
- 제기: 지시자 — "저 팝업은 안없어지노.."
- 원인:
.modalwrap{display:grid}가 브라우저 기본[hidden]{display:none}을 이긴다(클래스 선택자가 더 세다). 그래서hidden속성은 아무 힘이 없었다. 팝업은 페이지가 열리자마자 본문이 빈 채로 떠 있었고, 단추를 눌러hidden=true로 만들어도 화면은 그대로였다. - 조치:
.modalwrap[hidden]{display:none}을 명시했다. - 첫 조치로 부족했다:
.modalwrap[hidden]{display:none}을 더했지만 지시자 화면에는 여전히 떠 있었다. 배포는 됐는데도 그랬다. 속성 하나와 CSS 규칙이 서로 이기려 드는 구조 자체가 약하다. - 다시 고침: 보이게 하는 힘을 클래스 하나(
.show)에만 뒀다. 기본은.modalwrap{display:none}, 뜰 때만.show가 붙는다. 다툴 상대가 없다. 더불어 바깥 어두운 곳 클릭·ESC 로도 닫히게 하고, 페이지가 뜰 때 반드시 닫힌 상태로 시작하게 했다. - 확인: 캐시를 우회해 새로 받아
display:none· 높이 0 · 본문 0자 확인. 열고 [취소]·바깥 클릭·ESC 세 가지 모두로 닫히는 것 확인. - 왜 못 잡았나: 검증할 때
el.hidden속성만 봤다. 속성은true였으니 통과로 읽었다. 사람 눈에 보이는 것은getComputedStyle(el).display다. 앞으로 화면에 보이나/안 보이나는 computed style 과 높이로 확인한다.
2) 시트를 네 장 지우려다 여덟 장을 지웠다
- 제기: 지시자 — "치워라"(시험으로 붙은
_2시트 정리). - 원인: 지울 것을 고르는 정규식을
_\d+$로 썼다. 그런데 시트 이름20260828_#1_20260828014도 밑줄+숫자로 끝난다 — 인보이스 번호다. 그래서 원본 시트까지 전부 걸렸고 서식 한 장만 남았다. - 되살림: 지우기 전에
엑셀/이전/KPF_쿠시업로드_20260902_222717.xlsx로 백업해 둔 덕에 그대로 복구했다. 다시 고를 때는 이름 전체를날짜8_#건번호_인보이스6+ (_꼬리표)로 파싱해 꼬리표가 있는 것만 지웠다. 결과 5장(서식 + 4건). - 교훈: 지우기 전 백업이 실제로 사람을 살렸다. 그리고 무엇을 지울지 먼저 찍어 보고 지운다 — 이번에도 목록은 찍었으나 내가 그 목록을 안 읽고 넘겼다.
H-14 · 2026-09-02 · 같은 건을 다시 변환하면 팝업으로 묻는다 (덮어쓰기 / 따로 추가)
- 제기: 지시자 — "같은 인보이스를 한 작업 안에서 두 번 변환하면 팝업이 뜨게 하고 선택하게 하라."
- 발견(왜 시트가 쌓였나): 다시 변환할 때
run_pipeline이 판독 결과out을 새로 만들면서 지난번sheet_name을 잃어버렸다. 그래서add_sheet의drop_sheet(out["sheet_name"])이 빈 문자열을 받아 아무것도 못 지웠고,sheet_name()이 겹친다고_2·_3을 달았다. 인보이스 9개짜리가 시트 42장이 된 진짜 원인이다. - 무엇:
run_pipeline이판독/{doc_id}.json이 이미 있으면 그sheet_name을 물려받는다.- 엔진
POST /convert/{upload_id}가{"mode": "replace"|"add"}를 받는다.replace(기본)는 옛 시트를 지우고 같은 이름으로 다시 붙이고,add는 두고_2를 단다. 사이트가 그대로 중계한다. - 팝업. 작업대에서 [다시 변환] 을 누르면 겹치는 인보이스 번호를 짚어 보여주고 [덮어쓰기] · [따로 추가] · [취소] 를 고르게 한다. 겹치지 않으면 안 뜨고 그냥 돈다. 여러 건을 한꺼번에 변환할 때는 건마다 묻지 않고 한 번만 묻는다.
- [다시 변환] 단추를 새로 달았다. 전에는
변환 대기일 때만 [변환] 이 있어서 재변환할 방법이 화면에 아예 없었다 — 팝업을 만들어도 뜰 일이 없었다. 이미 건이 있는 접수에는 [다시 변환] 이 붙는다. - 겹침 판정: ① 이 접수의 건에 이미
sheet_name이 있나 ② 다른 살아 있는 접수가 같은 인보이스 번호로 시트를 붙였나. 둘 중 하나면 겹친 것이다. - 확인: 4건 샘플로 실제 사이트에서 밟았다. 1차 변환 → 시트 5장(서식+4). [다시 변환] → 팝업이 인보이스 20260828014~017 넷을 짚어 뜸 → [덮어쓰기] → 시트 5장 그대로. 다시 [다시 변환] → [따로 추가] → 9장(
_2넷 추가). 엔진 로그가replace · replace · add로 선택을 그대로 따랐다. - 한계: 새로 올린 파일은 쪼개기 전이라 인보이스 번호를 모른다. 그래서 처음 변환할 때는 겹침을 미리 못 잡는다 — 서로 다른 두 PDF에 같은 인보이스가 들어 있으면 붙고 나서야 보인다. 그때는 [다시 변환]·[덮어쓰기] 로 정리한다.
H-13 · 2026-09-02 · 작업대를 단계 상자 넷으로, 새 작업이 엑셀까지 새로 연다
- 제기: 지시자 — "위에서 아래로 프로세스 별로 버튼이 잘 배치되어야 하고, 박스 밖에 있는 버튼과 안에 있는 버튼은 좀 차이나 보이게 해야하는데, 뒤죽박죽이네… 한 번 프로세스가 돌고 새작업 하면 서류창고든, 고객사 엑셀이든 깨끗한 상태가 되어야지… 단계별로 확실하게 직관적으로 보이게."
- 무엇:
- 단계 상자 넷. ① 파일 올리기 ② 쪼개기·읽기 ③ 서류 창고 ④ 고객사 엑셀. 위에서 아래로 하는 순서 그대로다. 상자마다 머리에 번호·제목·한 줄 안내가 있고, 그 단계의 단추는 그 상자 안에 있다. 지금 있는 단계는 번호 동그라미가 파랗다.
- 단추 두 종류. 상자 밖 [새 작업] 은 꽉 찬 검정(
.dl), 상자 안 [변환]·[아카이브]·[엑셀 미리보기]·[현재본 내려받기] 는 테두리형(.act). 밖은 판 전체를 바꾸는 것, 안은 그 단계에서 하는 일이다. - [변환] 을 2단계 상자 안으로. 전에는 작업내역 도구줄에 [대기 N건 모두 변환] 으로 있어서 쪼개기 단계와 멀었다.
- [새 작업] 이 엑셀도 새로 연다. 엔진에
POST /workbook/reset을 두어 현재본을엑셀/이전/으로 물리고 치운다. 다음 건이 붙을 때 서식 견본에서 새로 만들어진다. 사이트는xlsx_version도 비운다. - 왜: 시트는 한 파일에 계속 쌓인다. 그래서 같은 서류를 여러 번 변환하면 시트가 겹겹이 남았다 — 인보이스 9개인데 시트 42장까지 갔다. 작업이 끝나고 새로 시작하면 엑셀도 빈 장에서 출발해야 사람이 헷갈리지 않는다. 지우는 것이 아니라
엑셀/이전/에 물려 두는 것이므로 지난 신고분은 그대로 남는다. - 확인: 새 작업을 눌러 42장짜리 현재본이
엑셀/이전/KPF_쿠시업로드_20260902_220844.xlsx로 물러나고 현재본이 사라지는 것을 확인. 화면은 네 상자가 전부 빈 상태로 돌아갔다(창고 "회사 공유폴더", 엑셀 "아직 시트가 추가된 엑셀이 없습니다", 미리보기 잠김, 신호등 전부 회색). - 남은 것: 쌓여 있던 중복 시트는 이번 새 작업으로 통째로
이전/에 들어갔다. 다만 같은 인보이스를 다시 변환하면 여전히_2가 붙는다 — 한 작업 안에서 같은 건을 두 번 변환하면 시트가 두 장이 된다. 아직 안 고쳤다.
H-12 · 2026-09-02 · [새 작업] 으로 작업대를 비운다, 서류 창고를 올린 파일별로 묶는다
- 제기: 지시자 — "내가 올린건 8건짜리인데.. 너무 많이 쪼가리 낸 것 같은데? 뭐가 잘못된거고???" 이어서 "새작업이라는 버튼 하나 만들어서 작동하게 만들어라."
- 발견: 쪼개기는 틀리지 않았다. 서류 창고에 뜬 12개는 8건짜리 8개 + 그전 4건짜리 4개였다(인보이스 번호로 갈린다: 20260827007~013·20260818026 대 20260828014~017). 목록이 어느 접수에서 나온 파일인지 안 알려 준 것이 원인이다. 화면이 사실을 숨기면 사람은 시스템을 의심한다.
- 무엇:
- 서류 창고를 올린 파일별로 묶었다. 묶음 머리에 원본 파일 이름과 "N건", 그 아래 건별 파일. 머리에 [전체 아카이브] 도 붙였다. 메모줄은 "건별 파일 12개 · 올린 파일 2개".
- [새 작업] — 작업대를 비운다. 살아 있는 접수를 전부
done으로 내리고 작업내역·서류 창고에서 뺀다. 파일은 하나도 지우지 않는다(원본·건별·엑셀 그대로). 「지난 작업」 칩에서 다시 본다. - 딸려 나온 버그 셋:
listUploads의rollup()이 건들 상태로 접수 상태를 다시 계산하면서done을 덮어썼다 — 새 작업을 눌러도 한 건이 되살아났다.deleted처럼 사람이 정한 상태는 건드리지 않게 했다.- 중복 차단이
done을 안 빼서, 새 작업 뒤 같은 파일을 다시 못 올렸다. 새 작업은 "그 일은 끝났다"는 뜻이므로 빼야 맞다. build_docs.py의 JS 는 비-raw 파이썬 문자열이라 `
이 진짜 줄바꿈이 되어 JS 문자열을 깨뜨렸다. \n 으로 써야 한다. 깨진 판이 한 번 배포됐다 — 빌드 뒤 new Function()` 으로 문법을 보는 절차를 반드시 거친다.
- 확인: 작업대를 비운 뒤(작업내역 0줄·창고 0개·「지난 작업 5」) 8건 PDF 를 새로 올려 한 바퀴 돌렸다.
received(건 0) → [변환] → 8건 → [전체 아카이브] →보관/20260902/에 정확히 8개. - 그림: 없음.
H-11 · 2026-09-02 · 일하는 순서를 다시 짬 — 올린다 → [변환] → 건별 목록 → [아카이브]
- 제기: 지시자가 순서를 다시 정리했다. "1. 파일을 업로드한다(드래그앤드롭 때는 탐색기 활용 선택). 2. 변환 버튼 누르면 프로세스 시작. 3. 변환 작업 하면 각 건별 파일로 재생성하고, 파일 목록이 서류 창고 부분에 나타남. 4. 아카이브 버튼(각 파일별로도, 일괄로도) 누르면 아카이브. 5. 고객사 엑셀에서는 현재와 같이 미리보기, 현재본 내려받기."
- 무엇:
- 올리기와 변환을 떼어 놓았다. 전에는
/ingest가 원본을 저장하면서 곧바로 큐에 넣었다. 이제 저장만 하고 멈춘다(상태received= "변환 대기"). 사람이 [변환] 을 눌러야POST /convert/{upload_id}로 시작한다. 텔레그램처럼 누를 화면이 없는 경로는start=1로 예전처럼 바로 돈다. - 변환 단추는 두 자리. 작업내역 줄마다 [변환], 도구줄에 [대기 N건 모두 변환]. 대기가 없으면 뒤엣것은 숨는다.
- 서류 창고가 건별 파일 목록을 보여준다. 전에는 공유폴더 경로만 있었다. 이제 변환으로 만들어진 건별 PDF를 줄줄이 세우고 파일 이름을 누르면 그 PDF가 열린다.
- 아카이브.
POST /archive에{upload_id}(일괄) 또는{doc_ids:[…]}(고른 것)를 준다. 건별 PDF 사본이 보관 저장소<창고>/보관/YYYYMMDD/로 간다.--archive로 다른 곳(회사 공유폴더의 보관 자리 등)을 가리킬 수 있다. - 왜 옮기지 않고 복사인가: 아카이브는 치우는 것이 아니라 정해진 곳에 쌓는 것이다(지시자 답: "미리 정해 놓은 저장소에 각 PDF 파일을 저장"). 건별/ 에도 그대로 남아 작업대에서 계속 열어볼 수 있다. 같은 이름이 이미 있으면 시각을 붙여 따로 남긴다 — 덧쓰지 않는다.
- 확인: 배포 후 실제 사이트에서 전 구간을 밟았다. 8건 PDF를 올려
received(건 0개)로 멈추는 것 확인 → [변환] 눌러 8건 쪼개짐 → 서류 창고에 건별 파일 12개 표시 → 파일 하나 [아카이브] →C:\KPF창고\보관60902\KPF_20260827007_EARNEST_#1.pdf생김 → [전체 아카이브] → 8건 추가, 겹친 이름은_212539로 따로 남음. - 버린 대안: 아카이브를 '옮기기'로 하는 것. 옮기면 작업대의 [PDF 보기] 가 깨지고 되돌리기가 번거롭다.
- 그림: 없음. 흐름의 여섯 단계(
docs/flow.md)는 그대로고, ①과 ② 사이에 사람이 누르는 문이 하나 생긴 것이다. 다음에 그림을 손볼 때 그 문을 그린다.
H-10 · 2026-09-02 · 엑셀 미리보기에서 칸을 직접 고친다, 시트는 확인 대기여도 붙인다
- 제기: 지시자 — "현재본 내려받기는 일단 엑셀 미리보기를 눌러서 엑셀을 확인할 수 있도록 하고, 미리보기에서 셀을 수기로 수정할 수 있도록 만들어라. 그리고 확인이 필요한 칸이 남아 있어도 시트를 붙일 수 있도록 해라."
- 무엇:
- 시트 붙이는 조건을 없앴다. 전에는 확인 필요 칸이 0일 때만 붙였다(server.py 의
if rep["review_count"]: continue). 이제 항상 붙이고, 자신 없는 칸은 시트에 노랗게 칠한 채로 둔다. 상태는 남은 칸이 있으면review, 없으면sheet. - 엔진에 시트 API 3개:
GET /sheet(시트 목록·줄 수·노란 칸 수) ·GET /sheet/{name}(표 + 칸마다 review 여부) ·POST /sheet/{name}({edits:{"A2":"…"}}를 엑셀에 그대로 쓴다). 쓰기 전에엑셀/이전/에 사본을 남기고, 고친 칸은 노란색을 지운다(사람이 봤다는 뜻). 첫 장(서식 견본)은 못 고친다. - 사이트:
/api/sheet*를 엔진으로 중계(proxySheet). 작업대에 [엑셀 미리보기] 단추와 표 화면을 붙였다. 칸을 눌러 고치면 고친 칸만 모았다가 한 번에 저장한다. 미리보기를 열어 둔 동안에는 20초 자동 새로고침을 멈춘다 — 고치던 값이 날아가면 안 된다. - 발견: 시트가 바로 붙게 되니
/confirm으로 값을 고칠 때 같은 건이 두 번째 시트로 붙는 문제가 생긴다(sheet_name이_2를 달아 준다).excel.drop_sheet()를 만들어 다시 붙이기 전에 옛 시트를 지운다. - 확인: 배포 후 실제 사이트에서 [엑셀 미리보기] → 시트 목록 2장 로드 → L2 칸을 고쳐 저장 →
C:\KPF창고\엑셀\KPF_쿠시업로드.xlsx의 L2 가 실제로 바뀌는 것까지 확인(엔진 로그[엑셀] 시트 … 1칸 고침: L2). 시험 값은 되돌렸다. - 버린 대안: 별도 페이지(
/xlsx.html). 작업대의 엑셀 카드 바로 아래 펼치는 쪽이 오가는 수고가 없다. 확인 화면(/review.html, 8장 3번)은 쪽 그림을 보며 고치는 용도로 따로 남는다. - 위험: 사람 확인을 안 거친 시트가 고객사 엑셀에 그대로 쌓인다. 노란 칸이 그 표시이고, 작업대 목록에도
review로 남는다. 세관에 내기 전에 노란 칸이 없는지 봐야 한다. - 그림: 없음(흐름 ⑥의 조건만 바뀜 — 그림의 상자·화살표는 그대로다).
H-09 · 2026-09-02 · 저장소·지휘 인계, 엔진 PC 정식 설치, 정식 터널
- 제기: 지시자 인계. 이태경(형)의 PC에서 하던 작업을 폴더째 전달받아 현 지시자(customsshin@gmail.com) PC 호스트
SHIN에서 이어간다. "이 파일들을 내 깃 계정에 연결하고 kpf.dreamcustomsservice.com 에서 시스템이 돌게 하라." - 발견(근거):
- 깃 자격증명이
customsshin-alt인데 저장소는DavidRheeTaekyoung/kpf비공개 →git pull이Repository not found. 권한 없는 계정에는 비공개 저장소가 "없는 것"으로 보인다. - Cloudflare 는 이미 현 지시자 계정이었다.
wrangler whoami의 계정 ID600295b4779b795b1e580a613d632e43이 이 문서에 적힌 것과 일치. 도메인·워커·D1(cd2cfbef-…) 전부 제자리 → 옮길 것이 없었다. - 이 PC에 네트워크 드라이브가 하나도 없다(C/H/S/U 전부 로컬, 기억된 UNC 경로도 없음) → 회사 공유폴더 경로를 도출할 근거가 없다.
ANTHROPIC_API_KEY도claudeCLI 도 없어 판독이engine.vision.VisionError: 판독 실패(cli, 10장): claude CLI 를 PATH 에서 찾지 못했다로 죽는다(engine/split.py:74 → engine/vision.py:112). 접수·쪼개기는 통한다.src/index.js의 커밋 안 된 110줄에서parseMultipart가 정의만 되고 호출되는 곳이 없다. 문법은 유효하나 죽은 코드다.- 무엇:
- 저장소를
customsshin-alt/kpf(비공개)로 옮겼다. 전체 커밋 이력에샘플/·.env·PDF·xlsx 가 한 번도 들어간 적 없음을 확인하고 푸시했다. 형 저장소는upstream-taekyoung원격으로 남겨 출처를 보존한다. - 엔진 PC 설치: 파이썬 의존성(anthropic·fastapi·uvicorn·python-multipart), cloudflared 2026.8.3, 정식 터널
kpf-engine(bc6677fc-…) → CNAMEengine.dreamcustomsservice.com,ENGINE_URLsecret 을 임시 trycloudflare 주소에서 이 주소로 교체. - 창고는 임시로
C:\KPF창고(Q-07 답: 회사 공유폴더 미정). 공유폴더가 정해지면 폴더째 옮기고--root만 바꾼다. - 확인: 전 구간을 실제로 통과시켰다. 사이트
POST /api/upload로 8건 샘플을 올려C:\KPF창고\원본60902\케이피에프서류-8건.pdf(365KB, 37쪽) 가 떨어지는 것을 확인(접수번호 01M1GVF4K385EW0RZY42T3YHCH). 4건 샘플은 sha256 중복으로 정상 거절됐다.https://engine.dreamcustomsservice.com/health가 바깥에서 응답한다. - 버린 대안: 이 PC에 형 계정으로 로그인하는 방법. 동생 PC에 형 자격증명이 남으므로 협업자 초대·저장소 이전 쪽을 택했다.
- 남은 것: API 키(판독),
cloudflared service install+ 엔진 자동 시작(관리자 권한 필요 — 지금은 사람이 띄운 프로세스라 로그아웃하면 끊긴다). - 그림: 없음(구조 변경 아님 — 구조도의 "엔진 PC"·"창고" 상자가 실제로 채워진 것).
H-08 · 2026-09-02 · 판독기 1차 결과 — 서류가 설계와 다른 곳 7군데, 확정 규칙 반영, 엔진 서버
- 제기: 판독기(engine/)를 4건 샘플 20쪽에 끝까지 돌리고 8건 샘플 37쪽을 쪼개 본 결과.
- 발견(근거는 쪽 번호):
- 패킹리스트 상세 둘째 장부터 큰 제목이 없다(4건 5·10·15·20쪽). 표 머리(PLT NO/KEG NO)로 판정해야 한다.
- 단가 기준이 서류마다 다르다. 4건 17쪽은
UNIT PRICE PER 1 PCS· 통화 JPY. 나머지는 PER 1,000 PCS · USD.
확정 규칙 "÷1000"을 그대로 박으면 이 건은 1,000배 틀린다. → Q-14.
- PART NO 칸이 통째로 빈 서류(4건 17·19·20쪽). 인보이스·패킹 양쪽 다 비어 SIZE 로 맞춰야 한다. → Q-15.
- 인보이스 상세 한 쪽에 P/O 묶음이 여러 개(4건 12쪽은 3묶음). 묶음마다 규격 줄과 SUB TOTAL 이 따로 있다.
즉 규격은 건 단위가 아니라 품목 단위. SUB TOTAL 을 표 끝으로 착각해 두 건이 통째로 틀렸었다.
- 패킹 상세의 SUB TOTAL 도 P/O 묶음 단위다. 부품별 순중량은 파렛트 줄을 합해서 만들어야 한다.
- 수입자 칸 이름이 서류마다 다르다. 4건은 APPLICANT/CONSIGNEE, 8건은 BILL TO/SHIP TO(8건 13쪽).
- 손글씨: docs 예시
#1 BNLY 188은 실제로#1 BNLY 198. 포장수가 아예 없는 건도 있다(4건 11·16쪽).
8건 30쪽은 유럽식 7 을 1 로 오독 → 손글씨 건번호와 쪼갠 순서를 대조하는 검산을 한 겹 더 넣었다.
- 조치(반영): 위 7건 모두 engine/ 에 반영. 확정 규칙(3장)도 반영 — 고정값 KR/Y/GT, 규격 = DESCRIPTION 줄 + SIZE,
규격→HS 대응표(engine/spec_master.csv, 부품번호 기준표는 폐기), 포장수량·순중량만 HS별 합산. engine/server.py(FastAPI, 큐 하나, /ingest · /confirm · DELETE /ingest/{id} · /files · /health), engine/answer_watch.py(답 온 질문 수거).
- 조치(질의 전환): Q-13 포장수량이 keg 수인지 파렛트 수인지 · Q-14 단가 기준 PER n 과 통화 ·
Q-15 PART NO 빈 서류의 규격2 · Q-16 규격→HS 대응표 초기값(샘플에서 읽은 규격 36개 목록 첨부).
- 성적: 4건 20쪽 → 4건·품목 17줄, 산술·서류간 대조 검산 전부 통과. 8건 37쪽 → 8건, 경계 전부 정확.
확인 필요 118칸은 대부분 대응표의 HS 가 아직 비어 있어서다(Q-16 답이 오면 대부분 사라진다).
- 버린 대안: 부품번호 기준표(H-06 에서 이미 폐기) · 단가 ÷1000 고정(서류가 반증).
- 그림: 없음 — 구조는 그대로고 규칙만 채웠다. 사후 기록.
- 확인 상태: 지시자 확인 대기.
H-07 · 2026-09-02 · 페이지 3개(작업대·질의응답·개발), 텔레그램 양방향, 작업내역 표
- 제기: 지시자 — "페이지 구성은 작업(이름 바꿈)·질의응답·개발. 텔레그램방은 작업과 질의응답 둘 다 상호작용. 텔레그램 글은 자동으로 질문(답 있으면 답도), PDF는 작업창에서 처리, 작업내역 다 남기고 표로 정리. PDF·엑셀 바로 보기 버튼, 저장소 접근 버튼. UI는 리서치해서."
- 무엇: 상단 메뉴 [작업대] [질의응답] [개발]. 작업대 = 드롭존 + 작업내역 표(단계 칩, PDF 보기·엑셀 보기·확인하기, 저장소 열기) + 작업 로그. 텔레그램 → 질문 자동 생성/답 저장/PDF 접수. work_log 표.
- 버린 대안: 질의응답을 개발 하위에 두기 — 사람이 매일 들어갈 곳이라 최상위로.
- 그림: 구조도_v0.2 (변경 없음, 페이지 목록만 갱신 예정).
- 확인 상태: 구현 중.
H-06 · 2026-09-02 · 지시자 답변 7건 반영 → 데이터 정의 v0.2, 창고 = 공유폴더
- 제기: 지시자가 통관 담당자 답변 전달: HS CODE·세번품명·거래품명은 서류(규격)로 결정 / 원산지 KR 고정 / FTA Y 고정 / 포장단위 GT 고정 / 인보이스 번호 = 첫 장 오른쪽 위 / 규격 = DESCRIPTION 줄 + SIZE / 단가 ÷1000 / 같은 HS끼리 포장수량·순중량 합산.
- 무엇: 부품번호 기준표 → 규격→HS 대응표로 교체. 고정값 3개는 표에서 제거. 포장수량·순중량은 HS별 합산. 창고는 R2 대신 회사 공유폴더(구조도 v0.2), 사이트와는 Cloudflare Tunnel.
- 되물음: Q-11 엑셀 한 줄의 단위 → 최종 답(정정본): 수량·금액·HS CODE는 아이템별 각각, 포장수량·순중량만 HS별 합산. Q-12 규격별 HS 값, Q-03 운임 줄은 대기.
- 버린 대안: R2 클라우드 창고 — 지시자가 "R2 없이는 안 되나" → 공유폴더로 결정. 부품번호 기준표 — 담당자가 규격 기준이라 답함.
- 그림: 데이터정의_v0.1 → v0.2, 구조도_v0.1 → v0.2.
- 확인 상태: 답변 반영, 지시자 재확인 대기.
H-05 · 2026-09-02 · 기록 사이트 첫 배포
- 제기: 지시자 — "필요한 페이지들 일단 넣어라. 깃 레포 만들고 커밋·푸시까지."
- 고민: 업로드 기능은 설계 미확정(Q-01~Q-05)인데 사이트를 먼저 올리는 이유는, 기록 페이지가 있어야 맥락이 끊겨도 이어갈 수 있기 때문.
- 무엇: docs/*.md → build_docs.py → public/*.html, 7페이지(홈·흐름·구조·정의·리서치·이력·규칙). GitHub 비공개 저장소
kpf. Workers 정적 자산 배포. - 왜: 워시존과 같은 방식(문서가 원본)이라 후배가 바로 이어받을 수 있다.
- 버린 대안: React 앱으로 시작 — 아직 화면 요구가 없어서 과하다.
- 그림: 전체그림_v0.2, 구조도_v0.1, 데이터정의_v0.1.
- 확인 상태: 미확인(배포 후 지시자 확인 대기). 고객 서류 샘플은 저장소에 넣지 않았다.
H-04 · 2026-09-02 · 데이터 정의 v0.1
- 제기: 지시자가 샘플 PDF 2개(4건 20쪽, 8건 37쪽)와 엑셀 견본("쿠시 업로드", 열 17개)을 제공.
- 발견: PDF는 전부 글자층 없는 팩스 스캔. 한 건 = 인보이스 + 인보이스 상세 + 패킹리스트 + 패킹리스트 상세(1~3장), 건마다 장수가 다름. 인보이스 단가는 천 개당 표기(@1,551 × 7,560 EA = 11,725.56). 엑셀 견본의 예시 줄은 다른 회사 것.
- 무엇: 엑셀 한 줄 = 품목 줄 하나. 열 17개를 A 품목 값(인보이스 상세) / B 무게·포장(패킹 상세) / C 세관 코드(부품 기준표) / D 거래품명 네 묶음으로 정의. 부품 기준표를 새로 둔다.
- 미확정: 포장수량 단위(Q-01), 거래품명 규칙(Q-02), 운임 줄(Q-03).
- 그림: 데이터정의_v0.1.
H-03 · 2026-09-02 · "쪼개 저장" 단계 추가, 파일명 임시 규칙
- 제기: 지시자 — "PDF가 여러 덩어리로 오니 1건 단위로 쪼개 저장하는 걸 먼저 해야 한다. 파일명 규칙은 따로 알려줄 테니 대충 먼저 정해라." 이어서 "②와 ③ 사이에 단계가 있는 거지."
- 무엇: 전체 그림에 ③ "건별 PDF로 쪼개서 이름 붙여 창고에" 추가(다섯 단계 → 여섯 단계). 쪼개기 기준 = 큰 제목 "COMMERCIAL INVOICE" 장이 새 건 시작. 임시 파일명
KPF_인보이스번호_수입자약칭_#건번호.pdf, 실패 시KPF_읽기실패_원본이름_p쪽범위.pdf. - 확인: 지시자 — "그 네이밍 규칙대로 해라." (정식 규칙은 추후 통보)
- 그림: 전체그림_v0.1 → v0.2.
H-02 · 2026-09-02 · 구조도 v0.1
- 제기: 지시자 — "구조도 먼저 그려라."
- 무엇: 부품 여섯 개(고객, 사이트, 읽기 엔진, 엑셀 제작기, 서류 창고, 장부). Workers가 Python을 못 돌리므로 읽기 엔진은 밖에 둔다.
- 미확정: 읽기 엔진 자리(작은 서버 vs 지시자 PC), Q-04.
- 그림: 구조도_v0.1.
H-01 · 2026-09-02 · 프로젝트 시작, 전체 그림 v0.1, OCR 리서치
- 제기: 지시자 — kpf.dreamcustomsservice.com 에 고객사 하나를 관리하는 페이지. PDF 올리면 OCR로 읽어 엑셀 시트를 자동 추가. OCR 100% 아닌 게 가장 큰 위험이니 리서치해 서브 페이지로. 데이터 정의·흐름·구조도·개발이력 페이지로 유지 가능하게.
- 무엇: 전체 그림 v0.1(다섯 단계), OCR 리서치(서브에이전트), 작도 규칙은 워시존 작도 규칙 8을 승계.
- 확인: 지시자 — 그림은 "중학생도 이해할 수 있게", 입력이 맨 위로. 상자 7개 이하로 단순화.
- 그림: 전체그림_v0.1.