개발 이력/06 — 07

개발 이력 — 결정과 버린 대안을 그림과 함께 남긴다

규율: 구조가 바뀌면 코드보다 그림을 먼저 그려 지시자 확인을 받고, 확인된 것만 여기 적는다. 그림 없이 들어간 변화는 "사후 기록"으로 표기한다. 항목마다 발견 → 확인 → 조치 순서로 쓴다.


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.pywhen() 두 곳(작업대·질의응답)이 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 로 나온다.

H-25 · 2026-09-03 · 실제 업무 서류가 판독 전에 죽은 원인 (진단만, 고치지 않음)

engine.vision.VisionError: 판독 실패(sdk, 10장): $.pages: array 여야 하는데 str

정상 : {"pages": [{"n":1,...}, ...]}
실패 : {"pages": "{\"pages\":[{\"n\":1,...}]}"}      ← 문자열 한 겹

H-24 · 2026-09-03 · 검증용 두 번째 판독기 — Tesseract 를 재 보고 버렸다

결과
3건 · 36개 항목 대조23개 일치 (63%)
못 찾은 것대부분 수량(7560, 9698, 6296, 29860 …)과 소계
속도건당 4쪽에 15~22초
200dpi psm6  7/11 (63%)   ← 제일 나음
300dpi psm6  6/11 (54%)
400dpi psm6  4/11 (36%)

올릴수록 나빠진다. 팩스 스캔의 잡티까지 같이 커지기 때문이다. 우리가 준 그림이 나쁜 게 아니라 원본이 그런 것이다.

H-23 · 2026-09-03 · 엑셀 미리보기를 겹쳐 뜨는 창으로

H-22 · 2026-09-03 · 작업 하나 = 엑셀 하나. 아카이브를 없애고 내려받기로

① 엑셀은 쌓이는 장부가 아니다

그동안 워크북 하나(KPF_쿠시업로드.xlsx)에 시트를 계속 붙였다. 틀린 전제였다. 올리고 → 나누고 → 읽고 → 엑셀 하나 나오면 그 사이클은 끝이고, 다음에 올린 파일은 또 다른 엑셀 하나를 낳는다.

엑셀워크북 하나에 계속 붙임작업 하나 = 워크북 하나
이름KPF_쿠시업로드.xlsx 고정YYYYMMDD_NNN.xlsx (20260903_001.xlsx)
자리엑셀/엑셀/YYYYMMDD/
새 작업워크북을 엑셀/이전/ 으로 물림필요 없다. 새 작업은 새 파일을 연다
다시 변환(replace)옛 시트를 지우고 다시 붙임같은 이름으로 파일째 새로 만든다
따로 추가(add)시트에 _2 를 닮다음 차수 엑셀을 하나 더 만든다

이 전제가 틀렸던 탓에 생긴 군더더기가 같이 사라졌다 — drop_sheet 로 옛 시트 찾아 지우기, sheet_name 물려받기, 시트 이름 _2 붙이기, /workbook/reset. 시트가 42장까지 쌓였던 H-12 도 애초에 이 전제 때문이었다.

open_book()·book_of()·job_file(). /sheet*?book=20260903_001.xlsx 를 받는다 ("현재본" 이 없으므로 어느 권인지 늘 말해야 한다). /books 로 만들어진 엑셀 목록을 준다. src/index.js 는 그 질의문자열을 엔진까지 넘기고, newJob 에서 /workbook/reset 호출을 뺐다.

(서식 견본 포함 5장). 첫 시트가 4줄 = 품목 3 + 운임 1 (H-19 그대로).

② 아카이브를 없애고 내려받기를 넣었다

쪼개진 파일은 만들어지는 즉시 건별/ 에 저장되고 클라우드에도 올라간다. 아카이브는 그것을 보관/또 한 번 복사하는 것이라 아무것도 더 지켜 주지 못했다. 사람이 정작 원한 것은 "내 PC 로 가져가기" 였다.

zip 하나로). 엔진에 GET /zip/{upload_id}, 사이트에 /api/zip/{upload_id} 를 냈다. 한글 파일명이라 Content-Disposition 은 RFC 5987 형식으로 준다.

그대로 둔다.

③ 서류 창고 표시를 네이버 클라우드로

작업대 3단계가 아직 "회사 공유폴더" 라고 적혀 있었고, 창고 칸은 "경로가 아직 없습니다 · 질의응답 Q-07에 경로를 적어 주세요" 를 띄우고 있었다. 창고는 H-17 에서 네이버 클라우드로 정해졌으므로 맞지 않는 안내였다. /api/healthcloud {on, bucket} 을 돌려주게 하고, 그 자리에 버킷 이름과 무엇이 쌓이는지를 적었다. Q-07(회사 공유폴더 경로)은 이로써 무의미해졌다.

H-21 · 2026-09-03 · 판독용 이미지도 클라우드 창고에 올린다 (H-17 결정 뒤집음)

| 파일 | 고친 것 | |---|---| | engine/cloud.py | KEEP이미지 추가, SKIP_DIRS 에서 제거. 이제 안 올리는 것은 판독/_cache/ 뿐이다 | | engine/server.py | run_pipeline 끝에서 cloud.put_many(work_dir). 건마다 올리지 않고 한 번에 — 분류용 띠와 건별 그림이 다 만들어진 뒤라야 빠짐이 없다 | | src/ncloud.js | inCloud()이미지 추가 (엔진의 KEEP 과 짝) |

H-20 · 2026-09-03 · 엔진을 이 PC에서 네이버 클라우드 서버로 옮겼다

서버

이름kpf-engine (Instance ID 144917022)
사양s2-g3a vCPU 2 / 메모리 8GB / SSD 10GB · KVM(3세대)
OSUbuntu 24.04.1 LTS · Python 3.12.3 · 로케일 en_US.UTF-8(한글 파일명 정상)
위치한국 KR-1 (창고와 같은 리전)
공인 IP49.50.128.41
VPC kpf-vpc (10.0.0.0/16) · Subnet kpf-subnet (10.0.1.0/24, Public)
방화벽ACG kpf-vpc-default-acgTCP 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

걸린 것 네 가지

  1. 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원이 된다.
  2. 400 이라는 것 자체가 단서였다. 401/403 이면 권한·결제, 500 이면 네이버 장애인데 400 은 "우리가 보낸 값" 이다. H-17 에서 배운 대로 남의 설정을 탓하기 전에 요청을 봤다.
  3. Pillow 을 빠뜨렸다. render.py 가 함수 안에서 from PIL import Image 를 하는 늦은 import 라 코드를 훑을 때 놓쳤고, 첫 변환이 ModuleNotFoundError 로 죽었다. 같은 식의 늦은 import 가 boto3·anthropic·uvicorn 에도 있다. → engine/requirements.txt 를 만들었다. 눈으로 훑지 말고 이 파일을 쓴다.
  4. systemd ExecStart 는 공백 있는 인자에 따옴표가 필요하다. --template 샘플/케이피에프 엑셀업로드.xlsx 를 그냥 쓰면 systemd 가 공백에서 쪼개 인자가 둘이 된다.
  5. --token 을 환경변수로도 받게 고쳤다. 명령줄에 토큰을 쓰면 ps 목록에 그대로 보인다. --bucket 이 이미 쓰던 방식(os.environ.get)에 맞췄고, systemd 는 EnvironmentFile 로 넘긴다.

확인

남은 것

H-19 · 2026-09-03 · 운임 줄을 표에 넣는다 (Q-03 반영)

| 열 | 값 | |---|---| | A~I (HS·원산지·FTA·포장수량·포장단위·인보이스넘버·세번품명·거래품명·발급협정코드) | 바로 윗줄과 똑같이 | | J 규격1 | CIF Freight cost | | K 규격2 · L 규격3 | 공란 | | M 수량 | 1 (항상) | | N 수량단위 | ZZ (항상) | | O 단가 · P 금액 | 인보이스에 적힌 운임 금액 | | Q 순중량 | 0 (항상) |

H-18 · 2026-09-03 · 클라우드 창고 2단계 — 엔진이 꺼져 있어도 파일이 열린다

H-17 · 2026-09-02 · 클라우드 창고 1단계 — 네이버 Object Storage 에 사본 (R-12 뒤집음)

하루를 잡아먹은 오진 — 남겨 둔다

PutObject403 AccessDenied. 버킷 만들기·목록·ACL 읽기는 전부 통했다. 권한 문제로 보였다.

내가 틀린 순서:

  1. 키가 ncp_iam_ 로 시작하는 것을 보고 서브계정 키라고 단정했다 → 서브계정은 하나도 없었다. 접두어만 보고 지레짐작했다.
  2. 결제수단 미등록이라고 했다 → 카드가 등록돼 있었다.
  3. 이용 신청 미완료라고 했다 → "상품 이용 중" 이었다.

지시자가 화면을 두 장 보여주고서야 멈췄다. 진짜 원인은 내 코드였다:

boto3 1.36+ 는 올릴 때 AWS 전용 체크섬 헤더(x-amz-checksum-*)를 기본으로 붙인다. 네이버 Object Storage 는 그것을 거부하고 403 AccessDenied 를 돌려준다. 권한 메시지라서 권한을 뒤지게 만든다.

Config(request_checksum_calculation="when_required",
       response_checksum_validation="when_required")

놓친 단서: 처음부터 "본문이 있는 요청만 실패한다"가 보였다. 버킷 만들기(본문 없음) OK, 파일 넣기(본문 있음) 실패. 계정 설정이 아니라 요청 내용을 의심했어야 했다. 남의 설정을 탓하기 전에 내가 보내는 것부터 본다.

H-16 · 2026-09-02 · 화면 정리: 이름 변경, 드롭존 축소, 신호등 색, 노란 칸 이유, 접기

H-15 · 2026-09-02 · 팝업이 안 닫히던 것, 시트를 너무 많이 지운 것

두 가지 다 내(구현) 실수다. 둘 다 "확인했다"고 말했는데 확인이 틀렸다.

1) 팝업이 처음부터 떠 있고 닫히지도 않았다 (두 번 고침)

2) 시트를 네 장 지우려다 여덟 장을 지웠다

H-14 · 2026-09-02 · 같은 건을 다시 변환하면 팝업으로 묻는다 (덮어쓰기 / 따로 추가)

H-13 · 2026-09-02 · 작업대를 단계 상자 넷으로, 새 작업이 엑셀까지 새로 연다

H-12 · 2026-09-02 · [새 작업] 으로 작업대를 비운다, 서류 창고를 올린 파일별로 묶는다

이 진짜 줄바꿈이 되어 JS 문자열을 깨뜨렸다. \n 으로 써야 한다. 깨진 판이 한 번 배포됐다 — 빌드 뒤 new Function()` 으로 문법을 보는 절차를 반드시 거친다.

H-11 · 2026-09-02 · 일하는 순서를 다시 짬 — 올린다 → [변환] → 건별 목록 → [아카이브]

H-10 · 2026-09-02 · 엑셀 미리보기에서 칸을 직접 고친다, 시트는 확인 대기여도 붙인다

H-09 · 2026-09-02 · 저장소·지휘 인계, 엔진 PC 정식 설치, 정식 터널

H-08 · 2026-09-02 · 판독기 1차 결과 — 서류가 설계와 다른 곳 7군데, 확정 규칙 반영, 엔진 서버

확정 규칙 "÷1000"을 그대로 박으면 이 건은 1,000배 틀린다. → Q-14.

  1. PART NO 칸이 통째로 빈 서류(4건 17·19·20쪽). 인보이스·패킹 양쪽 다 비어 SIZE 로 맞춰야 한다. → Q-15.
  2. 인보이스 상세 한 쪽에 P/O 묶음이 여러 개(4건 12쪽은 3묶음). 묶음마다 규격 줄과 SUB TOTAL 이 따로 있다.

즉 규격은 건 단위가 아니라 품목 단위. SUB TOTAL 을 표 끝으로 착각해 두 건이 통째로 틀렸었다.

  1. 패킹 상세의 SUB TOTAL 도 P/O 묶음 단위다. 부품별 순중량은 파렛트 줄을 합해서 만들어야 한다.
  2. 수입자 칸 이름이 서류마다 다르다. 4건은 APPLICANT/CONSIGNEE, 8건은 BILL TO/SHIP TO(8건 13쪽).
  3. 손글씨: docs 예시 #1 BNLY 188 은 실제로 #1 BNLY 198. 포장수가 아예 없는 건도 있다(4건 11·16쪽).

8건 30쪽은 유럽식 7 을 1 로 오독 → 손글씨 건번호와 쪼갠 순서를 대조하는 검산을 한 겹 더 넣었다.

규격→HS 대응표(engine/spec_master.csv, 부품번호 기준표는 폐기), 포장수량·순중량만 HS별 합산. engine/server.py(FastAPI, 큐 하나, /ingest · /confirm · DELETE /ingest/{id} · /files · /health), engine/answer_watch.py(답 온 질문 수거).

Q-15 PART NO 빈 서류의 규격2 · Q-16 규격→HS 대응표 초기값(샘플에서 읽은 규격 36개 목록 첨부).

확인 필요 118칸은 대부분 대응표의 HS 가 아직 비어 있어서다(Q-16 답이 오면 대부분 사라진다).

H-07 · 2026-09-02 · 페이지 3개(작업대·질의응답·개발), 텔레그램 양방향, 작업내역 표

H-06 · 2026-09-02 · 지시자 답변 7건 반영 → 데이터 정의 v0.2, 창고 = 공유폴더

H-05 · 2026-09-02 · 기록 사이트 첫 배포

H-04 · 2026-09-02 · 데이터 정의 v0.1

H-03 · 2026-09-02 · "쪼개 저장" 단계 추가, 파일명 임시 규칙

H-02 · 2026-09-02 · 구조도 v0.1

H-01 · 2026-09-02 · 프로젝트 시작, 전체 그림 v0.1, OCR 리서치