pfs 작업 규칙 (rules)
이 파일 = 규칙(하면 되는/안 되는 것). 상태·진행·이력은
handoff.md, 함정은traps.md, 유사작업 절차는playbook.md. 새 규칙은 🆕 신규에 1줄 append → 안정화되면 확립 규칙 §로 원문 이관.
📑 인덱스
- 🆕 신규 (최근 확립, 미분류)
- 🔑2026-10-25 [코디] master 와 갈라진 deploy-live 에 pncad 묶음을 옮길 땐 cherry-pick 충돌을 손으로 풀지 말고 «hunk 선별»로 한다 — ① 참조 트리 = origin/master 에서 «안 실을 커밋»(예: 1번)만
git revert -n②git diff -U0 origin/deploy-live <참조> -- 파일의 hunk 마다 변별 줄(6자+)이 어느 묶음 커밋의 +/− 줄인지로 주인 판정 ③ 묶음 주인 hunk 만 deploy-live 판에 적용(주인 없는 줄 = 운영 전용 변경 · 섞인 hunk 는 눈으로) → 묶음마다 파일 상태를 만들어 커밋. 도구 =C:\dev\tools\pfs-hunksel\hunksel.py(report/apply · 안의 REPO·BUNDLES 를 그때 맞춘다). 함정: 운영 전용 함수(예: nextWallId)가 묶음 hunk 와 같이 있으면 중복 function 선언 = 모듈 SyntaxError →jscheck.py(인라인 스크립트 node --check + 중복 선언 검사) 필수 · git 작업트리는 autocrlf 라 충돌 표식이 CRLF(정규식\r?\n). - 🔑2026-10-19 확정 자재발주서(PFS-MAT)가 있는데 자재관리에 품목이 안 보이면 = 먼저
orderpaper.del_manual을 본다. 자재관리에서 사람이 지운 행(del_manual=1)은 amend·복구 sync 가 절대 안 살린다(설계). 처방 = 그 품목lines/remove+ 올바른 업체로amend add수동 행(현장 1개·receive_date 지정). 사례·스크립트 =C:\dev\_work\gs-h2041b-pndeco-1019\apply1.py. - §1 UI·표시 규칙 (색상·굵기·폭)
- §2 배포 규칙
🆕 신규
- 🔴2026-10-25 [코디] Jev 는 판을 고정한다 =
jev-1.13.0(C:\dev\tools\jev.py기본값 · 바꾸려면JEV_MODEL). 26.10.14jev-latest가 말없이 1.14.0 으로 바뀌었고(GET /v1/models는 여전히 9/10 날짜만 보여 줘 못 알아챈다 — 응답model필드만 진실) artjoy 쌍 127 AUC 0.885→0.838 회귀 · door_part 62.0→59.2%. 운영 임계(dedupe 0.8/0.2 · intake 0.9 · pfs_done 0.2 · worklog-ai 0.7)는 전부 1.13 값이다 → 새 판은 회귀(C:\dev\_work\jev\pairs_fair.py류, 같은 state 로 두 판 비교) 통과 뒤에만 올린다. 이미지 입력(images={"이름": 경로}· 헤더TypeSafe-Danger-Zone-Images자동)은 «Dreamer-ready» = 운영 판정에 바로 쓰지 말고 섀도부터 · 상품 매칭에 사진을 더하면 AUC 가 깎였다(0.851). 상세 = devplanjev-map.html#v114. - 🔑2026-10-23 [코디] 사무실 GT800 샘플 스티커는 미니PC에서 화면 없이 원격 인쇄된다 — GT800 =
poinimos01USB001ZDesigner GT800 (EPL). ①운영 스티커 화면을 Playwright(pfs_login.env, dsf 3)로 열고 현장을 선택해.stk-sheet(90×90mm)를 PNG로 뜬다(선택 0건이면 빈 양식 = 로고·현장명) ②ssh poinimos01+ System.DrawingPrintDocument(PaperSize 394×394 = 100mm · 5mm 여백에 90mm로 그림 ·StandardPrintController= 대화상자 없음)로 Session 0에서 찍는다 ③성공 판정 = PrintService/Operational 이벤트 307(인쇄된 페이지: 1). 업체 로고 =company_logo/{company_no}.png파일 1개(코드 무변경) · 첫 적용 SLS(465). [코디] - 🔴2026-10-23 [페페] 「HS2329 자재 포인 생산분은 모두 15T」 — 26.10.4 「도어 18T 기본」의 예외(자재코드 HS2329 = 포인 자체 생산분 전부 15T). 두께만 바꾸면 안 되고 조립 치수 재계산까지(ㄱ자 함 측판 = 폭−T · 멍장 천지판 = 폭−2T). 첫 적용 = GS대전 도마MH 수입분 도어발주서 「포인」 탭 21셀(원본
이전버전\…_261023_0957이전.xlsm), 도구 = zip XML 치환(기대 옛값 assert). [코디] - 🔴2026-10-23 pfs 본체(
C:\dev\pfs)에서 baregit stash로 «변경 전 기준 테스트»를 돌리지 마라 — 다른 세션 미커밋 변경까지 같이 들어갔다 나온다(이번엔 drawer.js CRLF 차이만이라 무해). 기준 비교는git worktree add --detach <임시> HEAD에서. · 발주서 엑셀 다운로드 표기(현장명+유형 · 발신=로그인 직원)는api/pfs/orderpaper/excel_meta.py한 곳 — 새 발주서 종류도 여기를 쓴다. [코디] - 🔴2026-10-22 도어발주서(xlsm) 「포인」 탭만 붙여넣기 불가(데이터 안 옴 · 병합만 풀림 · 월드 탭은 정상) — 파일 구조(보호·공유·이벤트매크로·CF·도형)는 원본과 같고 미니PC 엑셀에선 재현 0 · 사무실PC 엑셀 2016 에 10.15
KB5002934설치(9월 KB5002914 = 같은 증상 선례) · 포인만 있는 것 = 빈 AutoFilter(P3:S). 처방 = 새 시트 「포인」 재작성(옛 시트 Cells 통째 붙여넣기 → 「포인_구」 숨김 · 머리 라벨 사각형 3개는 따로 복사 · 인쇄설정은 COM 거부(기본 프린터 없음) → zip XML 로 옛 시트 pageSetup 이식) · 도구C:\dev\order-automation\daejeon-doma-suip_2026-10-21\{rebuild1022,shapefix1022,xmlpage1022}.py. ⚠NAS 교체 전 최신본을 다시 받아 대조하라 — 페페가 그 사이 저장(10:29 편집 33칸)해 한 번 덮어썼다가 재반영함. [코디] - 🔴2026-10-20 [페페] 기본규칙 — 견적은 기본 견적행·추가금액(sub)행 모두 «공통마진 연동»이 기본이다. 스크립트로 견적을 만들 때 master margin_percent 를 수동 0 으로, sub 를 35%·0% 같은 고정값으로 박지 마라(옛 선례 781·803·806 방식 폐기). 연동 = detail NULL + sub 마진 = 공통값 +
column-setting/bulk-save의est_sublink:<no>표식(화면 「저장된 추가금액 연동」과 같은 상태). 선례 스크립트C:\dev\order-automation\najin_as_2026-10-20\b9_link_margin.py. ⚠운임 «행»에도 마진이 붙는다(부가비 운임엔 안 붙음). [페페/코디] - 🔑2026-10-20 네스팅 여유자재 기본지침 = 몸통/도어 «구간 정액» 표(단일 출처 =
C:\dev\seedchain\projects\furniture-expert\kb\11-material-orderpaper-defaults.md§2-1 · 근거 =session-2026-10-20-nesting-spare-pattern.md). 코디가 네스팅을 준비·보고할 땐 이 표로 «여유 추천»을 같이 낸다 · 운영 저장본 여유 칸은 페페가 넣는다(코디 임의 입력 금지). 구현 예 =C:\dev\_work\enex-gyeryong-1020\plan_e.pyspare_rec(). [페페/코디] - 🔑2026-10-20 원부자재 발주(자재 발주서) 기본지침 =
C:\dev\seedchain\projects\furniture-expert\kb\11-material-orderpaper-defaults.md단일 출처(업체 배정·유사PP·샘플·피스마개·엣지 폭 통합·엣지 여유 +50/+200·50M 단위·중복 자재 합산·입고일·타입칸·비고 내부메모 금지·카톡 문구). PFS 생성기는 아직 엣지 통합·여유·유사PP·샘플·타입칸을 자동 적용하지 않는다(문서별 데이터 수정). 같은 날 코드: 나눠 저장한 도어/몸통 저장본 덮어쓰기 안 함(632af80f) · 오엠씨 탭 여러 장 칼라 그룹 균등(6ee8202a). [코디] - 🔑2026-10-20 「추가건 이관」 기능은 같은 현장 안 차수 간 이동 전용이다 — 다른 현장(AS 하위현장 2200)의 견적을 본건 밑 추가건으로 붙이는 건 PUT(parent_estimate_no·estimate_name)으로 했다(나진 806→807, sub 0 이라 소실 위험 없음). 하위내역 있는 견적이면 PUT 전 subList 채우기 규칙 그대로. 월드 7차 단가 원본
n11_price7.py는 실행하면 803 산출 파일을 덮어쓴다 → 규칙만 옮긴 모듈C:\dev\order-automation\najin_as_2026-10-20\b4_world7.py(803 85줄 재현 일치) 재사용. [코디] - 🔴2026-10-20 [코디] flask_restplus
Resource메서드에서(jsonify(body), 200)튜플을 돌려주면 운영 500 — restplus 가 튜플을 보면 Response 를 다시json.dumps해TypeError: Object of type 'Response' is not JSON serializable. ⛒튜플이면return body_dict, 코드, jsonify 는 단독 반환일 때만. 네스팅 소재 칸 PUT/api/poin/color-types가 이걸로 5회 500(페페 「소재 변경 선택이 안 된다」) — 단위테스트가 본체 함수만 재서 못 잡았다 →tests_cody/test_color_types.py::test_put_http가 Resource 반환값 직렬화를 잰다. 수정 masterfab01add→ deploy-live3ff24096. ⚠진단 = nginx access.log 상태코드 →journalctl트레이스백(헤드리스 재현은 PUT 을 가로채면 정상으로 보인다). [코디] - 🔑2026-10-20 도어발주서 파서는 «유상» 행이라도 품명이 바 부재명(「선반」 등
COMPONENT_NAMES)이면 접어 버린다 — 0줄 페이지는 U열과 함께 D열 품명을 본다. 나진 AS p7 「선반」 2줄(U=「유상, 시스템 선반 추가됨」)이 유상·전량 두 모드 다 0줄이었다 → 수기로 「시스템 선반」 2줄 추가(806). 같은 날 함정: F열에70*60*2700통째·H 빈칸이면 규격이…2700*mm로 나온다. [코디] - 🔴2026-10-20 세로 띠(
writing-mode:vertical-rl)에 flex 를 쓰면flex-direction:row가 «위→아래»다 —column으로 두면 항목이 가로로 늘어서 좁은 띠(36px) 밖으로 잘려 글자가 통째로 안 보인다(네스팅 사용컬러 폴딩 띠 실측). 그리고 네스팅 사용컬러 패널 폭 상한 = 310px — 340 은 1280px 에서 패널이 표 아래로 떨어진다(문턱 = .col min 900 + gap 18 + 패널 + .wrap 좌우 패딩). 폭이 더 필요하면 칸을 줄이거나 접기로 푼다. ⚠pfs.venv의 옛 Flask 는test_request_context·Flask(__name__)가 pytest 아래서 깨진다 → 컨트롤러 본체를 순수 함수로 빼서 MySQLUtil 가짜로 테스트(tests_cody/test_color_types.py). [코디] - 🔑2026-10-20 [페페] 원부자재발주서 「타 입」 표기 = 기본 규칙 — ①타입을 숫자 오름차순(59→77→84→99) ②같은 숫자면 기본 → 옵션 ③꼬리표(
[초안 rN — 확정 전 검토용]) 안 붙인다(확정 전 표시는 발주서 탭 L열 메모만) ④기본·옵션 중 한쪽이 전체 타입 중 1개뿐이면 그 1개에만 「기본」/「옵션」 글씨, 나머지는 숫자+알파벳만(둘 다 1개씩이면 둘 다 글씨 — 코디 추천·미확인) ⑤뒤에:주방N,일반M(공백 없음 · 수량 = 현장 타입 등록 주방·일반 개수, 등록 없으면 도면·필기 표 기준). 예 = DSD삼호 남판교 3차59D,77C기본,77C,84B,99C:주방5,일반4. 손글씨 표의 X 로 지운 O 는 O 로 본다. 코드 반영 대상 =C:\dev\pfs\api\pfs\mat_orderpaper\mat_orderpaper_excel.py_site_type_note·_type_text(⏳미반영). [페페/코디] - 🔴2026-10-20 [코디]
C:\dev\pfs_deploy_wt에서git reset --hard origin/deploy-live전에git status --short결과에M이 있으면 멈춘다 — 다른 세션이 배포 준비 중 남긴 미커밋 변경이다(이날web/pncad/app.htmlM 1건을 확인 없이 날림 · master 6b8f9f93 「작업건 더블클릭 열기 복구」 미배포분으로 추정 · git 으로 복구 불가). 처방 = M 이 있으면 그 세션 진행 여부 확인 후, 필요하면git worktree add --detach C:\dev\pfs_wt_<이름> origin/deploy-live별도 트리에서 배포. · 같은 날 TREAD rows POST 신규 칼라 자동 등록(register_colors공용) master27aa37b5→ deploy-livedc6778cf· 반영 확인 = rows POST 응답new_colors키. [코디] - 🔴2026-10-19 [코디]
user테이블에도regist_user칸이 있다 →worklog를 거르는 하위질의(EXISTS (SELECT 1 FROM user u WHERE u.user_no = regist_user …)) 안의 «맨» 컬럼명은 바깥worklog가 아니라user쪽에 묶인다(SQL 은 가장 안쪽 범위부터 이름을 찾는다 · 에러 없이 조용히 틀린 결과). ⛒하위질의 안에선 바깥 컬럼을 테이블명·별칭으로 한정(w.regist_user). 업무패널 직원 분리(worklog_access.pySQL 절) R2 에서 잡힘 · masterc7f9781d→ deploy-live19f10d64. [코디] - 🔑2026-10-19 [페페/코디] 피스마개스티커 = «장» 단위(1장 160EA) — 「1장당 수량이 160EA면 낱개수량 말고 앞으로 장수로 표기」. 장수 = ceil(필요 EA ÷ 160) · 필요 EA = 18·15T 원장 ×
cap_per_board160. 노브 1곳 =common/material_order/calc.pyCAP_EA_PER_SHEET·cap_sheets()(규칙판 2026-10-19.1 · 확정 문서 데이터는 안 바꿈 · 새 계산·재가져오기부터). 엑셀 표기 「N장」(서버ad.qty_text· 로컬nesting_draft.qty_text). 선재 SE-021L 처럼 업체가 「1세트(최소 생산 분량)」 로 받는 건 엑셀 정본 따라 수동 행. mastera4b6b2f2→ deploy-live7678b404. [코디] - 🔑2026-10-19 [코디] 자재관리 상태 = 대기·진행·완료뿐(완료 = 입고) → 페페 「발주 완료」 = «진행». «진행» 행은 서버가 수량 교체·삭제를 거절한다 → 확정 발주서를 엑셀 기준으로 다시 맞출 땐 그 행만 «대기»로 되돌린 뒤 apply(
sync.py apply --mark-ordered로 다시 진행). 확정 문서 규격 글자 = amend 로 바뀐다(4.5T→6T 3줄 · 자재관리 행 유지). 차수 생성(round)은 부모 줄을 수량 0 으로 자동 복사한다 → 안 쓰는 줄 소프트삭제. [코디] - 🔴2026-10-19 [페페/코디] 한샘 양식 견적 위치도(도면 캡쳐 + 위치명 : 가로치수)는 갑지(주방·수납 탭)가 아니라 그 구분 «내역서 탭»(예 99A 내역서-주 · 99A 내역서-수) 총합 + 빈 행 1줄 뒤 새 쪽 — V9 에서 갑지 아래에 넣어 V11 에서 다시 옮겼다. hansem-convert =
drawing.target없음(내역서 모드)이 기본 ·groups는 내역서 모드에서도 됨(구분별 탭) ·target:"cover"는 페페가 갑지를 명시할 때만. 상세 =session-2026-10-19-hannam2-hansem-v9.mdV11 절 - 🔑2026-10-19 [코디] 부자재 발주서 행에는 소프트삭제 칸이 없다 — 저장이 행 전체를 지웠다 다시 넣는다 → 행을 빼야 하면 「[삭제] 」 품명 머리 + 수량 「0(삭제)」 + 비고에 원문 보존 관례로(line_id 유지). 자재 발주서(#n)는 lines/remove 소프트삭제 · 확정 문서 수량 변경 = 줄 소프트삭제 + amend 수동 행 추가. 통합 엑셀 ↔ PFS 반복 동기화 도구 =
C:\dev\_work\sohan2-merge-1019\sync\sync.py diff|apply+verify.py(소요한남 2차 선례 · 본문session-2026-10-19-hannam2-gy01dmt-order.md). [코디] - 🔑2026-10-19 [페페/코디] 한샘 변환 도구 26.10.19 옵션(소요 한남 #803 V9) — fold 라도
rules.addon_split(이름에 「유리」 든 sub 만 -추가 블록 · 규격nEA· 비고=유리도어/유리선반) · 몸통 안 만드는 행 =rules.name_only(위치명만 · 끝수는 분리 칸) · 보이는 규격 =rules.display_width(왼쪽 치수 칸만 · 길이 칸 J = 순길이 · DWG 입면도 DIMENSION 실측 근거 json 필수) · 위치도를 갑지 탭에 =drawing.target="cover"(갑지엔 도장 그림 drawing 이 이미 있어 앵커를 «추가» · twoCellAnchor 늘어남 대신 oneCellAnchor+ext · 인쇄 끝+1 행 뒤 쪽나눔 · 갑지 30열 = 950px(MDW 8) × 배율 0.70 = 498.8pt) · 비노출 금액 검사는 분리 칸·그 블록 합계 칸(I{b+6})을 빼야 오탐 없음(유리 270,000 = 다른 행 fold sub 270,000). 본문 =session-2026-10-19-hannam2-hansem-v9.md. [코디] - 🔑2026-10-19 [페페] 엑셀 검토는 데이터로 한다 — PDF 육안 확인은 기본에서 뺀다(「엑셀은 모두 데이터로 코디가 읽을 수 있으니 가능하면 pdf 육안 확인하지 말고 엑셀 형식 또는 json 형식으로 검토」). 도어발주서·발주서·견적서 검토와 검증 = openpyxl/zip XML 로 셀 값·수식·서식·병합·인쇄영역을 읽어 JSON/CSV 로 대조. LibreOffice PDF 렌더는 데이터로 확인할 수 없는 것(도형 겹침 등)이 있을 때만 예외로 쓰고, 그 이유를 기록에 적는다. 선례 검증 항목의 「PDF 인쇄영역에 검토글자 0」은 «인쇄영역 범위 밖 셀에만 썼나»를 셀 좌표로 판정하는 것으로 대체. 도면(DWG) 판독은 해당 없음(형상 확인용 렌더는 그대로). [코디]
- 🔴2026-10-19 [페페/코디] 도어발주서 검토 = 크기 차이 방향 비대칭 + 속대 40 고정 — 발주가 도면·큰제목보다 «작아짐» = 설치 불가 위험 → 빨강 강하게 + 1쪽 머리 「작아짐 = 설치 불가 위험」 목록 · «커짐» = 여유폭 재단 → 도면에 여유자재·「비규격 N」·현장재단 표기 있으면 정상, 없으면 주황 「여유폭 재단 확인요」 · 속대 = 피스 조립용 40 고정(40 이면 큰제목 계산과 달라도 정상, 40 아닌 속대만 확인요). 조립형 정합 =
C:\dev\order-automation\tools\door_assembly_check.py(severity red/purple/orange) 가 기본 단계 · 절차 = playbook §9. 실례(대전 도마) = 도면 개정에서 「비규격 100」 이 새로 붙으면 큰제목만 100 으로 고치고 하위 휠라(45·80)는 그대로 두는 실수가 2곳 — 큰제목↔하위 대조가 잡는다. [코디] - ⚠2026-10-19 [코디] 포인검토 사본 쪽 머리 비고 박스 = 각 쪽 U{첫행}:V{첫행+8} 병합(1쪽만 U1:V3) — 1쪽 4~7행 U~Y 는 템플릿 병합 Q4:Y7 이 덮어 못 쓴다 · 행 삽입·행높이 변경 없이(36행 쪽 블록·
ROW()수식 보존) 인쇄영역 A:O 밖이라 parse_poin(A~T)·원본 「포인」 diff 0 유지 · 검증 = 셀 좌표(daejeon-doma_2026-10-19\work\_cellcheck.py). ⚠ 검토 중 원본이 재저장될 수 있다(대전 10:24 · 값 diff 0 · 보기 위치만) → 게시 가드 sha1 재대조에서 멈추면 새 스냅샷으로 사본을 다시 만든다. [코디] - 🔴2026-10-16 [코디] deploy-live 에 cherry-pick 할 땐 «내 코드가 부르는 공용 함수가 deploy-live 에 있나»를 먼저 본다 — 초대가입이
MySQLTransactionUtil.execute_count/execute_id를 썼는데 그건 미배포 pncad 커밋eaabdbc5에서 생긴 것 → 운영에서만AttributeError(테스트서버는 master 파일이라 통과, 운영 점검은 발급을 안 해서 못 잡음). 검사 =git diff origin/deploy-live <내 커밋> -- common/+ 내 코드가 부르는 메서드를git show origin/deploy-live:<파일> | grep def. 핫픽스 deploy-live4e58e5c2(그 메서드 «추가만» 이식). - 🔴2026-10-16 [코디] uWSGI ini 에
enable-threads가 없다 → 앱이 띄운 스레드(데몬 포함)는 다음 요청 전까지 안 돈다. 초대가입 텔레그램 알림을 스레드로 보냈더니 몇 분 지나도 0건. 응답 뒤 실행은response.call_on_close로(C:\dev\pfs\api\pfs\invite\service\invite_service.py). ⚠기존telegram_notify.send_async사용처(field_memo_service·pncad_stage_controller)도 같은 증상 의심 — 미수정. - 2026-10-16 [코디] 계정 생성은
user_create_guard(C:\dev\pfs\api\pfs\user\service\user_create_guard.py) 를 거친다 —GET_LOCK('pfs_user_create')(별도 커넥션) 안에서 아이디 중복검사→INSERT→COMMIT. user 테이블엔 user_id UNIQUE 가 없어서(ALTER 안 함) 잠금이 유일한 방어다 — 새 계정 생성 경로를 만들면 반드시 이걸 쓴다. 옛/api/user추가 경로는 아직 밖. - 2026-10-16 [코디] hansem-convert 새 설정 옵션 3개(키 없음 = off) —
rules.unit_price(단가칸 = 단가기준 탭 표 값 상수 수식 · 숨김 백데이터 LOOKUP 안 씀) ·rules.cover_extras(주방 갑지 기타 행 품명별 · PFS 부가비 품목명 1건 매칭·전부 사용·운반비 필수) ·rules.conceal_words(verify 비노출 grep). ⚠ 단가기준 탭 «무광,H/G 도장»·백데이터 «H/G 도장» 은 한샘 양식 원본 글자라 「도장」 0건은 불가능 — 검사는 «양식 원본에 같은 칸·같은 글자로 있던 것만 허용 · 내역서·갑지 0». ⚠ verify(LibreOffice)를 두 개 동시에 돌리면 같은 LO 프로필 충돌로 «LibreOffice 변환 실패» — regress 와 run 은 순서대로. 사용법 = playbook §8 🆕 26.10.16. - 🔴2026-10-16 [코디] 크롬 자동완성 팝업은 2종이고 처방이 반대다 — 하나만 막으면 다른 게 뜬다(이날 2회 재발). ①«주소 자동채우기»(이름·주소·우편번호, 프로필 기반) =
off를 무시 → 미인식 토큰autocomplete="pfs-nofill"로만 막힌다 ②«입력 이력»(칸name별 저장·제시) =off로만 막히고 토큰을 넣으면 다시 켜지고 저장도 된다. ⛒둘 다 막기 = 공용pfsNoHistory(form)(pfs-component.js): 텍스트칸 autocomplete=pfs-nofill+name에 로드마다 바뀌는 꼬리, 원래 이름은data-n→ 그 화면 코드는[data-n=X]·form.X.value금지. 공용 팝업(member_popup·company_member_popup)·pfs.js daumPostcode는name·data-n둘 다 잡게 해 둠. ⚠base.html 은{% block javascript %}가{% block popup %}보다 앞 → 화면 스크립트에서 팝업 함수를 재정의해도 팝업 쪽이 이긴다. 증거 = 페페 크롬Default\Web Dataautofill 표(이 폼 칸 이름 이력 378건). 적용/field/regist·/field/modifymasterb0ca25b2/deploy-live60aff96f, 검사 =C:\dev\pfs\scripts\_field_nofill_check.py [test|prod](저장 요청 abort). 검색칸(blockChromeAutofill)은 이력형이라off그대로. - 2026-10-15 [코디] 부자재 발주서 「★ 강조」 = 비고가
★로 시작하는 행(화면 연노랑#fef9c3+좌 4px#f59e0b, 엑셀 A:J 노랑FEF08A· (1차) 초록과 겹치면 ★ 우선 · 스키마 변경 0 · 값 그대로 저장) — 피드백·확인대기 품목 표시용. 운영배포 b4cc3723(master)/248302d2(deploy-live). 소요한남2차 #8 피드백 반영(문희라 차장 26.10.15 단톡방 검토본)에서 ★ 51행.is_star/isStar공용 기준(sub_orderpaper_excel.py·sub_orderpaper.js). 피드백 엑셀 「규격칸 멘트」는 SoftCamp DRM(SCDSA004) 이라 미반영 → 발신자 외부반출 해제본 필요(로컬 복호화 불가). - 2026-10-15 [코디] 자재 발주서 몸통 엣지 = 3·4.5·6T 빼고 전 두께 · 줄 = 컬러 × 폭(30T 34 · 23T 27 · 18/15T 22 · 9T 16 · 0.6T) — 옛
body_edge_thk [18,15]가 30·23·9T 엣지를 뺐다(소요한남2차 #7). 18/15T 22mm 행 키bedge|컬러는 불변(재가져오기 rid 유지) ·common/material_order/calc.py_body_items - 2026-10-15 [코디] 「다시 가져오기」는 사람이 [복구]한 품목군 변경 연관 행(예: 피스마개)을 지우지 않는다(
_held_orphans) — 보정 전 운영 데이터로 재가져오기 모의 실행(sim_reimport.py)을 먼저 돌려 사람 값 소실을 잡는다 - ⚠2026-10-15 [코디] 배포 워크트리
C:\dev\pfs_deploy_wt에 다른 세션이 남긴 «빈 cherry-pick 진행 상태»가 있으면merge --ff-only가 「commit your changes before you merge」로 멈춘다 —git status로 CHERRY_PICK_HEAD 시각·origin/deploy-live반영 여부를 보고, 이미 push 된 빈 상태면git cherry-pick --quit(작업트리 무변경)로만 지운다(--abort·reset금지). · 로컬 master 에 일부러 안 올린 커밋(patch63·64)이 있으면git worktree add --detach <임시> origin/master→cherry-pick→push origin HEAD:master로 그 커밋만 테스트 배포한다(26.10.15 2회). - ⚠2026-10-14 [코디] 월드산업(헤움디자인) 견적 엑셀 롯데 양식 = H열은 ㄱ·ㄷ 성형 부재의 3번째 치수(두께 아님) · 두께는 같은 부재 묶음 첫 줄에만 — 단가 추출 시 묶음 단위로 두께를 전파하고 H열을 두께로 읽지 마라. 무늬목 단가 = 같은 규격 도장 단가 1:1([페페] 26.10.14 · 옛 ×2.0 폐기) · 현장별 가격 수준 차(소요1차 ×1.07 · 일레븐 ×1.19 · 한남소요MH ×0.50)를 보정하고 쓴다. 근거 =
C:\dev\_work\sohan-estimate-1013\world\월드산업_단가기준.md6차. - ⚠2026-10-14 [코디] 운영 화면 «읽기 전용» Playwright 확인의 쓰기 가드는 POST 를 통째로 막으면 안 된다 — 견적 v2 init 이 코드 조회를
POST /api/common/code/search·company/member/search로 해서 화면이 안 뜬다(wait_for_function 30s 타임아웃). 가드 = GET + 경로에/search있는 POST 만 통과, 나머지 abort 하고 «가로챈 건수 0» 을 판정에 넣는다(C:\dev\_work\est-submargin-linkall-1014\prod_803_check.py). · 🔑push 전 화면 사전검사 = 테스트 서버 화면에 작업 트리 JS·CSS·템플릿 조각을 Playwright route 로 끼워 넣는--local(scripts/_est_submargin_check.py install_local) — master 를 안 더럽히고 실서버 데이터로 돈다. · 견적 v2 「모두 연동으로」 없음(26.10.7) 결정은 26.10.14 [페페]로 뒤집힘 = 「저장된 추가금액 연동」 버튼(session-2026-10-14-est-submargin-linkall.md). - 🔴2026-10-14 [코디] 네스팅 행의 «제외» 표시는
ok가 아니라exc == '1'이다(GET /api/nesting/<job>/rows· job.parts 같음 · 26.10.12 TREAD 행 동기 v2 · 화면 tr.exc 회색 취소선) — 10.13 #803 sub 생성 스크립트가ok=='0'만 보고 「제외 0」으로 읽어 #28 제외 행 7개(끝판 30T 4 · 목찬넬 2 · 진열칸 내측 E.P(앞) 1)가 견적 sub 에 들어갔다. 네스팅 행으로 견적·발주를 만들 땐exc를 거르고, 몸통/도어 무늬목 판정은 칼라(INEK10 등 천연 무늬목)로 — 「무늬목유사 LPM」은 무늬목 아님. - ⚠2026-10-14 [코디] 부자재 발주서(
/api/sub-orderpaper)의title은 API 로만 들어가고 화면엔 안 보이며, 화면에서 한 번 저장하면 지워진다(sub_orderpaper.js payload()에 title 없음 → 서비스가 '' → NULL). 초안 표시는 title 에 기대지 말고 비고·목록 현장명으로 · 업체 칸은 자유입력(VARCHAR · 드롭리스트도 Esc+Enter 로 친 글자 유지)이라 「미확인 업체」 같은 문자열을 그대로 넣으면 엑셀에 그 이름 시트가 생긴다(#8 소요한남 2차 · 72행). - 🔴2026-10-14 [코디] 견적 기타 부가비(
estimate_extra_cost.price/amount)와 견적 총액은 MySQL FLOAT 라 읽으면 유효숫자 6자리로 나온다 — #803 부가비 14,848,685 를 넣었더니 GET·화면은 14,848,700, 총액 45,334,685 → 45,334,700(천만 원대 = 100원 단위로 반올림돼 보임). 원 단위가 중요한 금액은 넣기 전에 100원 단위로 맞추거나 「PFS 읽은 값」을 정본으로 쓴다(한샘 변환은 읽은 값 사용). 견적 행·sub 금액은 영향 없음(대조 일치). - ⚠2026-10-14 [코디] 한샘 변환 총액은 지시문 숫자가 아니라 fetch 시점 PFS 가 정본이다 — #803 지시문 23,585,000 은 옛 값, 10.13 18:01 다른 세션이 기타 부가비를 12,911,900(다각형 실견적)으로 바꿔 33,860,900 이 됐는데 시드 기록이 없었다. 견적 「기타 부가비」는 견적 GET·sub/list 에 없다 →
GET /api/estimate/extra/list/<no>(hansem-convert fetch 가 26.10.14 부터 받음 · separate 모드에 부가비 있으면 plan 이 멈춤). PFS 견적 PUT/부가비를 고친 세션은 금액 바뀐 걸 시드에 꼭 남긴다. 한샘 마감 칸(C{b+3})은 단가 LOOKUP 키라 종류 단어로 바꾸려면 LOOKUP 안 참조를 한샘 마감명 문자열로 고정해야 한다(finish_label: type_only). - ⚠2026-10-13 [코디] 화면 boot 가 드롭리스트용 목록(
/api/field/search등)을 기다린 뒤 본문을 그리면 그 API 시간이 그대로 로딩이 된다 — 자재 발주서 2.3s 원인. 본문 먼저, 드롭리스트 데이터는 비동기 · 필요한 칸만 주는 전용 API(/api/mat-orderpaper/fields). 인덱스 추가 후 GROUP BY 파생테이블은 옵티마이저가 인덱스 풀스캔을 골라 오히려 +15ms 될 수 있다(patch63 견적 목록, 단건은 11배 개선). - ⚠2026-10-13 [코디] 목록 SQL 에 «행마다 상관 COUNT 서브쿼리»를 쓰면 자식 FK 무인덱스라 초 단위가 된다 — 현장목록(
field_type.field_no) 6.6초 = 업무패널 콜드 로드 p90 7초의 진범 · 견적목록(estimate_detail.estimate_no) 10.4초. 새 목록 SQL 은 GROUP BY 파생테이블 LEFT JOIN +COALESCE(…,0)(field_dao._FIELD_TYPE_CNT_JOIN선례). 느린 API 고르기 = 운영journalctl -u pfs-live.uwsgi.service의in N msecs(pfs-log/uwsgi.log는 26.5.4 정지). SQL 교체는 운영 읽기 전용 구/신 DAO 동시 실행으로 행·순서·타입 비교(PFS-D047·D048, 브랜치 perf-1013). [코디] - 🔴2026-10-13 [코디] openpyxl 은 파일을 열 때·병합할 때 병합칸 둘레를 «첫 칸» 테두리로 다시 칠한다 → 원부자재발주서 H:J 병합의 H 오른쪽 hair 가 장 외곽 J 오른쪽을 점선으로 바꾼다(NAS 원본 외곽 = 네 변 thin · 안쪽 = hair). 양식을 채운 뒤 반드시
xlsx_writer.seal_frame로 외곽을 thin 으로 다시 닫는다. 부자재·AL 발주서 엑셀 블록 복제에도 같은 결함 가능(미점검). - 🔑2026-10-13 [페페] 월드산업(견적서 상호 「헤움디자인」) 단가 = 도장 포함 «임가공비만», 보드값 미포함 — 무늬목 패널 견적은 보드(다각형 등 공급처)와 월드 가공비를 따로 잡는다 · 소요한남 1차 항목별 월드 단가 = NAS
2026현장/견적/04월/월드산업4월마감/포인 견적서 - - 복사본.xlsx「소요한남」 시트 · 무늬목 기준표 =C:\dev\_work\sohan-estimate-1013\world\월드산업_단가기준.md - ⚠2026-10-13 [코디] master push 직후 다른 세션이 또 push 하면 내 테스트 배포 런은 «cancelled»(
Canceling since a higher priority waiting request for pfs-deploy-master exists)로 끝난다 — 실패가 아니다.git log origin/master로 내 커밋이 뒤 런의 조상인지 보고 뒤 런을gh run watch <id> --exit-status로 본다(PNCAD 숨김 3cb96961 → 38364705158 취소 · 38364763305 success). 테스트 반영 확인은 본문 대조(qa_helper 2줄 주입 제외)로. [코디] - 🔑2026-10-13 [코디] 자재 발주서 실효 입고일 =
effective_receive_date(line, header)(품목 → 공통) · 확정 문서 입고일 변경은 실효가 바뀐 품목의 자재관리*_user입고일을 비운다(이전 값 = 판이력extra.op_user_replaced) ·pfs_link.sync_multi는 입고 완료 행 입고일 3칸을 덮지 않는다. ⚠같은 날 worker 4개를 별도 worktree(C:\dev\pfs_wt_<이름>)로 나눠 순차 rebase·선별 포워드하니 충돌 0 — 같은 화면 다건 동시 작업의 기본형. - 🔑2026-10-13 [코디] 원부자재발주서 xlsx 작성 정본 =
C:\dev\pfs\common\material_order\xlsx_writer.py(서버 PFS 다운로드·로컬orderpaper_build공용 — orderpaper_build 는 재내보내기만). 서버는 NAS 를 안 읽고 리포 양식 사본orderpaper_form.xlsx를 쓴다 → 업체 마스터가 바뀌면python orderpaper-sync\orderpaper_build.py --refresh-form후 커밋. ⚠동기화 대조는PFS-MAT#표식 파일 현장을 파일명으로 맞춰 오매칭한다(#5→2130 · #4→2124, 26.10.13 미수정). - 🔑2026-10-13 [코디] 자재 발주서 → 자재관리 적재 수량 =
calc_adapter.link_qtys(현장 합 = 문서 최종, 잔여는 실소요 비율 정수 배분) — 현장alloc_qty를 그대로 실으면 통합 1회 여유·올림(시트·엣지·샘플·피스마개)이 빠진다. 재적재(pfs_link.sync_multi)는 재고를 안 바꾸므로 데이터 보정 땐modify_item_stock을 같이 부른다(재고 이력 남음). - ⚠2026-10-13 [코디] 같은
C:\dev\pfs작업 트리에서 두 세션이 같은 파일을 고치면 남의 커밋에 반쯤 된 편집이 섞인다(자재 발주서 품명 이력 세션 ↔ 입고처·품목군 worker, 14:41 실측) → 동시 작업이 보이면(git status남의 미커밋 · mtime 변동) 별도 worktree(git worktree add C:\dev\pfs_wt_<이름>)에서 하고, 상대 deploy-live 가 끝난 뒤 master 에 넣는다. · 자재 발주서 품목군 변경 상태 =calc_meta.cat_overrides— 지우면 재가져오기 결과가 달라진다. - 🔴2026-10-13 [코디] 자재 발주서 자동 행 품명·규격을 바꾸는 새 경로는 반드시
_name_hist(서비스)를 거친다 — 불변식 «지금 값 = 최신 적용(is_applied 1) 이력»(이력 있는 행). 안 거치면 칩(「수정됨」 = 지금 값 ≠ 최신 계산값)·재가져오기 사람 값 유지(최신 적용 이력이 user/restore)·되돌리기가 어긋난다. 현재 경로 = 저장_apply_edits· 복원restore_name· 정정amend(규격) · 재가져오기/대응 연결_carry_name. ⛒line_key를 품명으로 다시 만들지 마라(매칭 = 계산 키) · 대응 불명(link_state new) 행은 연결 전 품명 수정 금지(연결 =rename_line이 line_id 만 바꿔 이력이 고아 됨 — 반박검증 26.10.13). 이력 테이블mat_orderpaper_item_hist(patch61). 그리고 🔑같은 파일을 다른 세션이 동시에 고치고 있으면 커밋은 «내 변경만» blob 으로 — HEAD 사본에 같은 Edit →git hash-object -w --path+git update-index --cacheinfo→ commit(작업트리엔 둘 다 남김 · 26.10.13 mat_orderpaper.js/서비스/템플릿 실례). [코디] - ⚠2026-10-13 [코디] NAS 도어발주서 「포인」 탭을 직접 고칠 때 함정 3(안산 성포 추천안 반영) — ① 「포인」 L:N 「비고」 열 = 생산지시 칸이라 parse_poin 이
note로 운영 스티커에 싣는다 → 변경 이력 비고는 블록 밖 U열(인쇄영역·파서 A~T 밖) ② 백업·임시 파일명에 「도어」 금지 — door_order_sync 가*도어*.xls[mx]를 스캔해 옛 값이 운영 스티커 DB 에 올라간다 ③ 품목 추가 = 행 삽입 말고 같은 쪽 빈 줄(36행 쪽 고정 블록 · A열 번호·T열 바코드가ROW()수식이라 삽입하면 뒤 품목이 전부 밀림) · 수량 K열은 숫자로. NAS 교체 후 ~2분이면door_sync_watch가 운영·테스트 업서트한다. 기록 =session-2026-10-13-ansan-seongpo-door-review.md「반영」. - ⚠2026-10-13 [코디] 도어발주서 도면 대조 함정 3(안산 성포 포인검토) — ① 경첩 좌우 = 입면 열림표시(점선 V) 꼭짓점 쪽(「90°힌지」 표기 쪽 · PUSH 반대쪽으로 검증) — 시트 230dpi 렌더로는 꼭짓점을 잘못 읽는다(99A B" 좌2우1 로 오독 → 520dpi 로 좌1우2) → 도어 줄만 380~520dpi 확대(
C:\dev\order-automation\ansan-seongpo_2026-10-13\work\boxes_zoom.json) ② 한샘 도면 「비규격 N」 = 현장재단용 큰 치수 — EP·FIX 는 N 으로 발주가 정상(60·40→100, 720→750, 705→850), 단 12T 벽마감(메지판)은 「(벽마감)」 실치수로 발주 · 바로 옆 「(조절)」 치수를 벽마감으로 옮겨 적은 오기 실례(전시부스 123 ← 145) ③ ㄱ자 EP 의 좌/우 = 설치 위치(왼쪽 끝/오른쪽 끝) — 다른 타입 행을 복사하면 비규격 쪽이 뒤집힌다(99A 100 좌 ← 도면 오른쪽). [코디] - ⚠2026-10-13 [코디] nesting parts 를 읽는 서버 계산은 exc='1'(제외) 행을 빼야 한다 — 선례 = calc._parts · mat_view · calc_adapter(소요한남 행 동기 v2). 새 계산 경로가 parts 를 읽으면 같은 필터를 넣고, TREAD 동기 add 는 ok·exc·fno 칸을 싣지 않는다(서버가 모르는 칸 400).
- ⚠2026-10-13 [코디] 비로그인 노출 검사는 상태코드가 아니라 본문으로 판정한다 — pfs 의 비로그인 응답은 302 가 아니라
200+ 로그인 페이지 HTML이다(/production/pncad/photo.js·tex/oak_g.jpg·data/finish-map.json전부 200 + 데이터 0줄). 「200 이면 노출」로 읽으면 오탐 — 본문이 로그인 HTML 인지(자산 데이터 0줄) 본다. 출처 = pncad 실사 모드 v1 테스트 배포(C:\dev\seedchain\projects\pncad\session-2026-10-12-photo.md). - ⚠2026-10-12 [코디] TREAD rows-ok: tread 행(ok_src=tread)의 ok 는 웹 전체 PUT 이 못 바꾼다 — 정본=도면(TREAD 「확인」 탭 →
POST /api/nesting/<job>/rows-ok). 웹에서 그 행 ok 를 고쳐도 서버가 tread 값으로 되돌림. 상세 =projects/acad-tread/handoff.md맨 위. - ⚠2026-10-12 [코디] 도어발주서에 «검토 탭» 추가 함정 4 — ① COM
ws.Copy(After=src)키워드는 동적 디스패치에서 무시돼 새 통합문서로 복사된다 → 위치 인자src.Copy(wb.Worksheets(src.Index+1))(Before) ② 탭 추가는 Excel 이 vbaProject 를 다시 쓴다(모듈 소스 동일·빈 Sheet6 추가·프로젝트 ID/폼 GUID 재생성 · OLE 스트림 «동일» 검증은 불가 → olevba 소스 대조로) · 파트 번호가 시트 순서로 재번호돼 zip 대조는 «시트 이름 기준»으로(C:\dev\order-automation\wonju-danggu_2026-10-12\s5b_parts_by_sheet.py) ③ 포인탭 템플릿 블록 밖에 병합Q4:Y7(AS건 안내)·AB3:AD3+ 3행 축소맞춤 서식 → 1~7행 U~AE 에 쓰면 글자가 사라진다 ④ 사람이 고친 행은 P~T(공정·바코드) 수식이 빠져 있곤 하다(원주 106·107·379행) → 검토 때 A·P~T 빈칸 스캔을 기본으로. - 🔑2026-10-12 [페페] 운영배포(deploy-live) 때 master push 도 같이 한다 — 기본 지침, 따로 묻지 않는다(「마스터 푸시는 운영배포시 당연히 같이 작업하는 항목」). 순서 = master 커밋 → deploy-live 선별 포워드·배포 확인 →
git push origin master(origin/master..master 목록 확인 후) → 사이트맵 재빌드. master push 는 테스트 서버 8001 자동 배포도 같이 일으킨다. 배포 worker 지시문에도 넣는다. - 🔑2026-10-11 [페페] 도면 자재 소요량의 높이 기준 = 실제 그림(형상) 높이 — 치수 문자 일부가 틀린다. 문자 ≠ 형상이면 형상을 쓰고 위치·문자·형상·택한 값 목록을 남긴다 · 장 높이 일괄 가정(2600 등) 금지 · 표기 도면에 형상 높이 치수 덧그림 · 이미 POST 한 저장본이 어긋나면 PUT 말고 새 POST + 옛 번호 «폐기 대상». 첫 적용 = 소요 한남 2차(#23 → #25 ·
C:\dev\_work\hannam2-1011\height_mismatch.json21건). - ⚠2026-10-11 [코디] dwg-takeoff 새 현장 함정 3 — ① 같은 시트가 바깥·안쪽 도곽 2벌로 잡힌다(소요한남 44 → 18장) → 한 벌만
--types로 ② DIMLFAC −1 도면은 doors/hybrid 치수가 음수(−2760)로 나온다 → 절댓값 우회(C:\dev\_work\hannam2-1011\door\abs_cache.py) · 도구 수정 제안 ③ INNER·SIDE 뷰 오픈장 세트·목대 판이 많은 도면은 hybrid 단독 재현율 20%대 → 시트마다 형상 확정이 기본. 한샘 넥서스 마감재 리스트 xlsx 는 DRM(SCDSA004)이라 못 읽는다 → 칼라는 도면 주기로. - 🔑2026-10-10 [페페] Jev 기존 규칙 완화 허용(「기존 규칙 변경해도 되니 활용처 다양하게」) — 제안안 3단: 가.거르기(되돌릴 수 있고 사람 목록 남김) = 라벨 n≥30·정밀도≥95% 구간 자동 / 나.후보 고르기 자동반영 = n≥50·정밀도≥98%·섀도 2주 / 다.금액·권한·보안·삭제 금지. 방향 비대칭(등급은 올리기만·ruleguard 는 경고 추가만). 오타 유사 후보는 문자열 매칭으로 ≤5개 좁힌 뒤, p(해당없음)<0.6 & 2위≥0.3 = 불확실. 후보 상위 = 도면 부재 소속 choice · 통화 업무/비업무 noul · ruleguard judge형 경고·sgc watcher noul 프리필터. 스모크 18호출 185~236ms. [코디]
- ⚠2026-10-10 [코디] 도면 부재 «종류» 판정에 Jev 는 규칙보다 못하다 — dwg-takeoff
doors섀도(규칙 신뢰도<0.6 후보 103건) = 보류 50건 · 정답 짝 18건 중 Jev 5 vs 규칙 14 정답 · 재실행 시 짝 수 19↔18 흔들림. 기하·텍스트 근거만으로 묻지 말고, 넘길 땐 근처 라벨·치수·시트 맥락을 후보 근거로 같이 준다. 승격 전 라벨 50건+. 상세 =C:\dev\tools\dwg-takeoff\tests\out\p5_doors_dj_jev.jsonl. - ⚠2026-10-10 [코디] 운영 네스팅 저장본 GET
/api/nesting/job은 에이전트 토큰으로 401 — 세션 로그인(POST)이 필요하고 그건 자격증명 자동입력이라 승인 항목. 홀드아웃 평가용 저장본은 미리 로컬 사본(C:\dev\_work\…\job_*_snapshot_*.json)을 떠 두거나 승인 후C:\dev\_work\gs-seongpo-p5-1010\fetch_sp_job.py. - ⚠2026-10-10 [코디] dwg-takeoff 현장 프로필 치수 규약은 한 현장 근거면 다른 현장에 과적용된다 —
hanssem_gs_mh주방 측면EP 480 을 성포에 대니 82 vs 9. 프로필 키는 «현장 2곳 이상 근거» 전까지 현장별 오버라이드로 둔다. - 🔑2026-10-10 [코디] dwg-takeoff
doors는 이제 현장 파일을 같이 줘야 한다 —--site C:\dev\tools\dwg-takeoff\golden\<site>.site.json(대전 = gs_daejeon_2175 · 성포 = gs_ansan_seongpo_2180). 빼면 프로필 기본값(2현장 공통 키 3개: main_band·door_min_side·side_ep_depth)만 돌아 대전 수치가 안 나온다. 새 현장 = site.json 먼저 만든다. - ⚠2026-10-10 [코디] 한샘 GS 사양서 도어 CORE 「15T」 ≠ 생산 두께 18T(성포 104 자료차단 실험에서 15T 로 적어 도어 19매 오답 — 18T 면 61.0%). 도어 두께는 사양서 글자 말고 현장 파일 키로. 같은 실험 Jev 섀도: 맥락 없는 질문 보류 89.6%·1 · 10 정답, 근처 라벨·치수 맥락 준 질문 보류 53.3%·9 · 14(규칙 11 · 14) — 맥락은 필수지만 여전히 규칙보다 못하다. 상세 =
C:\dev\_work\cmp-sp\score.json. - 🔑2026-10-10 [코디] 도면 부재 판정 Jev = 3현장 연속 규칙 미달(대전 5 · 18 vs 14 · 18 · 성포 9 · 14 vs 11 · 14 · 원주 단구 4 · 19 vs 11 · 19) → dwg-takeoff 에선 Jev 섀도 끄고, Jev 확대는 통화 분류·ruleguard 경고 쪽에서. 새 현장(자료차단) 첫 실행은 dwg-takeoff 가 시간 −40%·토큰 −32% 이지만 도어 정확도는 기존 손작업보다 낮다(단구 84C 50.0% vs 61.8%) — 사양서 머리글 변형(「도 어 사 양」)·EP 깊이·양단면이 주 원인. 상세 =
C:\dev\_work\cmp-3\score.json. 한샘 GS 주방은 평형마다 기본·확장·Style Up 시트가 2~3벌 → 저장본은 1벌(Style Up)만 센다. - 🔴2026-10-09 [페페] 테스트 배포 확인(
gh run watch <id> --exit-status)을 건너뛰고 운영배포하지 마라 — 「정석 작업이 아니다」. gh 미로그인이면 멈추고 보고한다. 원인 =.secrets\.env의 PAT(repo·workflow)는gh auth login --with-token이read:org없다고 거부. 26.10.9 [코디] 디바이스 플로우로 재로그인(gho_ 토큰, 만료 없음,%APPDATA%\GitHub CLI\hosts.yml). 다시 끊기면gh auth login -h github.com -p https --web -s read:org,repo,workflow백그라운드 → 코드를 페페에게 전달. 배포 지시서에 이 줄을 넣는다. [코디] - 🔴2026-10-09 [코디] 상주 워커 런처에
cmd /c … >> 로그파일 2>&1을 쓰고 워커도 같은 파일에 open('a') 로 쓰면 로그가 영영 0줄이다 — cmd 리다이렉트가 쓰기 공유 없이 파일을 잡아 워커의 append 는 PermissionError(삼킴), print 는 블록 버퍼라 프로세스가 끝날 때까지 안 나온다. 스티커door_sync_watch가 그래서 9.2~10.9 로그 0줄 = «죽은 줄 알았는데» 운영 sync-poll 200 으로 살아 있었다(판정은 nginx access.log). → 런처 =conhost --headless python.exe 스크립트(리다이렉트 없음) + 워커 자체 로그. 생존 판정은door_sync_watch.py --status(하트비트 + 락 포트 bind 시도 — connect 로 보면 accept 안 하는 소켓 backlog 가 차서 2번째부터 거짓 「없음」). 도어발주서는 이제 워커가 5분마다door_order_sync.py all --if-changed로 자동 동기화(버튼 불필요) · 정시·버튼·자동은_door_sync\door_sync.lock으로 직렬화 · Session 0 실행은 exit 3. [코디] - 🔴2026-10-09 [코디] 스티커 도어발주서 정시 동기화 = 반드시
door_order_sync.py all(운영→테스트) — 런처C:\dev\pfs\orderpaper-sync\door_order_autosync_hidden.vbs(gitignore·미니PC 로컬, schtaskspfs_door_sync매시)가test하드코딩이라 운영door_order_file은 「엑셀 동기화」 버튼으로만 갱신됐다(마지막 26.8.26) → 9월 이후 도어발주서가 운영 /production/sticker 검색에 0건(넥서스 학동 매장 등 신규 31건). 「검색 안 된다」 신고는 ①운영door_order_file에 행이 있나 ②연결(field_no) 순 — 운영·테스트 상태 파일(_door_sync_state.prod.jsonvs_door_sync_state.json) 대조가 제일 빠르다. 자체검사 =orderpaper-sync\test_door_sync_targets.py(런처 인자까지 본다). 🔴door_order_sync 를 Bash 도구(Session 0 = NAS UNC 안 보임)에서 돌리지 마라 — scan_files 가 조용히 [] → reconcile 이 운영 243건 전부 remove_yn='1'(26.10.9 13:45:37~13:46:48 KST 운영 목록 0, 즉시 SQL 원복) + 상태 파일까지 비웠다. 이제 인벤토리 0 = exit 2·무변경 / 직전 상태 50% 미만 = 그 환경 중단. 수동 실행은Start-ScheduledTask pfs_door_sync(사용자 세션). 상태가 비면 전량 재파싱 ≈ 22분. [코디] - [코디] 26.10.9 배포 위임 에이전트가 C:\dev.secrets\pfs_login.env 를 «확인»하려다 sed 마스킹 패턴이 안 맞아 비밀번호를 출력에 찍었다(로컬 transcript) — 시크릿 파일은 내용 출력 금지, 키 존재만
grep -c '^PFS_PROD_' 파일로 본다. 배포 지시서에 이 한 줄을 넣는다. - 🔑2026-10-08 [페페] 레일별 서랍 바닥판 — 언더레일·볼레일 서랍 = 바닥판을 등판 자재와 동일 자재(등판 넘버·두께, GS 대전 = 4.5T 70-115) · 텐덤서랍 = 바닥판 15T(몸통 자재). PNCAD 엔진은 텐덤 바닥판을 이미
15T_서랍바닥판(15T PB · 몸통 넘버)로 내지만 레일형 바닥판은 등판이 아니라 몸통 넘버(colorNoOf)로 가서 작업본 nest_rows 엔 몸통 칼라로 남는다 → 네스팅에서 칼라를 맞추고 엔진 수정은 제안. 레일 판정 근거 = PNCAD fill.rail · 도면 하드웨어 글자 · 사양서 「서랍레일」(Quadro V6 = 언더레일). GS 대전 지시 — 전사 적용 여부 미확인. [코디] - 🔑2026-10-08 [페페] PP시트 규칙 — 서랍 바닥판 3T → 4.5T · 4.5T 몸통 부재(등판·서랍바닥)는 몸통 넘버가 아닌 70-115 PP시트(특수 지정 몸통 넘버 장만 예외 · 시트라 경면 없음) · 도어류(도어·서랍 앞판) 후면 = 70-113 피앤데코 PP시트 · 그 밖 양면자재가 아닌 단면 자재 후면 = 펫유사PP(오엠씨 재고 시트). GS 대전 변동(field 2175) 지시 — 전사 적용 여부 미확인. 사례 =
projects/pfs/session-2026-10-06-gs-daejeon-body-door-nesting.md「26.10.8 PP시트 규칙」. - ⚠2026-10-08 [코디] 네스팅 부재표엔 «후면» 칸이 없다 — 후면이 다른 단면 판은 컬러 칸에 「전면/후면」(
IV075MT/70-113)으로 적어 원장을 가른다(전판 라미네이팅이라 후면이 다르면 다른 원장). ⛒괄호전면 (후면)금지 — 화면이 컬러 꼬리 괄호를 경면코드로 자동 채워 「경면단면」이 된다(10.6 임시명유사PP(PBNB6T)가 실제로 경면양면으로 저장돼 있었다). 대가 = 소계·시트길이가 전면 칼라 2줄로 나뉨 · 저장 시 조합명이 칼라 자동등록 대상(자동화 저장은 abort). 서랍바닥판 4.5T 는 PNCAD 엔진상 등판이 아니라 몸통 넘버(backMat 안 먹음) → 네스팅에서 칼라를 맞춘다. [코디] - ⚠2026-10-08 [코디] PNCAD 작업본 저장 뒤 엔진이 바뀌면(예: 26.10.8 이동선반=내경−3) 앱 표시값과 저장된 nest_rows·네스팅이 달라진다 → 장별 대조는 저장 당시 엔진 기준으로 전개하고 차이를 같이 적는다.
- ⚠2026-10-08 [코디] ODA 재저장은 익명 블록(
*U·*D) 번호를 다시 매기고 용량도 줄어든다(48→22MB 정상) → DWG 원본 불변 검증은 블록 번호를 무시하고 블록 내용으로 대조한다. - 2026-10-08 [코디] hansem-convert 1줄짜리·정액(pr) 견적(몰테니 #802) — 설정 3개로 도구 안에서 처리: ①
form.mode=use+rename{옛 탭:새 탭}·s6·cover_nums{"AG26":시공비}(옛 현장 1평형 양식 재사용 · 안 하면 verify 가 84A 흔적·결재일·시공비로 FAIL) ②rules.length_override{detail_no:[mm,근거]}— 정액 품목은 포인 dimension1 이 길이가 아니라(300) 적용률 15.48 범위 밖 → 벽 실길이(size_display 합)로 ③ K 의 U1(벽판넬) 배치 규칙 없음 → 설정assign.K에 U1→39(키큰 FIX PANEL). 사례 =C:\dev\tools\hansem-convert\sites\molteni-booth-802.json. [코디] - 🔴2026-10-08 [코디] /api 는 이제 «계정유형별 허용목록 · fail-closed» 다 — 거래처 화면에 새 API 를 붙이면
api/pfs/permission/service/api_gate.py의ALLOW_EXTERNAL에 (endpoint, method) → 메뉴 한 줄을 같이 넣는다. 안 넣으면 그 거래처 화면만 403(code 0006)이 난다. 키 = Flask endpoint 이름(flask_restplus = 네임스페이스 이름 +_+ 리소스 클래스 snake). 자사(company_type '1')·총관리자는 종전대로 전부 통과, 잠금 협력사('2')는/api/ext-nesting/*만, 업체 없음·삭제 업체·판정 예외 = 차단. 자기검사 =api_gate.selfcheck(app)(허용목록 키가 url_map 에 다 있나). ⛒롤백 주의 — 이 게이트(master6f20be40·deploy-livef69b484a) 이전으로 되돌리면 협력사 계정이 전 API 를 다시 연다 → 되돌리기 «전»에 company_type '2' 계정부터 비활성화. [코디] - 🔴2026-10-08 [코디] 포인
web/nesting/nesting.html을 고치면 협력사 화면(/partner/nesting)은 자동으로 안 바뀐다 —python tools/partner_nesting/build.py재실행 + 산출물(web/nesting/partner/**) 커밋까지가 한 작업.--check= 재빌드 누락 탐지(원본과 커밋본 비교). 새 API 를 쓰면 원본NEST_API와 빌드의PARTNER_API둘 다에 넣고, 협력사에게 안 줄 기능이면PARTNER_API값을 null 로(화면이 그 기능을 숨기거나 막는다). 협력사 자산은_ASSETS(droplist.js · drawer.js) 허용목록만 — PNCADschema.js·lindiv.js통째로는 절대 안 낸다(drawer.js = drawerBox 추출본, 빌드가 1624 조합 동치 검증). [코디] - 🔴2026-10-08 [코디] 협력사(company_type '2') 계정에 총관리자 플래그를 주지 마라. /api 판정은 협력사로 고정(
0e175294)했지만 화면 메뉴 판정has_menu_permission은 총관리자를 무조건 통과시켜 포인 화면 HTML 이 열린다. 협력사 계정 발급 = 거래처 담당자 등록 →POST /api/admin/user(member_no 연동 · is_super_admin '0'). [코디] - ⚠2026-10-08 [코디] 운영 broker 훅은 경로에
production이 든 파일의 Edit/Write 를 자동 거부한다(production_status_service.py등). 그 파일 로직을 바꿔야 하면 우회하지 말고 다른 층(예: /api 게이트ARG_CHECKS— 생산현황 일괄조회 «자기 업체 현장번호만» 인자 검사)에서 같은 효과를 낸다. [코디] - 🔎2026-10-08 [코디] 발견(미조치 · 보고만): 비로그인으로 열린다 —
/static/uploads/memo/field_memo.json(200) ·/static/reports/license.key(200) ·/swagger.json(전 API 스펙)·/doc. 비로그인 요청은 before_request 의 메뉴 판정을 안 타서 로그인 사용자(403)보다 더 많이 본다. [코디] - 🔑2026-10-08 [코디] 한샘 양식 변환은 이제 도구
C:\dev\tools\hansem-convert\hc.py로 한다 — _work 스크립트를 복사하지 마라(절차 = playbook §8 · 골든 =test_hansem_convert.py가 26.10.8 산출 2건과 xlsx 파트 바이트 대조). 이식하며 확인한 사실 2개: ①엔진build()(K_layout 경로)와 «탭마다build_ledger+inject_coverK·S +finalize» 경로는 산출 파트가 바이트까지 같다(rename_slots는 이미 이름이 맞으면 왕복 무변화) → 주방·수납·혼합을 한 경로로 돈다. ②KEY PLAN 라벨 «탭»(글자 둘레 작은 폴리라인)에 딸린 «구역 박스»를 «맞닿은 가장 큰 박스»로 고르면 틀린다 — 옆 구역이 탭을 통째로 덮는 일이 있다(한국유리부지 103 침실3 붙박이장 탭 ⊂ 거실 픽스판넬 구역, 111 거실 픽스판넬-1 탭이 복도 픽스판넬-2 구역과 겹침) → 탭 변과 같은 선에 놓인 구역 변 개수로 고른다(31/31). ③옛nas_put_*.py는 지문 재확인 때fetchXXX.fetch()로 스냅샷 파일을 덮어썼다 — 도구는 메모리로만 비교한다. [코디] - 🔴2026-10-08 [코디] 한샘 견적서 내역서 탭 밑에 그림을 «새 쪽»으로 넣을 땐 쪽나눔을 총합 행 바로 뒤가 아니라 «빈 행 1줄 뒤»에 건다 — 총합 바로 뒤에서 끊으면 LO 가 총합 행 굵은 아래 테두리를 쪽 경계에서 반쯤 잘라 얇게 그린다(PDF 실측). 그림 = twoCellAnchor A열~O열 끝(15열×95px · 맑은 고딕 11 MDW 8) × N행(22px) + 흰 여백 비율 보정 · PIL
quantize팔레트의 «거의 흰색»(≥240)은 255 로 고친다(안 하면 바탕이 254 로 잡혀 인쇄에 옅은 회색 상자) · 검증 픽셀 대조는 «새로 연» fitz 문서로(같은 문서에서 큰 그림을 먼저 디코딩하면 다른 쪽 도장 그림 렌더가 미세하게 달라진다 — 파일 차이 아님). 도면 위치 ↔ 글 연결선은 글을 «점 높이 순»으로 두고 차선 규칙(내려가는 선 = 위쪽 줄이 바깥 차선, 올라가는 선 = 아래쪽 줄이 바깥)이면 교차 0. 실례 =C:\dev\_work\hankukglass-hansem-v2\{compose,build_v2,verify_v2}.py. [코디] - 🔴2026-10-08 [코디] DWG→PNG(ezdxf
draw_entities) 흑백 평면도 =RenderContext.set_current_layout(msp)+current_layout_properties.set_colors("#ffffff")필수, 그리고 WIPEOUT 은 그리지 않는다 — 안 하면 WIPEOUT 이 모델공간 어두운 배경색으로 칠해져 검은 사각형(한국유리부지 84 욕실 1200×600), 흰색으로 고쳐도 평면 블록 안 WIPEOUT 이 가구 블록(욕실장)을 덮어 지운다(redraw order 로 안 풀림) →Frontend서브클래스draw_wipeout_entityno-op.ColorPolicy.BLACK은 WIPEOUT 까지 검게 칠하니 쓰지 말고 임계 이진화. 해치 =HatchPolicy.SHOW_OUTLINE. 예 =C:\dev\_work\hanguk-glass-plan84\render.py. - 🔴2026-10-08 [페페] 한샘 전환 C-3(길이 합산 항목의 분리 치수 문자열
1115+3100-600→ J{b+4}) = 기본 안 함 — 지시 있을 때만. 기본은 J{b+4} 빈칸 · 길이 칸(C{b+2}=J{b+2}) = 합산 숫자 그대로(「이전에 얘기한 항목별 치수 분리 빈칸에 넣기는 안하는 것을 기본지침」). 정본 = devplanhansem-convert-rules.htmlC-3. 첫 적용 = 한국유리부지 MH #800. 🔑[코디] 같은 건 실측 — 수납(일반가구)만 있는 견적은 form 11(내역서-수 3탭 정상)을 깎아 «수납 전용» 양식으로(주방 갑지·내역서-주 삭제 → 엔진build()대신build_ledger를 탭마다), 내역서-수 원본 인쇄 배율 70 은 7블록+총합(902pt)이 A4 가로를 넘쳐 쪽이 갈린다 → 62, sub 규격 문자열(440 × 440 × 352)은 가운데 정렬이 양옆을 잘라 「40 × 440 × 35」로 보인다 → «셀에 맞춤» 서식. 실례 =C:\dev\_work\hankukglass-hansem-1008\{make_form_hk,hansem_hk,verify_hk}.py. [페페·코디] - 🔴2026-10-08 [코디] 한샘 엔진
build_ledger(K_layout) 경로는 본블록 단가 LOOKUP 을 «기증 블록 없이» 보장한다 — 양식에서 LOOKUP 이 없는 단가군 블록(form 14 의 25·39·46 등, 원본 현장이 안 쓴 블록)을 src 로 복제하면 그 블록 단가가 조용히 0 → 단가군 원형(KIT_BASE = 블록 4 C9, KIT_TALL = 블록 32 C37 —$B$59:$B$107원본 오타 그대로)에서 행만 옮겨 «× 이 블록 배수»로 늘 새로 건다. 또 ①블록 7개 넘는 내역서 탭은 자동 쪽나눔이-추가블록을 반으로 가른다 → 위치행 블록 앞rowBreaks(brk id= 새 쪽 첫 행 − 1, pageSetup 뒤·drawing 앞) ②합산 치수 칸 J{b+4} 는 옛 적용률 메모 서식(빨간 글씨) → 왼쪽 등급칸 서식으로. 실례 =C:\dev\_work\gs-ansan-seongpo-1008\hansem_v2.py(fill 두 함수만 프로세스 안에서 교체 · 엔진 파일 수정 0). [코디] - 🔴2026-10-08 [페페] 한샘 전환 — 추가금액(sub)은 위치행 블록 바로 밑
<블록명>-추가블록에, 치수는 적힌 대로(없으면 빈칸·1000 금지), 금액만 표기 · 기본·추가 블록 모두 왼쪽(C~H)·오른쪽(J~O) 이름·치수 동일. 26.9.28 「하위=같은 블록 세트 칸·길이1000」 폐기. 정본 = devplanhansem-convert-rules.html. 엔진 반영은 다음 변환 때(🔜). [페페] - 🔑2026-10-08 [페페] 추가금액(sub) 「자당포함」(
estimate_detail_sub.in_base_yn, patch59) = 한샘 양식 변환 흡수 규칙 —in_base_yn='1'인 sub 는 한샘 양식에서<블록명>-추가블록·별도 칸으로 빼지 않고 부모 행 본항목에 남긴다. 적용률 역산 목표 =행총액(margin_total_amount) − Σ(미체크 sub.amount)(26.9.7target = 행총액 − Σsub의 Σ 를 미체크만으로 좁힌 것 — 체크 sub 금액은 목표 안에 그대로 있다). 한 행의 sub 가 전부 체크면-추가블록을 아예 만들지 않는다 · 한샘 양식엔 흔적(비고·「포함」 표기) 안 남김. ⚠예외 = 본항목 순길이 0 인 부모(옵션 고정금액 행 등 — 적용률 분모 0)에 체크 sub 가 붙으면 0나누기 → 강제로 별도 표기(-추가블록) + 경고 출력. PFS 금액·견적서 출력은 불변(표시 플래그만) · 이미 낸 한샘 견적은 재발행 안 함. 읽기 =/api/estimate/sub/list/<detail_no>의in_base_yn(문자열 '0'/'1' — 판정은== '1', truthy 금지). 🔜변환 코드는 아직 안 고쳤다 — 다음 한샘 변환(C:\dev\_work\hansem-0926\파이프라인 재사용) 때 이 규칙을 넣는다. 화면 = 견적 v2 sub 그리드 「자당포함」 칩 · 별도항목 집계 행 3상태 일괄. [페페] - 🔴2026-10-08 [코디] 견적 저장
_save_detail_subs는 이제 sub_no 를 «재사용»한다 + 자당포함 키가 없으면 기존 값 유지 — 전엔 delete-reinsert 가 저장마다 sub_no 를 새로 매겨, 저장 뒤 새로고침 안 한 탭의 두 번째 저장에서 번호 매칭이 끊겼다. 지금 = 그 detail 의 기존 sub_no 면 같은 번호로 INSERT(남의 detail 번호·중복은 새 번호) ·in_base_yn키 누락/null/빈값 = 같은 sub_no 기존값 유지(신규 '0') · 명시 '0'/'1' 은 그대로 · 기존값SELECT … FOR UPDATE→ DELETE → INSERT 한 트랜잭션(C:\dev\pfs\api\pfs\estimate\dao\estimate_dao.py). ⛒subList를 아예 안 보내는 PUT 은 여전히 sub 전부 삭제(26.9.28 함정 그대로). 🔴sub 컬럼을 더 늘릴 땐 INSERT(_save_detail_subs)·SELECT(estimate_detail_sub_dao.select_estimate_detail_sub_list) 둘 다 — 한쪽만이면 저장마다 기본값으로 리셋. 복사(copy_estimate_contents)는 SHOW COLUMNS 동적이라 자동. 검사 =C:\dev\pfs\scripts\_est_sub_inbase_check.py(test 49항목 · prod 읽기). [코디] - 🔑2026-10-08 [코디] 한샘 가구도면(일반가구·시스템)의 견적 범위도 표지가 말한다 — 「구분 : FUR. / SYS. 건립 : 84㎡, 103㎡, 111㎡」 = FUR.(일반가구 시트 열) + SYS.(일광 시스템 시트 열) × 적힌 타입 전부(9.7 KIT 규칙의 일반가구판 · 한국유리부지 #800 실측). 시트 제목 「…디자인2안」 은 대안이라 제외, INT 도어·석재 상판(현장분)은 범위 밖. 시스템은 일광 자재(AL기둥·철물·가죽서랍·조명)가 섞이니 포인 몫(판넬·선반)만 직접단가 줄로. [코디]
- 🔑2026-10-08 [코디] 도면→견적 치수는 손으로 옮기지 말고 DIMENSION 정의점 «핸들 쌍»(hl,hr)으로 잰다 — 폭 = max(x of hr) − min(x of hl), .5 는 올림 → 한 파일(
plan.py)이 견적 치수와 도면 사본의 코디 치수(같은 x 로 DIMENSION 재생성 · 글자값 [줄번호])를 같이 만들어 원단위 자동 일치. 기준 = 몸통·선반 실폭(벽 간격 10·50·100·158 제외, 매지 5 는 포함) — 반박검증이 이 기준 흔들림 4줄을 잡았다. 실례 =C:\dev\_work\hanguk-glass-1008\{plan.py,annotate.py,verify_dwg.py}. [코디] - 🔴2026-10-08 [코디] DWG 사본에 글·표를 «안 겹치게» 놓으려면 ezdxf 경계상자를 3가지로 고쳐 써야 한다(성포 견적 표기 실측) — ①큰 INSERT(시트 프레임·입면·평면 블록)는 통째 상자 대신
virtual_entities()로 풀어 안쪽 도형 상자를 쓴다 ②단 XCLIP 된 블록(단위세대 평면 등)은ezdxf.xclip.XClip(e).get_wcs_clipping_path()상자로 잘라야 한다 — 안 자르면 안 보이는 선이 «장애물»로 잡힌다 ③TTF 글자높이 = 대문자 높이라 실제 폭 = 한글 ≈1.35h · 숫자 ≈0.77h(1.0/0.6 으로 잡으면 25% 넘쳐 후드·장 위에 앉는다). 🔴PNG 검수 렌더도 삽입점이 아니라 경계상자로 거른다 — 삽입점 필터는 표제란·프레임·입면 블록을 통째 빠뜨려 «빈 곳»처럼 보이게 한다 + 색 7(흰/검)은ColorPolicy.COLOR_SWAP_BW없으면 흰 바탕에서 안 보인다. 실례 =C:\dev\_work\seongpo-annot-1008\{_occupancy.py,annotate.py,_render.py}. [코디] - 🔑2026-10-08 [코디] GS 안산 성포 포인 769 의 기존 4타입 줄은 9/4 「견적의뢰용」 도면 기준이다 — 9/30 자재준비용 이후 «건립» 도면과 구성이 다르다(99A·104 아일랜드 후면 유리장 없음 · 84A 홈바/장식장 입면 없음 · 10/2 하부 −200 · 10/6 OT84A 상부 수정). 성포 견적을 다시 만질 땐 최신 제작도면과 줄 단위로 대조부터(검토 표 = handoff 26.10.8 절). [코디]
- ⚠2026-10-07 [코디] ruleguard
web-designer-required는 «메인 transcript» 만 본다 — worker 서브에이전트가 web-designer 를 직접 불러도 UI 편집이 막힌다(서브 transcript 미인정). 우회 금지(Bash 쓰기 X) → 총괄 세션이 착수 전에 web-designer 를 먼저 부르고, 그 처방을 worker 브리프에 넣는다. 훅 보완(서브 인정)은 페페 결정 대기. - 🔑2026-10-12 [페페] 원부자재발주서 입고처(이름·주소)는 양식 하단에 두지 말고 마지막 품목 바로 아래 행으로 옮긴다(하단 원래 자리는 빈 품목행 · 24행 캡노트 금지 · 동기화 파서 재검사). 🔴[코디] 함정 2:
orderpaper_build._blank_item_row는 병합 풀기 전에 서식을 복사해 테두리가 사라진다 → 병합 정리 뒤_copy_row_style재호출 · L열 메모 쓴 발주서 탭을 복제하면 L폭 120 이 따라와 인쇄 40% 축소 → 칸·열 정의 삭제(선례C:\dev\_work\gs-daejeon-byeondong\order_door_1012\build_door.py). [코디] - 🔴2026-10-12 [코디] 현장명에
*가 든 현장(2175 「GS 대전 도마*변동」)의 원부자재발주서를 NAS 월 폴더에 두면 동기화 현장매칭이 다른 현장(2130 「대전」 85점)에 붙는다 — 파일명엔*를 못 쓴다. 월 폴더에 넣기 전에 운영 현장명 목록으로 매칭을 모의해 본다(C:\dev\_work\gs-daejeon-byeondong\order_1012\_q_fields_LIVE.py) · 안 맞으면통합발주(동기화제외)\하위폴더 + 수동 연결. [코디] - 🔴2026-10-07 [코디]
web/static/아래 런타임 파일은 로그인 없이 열린다 — 생산 메모/static/uploads/memo/field_memo.json운영 비로그인 200(전문 노출, 미수정·페페 결정 대기). 새 런타임 저장본은 static 밖web/resource/<이름>/+ .gitignore(배포git clean -fd는 무시 파일을 안 지운다) — ⛒deploy-live .gitignore 는 master 와 다르니 파일째 포워드 말고 «그 줄만» 직접 넣는다(안 넣으면 배포마다 저장본이 지워진다). 선례 = 미입고 보드 비고field_oob_dao. / field/list 칸은blockChromeAutofill이 readonly 로 두고 focus 때 푼다 → Playwright 는fill전에click. - 🔑2026-10-07 [코디] PFS-MAT#n 표식 현장별 파일 ↔ 자재관리 차이 = 동기화가 «경고만» 낸다(적재 0 · pfs
82660853) — 로그C:\dev\pfs\orderpaper-sync\_autosync.log「[!] PFS 자재 발주서 표식 파일 ↔ 자재관리 차이」 + prod 텔레그램(새 차이만, 현재 목록_matcheck_seen.json). 대조 = 업체·규격·품명(파일 품명 = DB 품명+공백+꼬리 허용) 짝 + 수량(qty_user 우선) · 자재관리에서 지운 행과 짝 = 의도된 삭제(경고 없음) · 같은 탭 쪽 복사 블록 1회 · 입고일·비고는 안 본다 · D1 목록형(자재발주서_<n>_…)·다현장 통합본(현장명 '+')·AL/SUB 는 대상 밖. 원격 회수 =MATROWS_GZ한 줄(로그엔 안 찍음, SSM 24,000자 잘림이면 「대조 못 함」 한 줄). 확정 수량 정정은 자재관리PATCH /api/orderpaper/<no>/qty(qty_user)+/stock(소비이력 0일 때) · 지움 =/delete(del_manual). orderpaper-sync 는 서버가 import 못 해 deploy-live 대상 아님. [코디] - 🔴2026-10-07 [코디] 「현장별 원부자재발주서에 넣은 항목이 자재관리에 안 올라온다」 = 행 캡보다 먼저 문서속성 keywords 를 본다 —
PFS-MAT#n파일은 사람이 탭·행을 더해도 파서가 «파일째» 건너뛴다(설계). 추가분은 정본 #n 에POST /api/mat-orderpaper/<n>/amendadd(수동 행 · 현장 1개 · material_type R/T/E/S 만 — 유리도 S) + 품목 입고일PATCH /api/orderpaper/<no>/receive-date. ⛒키워드를 지워 NAS 로 적재시키면 PFS-MAT 행과 이중. 수량 변경은 확정 잠금이라 amend 불가. amend 의 sync_multi 는 자재관리 수동삭제(del_manual)·상태·*_user 를 안 건드린다(26.10.7 실측 69행 칸 변경 0). 선례·스크립트C:\dev\_work\mat-add-1007\amend_add_1007.py. [코디] - 🔴2026-10-07 [코디]
C:\dev\pfs는 여러 세션이 «인덱스 하나»를 같이 쓴다 —git add해 둔 채 다른 일을 하면 남의git commit에 내 파일이 섞여 push 된다(생산현황 출력 변경이37a563beworklog-panel 커밋에 섞였다가 그 세션이319beafb로 되돌림). ⛒커밋은git commit -m … -- <파일>(pathspec) 한 명령으로 하고 add 와 commit 사이를 띄우지 마라. 커밋 전git diff --cached --name-only에 남의 파일이 있으면 건드리지 말고 pathspec 커밋만. 섞였으면 운영은 «파일 단위 선별 포워드»로 내 파일만 보낸다. - 💰2026-10-07 「행총액(천원 올림)×배율 → 다시 천원 올림」 금액을 PFS 차수에 1원까지 맞추려면 전체 마진 1값으로는 일부 행이 1,000원 낮게 나온다 → 그 행만 행 마진율을 0.01 단위 최소 상향. 차수 생성 =
POST /api/estimate/<번호>/rewrite→ 새 차수에만 PUT(subList 채움) →PUT /api/estimate/<번호>/select. 원 견적은 PUT 금지(보관 플래그만 바뀜). 선례·스크립트C:\dev\_work\kr-jinju-panmun\pfs_rev.py· 세션session-2026-10-07-kr-jinju-panmun-versions.md. - 🔴2026-10-06 [코디] 업무패널 「완료하면 안 뜨던 통화가 뜬다」 = 미종결 백로그를 LIMIT 창으로 잘라 보여준 탓이다 — 큐가 미종결 통화 전체(3,014건)인데 통화 몫이
limit 60 − 수기= 42건이라, 카드를 털 때마다 창 밖 옛 통화가 한 장씩 밀려 들어왔다(새 통화는 regist_date 최신이라 늘 보였다 — 화면 JS 문제 아님). ⛒큐에 «숨은 나머지»를 남기는 LIMIT 을 걸지 마라 — 창은 조건(컷오프)으로 좁히고 그 안은 전량. 해법 =worklog_dao._PANEL_CALL_SINCE+_new_call_clause()(적재시각 ≥ 컷오프 AND 통화일(파일명) ≥ 컷오프 날짜 — 옛 녹음 백필은 regist_date 가 새것이라 둘째 겹이 필요, SQL 에서 걸러야 슬롯을 안 먹는다). 🔑컷오프를 «통화시각»만으로 잡으면 파이프라인 지연 적재분(26.10.6 실측 오후 통화 16건이 20:22 에 적재)을 잃는다. 원문 = archive.md 「✅ 26.10.6 · pfs-worklog-panel-newcall」. - 🔴2026-10-06 [페페] 오픈장은 항상 한 세트로 움직인다 — 측판만 마감재(도어)로 옮기고 천·지판·등판·뒤판각목을 몸통에 남기지 않는다. 오픈장 등판 칼라가 사양에 없으면 그 위치 오픈장(또는 마감재) 칼라를 따른다. 등판 두께는 기본 = 측판 두께, 표기가 있으면 그 두께. 옮기기 전 도면 주석(재질·칼라·등판 표기)부터 확인한다(GS 대전변동 84B 복도장식장 선례).
- 🔴2026-10-06 [코디] 네스팅 💾 저장은 성공 뒤
registerNewColors()가POST /api/poin/colors로 마스터에 없는 칼라를 «자동 INSERT» 한다 — 자동화(Playwright 등) 저장은 이 요청을abort/route 로 차단하고 «미등록 칼라 N개»를 보고한다(칼라 등록은 페페 승인 사안 · 저장본엔 칼라 문자열만 들어간다). 26.10.6 GS 대전 변동 도어 #22 = 차단해 마스터 858 그대로, 미등록 PBNB6T·70-113·유사PP(PBNB6T). 근거 =C:\dev\_work\gs-daejeon-byeondong\door_result.md§3. - 🔴2026-10-06 [코디] NAS 업로드 경로를 Bash 큰따옴표 안에서
"…\10월\\$f"로 조립하면\\$가\$로 풀려 변수 대신 «글자$f»가 붙는다 →원부자재발주서\10월$f라는 «파일»이 생겼다(nasget put 은 부모 폴더에 그대로 쓴다). ⛒NAS 경로는 작은따옴표 리터럴로 1건씩 쓰거나 파이썬에서 조립. 수습 = md5 대조 후 제자리로 rename(삭제 0,C:\dev\_work\mat-integrate-1006\nas_fix_stray.py). 동기화는 월 «폴더»만 돌아 그 파일은 안 읽혔다. [코디] - 🔑2026-10-06 [코디] D1 #n → NAS 표준양식(통합·현장별) 생성 =
C:\dev\_work\mat-integrate-1006\build_p2.py(nesting_draft.build 재사용) — 현장별로 나눠 넣은 수동 행은 엑셀에선 같은 업체·품명·규격·단위·입고처·비고끼리 «한 품목»으로 합친다(통합=합, 현장별=그 현장 몫) · 업체 탭 입고 날짜는fill_vendor를 감싸 업체별로(긴급 = 날짜 서식" (긴급)") · 피앤알 탭 시트 안내 병합행(현장별 파일은 「이 현장 nM (통합 N = N/2 *2롤 · 업체 발송)」) · 페페 최종본의 비고 세로 병합(H15:J17 「남는시트 사용」 = 피스마개 3줄 공통 → DB 3줄 모두 비고) · 점검verify_p2.py(통합↔페페 최종 2개 + 현장별↔#1 배분). [코디] - 🔑2026-10-06 [코디] NAS 파서가 이제
PFS-MAT#n도 건너뛴다(pfs85f1ea17) — 아래 「파서가 아직 모른다」(D1 이중적재·새 KIND 표식) 두 줄의 전제 해소. D1 엑셀(자재발주서_<n>_…·#n 현장별 출력물)을 월 폴더에 둬도 적재 0 — 단 표식이 «정확히»PFS-MAT#n일 때만(꼬리;draft면 다시 적재, test_pfs_link 대조군). 동기화 = 30분 예약작업\orderpaper_autosync(:08·:38)가 매번 새 프로세스 → 파서 수정은 재기동 없이 다음 라운드부터(실측 13:38 라운드 removed 93). 사이드카(8791)는 날짜 쓰기에만 파서를 메모리에 올린다(건너뜀 무관). 드라이런 = 월 폴더 xlsx 를 smbclient 로 받아 옛/새 정규식 parse_file 비교(C:\dev\_work\mat-integrate-1006\dryrun_parse.py). [코디] - 🔴2026-10-06 [코디] 자재 발주서 수동 행은 현장 1개 — 다현장 품목(몸통 합산 보드·케데코 등)은 정정
add에 «현장별로 나눠» 넣는다(배분 = 페페 결정 · GS 원주/성포 = 10.2 개별본 비율). amend 는 품명 칸을 못 고친다 → 엑셀 품명 꼬리(「H2041B-L1 246경면」)는 비고 앞머리로. 자재관리 qty 의 단위 표기는calc_adapter.QTY_UNITS(M·EA·매·세트·장, 「장」 26.10.6e89a0870) 안에 있을 때만 붙는다(그 밖 단위 = 숫자만). [코디] - ⚠**2026-10-06 [코디] Git Bash 에서
ssm_run.py <스크립트> /tmp/x.py처럼 원격 경로를/tmp/…로 주면 MSYS 가C:/Users/…/Temp/x.py로 바꿔 원격에서 「Directory nonexistent」로 죽는다 → 원격 이름은 맨이름(_q_x.py, 러너가 /tmp 를 붙인다). SSM 출력은 24,000자에서 잘린다 → 큰 SELECT 백업은 gzip+b64 를 EC2 /tmp 에 저장하고 조각(PART)으로 회수(C:\dev\_work\mat-integrate-1006\q_backup_select.py). EC2 python 은 3.6(capture_output없음). [코디] - ⚠2026-10-06 [코디] QA 보고 셀렉터
tbody#moGridBody > tr.mo-row:nth-of-type(N)의 N 은 «N번째 줄»이 아니다 —nth-of-type은 클래스 무관tr순번이고 자재 발주서 표는 줄마다tr.mo-row+tr.mo-alloc(배분, 숨김) 2개라 N번째 tr = seq 순 (N+1)/2 번째 줄(삭제 행·필터로 숨긴 행도 DOM 에 남아 센다). 「업체별 보기」를 켜면tr.mo-group머리행이 끼어 위치가 달라진다(그때 N 이 짝수 자리면 배분 행이라 매치 0). 실례 #1 N=55 → 28번째 줄 IV075EMT 펫시트(브리프 후보 GK102-G8 아님). 판정 = 운영 GET 을 seq 정렬해2i−1==N+ 셀 value 대조(C:\dev\_work\mat-door-1006\joun_probe.py). [코디] - 🔴2026-10-06 [페페] 피앤알 발주서(탭) = 발주 항목 행을 다 쓴 뒤, 그 «아래»에 시트 입고 안내를 칼라별 1행씩 적는다 — 각 행은 번호 칸만 빼고 나머지 칸 전부 병합(예
B:G) · 왼쪽 정렬. 문장 =<칼라> <PVC/PET>시트 <시트 총수량÷2>M *2롤 (<시트 발송업체> 발송) — <M/D(요일)> 입고 예정(2분할 권취라 절반×2롤 · 발송업체 = 그 시트 행vendor_confirmed· 날짜 = 발주서receive_date). 실례 26.10.6 #1 =GK102-G8 PVC시트 65M *2롤 (길동상사 발송) — 10/8(목) 입고 예정외 P9GA4T·PQFA2T 25M*2롤(조운IMT). ⚠이 병합 주석행은 동기화가 항목으로 안 읽는다(L1268_is_merged_note_row· '시트' 포함 주석행 수집 제외)는 전제 — 병합을 규격칸까지 덮어야 한다. [코디] - 🔴2026-10-06 [코디] openpyxl 산출물 «방어 코드»는 산출물 XML 로 검사해야 한다 — 부자재 발주서는 firstSheet 리셋을
wb.views.sheetView[0] if getattr(...) else None으로 써서 없는 속성 → 조용히 no-op, 26.9.15~10.6 업체 1곳·개별 출력 엑셀이 전부 「복구 필요」였다(L181 AL발주서 함정 재발). ⛒wb.views는 BookView 목록 →for view in wb.views: view.firstSheet = 0. 자체검사엔xl/workbook.xml의firstSheet·activeTab< 시트 수 assert(sub_orderpaper_excel._demo). 판정 도구 = Excel COM 은 Session 0 에서Open이 «피호출자 거부»로 흔들린다 →schtasks /IT+pythonw 로 열고DisplayAlerts=False·CorruptLoad=0에서Open실패 = 복구 대상. [코디] - 🔴2026-10-06 [코디] D1 자재 발주서 엑셀(
자재발주서_<n>_…xlsx, 목록형)을 NAS원부자재발주서\<월>\에 그대로 두면 동기화가 «발주서»로 읽어 자재관리에 이중 적재된다 — 실측 #1 = 11:02 저장 → 11:09 93행(가구업체 「자재발주서_1_GS」·현장 미연결·업체 칸 = 시트 이름). 파서(orderpaper_parse.py:359)가PFS-MAT#n을 아직 모른다. ⛒D1 확정분은 이미 DB 로 적재됐으니 NAS 엔 동기화 안 읽는 하위폴더(<월>\통합발주(동기화제외)\)에만 둔다. 업체용 표준양식 통합본 =orderpaper-sync\nesting_draft.build재사용(선례C:\dev\_work\mat-door-1006\build_nas.py). 확정 문서 글자 칸(제목·비고·규격·입고처)·업체·수동 행 추가 =POST /api/mat-orderpaper/<no>/amend(수량·여유·현장은 잠금 유지 · 업체는 26.10.6 12:16 추가 — 그 품목 자재관리 행이 전부 «대기»일 때만, 같은 item_key 갱신이라 새 행 0). [코디] - 🔴2026-10-06 [코디] 자재 발주서 품목 삭제 = 소프트(
mat_orderpaper_item.remove_yn) — 품목을 «세는» 새 경로를 만들면 반드시 삭제 행을 뺀다(서버_live()· 화면isDel/liveLines/liveRows· 엑셀_removed). 합계·검산·경고·업체 일괄·확정 적재·엑셀 중 하나라도 빠지면 «화면 합 ≠ 엑셀 합 ≠ 자재관리»가 된다. 확정 문서 삭제/복구는sync_multi재적용(같은 item_key = 같은 orderpaper 행)이고 입고·진행 행은 거절 · 화면 JS 는 저장(+다시 읽기) 진행 중이면 행 조작을 줄 세운다(S.saving— 안 하면 옛 rev 로 나가 4090). [코디] - 🔑2026-10-06 [코디] poincad 는 «고정치수 샘플»뿐이고 산식이 없다(
poin_sample_group116그룹·poin_sample_item233품목 = 사람이 칸을 고치는 템플릿) → 네스팅 「제품 계산기」 산식은 현장 실사용poin_item역산 최빈값이다(오픈장 천지판 W−37 · 깊이 D−20 · 뒤판 H−2 · 이동선반 Dt−5 / W−2T−3, ㄱ자 리턴 B−(T+2), 냉장고 ㄷ자 전면 H×A). 정본(SPEC-DRAWER 같은 imos 실측)이 없는 영역이라 근거 약한 값은 노브(입력칸)로 둔다. 상세 =projects/pfs/session-2026-10-06-nesting-product-calc.md. - ⚠2026-10-06 [코디] 서랍 계산기 천지판 W−2T 와 poincad 실사용 W−37 은 1mm 다르다(미해결) — T=18 이면 W−36 vs W−37. 제품 계산기 오픈장은 실사용(W−2T−1)을 따르고 서랍 계산기는 건드리지 않았다(Q10 = 보고만). 페페가 통일을 정하면 서랍 쪽
drawerCalc()몸통천지판 식을 고친다. - ⚠2026-10-06 [코디] droplist.js 는 문서 전체
input이벤트를 듣는다 → 일괄 값 채우기에서 행마다dispatchEvent('input')하면 팝업이 줄줄이 뜬다. 값만 넣고 화면 갱신 함수(mat_orderpaper 의setDirty·refreshCalc·applyView)를 직접 호출. / 테스트 헬퍼 스크립트는if __name__=='__main__'가드 필수(import 만으로 테스트DB 쓰기 사고). - ⚠2026-10-06 [코디] 새 발주서 종류 표식은
PFS-<KIND>#n정확히만 — NAS 파서orderpaper_parse.py정규식이$로 끝나;draft;rev꼬리를 붙이면 건너뛰기 실패→이중적재. 새 KIND 는 파서 정규식 확장이 배포보다 먼저. 부가정보는 문서 설명 칸. /pfs_link.sync()는 현장 1개 전제(_drop_missingsource_file 단위) → 다현장은sync_multi만. /C:\dev\pfs\.venv(Werkzeug 0.15.2)는 py3.9 로컬 부팅 불가→0.16.1 격리 venv. / Playwright 는 클릭 직전 자동 스크롤이 방금 연 droplist 를 닫는다→먼저 스크롤 후 클릭. - 📏[페페] 26.10.5 네스팅 부재 가로·세로 기본 지침(사람 판단용 · 코드 자동적용 금지) — 결방향은 세로 기준. 원장 폭(1220)을 넘는 부재는 가로·세로 교환. 서랍앞판은 가로 ≥450 이면 가로·세로 교환. 적용은 페페가 부재표 행 [↔] 버튼으로 직접 한다(「필요항목은 내가 직접」). [코디]
- ⚠2026-10-05 [코디] sticky
th에 z-index 가 없으면 position 있는 tbody 자식(.cellwraprelative)이 헤더 위로 그려진다(네스팅 부재표 「헤더가 떠 있고 틈으로 데이터가 보인다」) →th{z-index:3}. 행 색 덮개(확인 노랑 등)는 선택 파랑·경고 분홍보다 앞 순서·같은 특이도로 둬 그들이 우선하게. 운영6c28441e. - 🔴[페페] 26.10.4 도어 두께 기본 18T · 15T 없음 — 도면 사양표 CORE 15T 표기여도 18T 로 계산(도어·서랍앞판·FIX 포함 마감재 전부. 몸통재 PB·도면이 18T 아닌 두께를 명기한 9T 쫄대·12T 벽마감은 별도). 첫 적용 = GS 안산 성포 마감재 네스팅 124매 15T→18T · 엔진 46→41장 · 운영 #16. [코디]
- ⚠2026-10-04 [코디] flask_restplus Resource 에서
return jsonify(...), 409처럼 상태코드를 튜플로 돌려주면 500 이 난다 → 응답 객체에 status 를 직접 넣는다(nesting_controller._json_status()). 같은 패턴 미수정 잔존 =api/pfs/estimate/controller/estimate_print_controller.pyL160jsonify(...), 503. - 🔑2026-10-03 [코디] Jev 상시 판정의 뼈대 = «서버가 질문을 만들고 미니PC 워커는 전달만» — Jev 키는 미니PC 한 곳(PFS 사본 금지)이라 서버가 못 부른다 → 질문 문구·선택지·입력 해시·판(prompt_version)·임계는 서버
api/pfs/worklog/service/worklog_ai_service.py한 곳, 워커C:\dev\tools\worklog-ai\worker.py는 todo→jev.ask→result 만(기존_AGENT_PATHS토큰 · 핸들러도 토큰 재확인). 원장worklog_aiUNIQUE(worklog_no, kind, input_hash, model) + 조건부 UPDATE 영향 행수 + 사람 조작 override. 🔴jev.choice실패는 전부 None 이라 «키·크레딧·429(전체 멈춤)»와 «그 질문만 400»을 못 가른다 →jev.LAST_ERROR["code"]로 400/422 만 그 건 error, 나머지는 회차 중단. 측정 = 사람 정답이 «현장을 골라 넣은 카드»면site_name= 현장 마스터명(누설)이라 빼고 재야 한다(수기 33건 전부 해당). - 🔴2026-10-03 [코디] 현장 후보 문자열 점수는 띄어쓰기가 다르면 정답을 후보에서 놓친다 — 할 일 「나진상가 확정샘플…」 vs 현장 「용산 나진 상가」: 포함일치(정규화 «전체» 이름)도 토큰(
_hay는 띄어쓴 원문)도 안 걸려 후보 0. ⛒토큰을 정규화 현장명(_norm)에도 찾는다(3자 이상) → 정답 후보 10→12/33. 반대로 2자 이하 현장명(「집」·「LH」)의 포함일치는 긴 할 일 문장마다 80점이라 뺀다. 그리고 p 0.3~0.5 짜리 1순위는 같은 입력에서도 회차마다 바뀐다(3759 「학동매장」 r1 오답 0.33 → r2 정답 0.38) — 임계표는 최소 3회 반복으로 낸다(C:\dev\_work\jev\worklog-ai16\). - 🔴2026-10-02 [코디] 몸통 발주서 출처 = 네스팅 저장본이 «작업본보다 오래됐는지» 먼저 본다 — GS 성포 #12 는 00:59 저장, PNCAD 작업본 33~36 은 01:03 재저장(선반 18T 전환·등판 색 변경)이라 #12 로 발주하면 18T 경면 1장 부족·등판 색 오류. 판정 = job 저장시각 vs 작업본 mtime. ⛒
momtong_order.write_tab은 8~25행 순차 기록이라 품목 17건 초과 시 24행 캡노트에 써져 조용히 소실 ·orderpaper_build._receiver_row는 병합 해제 전 서식 복사라 중간 입고처 행 서식 소실(실례 우회C:\dev\_work\gs-momtong-order\build_orders.py). [코디] - 🔴2026-10-02 [코디] 한샘 엑셀 엔진 함정 2개(GS 대전 2차 수입주방 = 한샘 10) — ①
hansem_excel.rename_slots는 workbook·수식의 탭 이름만 바꾸고docProps/app.xml시트 목록은 안 고친다 → 평형명≠양식 슬롯명이면(59A←77) 출력 파일 메타데이터에 양식 슬롯명이 남는다(1차는 84A→84A 라 안 드러남) → 견적 전용 양식을 깎을 때 탭을 견적 평형명으로 미리rename_sheet+sync_app_xml(실례C:\dev\_work\gs-daejeon-doma-1002\make_form_suip.py). ②0원 줄(무상)을apply_rate=0으로 넣으면 서버 총액은 0인데 엑셀엔 금액이 생긴다 —hansem_service.build_excel의float(d.get('apply_rate') or 1)이 0 을 1 로 바꾼다 → 0원 줄은 내역서에 안 싣거나length_mm=0(양쪽 0)으로. [코디] - 🔑2026-10-02 [코디] 운영 칼라 마스터 읽기 =
GET https://pfs.poin.co.kr/api/poin/colors+ 헤더X-PFS-Agent-Token(값 =C:\dev\.secrets\pfs_agent.env의PFS_AGENT_TOKEN) → HTTP 200 · 855종(26.10.2) — 로그인 세션 없이 «저장하면 자동 등록될 미등록 칼라»를 미리 뽑는 길. 비교 규칙은 nesting.htmlregisterNewColors()그대로(trim·소문자·32자 초과 제외). 예시 스크립트C:\dev\_work\gs-finish-nesting\common\_colors_get.py(GET 만). - 🔴2026-10-02 [코디] PFS 네스팅 엔진(nesting.html
packSheet)은 긴 판(2207~2390) 옆에 2440 띠 막대를 못 넣어 원장을 독립 길로틴보다 4~8장 더 잡는다(GS 원주 31 vs 26 · 성포 46 vs 38 · 우드결 rot 1 칼라가 가장 심함). 분할 규칙이 «부재 오른쪽 조각 높이 = 부재 높이» 하나뿐이라서다. 발주 장수 보고엔 엔진값 + 참고 길로틴을 같이 적고, 줄이려면 수배치 좌표(indep_b_full.json)나 엔진 개선(별도 작업). - ⚠2026-10-02 [코디] Workflow 작업자(서브에이전트)는 Agent 도구가 없어 devplan 화면 파일을 못 만든다 — ruleguard
web-designer-required는 transcript 의 Agent(subagent_type=web-designer) 호출만 인정하고, 헤드리스claude -p --agent web-designer상담은 안 센다. 또 하네스가 작업자의REPORT.md같은 «보고 .md» Write 를 막는다. → devplan 페이지·보고 파일이 산출물인 일은 총괄 세션이 직접(또는 Agent 상담 후) 하거나 작업자에겐 초안 경로(…\devplan_draft\)만 맡긴다. - 🔴2026-10-02 네스팅 부재 표는 「출처(jobCtx.source = manual|pncad|imos)」가 다른 부재를 한 표에 섞지 않는다 — 도어(수동) 표에 PNCAD·몸통재 가져오기 = 추가·교체 없이 새 탭(
?scenes=·?imos=)으로만. [페페] 「도어 부재에 PNCAD 작업본이 섞이면 안 된다」. 26.9.29 처방 「입력 있으면 추가(기본)/교체/취소」는 소실은 막았지만 기본값 「추가」가 곧 섞기였다 — 「지우지 마라」 요구를 「합쳐라」로 풀면 다음 사고가 된다. ⛒규칙 자리 =nesting.html askImportMode한 곳(같은 출처만 mix) · 표를 통째로 바꾸는 곳(clearParts·loadSample=manual,runPncad·runMomtongreplace=그 출처)은 source 를 같이 바꾼다(안 바꾸면 PNCAD 저장본을 비우고 친 도어가 pncad 출처로 저장된다) ·window.open은 모달 클릭 핸들러 안에서 «동기로»(await 뒤 = 팝업 차단 위험) · 부팅 IIFE 아래에 선언된let/const(allOrders·selOrders)를 IIFE 가 동기로 만지면 TDZ ReferenceError →setTimeout 0. ⚠혼합 저장본 판정은 행 단위 출처가 저장되지 않아 이름·컬러로만 가능(#8 칠성 = 도어 2+PNCAD 11, 9.29 병합 복구분). 검사기 =scripts/_nesting_import_keep_check.py(local·--test·--prod). [코디] - 🔑[페페] 26.10.2(금) 「마감재는 기본 18t 제작이다」 — EP·휠라·찬넬·걸레받이·쫄대 등 마감재는 도면에 코어 두께가 따로 없으면(예: 「EP, SR = 도어 동일 마감」은 표면만 같다는 뜻) 18T MDF 로 산출한다. 도면 사양표가 도어 CORE 를 명시한 경우(GS 안산 성포 도어 「15T MDF (E0급)」)는 그 값이 우선하되, 18T 반대안을 같이 계산해 보고한다. 출처 = GS 원주 단구·안산 성포 마감재 네스팅 그릴링 Q2 답(작업폴더
C:\dev\_work\gs-finish-nesting\). - 🔴2026-10-01 PFS 앱 DB 커넥션은
charset="utf8"(3바이트)라 4바이트 글자(이모지)를 INSERT/UPDATE 하면 1366 으로 죽는다 — 테이블이 utf8mb4 여도 같다(common/database/mysql_database.pyget_cursor). LLM 산출물·외부 텍스트를 저장하는 API 는 저장 전에 BMP 밖 글자를 걷어낸다(실례 =api/pfs/worklog/service/worklog_service.py_bmp, 통화 정리본). 커넥션 charset 을 바꾸는 건 전 도메인 영향이라 별건. [코디] - 🔑[페페] 26.10.1 한샘 양식 전용판을 만들 때 수납 갑지도 주방처럼 자재비·운반비·절사 행을 빈칸이라도 남긴다(기본양식 항목, 지우지 마라) — 수납 갑지 = 19 본오더 · 20 절사 · 21 본오더 소계 · 22~24 기타(자재비·운반비·절사) · 25 소계 · 26 합계 · 27 발행처로 주방과 같은 행 구성. 원인 = 정본(라인건설 06월) 수납 갑지 33~36행(기타 3행+소계)이 숨김행이라
make_form7.py가 「숨김 = 안 쓰는 행」으로 보고 추가분 행(26~32)과 같이 지웠다 → 숨김행도 기본양식 항목이면 펼쳐서 남긴다. 실례 = form 8C:\dev\_work\gs-daejeon-doma-1001\make_form8.py(대칭 검사sym_check.py). ⚠페페가 NAS 에서 손수 넣은 판(「(국산)…」)은 기타 값 = 0(주방 모양) · 합계=O21· 20행 절사 = L열(정본 모양)이다 — 값 0/빈칸은 페페 확인 대기. [페페·코디] - 🔴2026-10-01 운영 한샘 양식 form 3·4 출력엔 주방(+숨김 수납) 갑지 S2 결재칸 「임서현」 도장(image1)과 내역서 탭 GS 안산 성포 모노크롬 도면이 그대로 실린다 — 한샘 견적 6·7·8 엑셀 전부(7 원주·8 롯데 = 타 현장 도면). 한샘 견적 모듈은 운반비·시공비 칸이 DB 에 없어(양식 갑지에 숫자) 견적마다 맞는 양식이 필요하다 → 견적 전용 비활성 양식을 깎아 올린다: 남는 평형 슬롯 탭·갑지 행 삭제 · 도장/도면 앵커 삭제 · 운반비·시공비·S6 날짜 박기 · 안 쓰는 공유문자열 비움(실례 = form 5,
C:\dev\_work\gs-daejeon-doma-1001\make_form5.py→ 한샘 견적 9). 🔴zip 수술로 시트를 지울 때 definedName 정규식은<definedName(\s[^>]*)>—<definedName([^>]*)>는<definedNames>래퍼를 물어 첫 이름이 재매핑 없이 남고(localSheetId 10 > 시트 수) LibreOffice 가 「source file could not be loaded」로 거부한다(Excel 도 손상 판정 가능). 지운 뒤max(localSheetId) < 시트 수assert. [코디] - 🔑2026-09-30 현장별 자재(material-spec) 웹은 빈 도어CORE 를 18T MDF 로 강제 표시·재채움 → 유리도어(CORE 없음)를 표현 못 한다(자동추출 시 결정적 id 로 18T MDF 코어를 미리 두고 「⚠도면에 없음」 메모) · 🔑
GET /api/poin/colors는 로그인 필요(무인증 401) — 무인증 조회면 전부 「미등록」으로 오판한다. 도면→자재 등록 실례 =C:\dev\_work\gs-ansan-seongpo\matspec_0930\_register.py(초안 /collect · --verify). [코디] - 🔑2026-09-30
blockChromeAutofill이 붙이는readonly는 테마.form-control[readonly](opacity .7·회색)를 불러와 입력칸이 전부 흐리게 뜬다 → 이 헬퍼를 새 페이지에 쓸 땐<페이지폼> input[data-noautofill]{background-color:#fff;opacity:1}를 같이 넣는다(/field/regist적용 ·/field/list는 미확인). 새 화면에서 자동완성이 뜨면 = 화면 ready 에서blockChromeAutofill(document)+shown.bs.modal한 쌍 + form/input/textarea/selectautocomplete=off. ⚠ruleguardweb-designer-required는 서브에이전트 세션에서 부른 web-designer 상담을 못 알아본다(오탐 · 총괄이 상담한 뒤 재시도하니 통과) → 워커에 위임할 땐 총괄이 web-designer 상담을 먼저 해 둔다. - 🔑2026-09-30 견적 «줄(탭) 일부를 다른 추가건으로 옮기는» 앱 기능은 없다 → 빈 추가건 마스터를
POST /api/estimate/(부모 번호·detail 0줄)로 만들고estimate_detail.estimate_no(+display_order) 만 UPDATE, 원 견적total_amount·margin_total_amount를 남은 줄 합(+시공·운송·부가비)으로 맞춘다 — detail_no·하위내역·값이 그대로 따라간다(PUT 금지 함정 회피). 전후 합계 1원 대조 · 월마감은 견적 실시간 연동이라 화면은 즉시 따라가고 저장 캐시만 재저장. 🔑추가건 «하나만» 엑셀 = 본건 출력GET /api/estimate/print/excel/<본건>?pages=종합,sub:<추가번호>— 파일명이 본건 이름(「에넥스 라인 VIP 내수 견적서.xlsx」)·종합 요약행이 그 추가건만·시트명 「추가n」으로 바로 나와 26.9.29 의 「본건→추가1」 zip 치환이 필요 없다(페페가 NAS 에 올린 본과 셀 차이 0 확인). 실례 = 라인 VIP 783→784C:\dev\order-automation\line_vip_2026-09-30\s2_move.py·s7_build.py. [코디] - ⚠2026-09-30 미니PC 에서 Excel COM 으로 견적서 xlsx 인쇄 = 새 Excel 인스턴스 첫 Open 때 「프린터 설정」 창이 떠서 멈춘다(기본 프린터가 Samsung WiFi IPP 라 Excel 이 활성 프린터를 못 정함). 처리 = 창에서
SINDOH D450/CM Series PCL선택 클릭 스크립트(선례C:\dev\order-automation\bongjeok_close_2026-09\_click.py·_print_phase.py, schtasks /IT+pythonw) · 인쇄 직전ActivePrinter가 PCL 인지 가드 · 수식 캐시 빈 PFS 출력본은CalculateFull후 인쇄(원본 저장 금지). 창이 페페 화면에 잠깐 보인다 — 다음엔ActivePrinter를 Open 전에 지정해 창 자체를 피하는 쪽을 먼저 시도. [코디] - 🔴2026-09-30 부가비 단독 저장(
POST /api/estimate/extra/bulk-save)은 견적total_amount·margin_total_amount를 안 고친다 — 총액은 화면recalcTotal(가구 margin_total 합 + 시공 + 물류 + 부가비 qty×price, 부가비엔 전체마진 안 걸림)이 PUT 때 같이 보낸다. 그래서 부가비만 넣으면 월마감(select_estimate_amounts= 견적 total_amount 재연동)·견적서 엑셀(_main_total= margin_total_amount 우선)이 안 따라간다. 월마감 확정금액(NEGO)을 견적에 반영할 땐 PUT 없이 부가비 INSERT + master 금액 2칸 UPDATE 를 한 트랜잭션 SQL(이전값 WHERE 가드·트랜잭션 안 재계산 1원 대조) — 선례C:\dev\order-automation\bongjeok_close_2026-09\s2_apply.py(봉적 751·756). 월마감 행은 안 건드려도 get_one 이 새 총액을 읽는다(캐시 total_amount 는 다음 화면 저장 때 갱신). ⚠LibreOffice 헤드리스 재계산은 새-env:UserInstallation첫 파일이 ~5분 걸린다(300초 타임아웃 넘김) — 백그라운드로 돌린다. [코디] - 🔴2026-09-30 도어발주서 큰제목을 COM 으로
Range(D:K).Merge()할 땐 D 값을 «먼저» 쓴다 — D 가 빈 채로 병합하면 Excel 이 범위 안 첫 값(G 열*)을 D 로 «옮겨» 그 행 P~T 열(공정상태·바코드)의$D참조가 전부#REF!가 된다.MergeCells=True·K 만 지우고 병합도 같다 · E:K 를 지우고 병합하거나 D 를 먼저 쓰면 멀쩡(6안 실측C:\dev\order-automation\hakdong_2026-09-30\work\_merge_exp.py). 파일은 정상으로 열려서 양식 대비 셀 전수 diff 로만 잡힌다. 또 🔴Session 1(/IT) Excel 의ExportAsFixedFormat은 「프린터 설정」 창을 페페 화면에 띄우고 멈춘다 → PDF 육안은 LibreOffice 헤드리스(soffice --headless --convert-to pdf, 전용-env:UserInstallation). vbaProject.bin 은 Excel 저장만으로 CRC 가 바뀐다(OLE 디렉터리 타임스탬프) — 보존 판정은 olefile 스트림 내용 대조로. [코디] - 🔑2026-09-29 월마감 「현장별 거래명세서」 = 마감 행 키가
close_no + seq다(item_no아님 — 저장마다 전량 교체). 화면은 서버 seq 를 행data-seq로 심고(화면 index 가 아니다 — 빈 현장명 행을 건너뛴 자리가 빈다) 저장 안 된 변경(mcDirty)이면 막는다. 금액 정본 =monthly_close_service.get_one()(화면=엑셀=명세서) · 공급자 =common/utils/poin_supplier.py1벌(견적서도 import — 사업자정보 바뀌면 여기만) · 불변식 「줄 공급가액 합 = 확정금액(없으면 합계)」 =scripts/_monthly_close_statement_check.py(읽기 전용, 운영 OK). 🔑field.shipping_date는'YYYYMMDD'문자열이다(운영 2109 ='20260821') —s[4]=='-'식 판정은 조용히 빈 값이 된다. 🔑scripts/_*는 .gitignore 라 자체검사 스크립트는git add -f(선례 동일). [코디] - 🔑2026-09-29 월마감 엑셀(
/api/monthly-close/<no>/excel) 새 출력은 수식 캐시가 빈<v />(+fullCalcOnLoad=1)다 — Excel 은 열 때 계산하지만 메일·폰 미리보기·openpyxldata_only는 합계가 빈칸이다. NAS 마감표를 새 출력으로 교체할 땐 ①그 전에 NAS 본의 손수정 비고(I열, 예 「추가1」·「본건, 추가1」)를 서버 행note로 먼저 맞추고(현장명 매칭, 금액·확정금액은 그대로) ②출력 뒤<v>를 계산값으로 채워(sheet1.xml 만 zip 치환) 올린다 — 교체 전 NAS↔새 출력 셀 대조(대상 행 외 차이 0)를 게이트로 둔다. 선례 = 라인 VIP 추가1 783(C:\dev\order-automation\line_vip_as_2026-09\s3_close.py·s5_nas.py·s7_close_cache.py). [코디] - 🔴2026-09-29 본건 차수를 바꾸면 추가건은 «사용 중 차수를 따라간다»([페페] 「변경한 차수건으로 추가건은 가져와야 한다」, 운영
5dee8ae6). 그 전엔switch_active_estimate가 옛 차수의 추가건까지 보관하고 부모도 옛 차수로 남겨 새 차수 밑에서 사라졌다(금호 구미 782). 지금은 보관/해제 전에 쓰던 본건(재작성이면 원본carry_from)의 추가건 부모를 새 차수로 옮긴다 — 이미 보관된 옛 차수(쓰던 게 아닌 것)의 추가건은 안 끌어온다. 🔑예전 코드로 이미 떨어져 나간 추가건 복구 = 그 옛 차수를 한 번 선택 → 다시 새 차수 선택(앱 경로 2회, 견적 PUT 불필요). 월마감은 추가건을 번호로 읽으니(select_estimate_amounts= 보관건도 읽음) 마감행은estimate_no만 새 차수로 바꾸고add_amounts번호는 그대로 둔다. 자체검사 =C:\dev\pfs\scripts\_est_revision_carry_subs_check.py(테스트 전용). 또 수작업이 들어간 NAS 견적서·마감표는 재출력 교체 금지 → 바뀐 칸 + 수식 캐시<v>만 zip XML 로 고친다(선례C:\dev\order-automation\enex_kumho_2026-09-29\s5_patch_estimate.py— 평가기가 원본 캐시를 전 칸 재현하는지 먼저 확인). ⚠Excel COM 검증은 Session 0·/IT둘 다 Open 에서 멈췄다(첫 인스턴스를 강제종료한 뒤라 숨은 대화상자 추정) — 이 작업은 평가기 재현 + PFS 총액 대조로 대신했다. [코디] - 🔴2026-09-28 견적 API
PUT /api/estimate/<no>를 GET 응답 그대로 되보내면 «하위내역(estimate_detail_sub)이 전부 지워진다» — GET 은subList를 안 주는데, 저장은 상세 줄마다_save_detail_subs(dno, item.get("subList"))로 지우고 다시 넣는다(C:\dev\pfs\api\pfs\estimate\dao\estimate_dao.py662·673행). 779 에서 69줄 11,525,000 이 날아갔다 → 같은 날 조회분으로 원래 sub_no 그대로 복구(C:\dev\_work\hansem-hwaseong-combined\restore_subs_779.py). 스크립트로 견적을 고칠 땐 PUT 전에 줄마다GET /api/estimate/sub?detail_no=로 subList 를 채워 보내거나, 하위내역 있는 견적은 금액 칸만 SQL 로 고친다. PUT 전후estimate_detail_sub건수·합을 반드시 대조. [코디] - 🔑2026-09-28 월마감 저장(
POST /api/monthly-close)은 행을 지웠다 다시 넣는다 — item_no 가 새로 붙는다. 전후 대조는 현장명·estimate_no 로. 본건main_amount=0이면 조회 재계산이 본건 견적을 아예 안 읽는다. [코디] - 🔴2026-09-28 LibreOffice
--convert-to xlsx는 기본 설정이면 수식을 «재계산하지 않는다» — 옛 캐시값을 그대로 다시 저장한다(향남 한샘 양식 AH36 이 89,150,000 그대로). 재계산 = 전용 프로필(-env:UserInstallation=file:///C:/temp/loprofile)의user\registrymodifications.xcu에/org.openoffice.Office.Calc/Formula/LoadOOXMLRecalcMode=0(항상) 을 넣은 뒤 변환(26.9.28 넣어둠). 🔑한샘 원본 양식 갑지 AG36(시공비)에 타 현장 숫자(12,930,000)가 하드코딩돼 있다 — 옆칸 「생산비+시공비」가 틀린다. 재사용 시 AG36 을 그 현장 시공비로. - 🔑**2026-09-28 LibreOffice(headless)로 xlsx 강제 재계산 =
--convert-to xlsx --outdir <dir> <file>(-env:UserInstallation=file:///C:/temp/loprofile로 프로필 지정,--headless --invisible로 창 없음).soffice --version단독 실행은 Session0 에서 수십 초간 멈춘다(GUI 초기화 대기로 보임) — 검증은 실제--convert-to잡으로 한다. 재계산 후 openpyxldata_only=True로 캐시값을 읽으면 실제 계산결과가 보인다(원래 수식만 있고<v>캐시가 없던 파일이 새 캐시를 얻는다). 부수효과 = 안 쓰는 sharedStrings·잔존 메타데이터까지 LO 가 정리해준다(우리 zip수술보다 깨끗). 설치 =winget install --id TheDocumentFoundation.LibreOffice -e --silent. 작업 후tasklist | grep soffice로 잔류 프로세스 확인(정상 종료면 0). - 🔴2026-09-28 zip수술로 xlsx 셀을 비울 때 "행 전체"를 지우면 그 행의 «수식 셀»(합계=L×M 등)까지 지워진다 — 입력 열만 비우고 수식 열은 건드리지 마라. 실측:
추가분견적(주방가구)안 쓰는 행을 A:N(수식 열 N 포함) 전부 clear() 했더니 소계가 전부 0으로 무너졌다. 수식 열(N=공급가액=L*M, 운반비 I==G*H)을 빼고 A:M(또는 A:H)만 비우면 그 행은 입력값 없음→수식이 자연히 0을 내고, 우리가 채운 행은 정상 계산된다. - 🔴🔴2026-09-28 한샘 등록양식(form3/4, "MH 주방전용")은 수납(S) division 시트가 통째로 비어 있다 — 주방(K) 항목만 있는 견적에만 쓸 수 있다. 780(추가1)은 K뿐이라 안 걸렸다. 779(본건, 수납 항목이 절반 가까이) 처럼 주방+수납 혼합 견적을 form3/4 로 만들면
84B 내역서-수등 시트가<sheetData/>(빈 스캐폴딩)라inject_ledger가 쓸 자리를 못 찾고 칸 0으로 조용히 드롭된다(예외 없음 — 검증 안 하면 절반 금액이 그냥 사라진다). 원본 3평형 정본(NAS한샘 견적 양식(가구,시공)\1.생산비 거래명세서_라인건설...xlsx, PFS 등록 form_no 2 와 동일 계열이나 NAS 실물 md5 는 다름)은 수납이 정상 작동하지만 슬롯 3개뿐(등록 form3/4 는 4개)이고 라인건설 실고객 데이터가 그대로 남아 있다(자재비/추가분견적 두 시트의 실품목·업체명·날짜,가격적용분시트 843행짜리 실사양표,자재비/내역서 6개 탭에 걸린 카톡 대화 스크린샷 3장 + 실제 아산현장 도면 6장 — 새 현장 파일에 그대로 나가면 다른 고객 데이터 유출이다). ⛒새 정본(수납 정상 + 4슬롯 + 잔존데이터 삭제)을 만들어 PFS 에 등록해야 이 구조가 재발 안 한다 — 지금은 케이스마다 수동 확인해야 한다. 실측·산출물 =C:\dev\_work\hansem-hwaseong-combined\(build.py·sanitize_and_fix.py·strip_leaked_images.py·drop_price_ref_sheet.py·verify2.py). - 🔑2026-09-28 한샘 갑지 절사(
-MOD(합계,10000))는 주방·수납이 «각자» 만원 내림이다 — 목표합이 10,000 배수가 아니면 최종액이 항상 목표보다 작다(최대 −19,999원). 두 커버(주방/수납)가 독립적으로 SUM 후 내림하기 때문에 손실이 두 배로 겹친다. 큰 금액(수천만원)에서는 무시할 수준(실측 76,231,000 목표 대비 −11,000, 0.014%)이지만 금액 대조표에 반드시 이 오차와 원인을 같이 적는다 — "틀렸다"가 아니라 한샘 양식 설계상 특성이다. - 🔴2026-09-28 컬럼 추가 전에 새 코드가 먼저 뜨면 «실패한 SELECT 가 메타데이터 잠금을 쥔 채» 남는다 → ALTER 가 무한 대기(
Waiting for table metadata lock)하고 그 뒤로 해당 테이블 조회 전체가 줄 선다(테스트 8001 실측 — 1054 로 실패한 패널 요청 3건의 유휴 연결).ensure_*런타임 ALTER 도 같은 이유로 조용히 실패했다. ⛒ALTER 스크립트엔SET SESSION lock_wait_timeout=10, 순서는 운영 ALTER → deploy-live 푸시(master 푸시가 곧 테스트 배포라 테스트는 먼저 ALTER 하거나 꼬이면 유휴 연결 KILL). 선례 =scripts/codi_worklog_mypin_alter.py. [코디] - 🔴2026-09-28 [페페] 추가건(AS) 운임 규칙 — 「앞으로 계속 조건을 맞춰나가자」 ①본건 배차(배차목록 job_type 본건)의 운임은 본건 견적 쪽으로 들어간다 → 같은 날짜에 실려 간 추가건은 운임 생략. AS 배차만 추가건 운임. ②페이지 상차일에 배차가 없으면 그다음 배차일에 한번에 나간 것으로 묶는다(오엠씨→포인 입고 배차는 무시). 상차일 오기는 페페가 짚는다(대우 용인 p11 → 8/29). ③아직 안 나간 예정 상차(배차 없음)는 «예정운임»으로 넣는다 — 넣을지·금액은 매번 페페에게 먼저 묻는다(코디가 임의로 넣지도 빼지도 않는다). ④묶음마다 운임 첫 줄 → 생산건 페이지 순, type_name = 배차일 「26.8.23(일) 상차」. 실례 = 대우 용인 781·금호 구미 782(
C:\dev\order-automation\enex_daewoo_as_2026-09\step5_freight.py). 🔑방향이 둘로 읽히는 지시(「동일하게 수정」)는 반영 전에 되묻는다 — 26.9.28 금호 운임을 거꾸로 지웠다가 되살렸다. - 🔑2026-09-24 Jev 는 «생성»에 못 쓴다 — 완성된 후보를 «고르기»만 시킨다. 실측(질문 「대한민국의 수도는?」): 자모 단위 조립 30호출 = 「사사사다사다다사사사」(끝 선택 0회) · 음절 253개 지선다 12호출 = 「대 대 」(질문 글자 베끼기) · 문장 후보 4개+해당없음 1호출 = 「서울입니다.」 p 0.98. 선택지 상한 = 255개(넘으면 HTTP 400 →
jev.choice가 None 을 돌려 조용히 폴백된다 — 현장 목록처럼 후보가 255를 넘을 수 있는 곳은 먼저 좁혀서 넘긴다). 스크립트 =C:\dev\_work\jev\speak\speak.py· devplanC:\dev\devplan\pfs-orderpaper-jev.html§6. [코디] - 🔴2026-09-23 오버레이(라이트박스)를 «아무 데나 눌러 닫기»로 만들 땐
if(!event.target.closest('button,a')) close()한 줄로 하고, 그 안의 컨트롤은 전부<button>·<a>로 만든다. 버튼마다stopPropagation을 붙이는 방식은 컨트롤을 새로 추가할 때마다 빠뜨린다 —closest조건이면 새 버튼이 자동으로 안전하다. ⛒반대로<div>·<span>으로 만든 컨트롤은 이 규칙을 안 타서 누르는 순간 닫혀 버린다. 확대 이미지엔cursor:zoom-out(썸네일zoom-in과 대칭). ⚠같은 파일을 두 경로에서 받게 되면 파일명을 반드시 맞춘다 — 라이트박스 다운로드를 URL 만 보고 만들면 서버 해시명(4b97….jpg)으로 떨어져 썸네일에서 받은 이름과 달라진다 →dcLbOpen(urls, idx, **names**)처럼 표시 이름을 같이 넘긴다(대기 사진은names없음 = 버튼 숨김). 선례 =dispatch_list.html.dc-lightbox· devplanpfs-dispatch-photo.html. [코디] - 🔑2026-09-23 썸네일 액션은 «아이콘 코너 버튼»보다 «하단 전폭 배너»가 누르기 쉽다(페페 지시 26.9.23 — 22px 원 → 26px 배너). 배너 색은
#2f74c4(흰 글씨 4.76:1) —#3f8de8은 3.40:1 이라 흰 글씨를 못 얹는다. ⚠같은 그리드에 다른 하단 배너(대기 「저장 시 업로드」 등)가 있으면 높이를 같은 값으로 맞춘다 — 한 줄에 나란히 서면 바닥선이 어긋나 보인다. [코디] - 🔑2026-09-23 썸네일·카드에 «다운로드»를 달 땐
<a download>한 줄이면 끝이고, «삭제» 와 같은 변에 붙이지 않는다. 같은 오리진 정적파일(/static/uploads/...)은 JS 저장 루틴도 서버 엔드포인트도 필요 없다 — 앵커가img의 형제라img.onclick(확대) 경로도 안 탄다(event.stopPropagation()은 보험). ⛒위험 액션(삭제 ×) 옆의 무해 액션은 오조작을 부른다 → 대각 배치(다운로드 좌상단·삭제 우상단) + hover 색 분리(파랑#3f8de8/ 빨강#d63b3b). PFS 선례 = 행 액션바 삭제margin-left:28px. 🔴download="…"에 사용자 업로드 파일명(original_name)을 그대로 꽂으면 따옴표 한 개로 속성이 깨진다 →escAttr()(& " <) 필수 · 옛 데이터는original_name이 비어 있을 수 있어 대체 이름(배차<번호>_<n>.<ext>)을 준비한다. 저장 전 «대기» 파일엔 달지 마라(로컬에 이미 있다). 선례 =dispatch_list.htmldlAnchor()· devplanpfs-dispatch-photo.html. [코디] - 🔴2026-09-23 색깔 버튼 «위»에 다른 색 배지를 얹을 땐 흰 링(
box-shadow: 0 0 0 2px #fff)이 필수다 — 글씨 대비만 맞춰서는 배지가 안 보인다. 배차 「사진」 초록 버튼(#2f9e6b) 위 빨간 배지(#c62828)의 인접대비는 1.3:1(WCAG 비텍스트 3:1 완전 미달) — 링 없이 3:1 을 넘기려면 마룬까지 내려가야 해서 「빨간색」으로 안 보인다. 링은box-shadow라 레이아웃에 영향 0이고 색약 사용자용 윤곽 구분도 겸한다. 배지 색은#c62828(흰 글씨 5.7:1) — 부트스트랩#dc3545(4.53:1 = 여유 0)·PFS--dc-danger #e85a4f(3.49:1 AA 미달)를 재사용하지 마라. 카운트 배지는border-radius:10px+min-width:20px로 두면 1자리=원·2자리↑=알약이 저절로 된다(분기 코드 불필요), 3자리는99+. ⚠배지로 버튼 하나만 높아지면 버튼마다 height 를 박지 말고 부모를align-items:stretch로 바꾼다. 선례 =dispatch_list.html.dc-badge-count· devplanpfs-dispatch-photo.html. [코디] - 🔴2026-09-23 같은 기능의 DOM 이 «두 벌»이면 컨트롤러도 두 벌이어야 한다 — 전역 컨트롤러 1벌 + 전역 대상번호 1개는 한쪽 화면을 통째로 죽인다. 배차 사진 실결함:
initPhotoDropzone()이#dcPhotoGrid한 곳에만 묶여 있어 목록 사진모달의#dcPhotoGrid2는 어떤 코드도 안 건드리는 죽은 요소였고(썸네일·삭제버튼 0 = 「삭제가 안 된다」의 정체), 전역dcPhotoDispatchNo는 모달을 닫으면 −1 이라 등록/수정 쪽 업로드가dispatch/-1/photo로 샜다. ⛒화면 요소 id 끝에2가 붙어 있으면 그 짝인 JS 가 실제로 있는지부터 확인한다 — 인라인onclick/onchange만 있는 드롭존은 드래그&드롭 리스너가 없다(안내 문구만 있고 기능 0). 고침 =init…(dropId,inputId,gridId,getNo)로 컨텍스트마다 한 벌 + 대상번호는 클로저. ⚠전역 ESC 핸들러가 있는 화면에 오버레이(라이트박스)를 얹으면 맨 위 레이어만 닫고return— 안 그러면 ESC 한 번에 아래 모달까지 같이 닫힌다. ⚠URL.createObjectURL은 취소·삭제·저장 세 경로 모두에서revokeObjectURL까지가 한 쌍. 선례 =dispatch_list.html· devplanpfs-dispatch-photo.html. [코디] - 🔑2026-09-23 저장 전 화면(신규 등록)에 첨부를 붙일 땐 «클라이언트 대기열»이 기본 — 번호를 만들려고 미리 저장하지 않는다.
File배열 + objectURL 썸네일(「저장 시 업로드」 배지)로 두고, 저장이 PK 를 돌려주면 그때 일괄 업로드·취소하면 폐기. 선저장 안은 사진만 넣고 취소하거나 저장이 실패하면 유령 행이 남고 정리 로직이 따로 필요하다(코덱스·web-designer 동일 판단). 첨부만 실패하면 본 저장은 살리고 「수정 화면에서 다시 첨부」로 안내한다. [코디] - 🔴2026-09-23 Jev choice 확률은 보정돼 있지 않다 — 임계를 코드에 박기 전에 «Jev 결과를 보기 전에 고정한» 정답 라벨로 오프라인 측정부터 한다. worklog 보류 묶음 723호출 실측 = 필드 선택 시 정답 p 0.45~0.93 / 오답 p 0.43~0.85 가 겹친다(설계 가설 0.90 에서 보류분 자동연결 0건 · 0.60 칩 정밀도 55%). ⚠문맥(통화상대=현장 담당자)을 주면 틀린 답의 확신이 커질 수 있다(「의정부 현장」 강동식 통화 → 2023 GS 의정부 0.85). 🔑반대로 「해당없음」 은 믿을 만하다(p≥0.90 정밀도 94.6%).
jev-preview는 26.9.23 현재jev-latest와 같은jev-1.13.0. 상세 =session-2026-09-23-worklog-jev-measure.md. [코디] - 🔑2026-09-23 규칙 판정에 Jev 를 붙일 땐 «경계만» 묻고, Jev 는 규칙보다 더 빼거나(제외) 더 반영(PATCH)하지 않는다. 확실한 쪽은 규칙 그대로 · 경계만 확률 · 애매 구간은 «살리고 사람 확인 목록» · Jev 실패 = 규칙 판정 그대로. 운영 데이터를 바꾸는 폴백(intake-check 완료 PATCH)은 섀도(원장 기록만)로 먼저 넣고 오답 0 누적 후 켠다. 같은 입력을 주기적으로 다시 묻는 곳(30분 동기화)은 입력 해시 캐시. 선례 =
orderpaper_parse.dedupe(judge=…)·intake_check.jev_match· devplanpfs-orderpaper-jev.html. ⚠intake-checkruns\run_*.json엔 판독 원문(품목·수량)이 없다 → 오프라인 재측정은 사진 재판독이 필요하다. [코디] - 🔴2026-09-23 PFS 발주서(AL·부자재) → 자재관리 행 키는
seq로 잡으면 안 된다 — 항목 고정 IDline_id로 잡는다. 두 발주서 모두 저장이 «항목 전량 delete-insert + seq 재번호» 라 행 하나 지우면 뒤 품목 seq 가 한 칸씩 당겨지고, seq 키면 입고완료·입고일(_user)이 엉뚱한 품목으로 옮겨 간다(코덱스 지적). ⛒item_key = sha1(kind, 발주서번호, line_id, 성분 A/G/S)· 화면이 새 행에 line_id 를 만들어 보낸다(http 테일스케일이라crypto.randomUUID없음 → 시간+난수) · 행 복사는 line_id 를 새로*(AL 화면 copyHere·DAO copy_items). 연동 정본 =api/pfs/orderpaper/pfs_link.py, 검사 =python -m api.pfs.orderpaper.test_pfs_link. [코디] - ⚠2026-09-23 orderpaper 에 NAS 외 출처(upload·pfs_al·pfs_sub)를 넣을 땐
sync_round를 NULL 로 두고, NAS 동기화의 «사라진 행 내림»은COALESCE(source_kind,'nas')='nas'로 한정했다(orderpaper_sync.py). 그 전엔 업로드분이NULL<>round가 거짓이라는 우연으로만 살아남았다. PFS 가 만든 발주서 엑셀엔 문서속성 키워드PFS-AL#n/PFS-SUB#n이 박혀 NAS 파서가 그 파일을 건너뛴다(이중적재 방지). [코디] - 🔴2026-09-23 발주서 엑셀 사이에서 «장(29행)»을 옮겨 붙일 땐 행 XML 을 그대로 복사하면 안 된다 — 파일마다
styles.xml의cellXfs번호가 다르다. 라인VIP 원본·복사본은 141번부터 달랐고 테두리 개수도 60·61 로 달랐다 → 같은s="166"이 다른 서식. ⛒스타일을 내용(폰트·채움·테두리 풀어서) 대조로 재매핑, 없으면 원본 같은 줄 서식 ·t="s"공유문자열 →inlineStr·<f t="shared">→ 명시수식 · 상대행 참조 +29 · 매수(ROUNDUP(ROW()/29,0)) 캐시값 재계산 · 드롭다운x14 sqref에 새 장 추가. 붙이는 자리는 항상 맨 뒤(중간이면 뒤 장 section_idx 가 밀려 진행상태가 옮겨붙는다). 옮긴 뒤 새 행은대기로 INSERT 되니 옛 행 상태를 이관한다. [코디] - 🔴2026-09-23 테스트에 운영 데이터를 복제할 때
orderpaper_site_map은 같이 옮기지 마라 —field_no는 환경마다 다른 현장을 가리킨다. 「테스트서버 데이터 운영데이터로 최신화」를 하며 두 테이블을 통째로 복제했더니is_manual='1'행이 운영 field_no 를 들고 와, 테스트에선 8.25·9.3 QA 검사가 만들고 소프트삭제한 임시 현장_QA5미확정_*을 가리켰다 → 자재관리에서 「금호 구미 형곡3차 MH」 150건이_QA5미확정_1787627487묶음으로 떴다(페페 제보). ⛒복제는orderpaper만 하고 현장 연결은 동기화가 그 환경field기준으로 다시 만들게 둔다. 복구 = 삭제된 현장을 가리키는 연결을 끊고(is_manual='0'·field_no=auto_field_no) 재동기화(C:\dev\pfs\orderpaper-sync\_fix_test_sitemap.py, 실측 site_map 7·항목 246건 정정). ⚠QA 스크립트의 임시 현장은 소프트삭제라 번호가 영구히 남는다. [코디] - 🔴2026-09-23
orderpaper-sync파서·동기화 코드는 «저장하는 순간 운영에 나간다». 예약작업orderpaper_autosync(30분,:08·:38)가C:\dev\pfs\orderpaper-sync\orderpaper_sync.py prod를 master 작업본 그대로 실행한다 — 배포 절차가 없다. 고치면 다음 회차 전에 드라이런을 끝내라. ⚠드라이런은 Bash 에서 안 된다 — Session 0 이라 NAS UNCos.listdir가WinError 5. ⛒1회성 예약작업/IT(schtasks /Create /TN <이름> /TR "wscript.exe <vbs>" /SC ONCE /ST 23:59 /IT /F→/Run, vbs 가python orderpaper_parse.py dups > 로그)로 사용자 세션에서 돌린다(26.9.23 실측 작동). [코디] - 🔴2026-09-23 발주서 «복사본» 파일을 파일째 버리면 안 된다 — 복사본을 새 발주서로 쓰는 경우가 있다.
SGC 용두동 … - 복사본·복사본 에넥스 LH 의정부 우정은 원본에 없는 탭·발주가 든 진짜 발주서였다. 반대로라인VIP … - 복사본은 원본의 옛판이라 원본이 고쳐진 뒤엔 내용 완전일치로도 안 잡힌다. ⛒블록 단위 =orderpaper_parse.dedupe()(R1 같은 자리·발주처 + 발주일 같음‖품목 부분집합 / R2 완전일치 하나만). 파서의 옛 「복사본 노이즈 제거」(26.7.21, 파일명 분해)는 그대로 둔다. [코디] - 🔴2026-09-23 발주서 xlsx 를 openpyxl 로 «열었다 저장」하면 안 된다 — 인쇄 설정이 통째로 날아간다. 나진 발주서 실측 =
printerSettings1~3.bin·sharedStrings.xml·calcChain.xml·sheet rels 3개가 사라지고 126,679 → 102,971바이트. 발주서는 인쇄해서 팩스로 나가는 문서라 이게 곧 사고다. ⛒zip 안 대상 시트 XML 한 장만 치환한다(orderpaper-sync/orderpaper_date_write.py) — 나머지 파트는 바이트 그대로 복사라 손실이 원리적으로 0(실측 파트 30→30). ⚠openpyxl 의cell.style_id는 XML 의s=와 다른 체계다 — 그대로 쓰면 저장본이IndexError로 안 열린다. 스타일도 XML 에서 직접 읽어야 하고(_xml_style_of), 「긴급」 같은 글자가 있던 칸은 날짜 서식이 아니라 같은 열의 날짜 셀 서식을 베껴야46285로 안 보인다. [코디] - 🔴2026-09-23
scripts/ssm_run.py는 «입력 JSON 파일명»도 고유해야 한다 — 고정이면 남의 스크립트가 실행된다. 사이드카 릴레이가 20초마다 같은_ssm_input.json을 쓴다 → 내 E2E 스크립트를 보냈는데RELAY_CLAIM none이 돌아왔다(26.9.23 실측). 청크 파일명만 고유화하고 이걸 놓쳤었다. 지금은_ssm_input_<ms>_<pid>.json+ 실행 후 삭제. 이 PC 에서 SSM 을 쓰는 모든 도구가 사이드카와 경합한다는 전제로 짜라. [코디] - ⚠2026-09-23 「날짜 미기입」의 실체는 빈칸이 아니라 「긴급」이다(입고일 63칸, 진짜 빈칸은 1칸뿐). 날짜칸(I열)에 사람이 글자를 써 넣은 것 — 파서는 날짜 없음으로 보고
receive_date=NULL로 넣는다. 그래서 원문을order_date_raw·receive_date_raw에 보존해 덮어쓰기 전에 무엇을 지우는지 화면에 보여준다. [페페] 결정 = 경고에 올리고 날짜를 넣으면 덮어쓴다(납기가 정해졌다는 뜻이라 「긴급」은 역할을 다한 값). [코디] - 🔴2026-09-23 「자재관리에 엑셀 수정분이 안 올라온다」 = 파싱을 뒤지기 전에 «동기화가 돌긴 했는지»부터 본다. 판정 =
tail C:\dev\pfs\orderpaper-sync\_autosync.log(마지막 실행 결과) ·_sidecar.log(릴레이error줄). 실제로 9.22 13:39~9.23 09:41 20시간 전량 실패였는데 파싱 규칙은 멀쩡했다. 원인 = SSM SendCommand 97KB 한도 — 파싱 결과를 스크립트에 구워 보내는 구조라 현장이 늘면 b64 가 99,036B 로 넘는다. ⛒예약작업orderpaper_autosync는wscript라 내부 실패와 무관하게 LastTaskResult=0 이다 — 예약작업 성공 = 동기화 성공이 아니다. 이제 실패하면 텔레그램이 온다(orderpaper_sync.notify_fail). [코디] - 🔴2026-09-23
ssm_run.py로 큰 스크립트를 보내면 원격 파일명을 «고유화» 해야 한다 — 고정 이름이면 동시 실행이 같은 파일을 반쯤 덮어쓴다. 30분 예약작업과 수동 실행이 09:38·09:41 에 겹쳐/tmp/_op_remote.py디코드본이 74,276 → 103,552B(청크 1개 중복 append)가 되며 SyntaxError. 지금은/tmp/_op_remote.<ms>_<pid>.py+ b64 길이·디코드본 크기 대조 가드(어긋나면 실행 거부 — 반만 디코드된 파이썬이 돌면 RDS 를 어중간하게 건드린 채 죽는다). 60,000자 초과분은 자동으로 청크 전송된다. [코디] - ⚠2026-09-23 발주서 엑셀에서 «표 중간에 행을 끼우면» 진행상태가 한 칸씩 옮겨붙는다. 항목 신분증 =
(파일명·탭·섹션번호·자재구분·표 안 순번)해시라 아래 행들의 순번이 전부 밀린다. 값은 맞게 들어가지만 대기/진행/완료·입고 체크가 어긋난다 → 추가는 맨 뒤에. 파일명을 바꾸면 전량 새 항목 + 옛 항목 삭제표시(상태 초기화). [코디] - 📌2026-09-23 한샘 견적서(생산비 거래명세서) 규칙은 여기 두지 않는다 — 별도 프로젝트로 갈라졌다(페페 지시). 정본 양식·양식 해부·파싱 규약·운임 규칙 전부
C:\dev\seedchain\projects\hansem-estimate\{handoff,design,rules}.md· devplanhansem-estimate.html. 이 파일엔 PFS 쪽 코드 함정(한샘 엑셀 엔진·적용률·양식 md5 등)만 남긴다. [코디] - 🔑2026-09-23 PFS 화면을 사무실 프린터로 뽑는 경로 = 미니PC 에서 바로 된다(원격 PC 경유 불필요). 미니PC 에
SINDOH D450/CM Series PCL(포트IP_192_168_0_201)이 이미 설치돼 있다. ⛒Playwrightpage.pdf(prefer_css_page_size=True, print_background=True)→ GDI 래스터 인쇄 2단계 =C:\dev\_work\pfs-print\{render_status.py, print_pdf.py}. ⚠PDF 뷰어 CLI(PXCEditor/print)는 쓰지 마라 — GUI 앱이라 Session 0(Bash)에서 창이 안 잡힌다. GDI 경로는 창 0개라 그냥 돈다. ⚠래스터 DPI 는 프린터에 맞춘다 — D450 은 600dpi(용지 4960×7014px)라 300dpi 로 뽑아 2배 확대하면 글자가 뭉갠다. 배치 =img.draw(hdc, (-ox,-oy, pw-ox, ph-oy))(PHYSICALOFFSET만큼 당겨 A4 1:1). 매수는StartDoc1개 안에서StartPage/EndPage반복 = 스풀 1건. 생산현황(/field/print/status)은 A4 세로 1장이 정상이다. [코디] - 🔴2026-09-22 한샘 양식에서 적용률은 «반올림」이 아니라 «올림»이다 — 갑지 절사가 만원 내림이라 1원만 밑돌아도 10,000원이 잘린다. 갑지
O23 = -MOD(SUM(W19:AD22),10000). 반올림 적용률로 합계가 539,951(목표 540,000 −49) 이 되자 절사가 −9,951 을 먹어 소계가 530,000 이 됐다. 올림하면 540,239 → 절사 −239 → 정확히 540,000. 소수 4자리 적용률로는 줄별 정확일치가 애초에 불가능하니(단가×길이/10 이 정수 약수가 아니다) 「목표를 밑돌지 않게 넘치고 절사에 맡긴다」가 정답이다. 검사는0 ≤ 합계 − 목표 < 10000. [코디] - 🔴2026-09-22 한샘 엑셀은 「블록이 밀리면」 금액이 조용히 틀린다 — 서버 총액이 맞다고 엑셀이 맞는 게 아니다(엔진 결함 3건, 운영
82a5052로 수정).inject_ledger는 옮기기 전 좌표로 수식을 쓰고drop_empty_blocks가 뒤에 블록을 당긴다._fix_block_formulas가 단가·적용가·합계만 다시 걸고 세트라벨 미러(J{b+1})와 10MM기준(J{b+3}=J{b+2}/10*적용률)을 빼먹어 남의 블록 길이를 물거나(하부장 11→4 가 키큰FIX 길이 993 을 물어 58,008 → 96,975) 없는 행을 물어 0 이었다. ②블록마다 1차(쓰는 칸)·2차(비우기)를 번갈아 돌아 뒤 블록이 베껴 올 기증 블록의 단가 LOOKUP 을 앞 순번 2차가 먼저 지웠다(제 단가 수식이 없는 블록 = form 3 키큰FIX·오픈장·공틀도어가 통째로 0) → 전 블록 1차 → 전 블록 2차로. ③빈 평형 슬롯 총합이=SUM()= 엑셀이 거부하는 수식(복구 경고) → 상수 0. ⛒한샘 견적을 등록했으면 엑셀을 내려받아 셀 단위로 재계산해 봐야 끝이다 — 목록 총액은hansem_calc라 언제나 멀쩡하다. [코디] - ⚠2026-09-22 한샘 «양식»은 PFS 에 올라간 것과 로컬 산출용이 다를 수 있다 — 쓰기 전에 md5 를 대조해라. 운영 활성 form 3(26.9.7 업로드,
d837cf7…)에는 갑지 절사행이 아예 없고 운반비도 0 이다. 같은 날 확정된 규칙 #8(절사 만원 내림 · 운반비 300,000 · 자재비/시공비 0)은build_form4.py재실행분(fbf17f6…)에만 들어갔고 PFS 에 다시 안 올라갔다. 로컬 빌드는 ALL PASS 인데 운영 산출물만 840,000 이 아니라 540,239 로 나와 잡혔다. 해법 = 활성판은 건드리지 말고(기존 견적 보존) 정본판을 비활성으로 새로 올려 그 견적만form_no로 물린다. ⚠그 양식은 운반비 300,000 이 박혀 있으니 운반비가 다른 건에 쓰면 안 된다. [코디] - ⚠2026-09-22 한샘 양식 슬롯보다 평형을 적게 주면 탭 이름이 겹쳐 파일이 깨진다. form 3 슬롯은
77·84A·84B·84C4개다. 평형을84B·84C·1203개만 주면 4번째가 원래 이름84C로 남아84C 내역서-주가 두 개가 된다(rename_slots의 임시이름 트릭은 «순서 교차»를 막을 뿐 진짜 중복은 못 막는다). → 남는 슬롯까지 빈 평형(예: 「미사용」)으로 채워 이름을 갈라 준다. [코디] - 🔑2026-09-22 「본건 제외하고 이 내용만」 = 추가건 견적 1건만 한샘 모듈에 새로 등록하라는 뜻이다(
parent_estimate_no=null— 한샘 모듈엔 그 현장 본건이 없다). 선례 = 신영 광주 추가2·추가3 = 한샘 견적 4. 평형 탭은 품명 접두에서 나온다(84B 식기세척기 마이다→ 탭 84B). 운반비는 양식 갑지 기타행이 갖고 있어 상세로 넣지 않는다 — 넣으면 이중 계상이다. [코디] - 🔑2026-09-22 「담을 값」과 「볼 값」을 한 필드로 겸하지 마라 — 화면에 금액을 더 보여달라는 지시는 서버 필드 신설로 푼다. 월마감 담기 모달에서
construction_amount는 마감행에 담기는 값이라 시공비 분리 거래처(한샘)가 아니면 언제나 0 이다. 그걸 그대로 「시공비」 칸에 찍으면 분리 OFF 거래처에서 가구총합 칸이 실제로는 시공비를 품은 값이 돼 라벨이 거짓이 된다. → 표시 전용furniture_amount·construction_cost2필드를 분리 여부와 무관한 실값으로 따로 내리고 담기 계산식은 한 줄도 안 건드린다. 불변식 =가구총합 + 시공비 = 전체총합이 모든 행에서 성립(검사 A12·A13), 그리고 분리 ON/OFF 가 표시용 값을 바꾸지 않는다(A16). [코디] - 🔴2026-09-22 검사기가 「0 + 0 = 0」 으로도 통과하면 그건 검증이 아니다 — 표본 수를 같이 찍어라. 금액 3분할 정합 검사가 테스트 DB 에선 시공비가 전량 0 이라 전부 OK 였다(실증 0건). 검사기에
시공비>0 표본 N/M출력을 넣자 운영 12/50 건에서야 실증됐다(2,180,000 + 700,000 = 2,880,000). ⛒「전부 0/전부 빈값」인 표본으로 통과한 항등식 검사는 환경을 바꿔 다시 돌린다. 같은 이유로 «운영 읽기전용 검증»이 형식절차가 아니다 — 테스트에 없는 데이터형이 거기 있다. [코디] - ⚠2026-09-22 페페 QA 보고의 예시값은 예시일 뿐이다 — 검사기 기대치로 박지 마라. 「견적일
4.12(월)포맷으로」의 2026-04-12 는 실제로 일요일이다. 포맷 지정을 그대로 단위검사 기대값('>월<' in ...)으로 옮겨 첫 실행에서 FAIL 이 났다(화면은 정상이었다). 같은 실행에서 검사기 자체 오류가 3건 = ①이 요일 ②parseInt('-')= NaN(시공비 0 을 «-» 로 찍게 해 놓고 숫자로 되읽음) ③th슬라이스 위치. 🔑FAIL 이 나면 화면보다 검사기를 먼저 의심하는 게 아니라, 실화면 값을 함께 찍는 항목(B7-16 «화면 견적일도 새 포맷»)을 같이 둬서 어느 쪽이 틀렸는지 한 번에 갈라라. [코디] - 🔑2026-09-22 BS4 기본
.badge(10.5px)·.small(11.2px) 은 13px 하한 위반이다 — PFS 화면 어디든 이 두 클래스가 있으면 이미 어기고 있다. 산식 =14px × .75/14px × .8. 고칠 땐 그 화면·그 모달로 스코프를 가둬라(#mc-pick-popup .badge, #mc-pick-popup .small { font-size:13px }) — 전역으로 올리면 다른 화면 배지가 전부 커진다. 검사는 클래스 문자열이 아니라 computed font-size 실측으로(클래스 검사는 CSS 정의에 걸려 오탐난다). [코디] - 🔴2026-09-22 워크패널에서 「내가 쓴 할일이 안 보인다」 = 한 큐에 통화+수기를 섞어
regist_date DESC LIMIT로 잘랐기 때문이다(운영배포399ed6e로 해결). 운영 실측 =limit 50의 source 분포가{call: 50}— 수기 42건 중 화면 도달 0건이었다(limit 200 까지 넓혀야 순위 36·73·74·98·170 에 등장). ⛒소스가 섞인 큐에 단일 LIMIT 을 걸지 마라 — 자주 쌓이는 쪽이 자리를 전부 먹는다. 해법 = 소스별로 나눠 뽑아 병합(_PANEL_MANUAL_CAP=80수기 몫 보장 ·_PANEL_CALL_FLOOR=20통화 최소 보장). 배포 후 ={call: 20, 수기: 42}. 🔑화면(grpKey)은 이미 수기를 맨 위 그룹으로 올리게 돼 있었다 — UI 를 고칠 문제가 아니라 서버에서 도달을 못 하던 문제였다(증상만 보고 화면부터 뜯으면 헛수고). - 🔴2026-09-22 패널 후보 칩이 「자동연결이 스스로 안 믿는 매칭」을 사람 클릭용으로 1등에 노출하고 있었다(같은 배포로 수정).
_panel_candidates가sc > 0이면 다 보여줬는데 자동연결 채택선은 80이다. ⛒패널과 자동연결은 «같은 기준»을 써야 한다 — 다르면 「자동연결은 안 됐는데 후보 1등이 다르다」가 생긴다. 운영 실측 = 1등 점수 52점 12건·64점 2건(=40+토큰히트1개, 「현장」 한 글자 겹침), 🔑80점조차 위험했다 — 현장 마스터의 일반명 현장(「사장님」·「전시장」·「아이파크」·「서울숲」)이 긴 이름을 통째로 흡수한다(월드(유재열사장님)→「사장님」). → 점수 하한 80 + 커버리지 가드를 패널에도 적용. 🔑커버리지 기준은 패널이 «더 엄격»해야 한다([페페] 26.9.22 =_PANEL_MIN_COVER0.6 vs 자동연결_MIN_COVER0.5) — 패널이 더 엄격하면 패널 후보 ⊆ 자동연결 후보라 모순이 안 생기고, 반대로 느슨하면 「자동연결은 안 됐는데 후보 1등이 다르다」가 즉시 생긴다. ⛒이 값을_MIN_COVER아래로 내리지 마라. 경계 실측 = 한남동 현장→「한남동」 3/5=0.6 통과(파이썬에서3/5 >= 0.6= True) · 사장님 0.30 · 전시장 0.27 · 아이파크 0.33 · 넥서스 0.43 전부 차단. 적용 후 통화 84종 중 후보 잔존 2종(한남동 현장→한남동 0.6 · 완전일치 1). ⚠칩이 사라진 카드는 검색창에 추정명을 프리필해 타이핑 0 으로 보완했다. ⚠scripts/_wt_panel_check.py의 「미연결에 현장 후보가 붙는다」 검사는 옛 기대치라 FAIL 한다(버그 아님 — 필터 강도 확정 후 갱신할 것). - 🔴2026-09-22 worklog 자동연결의 「81% 미연결」은 코드 주석값이고 운영 실측은 «100% 미연결»이다 — 주석을 근거로 쓰지 마라. 운영 dry(
C:\dev\pfs\scripts\_wl_autolink_run.py prod dry· 읽기전용) = 묶음 1,087 / 자동연결 0 / 후보없음 408 / 보류 679. 게이트 4개(점수≥80 · 최상위 유일 · 정규화명 ≥3자 · 커버리지 ≥0.5)가 전부 막는다. 상위50 reason = ambiguous 37 · lowcover 10 · weak 3 이라weak만 손대는 설계는 6%짜리다. 🔑ambiguous best=52=40+토큰히트1개= 흔한 토큰 하나 겹쳐 1등이 된 쓰레기 매칭이고, 「부산 민락(현장 강남)」 하나가 무관한 7건에서 반복 1등인 노이즈 자석이다 — worktray 패널 칩 0번이 지금도 이걸 보여줘 오연결을 유발한다(고치려면 후보 점수 하한을 따로 둬야 한다). 상세 = devplanC:\dev\devplan\pfs-worklog-jev-autolink.html - 🔑2026-09-23 Jev
questions는 «배열이 아니라 dict» 다 —{"<id>": {"type": "choice|noul|score", "instructions": "질문", "criteria": …}}(배열이면 422dict_type). criteria = choice{옵션: 설명}(≤255) · noul{true:…, false:…}(선택) · score 낮은→높은 단계 목록(2~10). 응답 =answers.<id>.{choice|noul|score, probabilities, confidence}+model(실제 =jev-1.13.0류) +usage. 로컬 자산 공용 래퍼 =C:\dev\tools\jev.py(실패는 전부 None → 폴백, 자가점검python jev.py). 26.9.23 얼리액세스 키로 실호출 OK(choice 1.0 · noul 0.93, 415/334 입력토큰). 429·529 = 지수 백오프. - 🔑2026-09-22 Jev(TypeSafe 판정모델) 호출 정본 — 공식 호스트는
model필드가 «필수»다(빼면 422). BASE =https://api.typesafe.ai/v1/systemone(POST) · body ={"model", "state", "questions"}· 모델 목록 =GET https://api.typesafe.ai/v1/models(현재jev-latest·jev-preview). 키 =C:\dev\.secrets\.envJEV_API_KEY(접두어apikey_= 공식). ⛒같은 키를jev-agent.com게이트웨이에 쓰면 401 — 그 사이트는 «비공식»이고 키 발급처가 다르다(공식 =docs.typesafe.ai). 한국어 정상(실측 choice 0.950 / conf 0.94 / 470토큰). ⚠criteria옵션에 「해당없음」을 반드시 넣어라 — 없으면 후보에 정답이 없을 때 모델이 억지로 하나를 고른다(폐쇄형 분류 최대 사고). ⚠운영은jev-latest를 쓰되 응답의model값을 판정과 함께 저장한다(latest 는 말없이 바뀌어 임계를 어긋나게 한다). - 🔑2026-09-22 재단치수는 «내림 정수」다 — 부재 치수에 소수점을 내보내지 마라(페페 지시). 반올림이 아니라 내림인 이유 = 올리면 부재가 자리보다 커져 갭을 먹는다(마이다 146.67 → 146). 예외는 두께 하나 — 자재 규격이라 4.5T 가 실재한다. 나누기가 들어가는 산출(단수 분할·갭 배분)은 전부 여기 걸린다. [코디]
- 🔑2026-09-22 결이 있는 «세워 쓰는» 부재는 가로/세로를 뒤집어 넣는다 — 높이쪽이 가로 재단, 나머지가 세로 재단, 결방향 세로(페페 지시). 대상 = 서랍측판 · 서랍전후판 · 텐덤후판. 결이 긴 변을 따라 흐르게 하려면 긴 변이 «세로»여야 한다. ⚠정본
drawerBox()는 설계 기준(l=길이 · w=높이)으로 준다 — 그대로 부재표에 꽂으면 결방향이 반대로 박힌다 → 화면에서 재단 기준으로 바꿔 넣는다(정본은 안 고친다, imos 대조가 깨진다). 바닥판류(3T HDF · 텐덤 15T PB)는 결을 안 따져 그대로 결무시. 몸통측판·마이다·날개는 원래부터 세로 = 높이라 손대지 않는다. [코디] - 🔴2026-09-22 ESM 모듈 하나에 import 를 몰아넣지 마라 — import 한 줄이 404 면 그 모듈의 «남의 기능»까지 통째로 죽는다. 네스팅 서랍 계산기의 산출식
import { drawerBox } from '/production/pncad/schema.js'를 droplist 모듈에 같이 넣었더니, schema.js 가 안 떠 있는 환경에서initDrops()가 아예 안 돌아 컬러·경면 자동완성이 통째로 죽었다(검사기 ⑦#pcDrop생성 FAIL 로 잡혔다 — 콘솔 404 하나 말고는 증상이 없어 눈으로는 못 본다). ⛒기능별로<script type="module">을 갈라 실패를 그 기능 안에 가둔다(모달이 「산출식 모듈 미로드」를 띄우고 나머지는 정상). 같은 이유로 새 정적 자산을 화면에 붙이면 그 화면 로컬 검사기의translate_path목킹도 같이 늘려야 한다(안 늘리면 없던 404 가 콘솔검사를 깨뜨린다). [코디] - 🔑2026-09-22 서랍·부재 산출식은
C:\dev\pncad\SPEC-DRAWER.md+web/pncad/schema.js가 정본이다 — 화면에 복제하지 말고/production/pncad/*에서 import 해라. 편측갭(언더 5.5 · 볼 14)은 SPEC §3.1·§6 이 「케이스별 오버라이드 · 언더 11.0 변동 미규명」으로 열어 둔 값이고, 원천C:\dev\pncad\schema.js→tools/sync_to_pfs.py동기화는web/pncad/까지만 닿는다 → 화면 사본은 안 따라와 조용히 갈라진다./production/pncad/<path>는default_controller가web/pncad/를 통째로 서빙하고@login_required게이트가 같다(droplist.js 선례). 서랍 기준은 장W 가 아니라 존 내경 Wz = W−2×몸통두께−2×날개폭(날개를 빼면 전후판이 100mm 틀어진다 = SPEC §3.5 경고). [코디] - 🔑2026-09-22 텐덤박스는 「완제품이라 목재 없음」이 아니다 — 바닥판·후판을 15T PB 로 우리가 만든다(페페 지시). 가로 기준이 특이하다 = 장W·내경이 아니라 마이다 폭에서 −103(바닥판) / −115(후판), 후판 높이는 박스 높이와 무관한 2단 고정규격(소 65 · 대 142). imos 실발주 3건 8/8 편차 0(
reference/catalog-2026-08-17/imos-tandem-parts.json). 측판·전후판이 없는 건 금속 U레일이 측판을 대신하기 때문(DR_MAIN.TYPE=3). ⚠SPEC-DRAWER.md§5 의 「텐덤 = 범위 밖」은 TYPE=5(이름만 텐덤인 4면 zargen) 얘기라 이 건과 다르다 — 두 그룹을 섞지 마라. [코디] - 🔴2026-09-22 「포커스를 누가 뺏나」를
focusin버블로 막으려 하지 마라 — jQuery 3.2.0 은 이 크롬에서 focusin 을 «캡처단계focus» 로 흉내 낸다. QA 헬퍼가 부트스트랩 모달 안에서 입력이 안 먹던 건(모달_enforceFocus가 사이드바 포커스를 도로 뺏음)을 패널에focusin+stopPropagation한 줄로 고쳐 배포했는데 테스트에서 그대로 FAIL 이었다. 실측 원인 ='onfocusin' in window가 false 로 읽혀($.support.focusin거짓) jQuery 가document.addEventListener('focus', h, true)로 focusin 을 시뮬레이트한다 → BS 핸들러가 타깃보다 먼저 돌아서 버블 차단이 닿지 못한다. ⛒문서레벨 리스너와 싸우지 말고 그 기능 자체를 한시적으로 끈다 —$.fn.modal.Constructor.prototype._enforceFocus를 검증 모드 동안 빈 함수로 두고$(document).off('focusin.bs.modal')(이미 열린 모달용), 모드 끄면 원복 +.modal.show에 되걸기(qa_helper.js setModalFocusTrap()). 프로토타입을 덮으니 모드 켠 뒤 새로 여는 모달까지 자동으로 커버된다. 🔑진단은 추측 말고'onfocusin' in window·$._data(document,'events')· 캡처/버블 카운터를 브라우저에서 찍어 봐라 — 그 한 번이 배포 1회를 아꼈다. [코디] - 🔑2026-09-21 도어발주서에 「유상」 표기가 없다고 「뽑을 게 없다」로 판정하지 마라 — 추가2 형은 U열이 통째로 비어 있다. 추가건 등록 스킬 §3 규칙1 이 「U열 유상만 수집」이라 그대로 돌리면 추가2(견적 713, 키큰EP·휠라 12줄)는 0줄이 나온다. 실측하니 그 발주서의 p8~11 은 U열이 전부 빈칸이고, 당시 사람은 페이지를 통째로 지정해 올렸다(정답지 12줄 = 전량 모드 결과와 정확히 일치). ⛒그래서 화면에 「유상만 / 페이지 전량」 토글을 뒀다(기본 = 유상만). 새 발주서에서 0줄이 나오면 「파일이 틀렸다」가 아니라 U열부터 본다. [코디]
- 🔴2026-09-21 「부품인지 완제품인지」는 이름 집합이 천장이다 — 집합에 없는 부재명은 조용히 견적 줄이 된다. 골든 페이로드 5벌 대조에서
기둥목·좌날개·우날개·엣지파 픽스4건이 부품인데 견적 줄로 올라왔다(같은 묶음의 측판·뒤판은 잡혔는데 형제 행만 샜다 — 눈으로는 안 보인다). 수식어 규칙(수식어 붙으면 완제품)은 유지하되 집합은 발주서 실물 전량 스캔으로 채운다. ⛒고칠 곳 =api/pfs/estimate/service/door_order_parse.py의COMPONENT_NAMES1줄 추가 →scripts/_est_door_import_check.py재실행(골든 5벌이 즉시 잡아준다). 새 현장 발주서를 처음 돌릴 땐 완료 요약의 「부품 접음 N건」을 감과 대조한다. [코디] - ⚠2026-09-21 옛 추가건(견적 712·713·716·717·720·737)은 줄 비고에
p{페이지}접두가 없어 페이지 중복 장부에 안 잡힌다. 새 「추가건 엑셀」 기능은 비고 앞p{N}으로 「이 페이지 이미 올렸나」를 판정하는데(26.8.19 금강 형식), 그 앞 시대 추가건들은 사유만 적혀 있다. 그 현장에 이어 올릴 땐 사람이 페이지를 확인해야 한다. [코디] - 🔴2026-09-21 PFS 테마가
.btn-sm을 12px 로 덮고 있다(web/static/css/style.css:2045) — 표 안 버튼은 기본이 13px 하한 위반이다. 견적서 목록 버튼칸 7개를 실측하니 전부 12px 였다(부트스트랩 기본 14px 을 테마가 덮는다). ⛒테마 파일은 건드리지 말고 그 표 스코프로만 올린다 —#<표id> tbody td:last-child .btn { font-size: 13px; }1줄. 다른 화면 표 버튼도 같은 부채를 가졌을 가능성이 높다(고칠 땐 그 표만, 전역 수정 금지). [코디/web-designer] - 🔑2026-09-21 「원본 개정 차이」를 「파서 결함」으로 오진하지 마라 — 골든이 안 맞으면 «지금 원본»을 먼저 본다. 추가5 골든의
124 현관 엣지바 픽스 400*60*2210이 안 뽑혀 규칙을 의심했는데, 현 NAS 원본에선 그 줄이 「무상」이었다(비고도 다르다 — 골든 「펫 EP에서 유리 EP로 변경」 vs 원본 「무상, 도어 높이 잘못 계산」). 그 품목은 개정되며 p20 → p23 으로 옮겨갔고 p23 은 유상이라 추가6 에서 정확히 잡힌다. ⛒이런 건 검사에 「설명된 예외」로 이유를 적어 고정한다 — 맞추려고 무상행을 수집하게 고치면 청구 규칙이 깨진다. [코디] - 🔴2026-09-18 SSH(Session 0)에서
Get-Printer·Win32_Printer로 「프린터 없다」를 판정하지 마라 — 사용자별 네트워크 프린터가 통째로 안 보인다. poinimos02 에 라벨프린터가 없다고 보고했는데 [페페] 지적으로 다시 보니\\POINIMOS01\ZDesigner GT800 (EPL)(사무실PC 공유 GT800)이 멀쩡히 걸려 있었다 — 같은 계정으로 붙은 SSH 세션인데도 목록에 안 나온다(드러난 곳은Get-PnpDevice의 PrintQueue 한 줄뿐). ⛒원격 프린터 판정은 ①HKCU:\Printers\Connections+HKCU:\…\Windows NT\CurrentVersion\Devices레지스트리 ②schtasks /IT로 사용자 세션에서[System.Drawing.Printing.PrinterSettings]::InstalledPrinters덤프 두 가지로 한다(앱이 보는 목록 = 후자). 공유 큐는 호스트 PC 가 꺼지면 같이 죽는다는 것도 같이 적는다. [코디] - 🔑2026-09-18 「PFS 생산 인쇄」 앱을 다른 PC 에 깔 땐 «파일+바로가기만» 넣고 앱은 안 띄운다 — 그리고
schtasks /RU는PC명\사용자로 준다. poinimos02 설치 실측(페페 조작 0, SSH 만) = ①remote_update.py --deploy의/RU %%USERNAME%%는 계정명이 PC명과 같은 기기에서(40,4):UserId:로 실패한다 →/RU "%COMPUTERNAME%\%USERNAME%"(pfsb2cd1ae반영) ②--deploy는 경로가 이미 설치자리라Relocate()가 안 돌아 바로가기를 안 만든다 →.lnk는 따로 만든다 ③BOM 없는.ps1은 원격에서 cp949 로 깨져 한글.lnk이름Save()가 던지는데 stderr 를 버리면 「만들었다」만 보이고 바탕화면엔 0개다(utf-8-sig 저장 +Test-Path재확인) ④검증은 프린터 없이--selftest를schtasks /IT로 사용자 세션에서 굽고tools/verify_print_size.py로 실측(스티커 99.82 · 파레트 297.01×209.89 = 생산팀PC 값과 동일). 남이 쓰는 PC 라 창을 띄우면 그 화면에 튀어나온다 — 첫 실행은 사람 몫. devplanpfs-print-app-poinimos02.html. [코디] - 🔴2026-09-18 «미리보기 DOM = 인쇄 DOM» 화면에서 빈 상태 안내문구를 시트 «안»에 두면 그게 그대로 인쇄된다. 화학물 라벨의 빈 그림문자 칸에 「경고표시를 선택하세요」를 넣어 뒀는데, 페페가 「스티커 선택 안되도 출력 가능하게」로 이름표 모드를 요구하자 그 문구가 라벨에 찍혀 나갈 구조였다(인쇄는 미리보기 시트 HTML 을 그대로 보낸다). ⛒안내·경고·카운터는 폼 쪽(
.stk-chem-hint등)에만 두고 시트 안에는 실제 인쇄물만 넣는다. 검사에 「라벨 HTML 에 안내문구 문자열이 없다」 한 줄을 박아 재발을 막았다. [코디/web-designer] - 🔑2026-09-18 접히는 글자를 이진탐색으로 맞추면 「더 큰 글씨 + 낱말 쪼개기」가 항상 이긴다 — 2단으로 맞춘다.
overflow-wrap:anywhere가 켜져 있으면 「자재창고 A-3」 이 「자재창 / 고 A-3」(27.4mm) 로 찍힌다(쪼개면 더 큰 글씨가 들어가니 「맞는다」 판정). ⛒1차는overflow-wrap:normal(+word-break:keep-all)로 맞추고, 결과가 하한(8mm) 미만이면 그때만anywhere로 2차 — 공백 있는 이름은 낱말이 살고(20.9mm), 공백 없는 24자 이름도 8mm 를 지킨다. 판정 검사는 글자크기 구간으로 한다(쪼개기 27mm 대 vs 낱말보존 21mm 대 = 구분된다). [코디] - 🔑2026-09-18 공유 함수에 상한·옵션을 추가할 땐 «옵션 인자 + 기본값»으로 넣는다.
fitFont(el, fits)는 파레트용지(.ps-detail·.ps-site)와 공용이라 상한을 상수로 고치면 그쪽이 같이 변한다 →fitFont(el, fits, maxMM)+hi = maxMM || 50로 두면 기존 호출부는 무인자라 그대로고 새 화면만 30mm 로 조인다(이름표 한 칸은 칸이 3배라 2~3자 이름이 40mm 넘게 튄다). [코디/web-designer] - 🔴2026-09-18 CSS 주석을 «닫아 놓고» 뒷줄을 더 쓰면 바로 다음 규칙이 통째로 죽는다 — 화면이 조용히 어긋난다. 화학물 라벨 3등분 주석에 한 줄 덧붙이면서 위 줄의
*/를 안 지웠더니, 뒤따르던.cs-pics { … }가 파서 오류복구에 삼켜져 규칙 자체가 사라졌다(밴드가 29.8/29.8/29.8 → 50/2.5/50mm, 콘솔 에러 0). ⛒주석을 손대면<style>블록에서/*개수 =*/개수 + 주석 제거 후 남은 줄에 한글 문장이 있는지를 한 번 센다(content:·font-family제외). 🔑자동검사가 이걸 잡았다 — 치수를 재는 검사(밴드 높이 ±0.4mm)를 붙여 놓으면 CSS 무음 사고가 숫자로 드러난다. [코디] - 🔑2026-09-18
.stk-sheet를 N등분할 땐flex:1 1 0이 아니라 «백분율 basis»(flex:1 1 33.333%)를 쓴다. 이 파일은box-sizing:border-box라flex-basis:0의 기본크기가 패딩+테두리로 클램프돼 칸마다 그만큼 어긋난다(실측 30.3/28.8/30.3 — 그림문자 줄은 패딩 4mm, 이름칸은 2+0.6mm). 백분율 basis 는 컨테이너 내용높이 기준이라 패딩과 무관하게 정확히 나뉘고, 인쇄 1mm 셰이브에도 비율이 유지된다(고정30mm는 마지막 줄이 잘린다). [코디] - 🔑2026-09-18
fitFont로 «접히는» 글자를 맞출 땐 폭 비교에 1px 여유를 준다. 줄이 접히면 span 폭이 가용폭과 딱 같아져 서브픽셀 반올림 하나로 2줄 후보가 전부 탈락하고, 이진탐색이 「한 줄에 다 들어가는 초소형 글씨」로 떨어진다(24자 이름이 8mm(2줄) 대신 3.5mm(1줄)). 높이는 등호 비교 그대로(기존ps-detail주석), 폭만+1. 낱말 중간 끊김은word-break:keep-all+ 탈출구로overflow-wrap:anywhere를 쌍으로 준다(하나만 주면 공백 없는 긴 이름이 못 접혀 글자만 작아진다). [코디] - 🔑2026-09-18 스티커 라벨 종류를 새로 만들어도 인쇄 앱·서버·DDL 은 안 고친다.
print-app/MainForm.cs는kind == "pallet"한 곳만 갈라 보므로 새 라벨은appPrint(sheets,'sticker',100,100,5)로 보내면 그대로 스티커 프린터·ScaleSticker로 간다(새 kind 문자열을 만들 이유가 없다 — 앱이 모르는 값이면 위험). 출력기록도sticker_print_log.source가VARCHAR(10)·검증 없음이라'chem'같은 값을 그냥 넣는다. ⚠/print-log/history는WHERE field_no = %s라 현장 무관 라벨(field_noNULL)은 이력조회에 안 잡힌다 — 필요해지면 그때IS NULL분기. [코디/Plan] - 🔴2026-09-18 스티커 프린터(GT800 계열)는 «흑백 열전사» — 적색 규격이 회색 디더로 뭉개진다. GHS 경고표지 그림문자는 흰 바탕 + 검정 심벌 + 적색 마름모가 규격인데(법 제115조·고시), 적색을 못 찍는 프린터는 테두리를 점무늬로 찍어 8~13mm 소형에서 형태가 무너진다. ⛒처방 = 도안을 적색 원본 / 검정 사본 두 벌(
web/static/images/ghs/·ghs/k/)로 두고 화면 체크박스로 고르게 한다. 기본은 규격(적색) — 「프린터가 적색을 못 찍는다」는 법적 예외가 아니다(코덱스). 실물 가독까지 맞추려면 적색 테두리가 인쇄된 라벨지에 흑백 덧인쇄가 정답이고, 그건 자재 구매라 페페 판단. 🔑사업장 내부 소분·보관용기는 「명칭 + 그림문자」만으로 족하다(고시 제6조) — 신호어·유해위험문구·공급자정보를 100mm 라벨에 억지로 넣지 않는다. [코디/GPT/web-designer] - 🔑2026-09-18 GHS 같은 표준 픽토그램은 손으로 그리지 말고 «UN 공식 도안»(퍼블릭 도메인)을 받아 쓴다. Wikimedia
Special:FilePath/GHS-pictogram-<키>.svg9종이 792B~17KB 순수 벡터(래스터 0)다 — 손으로 그린 근사 도형은 법정 표지에서 「그 그림문자가 맞느냐」 시비가 된다. ⚠연속 요청은 429 로 막힌다 →--retry 4 --retry-delay 3 --retry-all-errors+ UA 지정. 색 치환은fill="red"·fill="#f00"·fill:#ff0000세 표기를 다 바꿔야 한다(도안마다 표기가 다르다). [코디] - 🔴2026-09-17 운영배포는 «worktree
C:\dev\pfs_deploy_wt» 에서 한다 — 본체에서git checkout deploy-live는 안 된다(fatal: already used by worktree). 그리고 🔴git checkout … | tail -2처럼 파이프를 물리면 체크아웃이 실패해도 exit code 가 tail 것(0)이라&&뒤 명령이 그대로 돈다 — 이번에cherry-pick이 master 위에서 돌아 소스가 충돌 마커로 오염됐다(cherry-pick --abort로 복구, 운영엔 안 나감). ⛒배포 절차 =cd C:\dev\pfs_deploy_wt→git fetch→git reset --hard origin/deploy-live→git cherry-pick <내 커밋들>→git push origin deploy-live. master 를 통째로 포워드하지 마라 — 다른 세션 작업이 20여 커밋 쌓여 있는 게 정상이다(선별 포워드 전git diff --name-only+git diff --stat origin/deploy-live <직전>로 내 파일들의 선행차이 0 을 확인한다). [코디] - 🔑2026-09-17 자체 드롭리스트(
droplist.js)를 새 화면에 붙일 땐initDrops()호출과 기존<select data-native="1">부착을 «한 작업으로» 묶는다.dropOpen만 부르면 마우스 클릭만 되는 반쪽 팝업이고(화살표·Enter·Esc·바깥클릭 닫기·스크롤 추종이 전부initDrops의 전역 리스너다 = WCAG 2.1.1 위반), 반대로initDrops()만 부르면 그 화면의data-native없는 select 를 전부 자체 팝업으로 바꾼다(견적 등록화면에선 정렬 2 + 계산기 모달 3 + 행마다 새로 그려지는 마감재 select가 조용히 회귀할 뻔했다 — 동적 생성분은 위임으로 못 막으니 마크업 문자열 자체에 박아야 한다). ES module 이라 일반 스크립트에선 템플릿의<script type="module">이window.pfsDropOpen같은 함수 하나만 내보내 넘긴다. 팝업 기본 12/11px 은 13px 하한 미달이라 그 화면 CSS 에서만 올린다(공용 파일을 고치면 pncad·네스팅·업무패널 회귀). 동명이인은providers문자열 재매칭 대신dropOpen(anchor, items, it => pick(it._row))로 원본 객체 참조를 넘겨 푼다. [코디/web-designer] - 🔑2026-09-17 「현장목록에 아직 없는 현장」은
field에 임시 행으로 넣지 마라 — 견적 안에 가둔다.field_dao.insert_field는production_status행까지 같이 만들고, 사이드카orderpaper-sync/orderpaper_sync.py가 현장명 토큰 매칭으로 발주서를 자동 연결한다 → 임시 행을 만드는 순간 생산현황에 뜨고 임시 현장에 실물 발주서가 붙는다.field를 FROM/JOIN 하는 SQL 이api/안에만 51곳/14파일이라 「목록 쿼리에 필터 바르기」는 언제 새도 새는 부채다. ⛒대신 그 도메인 테이블에 임시 컬럼(estimate.temp_site_name/temp_company_name, patch52)을 두고 표시만COALESCE(f.name, a.temp_*)로 잇는다 — 리포에 같은 패턴 선례가 이미 둘(hansem_estimate.site_namepatch47 ·worklog.site_name). 불변식 =field_no있으면temp_*는 NULL(service_normalize_master+ dao 연결 UPDATE 두 겹 — 화면에만 두면 API 직호출로 뚫린다). 🔴field_no = 0은 구 데이터의 「미지정」 더미다(운영 견적 263건) — JS 에서'0'은 truthy 라!field_no판정이 연결된 견적으로 오판한다(판정 함수 한 곳으로 모으고 SQL 은(field_no IS NULL OR field_no = 0)). 현장 생성 ↔ 견적 연결은 한 트랜잭션 + 견적FOR UPDATE선점검(더블클릭·묵은 탭이 고아 현장을 만든다). 실물 = devplanpfs-estimate-temp-site.html. [코디] - 🔑🎨2026-09-17 견적 등록화면에서
readonly만 풀면 타이핑한 값이 «조용히 사라진다».syncBasicForm()이 걷는 건 견적명·견적일·지불조건·비고 4개뿐이라, 현장명·발주처 칸을 편집 가능하게 만들어도 저장 payload 에 안 실린다(에러 0 · 저장은 성공 · 값만 없음). ⛒입력칸을 새로 열면 sync 함수에 그 칸을 같이 넣는다. 같은 화면의 다른 함정 = ①#f-field-name클릭이 현장검색 모달을 열게 돼 있어 직접입력 모드에선 그 핸들러를 갈라야 타이핑이 된다 ②견적명 자동조립(applyAutoEstimateName)은pickSite에서만 불려서 직접 입력 땐input이벤트로 따로 걸어야 따라온다 ③목록 배지.label은font-size:75%(=10.5px, 13px 하한 미달)이고 템플릿의label-rouded는 오타(label-rounded)라 캡슐 모양이 원래 안 먹는다 → 새 배지는 그 오타를 복제하지 말고 전용 클래스 13px 로 만든다. [코디/web-designer] - 🔑2026-09-16 「글자를 눌러 그 자리에서 고친다」를 기존 클릭 위임 화면에 넣을 땐 «그 글자 자체»에
data-act를 박는다. 업무패널 수기 카드 제목이 그랬다 — 제목은data-act="fold"인 머리글 안에 있어서 span 만 바꾸면 클릭이 항상 fold 로 먼저 걸린다.closest('[data-act]')는 자기 자신부터 보므로 span 에data-act="editname"하나를 더하면 「글자 위 = 편집 · 바깥 = 접기」가 저절로 갈린다(더블클릭 타이머·연필 아이콘 같은 새 제스처가 0개). ⛒같이 안 넣으면 반드시 재현되는 가드 4종 = ①편집 중 fold 금지(입력칸이 그 머리글 «안»이라 커서 옮길 때마다 카드가 접힌다) ②스와이프/드래그 제외 ③호버 전문(.peek) 제외 ④주기 새로고침 건너뛰기. ④는 🔴render()만 건너뛰면 안 된다 — 데이터(ITEMS)는 바뀌고 DOM 은 옛것이라data-i가 남의 항목을 가리킨다 →load()자체를 거른다(document.querySelector('.card.editing, .memo:not(.hide) textarea:focus')). 같은 구멍이 비고칸에도 있었다(적던 글이 소리 없이 증발). 그리고 수정 게이트는 화면이 아니라 dao 의 WHERE 절에 박는다(source='수기') — 화면에만 두면 API 직호출로 뚫린다. 실물 =web/templates/basic/field/worklog_panel.html·PATCH /api/worklog/<no>/name. [코디] - 🔑🎨2026-09-16 도어발주서 「월드 탭」 = 월드산업 도장 발주서다 — 포인탭을 «제품 단위»로 접어 옮긴다. 두 탭은 36행 1페이지 기하가 완전히 같다(헤더 10~11행 · 품목행 12~36 = 25줄) → 페이지 1:1 이관이 기본이고, 월드탭 2페이지 이후 헤더엔
=$C$6·=$H$6·=$J$4·=$M$4가 이미 있어 1페이지만 채우면 나머지가 따라온다. 월드 양식 = 포인 − O열 엣지 − T열 바코드 − P~S 공정상태 − U열 추가내용, 수신월드산업(FAX:031-769-1498). [페페] 접는 규칙 = ①EP·S/R·휠라·목찬넬큰제목은 큰제목만 남기고 하위 비고를 큰제목 L 에 몰아넣는다 ②오픈장·이동(형) 오픈장은 블록 통째 유지 ③큰제목에 안 묶인 단독행(도어·키큰도어·문틀재·걸레받이)은 규격·수량·비고째 그대로. 🔴「하위 3개 이상」을 기계 기준으로 쓰지 마라 — 그러면키큰EP 85*390*2990/420*2990(하위 EP 3 + 속대 1 = 4)이 「유지」로 넘어가 ①과 정면으로 어긋난다. 품목 종류가 1순위, 개수는 그 종류를 설명하는 말이다. 🔴하위행을 「다음 큰제목까지」·「빈 줄까지」로 자르면 뒤에 붙은 단독행이 통째로 삼켜져 사라진다(실측 나진MH = 문틀재 8 · 도어/키큰도어 6 · EP 2 · 걸레받이 1행) → 큰제목 종류별 허용 부재집합(EP→{EP,속대}· S/R·휠라→{휠라,속대}· 목찬넬→{목찬넬,걸레받이})에서 벗어나는 첫 행에서 끊는다. 큰제목 행은D:K병합(비고는L그대로 살아 있다). 큰제목 문자열이 치수·수량을 이미 갖고 있어(키큰EP 35*390*2990=1 ㄱ자의=1이 수량) 접은 줄엔 규격칸을 안 채운다. ⚠한 현장 폴더에 「포인 …」·「월드 …」 두 파일이 같이 있고 내용이 든 쪽이 월드 파일일 수 있다 — 파일명이 아니라 포인탭 채움 페이지 수로 입력을 고른다. 도구 =C:\dev\pfs\orderpaper-sync\poin2world.py(--apply없으면 계획만 · 접는/유지 종류는 상단PARTS·KEEP_KEYWORDS두 줄). 상세 =projects/pfs/session-2026-09-16-door-order-world-tab.md· devplanpfs-door-order-world-tab.html. [페페/코디] - 🔑📠2026-09-16 발주서 「팩스까지 발송」 = 코디가 끝까지 한다 — SINDOH FAX 드라이버 팝업을 win32gui 로 조종한다.
ws.PrintOut(1,1,1,False,'SINDOH D450/CM Series FAX')를 걸면 PrintOut 이 블록되고 별도 프로세스에#32770「팩스 전송 팝업」이 뜬다. 🔴uiautomation은 이 창을 아예 못 본다(GetRootControl().GetChildren()에 안 나오고 스캔 루프째로 멈춘다) — GUI 조사 1순위는win32gui.EnumWindows다. 컨트롤 ID =1990명칭 Edit ·2053팩스번호 Edit ·2041「송부처 목록에 추가」(번호 전엔 disabled) ·2047송부처 ListView(LVM_GETITEMCOUNT 0x1004) ·2048「현재의 등록 건수 n/100」 Static ·1확인(=전송, 목록 비면 disabled) ·2취소. 순서 = 두 Edit 에 한 글자씩WM_CHAR(WM_SETTEXT 만 쓰면 EN_CHANGE 가 안 떠 추가 버튼이 안 살아날 수 있다) → 2041BM_CLICK→ 목록 1건·확인 enabled 를 확인하고 나서 1BM_CLICK. 팩스번호는 구분자 없이(0328151589). ⚠보낸 결과는 PC 에서 못 읽는다 —192.168.0.201은 KM PageScope 계열 SPA 라 통신이력 XML 이 관례 경로에 없다(전부 404) → 기기 통신관리보고서가 유일한 확인 경로. 대본 =projects/pfs/session-2026-09-16-hakdong-pnr-order-fax.md. [코디] - 🔴2026-09-16 발주서 xlsx 를 COM 으로 만질 땐 셋을 지킨다 — ①Bash 에서 돌리지 마라 ②날짜는 시리얼 정수 ③NAS UNC 를 직접 열지 마라. ①Bash 도구는 Session 0 이라
Version만 읽히고 그 다음부터 전부-2147418111 RPC_E_CALL_REJECTED다(재시도로 안 풀린다 = 코드 버그가 아니다) →schtasks /create … /it+ 절대경로 pythonw.exe + 파일 로그(pythonw.exe를 bare 로 적으면 예약작업이 로그조차 안 남기고 아무것도 안 한다). ②datetime.date는 COM 이 거부하고datetime.datetime은 VT_DATE 변환에서 KST+9 때문에 하루 밀린다(9.16 →09 .15(화)로 찍혔다) →(d - date(1899,12,30)).days. ③NAS UNC 를 Excel 로 바로 열면 보호된 보기·링크갱신 프롬프트가 COM 을 막는다 → 로컬 사본 작업 후nasget.py put(좀비 EXCEL.EXE 가 파일을 물고 있으면get이NtStatus 0xc0000043). [코디] - 🔴🔑2026-09-16 Playwright 검사기가 「화면 밖 요소」를 집으면 운영에서만 무더기 FAIL 한다 — 코드를 의심하기 전에 «좌표»를 의심해라. 업무패널 그룹 폴딩을 넣고 검사기가 모든 그룹을 펴자 목록이 길어졌는데(운영 60장·5그룹 / 테스트 38장·3그룹),
document.querySelector('.card:not(.open)')첫 번째가 뷰포트 위로 나가거나position:sticky머리글에 가려 마우스 드래그 5항목이 운영에서만 한꺼번에 죽었다. 테스트는 목록이 짧아 우연히 통과해 배포 전에 안 걸린다. ⛒요소 선택에 «화면 안에 있나»를 조건으로 넣는다(r.top >= listTop + 머리글높이 && r.bottom <= listBottom). 같은 뿌리로 그날 둘 더 잡았다 — ⚠호버 전문(.peek)이 걸린 카드를 재면-webkit-line-clamp가none이라 「3줄 클램프」가 실행마다 들쭉날쭉했다(앞 단계 클릭이 커서를 목록에 남기면 재렌더 뒤 아무 카드나 peek 가 된다 → 측정 대상을:not(.peek)로 못 박는다) · 🔴검사기 안에서 「오늘」을new Date().toISOString()(UTC)으로 재면 KST 오전에 그날 통화가 「미래」로 잡혀 헛 FAIL(09:16 KST = 00:16 UTC) →Date.now() + 9h로 잰다(전역 KST 규칙은 검사기에도 적용된다). [코디] - 🔴2026-09-15 pointer 이벤트로 만든 가로 스와이프는 폰에서
touch-action을 안 적으면 브라우저·시스템이 먼저 채간다. 판정식(|dx| <= |dy|*1.5면 스크롤 몫)이 맞아도 그 전에 스크롤·뒤로가기 제스처로 넘어간다 → 스와이프 대상에touch-action:pan-y(세로는 브라우저, 가로만 JS). PC 마우스엔 영향 0. ⚠터치스크린 PC 도pointer:coarse다 — coarse 로 버튼을 키우면 좁은 폭(패널 400px)에서 버튼 줄이 안 접히는지 검사로 박아라(scripts/_wt_touch_probe.py). [코디] - 🔴🔑2026-09-15
worklog.site_name은 «추정값»이다 — 화면에 현장 이름을 찍을 땐 연결된field.name을 써라. 업무패널에 현장별 그룹 머리글을 붙였더니 같은 현장(field_no=2183용산 나진 상가)에 붙은 카드들의site_name이 8종이었다(「욕실 현장(추정)」·「넥서스(김영민 부장)」·「넥서스(김영민 부장) 현장 - ULC 공용욕실」·빈값…). 전사 파이프라인이 방 이름에서 만든 이름이라 그렇다 → 머리글을site_name으로 잡으면 그날 맨 위 카드가 뭐냐에 따라 제목이 바뀐다. ⛒패널 큐 SELECT 에f.name AS field_name추가(LEFT JOIN field는 이미 있어 조인 0 추가)하고 카드 현장칸·머리글 둘 다field_name우선, 미연결일 때만site_name. 같은 뿌리 = 26.8.31 「현장 배지의 진실은field_no다」. [코디] - ⛔2026-09-15 패널에 카드를 넣을 때 상태 「완료」를 쓰지 마라 — 넣는 순간 사라진다. 패널 큐는
_PANEL_STATUSES = ('진행중','대기','보류')만 본다(완료는 「털어낸 것」이라 빠지는 게 맞다). 참고용으로 남길 기록도 진행중/대기로 넣는다. [코디] - 🔴🔑2026-09-15 BOM 그룹키에 속성을 더하면 그 키에 매달린 「사람이 넣은 값」이 통째로 고아가 된다 — 승격을 같이 넣어라. 네스팅 경면코드 자동적용이 그 경우 — 경면이 채워지는 순간
keySide가…|민자|양면→…|소프트엠보|양면으로 바뀌는데, 여유자재(opts.spare)·발주완료(opts.doneColors)는 옛 키 문자열에 매달려 있다. 승격(promoteFinishSlot) 없이 job2 를 열면 발주 98매가 75매로 조용히 준다(에러도 화면 경고도 없다 — 숫자만 틀린다). ⛒loadJob에 이미 있던 「5토막→6토막」 승격 옆 같은 자리에서 값도 승격한다. 새 속성마다 이 두 곳(여유·체크)을 같이 본다. [코디] - 🔴2026-09-15 자동채움을 넣을 땐 「값이 들어오는 경로」를 전부 세어라 — 붙여넣기는 행추가와 다른 길로 온다. 경면 자동채움을
addPartRow한 곳에만 걸면 저장본·pncad·imos 는 낫고 붙여넣기만 조용히 빠진다:setupPaste는addFn()으로 빈 행을 먼저 만들고(그 시점엔 컬러가 없다) 나중에fillRow가el.value=v로 박는다(이벤트도 안 쏜다). ⛒채우는 지점 = ①addPartRow(값과 함께 오는 경로 ·renumberParts「앞」에 채워야 그 호출의applyColorTints·컬러패널이 새 값을 본다) ②setupPaste두 분기의applyColorTints직전 전체 훑기 ③컬러칸input·change. ⚠자재표(matGrid)엔 경면 칸이 없다 →querySelectornull 가드 필수. [코디] - 🔑2026-09-15 컬러명 꼬리 괄호 = 경면코드다(페페 지시).
HS1470JB(UK)→UK ·HS-50-010(소프트엠보)→소프트엠보 ·HS2685LS(UM)→소프트엠보(UM 은 소프트엠보의 다른 표기, 페페 확정). 별칭은FINISH_ALIAS표로 둔다(늘어도 한 줄). ⚠finishOf('')가 '민자' 를 돌려주므로 BOM 키의 「민자」는 「안 쳤다」와 「민자라고 쳤다」를 구분하지 못한다 — 파이썬(nesting_order.finish_resolve)은 둘 다 코드를 유도한다. 괄호 있는 컬러에 「일부러」 민자를 지정하는 자재가 생기면 키에 표식을 따로 넣어야 한다. 덤 = 드롭리스트에서 「민자」를 고르면 규격이 「경면양면」·비고에 「(민자)」가 찍히던 잠복버그도 같이 고쳤다. [코디] - 🔴2026-09-15
orderpaper_build.fill_vendor_tab도 24행 캡노트를 안 피하고 있었다(부자재 발주서에서만 고쳐 놨다). 행번호를 누적하면 언젠가 24행을 밟는다 → 슬롯 리스트(ITEM_ROWS) 소비로 바꿔 그 경로 자체를 없앴다. 17건을 넘으면 행을 늘리지 말고 29행 블록 복제(_clone_block) + 🔴D26 입고처 주소 수식은$C26상대참조라 복제해도 26행을 계속 가리킨다 →RECEIVER_FORMULA.format(r=…)로 다시 쓴다.copy_worksheet는 인쇄설정을 안 옮기므로_copy_page_setup도 같이. [코디] - 🔑2026-09-15 엣지 폭 = 보드두께 + 4mm, 재고에 없으면 다음 상위 규격(가구KB 03 §7 · 운영
orderpaper실적 분포 22/19/16/24/27/29/34mm 가 정확히 이 규칙 · 페페 확인). 실사용 = 15·18T →1T*22mm· 22·23T → 27mm · 25·30T → 34mm. 엣지 발주 제외 = 3·4.5·6T(네스팅isEdgeThk가 원래 뺀다) + 9T·12T(페페 지시 26.9.15 「엣지발주 안해도 됨」). 발주 단위 = 50m 올림. [페페/코디] - 🔑2026-09-15 원부자재 발주서 실적 관행 — 보드 업체(상원보드·효산엘피엘 등)는 오엠씨 탭이 아니라 일반 「발주서」 탭 양식을 쓴다(품명=컬러코드, 규격=
18T PB 경면양면). 한 업체 = C2 하나 = 탭 하나, 입고처가 둘이면 중간에 「입고처」 행을 끼워 가른다(학동매장 선재기업 시트). 효산엘피엘 실적 =칼라샘플(→진영LDM, 엣지 제작사 색 대조) +피스마개 스티커(→포인) + 보드. 피스마개 실소요 = 보드 1매당 160EA(momtong_order.CAP_PER_BOARD· 가구KB 05 §15). [코디] - 🔴🔑2026-09-15 문자열로 HTML 에 뭘 끼워 넣는 코드는 「마지막」 닫는 태그를 기준으로 삼아라 — 첫 태그로 잡으면 JS 문자열·주석 안으로 들어가 조용히 죽는다. QA 헬퍼 자동주입이
body.replace('</body>', …, 1)이었는데nesting.html은 인쇄창을 만드는 JS 템플릿 문자열 안에 그 태그가 통째로 들어 있다(1344행) → 스크립트가 그 문자열 속으로 들어갔다. ⛒head, tail = body.rsplit('</body>', 1)— 진짜 닫는 태그는 언제나 마지막이다. 🔴증상이 「콘솔 에러 0 · 위젯만 없음」이라 눈으로도 로그로도 안 잡힌다 — 전수 검증기가 유일한 그물이었다(47화면 중 2화면만 FAIL 로 떠서 발견). [코디] - 🔴🔑2026-09-15 주석은 「데이터」다 — 문자열 검사를 하는 코드 앞에서 주석에 셀렉터·태그 리터럴을 적지 마라. 한 세션에 세 번 당했다: ①
field_orderpaper.html:473이 CSS 주석에 토글 id 를 적어 둬서 주입기 중복가드('#qa-helper-toggle' in body)가 「이미 있음」으로 오판 → 그 페이지만 위젯 증발 ②내가 새로 단 주석에 닫는 태그를 써서 주입 위치가 그리로 갔다 ③제거 표시 주석에 옛 id 를 써서 소스 검사기가 「안 지워졌다」로 읽었다(어서션이 잡았다). ⛒가드는 자산 URL 정확일치('/static/js/qa_helper.js' in body)로 좁히고, 주석엔 그 리터럴 대신 풀어 쓴다(「문서 끝에」·「옛 id 체계」). [코디] - 🔑2026-09-15
send_from_directory로 나가는 화면은direct_passthrough라after_request가 통째로 건너뛴다 — 전역 규칙에서 빠지는 구조적 구멍이다. PNCAD·네스팅이 이것 때문에 QA 헬퍼 자동주입을 못 받았고, 그래서 각자 손으로 때웠고, 때우면서 관리자 게이트가 빠졌다(견적 v2 3종도 같은 뿌리로 무게이트라 일반 사용자에게 QA 위젯이 떴다). ⛒고칠 곳은 라우트가 아니라 주입기다 —response.direct_passthrough = False로 풀고Cache-Control: no-store+ ETag/Last-Modified 제거(세션따라 본문이 달라지는데 원래public, max-age=43200이 붙는다). 라우트를 고치면 3곳이고 새 정적 페이지마다 또 빠진다. 실측 = Werkzeug 0.15.2 통과. [코디] - ⚠2026-09-15 Git Bash 에서 「경로꼴 인자」는 MSYS 가 윈도우 경로로 바꾼다 → 검사가 엉뚱한 화면에서 돌고 ALL PASS 로 속인다.
_qa_helper_live_check.py test /production/nesting이 인자를 잃고 기본값/field/list에서 돌았는데 결과는 ALL PASS 라 그대로 믿을 뻔했다(출력 첫 줄[target] … /field/list가 유일한 단서). ⛒MSYS_NO_PATHCONV=1. 판정 = 스크립트가 자기가 실제로 뭘 검사했는지 먼저 찍게 만든다. [코디] - ⚠2026-09-15 클립보드 검증은 테스트(http)와 운영(https)에서 코드 경로가 갈린다. http 는 보안 컨텍스트가 아니라
navigator.clipboard가 아예 없어 textarea+execCommand폴백을 타고, https 는 clipboard 를 쓰는데 헤드리스가 쓰기 권한을 거부한다(NotAllowedError— 실브라우저는 클릭 제스처로 허용되니 제품 버그가 아니다). ⛒context.grant_permissions(['clipboard-read','clipboard-write'])+ 두 경로를 다 받게 쓴다. 안 그러면 운영에서만 FAIL 2건이 떠 배포를 되돌릴 뻔한다. [코디] - 🔴🔑2026-09-15 «목록을 공급자에서 미리 자르지 마라» — 검색이 통째로 죽는다. 부자재 발주서 현장 칸이
FIELDS.slice(0,400)을 후보로 올렸는데 운영 현장은 571건, 「용산 나진 상가」가 568번째라 페페가 뭘 쳐도 안 나왔다(페페 지적).droplist.js는 필터 «뒤»MAX_ROWS로 자르고 「N개 더 — 검색으로 좁혀라」를 붙이도록 설계돼 있다 → 공급자가 먼저 자르면 잘린 뒤쪽은 검색 대상에서 영구 제외된다. ⛒공급자는 전량을 주고 정렬만 한다(현장은field_no역순 = 최근 먼저). 같은 함정 후보 =providers.so_recv·worklog 패널providers['field'](열 때 30건으로 잘라 둔다). [코디] - 🔴🔑2026-09-15 검색은 «공백 무시 + 낱말 AND» 로 짠다 — 사람은 마스터의 띄어쓰기를 안 맞춘다. 「나진상가」로 「용산 나진 상가」를 못 찾은 게 그 경우. 처방을 공용
droplist.js renderList()에 넣어 모든 칸이 한꺼번에 낫게 했다(낱말마다v·sub어느 쪽에든 있으면 통과 → 어순 뒤바뀜·업체+현장 혼합도 잡힌다). 옛 부분일치가 통과시키던 항목은 전부 그대로 통과한다(낱말은 질의의 부분문자열) = 목록이 줄어들 회귀는 구조적으로 없다. ⛒화면만 고치면 반쪽이다 — 서버 LIKE 검색도REPLACE(컬럼,' ','')로 같이 맞춘다(sub_orderpaper_dao.select_list). [코디] - 🔴🔑2026-09-15 자체 드롭리스트의 선택
change는 «이제 버블한다»(bubbles:true, pncada2309eb) — 아래 「캡처로 받아라」 항목은 그 전 이야기다. 원래new Event('change')라 버블하지 않아document·tbody위임 리스너에 영영 안 닿았다. 네스팅·업무패널은 캡처로 우회해 놨는데(그 둘은 지금도 정상), 부자재 발주서만 버블 위임이라 현장 선택이 통째로 죽어 있었다 — 골라도field_no·가구업체가 안 붙었다(페페 지적으로 발각, 화면엔 에러가 안 난다). ⛒합성 이벤트는 네이티브와 같게 만든다 — 네이티브change·input은 버블한다. 우회(캡처)로 덮으면 그 화면만 낫고 다음 화면이 같은 함정을 다시 밟는다. [코디] - 🔑2026-09-15 자동채움 값은 «비어 있을 때만» 이 아니라 «사람이 손댔나»(dirty)로 가른다. 부자재 발주서 가구업체가 그 경우 — 현장 A 를 골라 자동채움된 뒤 현장 B 로 갈아타면 「안 비어 있으니」 안 갱신돼 B 현장에 A 업체명이 남았다(발주서가 틀린다). ⛒입력칸에
input·change로dataset.dirty='1'을 찍고, 자동채움은dirty가 아닐 때만 덮는다. 저장본을 열 때 값이 있으면 사람이 정한 값으로 보고 dirty 로 시작한다. [코디] - 🔴🔑2026-09-15 「자재를 가르는 속성」을 새로 넣을 땐 «그 속성을 쓰는 키 전부»를 같이 올려야 한다 — 하나라도 빼면 조용히 데이터가 샌다. 네스팅 경면(빈칸=민자) 추가가 그 경우 — 원장 그룹키(
keySide)만 고치고 발주완료 체크 키(doneColors)를 컬러명 그대로 두면 「화이트/경면」을 체크했을 때 「화이트/민자」까지runNest에서 빠져 «발주 누락»이 된다(web-designer 가 실측으로 잡았다. 화면은 멀쩡해 보이고 결과만 틀린다). ⛒새 속성이 생기면 그룹키·체크키·구분색키·저장키를 한 함수(colorGroupKey)로 모아 한꺼번에 바꾼다. 옛 저장본은 키에 그 자리가 없으니 로드 때 기본값으로 승격한다(화이트→화이트|민자, 여유 키 5토막 → 6토막). [코디] - 🔴2026-09-15
setupPaste는 «PART_COLS 배열 순서»로 칸을 맞춘다 — 화면 순서로 컬럼을 중간에 끼우면 기존 엑셀 붙여넣기가 통째로 한 칸 밀린다.newCol:1은 «옛 9열» 판정에서만 빼줄 뿐, 11열·12열 경로(inputCols=allCols)는 못 지킨다(비고 칸 주석이 경고하던 그 함정). ⛒붙여넣기로 받을 필요가 없는 칸(손입력·드롭리스트 전용)은pasteSkip:1로 매핑에서 아예 뺀다 —allCols=cols.filter(c=>c.type!=='seq' && !c.pasteSkip). 그러면 화면 위치는 자유롭게 정하면서 9열·11열 붙여넣기가 한 칸도 안 밀린다. [코디] - 🔑2026-09-15
nesting_job은parts·mats만 LONGTEXT 고opts·summary는 «TEXT(64KB)» 다. 저장본에 뭘 더 담을 땐 이 구분부터 본다 — 스키마리스 JSON 이라 작은 값은 DB 변경 0 으로 얹히지만(여유자재opts.spare, 소요량summary.bom= 실측 764B), 네스팅 배치도(sheetsOut.placed) 같은 좌표 통짜는 64KB 를 넘길 수 있어 LONGTEXT 컬럼 신설 = DB 스키마 변경(§2-5 승인 게이트) 이 붙는다. ⛒「결과를 저장해달라」는 요구는 먼저 «재계산으로 되살아나는 부분»을 빼고 본다 — 저장본을 열면loadJob이runNest()를 부르므로 배치도·소요량은 다시 그려지고, 정말 못 되살리는 건 사람이 손으로 넣은 값(여유자재)뿐이었다. 그 구분을 안 하면 수백 KB 스냅샷 + 스키마 변경을 떠안는다. [코디] - 🔴🔑2026-09-15
position:absolute; inset:0의 부모가<span>이면 «display 를 반드시 준다» — 안 주면 자식이 엉뚱한 자리로 튕겨 나간다. 네스팅 4차 배포 직후 페페 지적 「컬러 컬럼이 갑자기 선택이 안된다」의 원인..cellwrap을 패널엔inline-block으로 줬는데 부재표엔 안 줘서 기본값inline이었고, inline 을 컨테이닝 블록으로 삼으면inset:0이 「라인 조각」 기준으로 계산된다 — 실측 라벨x=321 w=51인데 input 은x=420 w=36(=.grid-scroll input{min-width:36px}만 남은 폭)으로 칸 바깥에 있었다. 투명 input 이라 눈에 안 보이고, 칸을 눌러도 hit test 가span.cell-label에 걸려 아무것도 포커스되지 않는다. ⛒처방 =display:block; width:100%+.cellwrap input{min-width:0}. 같은 클래스를 두 표에 쓸 땐 display 를 공통 규칙에 박는다(한쪽에만 주면 다른 쪽이 조용히 깨진다). [코디] - 🔴🔑2026-09-15 화면 검증은 「요소」가 아니라 «사람이 누를 좌표»를 눌러야 한다 — 이게 위 사고를 통과시킨 구멍이다. 23항목 자체검사가 ALL PASS 였는데도 페페가 첫 클릭에서 잡아냈다. 이유 =
page.click('input[data-k=color]')는 Playwright 가 그 요소를 찾아 제 위치를 계산해 누르므로 input 이 화면 밖·엉뚱한 자리에 있어도 통과한다. ⛒mouse.click(<보이는 것의 좌표>)+document.elementFromPoint()hit test 를 넣어라 — 「보이는 자리를 눌렀을 때 의도한 요소가 잡히는가」는 이 두 가지로만 증명된다. 겹침·오버레이·pointer-events·z-index를 쓴 화면은 예외 없이 이 항목을 넣는다. [코디] - 🔴2026-09-15 표 셀에서 「길면 2줄·짧으면 폭 오토」를 하려면 라벨(span)이 폭·높이를 만들고
<input>은 그 위에 «투명하게» 겹친다. 함정 3개가 연달아 나온다 — ①<input>은 네이티브로 줄바꿈이 안 된다(CSS 로도 불가) ②<textarea>로 바꾸면 자체 드롭리스트가 통째로 죽는다 —droplist.js가t instanceof HTMLInputElement로 게이트한다(focusin·click·input 3곳) ③비편집 input 을display:none으로 숨기면 방향키 셀 이동이 조용히 죽는다 —focusCell()의el.focus()가 숨은 요소엔 안 먹는다. ⛒opacity:0+position:absolute; inset:0이면 포커스·클릭·탭 순서가 다 살아 있고 클릭하면 곧바로 편집이 된다. 🔴-webkit-line-clamp(=display:-webkit-box)를 쓰면 «테이블 auto-layout 이 그 컬럼을 안 늘린다» — 라벨이 thmin-width에 갇혀 20자가 4줄로 접히고 2줄만 보인다(실측scrollH 78vsclientH 43). 폭이 고정인 표(table-layout:fixed)에선 clamp 가 맞고, 폭을 내용에 맡기는 표에선display:block+width:max-content+max-width:<N>em+max-height:calc(2줄 + 자기 padding)이다(box-sizing:border-box라 padding 을 안 더하면 2줄이 안 들어간다). 한글 1자 = 13px 에서 정확히 1.000em(ch는 '0' 폭 기준이라 한글엔 못 쓴다). [코디] - 🔴2026-09-15 자체 드롭리스트는 목록에서 고를 때
change«만» 쏜다(droplist.js comboOpen→dispatchEvent(new Event('change'))).input이벤트만 듣는 화면은 목록으로 고른 값에 반응하지 않는다 — 네스팅에서 「컬러를 목록에서 고르면 구분색·사용컬러 패널이 안 따라오는」 조용한 누락이 그 이유였다(손입력만 동작해 눈치채기 어렵다). ⛒드롭리스트를 쓰는 칸의 위임 리스너는['input','change']둘 다 등록한다. [코디] - 🔴2026-09-15 원부자재발주서 양식 24행은 품목행이 아니라 「캡노트」 행이다(
C24:H24병합 = 「현장명, 상품 내역 표기 바랍니다」). 거기 규격·발주량·입고일을 쓰면 openpyxl 이 예외도 없이 값을 버린다 — 발주서 한 장마다 품목 1건이 규격·수량 없이 나간다. 품목행은[8..23]+[25]= 17행. ⛒자체검사가 이걸 통과했었다 — 품명(B열)만 봤기 때문이다(B24 는 병합 밖이라 멀쩡히 써진다). 엑셀 산출물 검사는 품명이 아니라 «규격·수량»까지 찍어 본다. 29행 = 1페이지 블록(NO=ROW()-7-(29*ROUNDUP(ROW()/29,0))+29· 매수=ROUNDUP(ROW()/29,0))이라 넘치면 행을 늘리지 말고 블록을 통째로 복제한다. 복제는 채우기 전에 끝낸다(병합 뒤엔 MergedCell 이라 value 대입이 read-only).copy_worksheet는 인쇄설정·페이지나눔을 안 옮긴다. [코디] - 🔴2026-09-15 pymysql 집계값(
SUM/COUNT)은DECIMAL이라jsonify가 못 싼다 → 500. 부자재 품명 자동완성/api/sub-orderpaper/suggest가 첫 배포에서 그걸로 죽었다(SQL 자체는 서버에서 855행 정상). ⛒집계는ORDER BY에만 쓰고 응답에서 뺀다. 날짜도 같은 이유로 필요 없으면 빼는 게 싸다. [코디] - ⚠2026-09-14 사이트맵 「미반영 N」은 남의 세션 작업에 오염된다 — 숫자로 판정하지 마라. 내 배포 직후 페이지 58→59·미반영 1→20 으로 튀었는데 범인은 다른 세션이 master 에 밀어 둔 worktray 커밋 2건(12파일 미배포)이었다. ⛒판정은
git diff --stat origin/deploy-live origin/master -- web/ api/에 «내 파일»이 있는지로 한다(없으면 내 배포분은 반영된 것). pfs 는 여러 세션이 동시에 쓰는 리포다 — 선별 포워드가 남의 미완성 코드를 막아 주지만 보고 숫자는 내 것만이 아니다. [코디] - 🔴2026-09-14 셀을 꽉 채우는 버튼이 있으면
td배경을 칠해도 안 보인다. 네스팅 MDF/PB 칸이 그 경우 —.toggle-cell{width:100%}+.grid-scroll td{padding:0}이라 버튼이 td 를 완전히 덮는다. 버튼에 발라야 한다. ⛒그리고.x.warn은.x:hover와 특이도가 같다(둘 다 0,3,0) → 상태 클래스를:hover·td.cellsel규칙 뒤에 안 두면 마우스만 올려도 경고가 조용히 사라진다. 범위선택까지 이기려면td.cellsel .x.warn(0,4,1)을 따로 쓴다. [코디] - 🔑2026-09-14 네스팅 「사용 컬러」 패널 = 이제 편집 표다. 컬러명을 고치면 부재 표만 일괄 치환된다(자재표는 페페 확정으로 제외 — 고친 뒤 「⚙ 자재 자동등록」 재실행 필요). 병합은 확인창 1번. 발주완료 체크는 미완료 쪽으로 보수적(개명=해제 · 병합=양쪽 완료일 때만 유지) — 체크가 남으면 그 자재가 네스팅에서 빠져 발주 누락이 된다. 구분색은 단순 개명일 때만 이관. 검사기 =
scripts/_nesting_rename_warn_check.py. [코디] - 🔑2026-09-14 공용 컴포넌트의 폰트가 하한(13px)을 어길 땐 «공용 파일」을 고치지 말고 그 화면에서만 올린다.
droplist.js팝업은#pcDrop{font-size:12px}·.dp-sub{11px}인데, 공용 파일을 고치면 pncad·네스팅까지 같이 커진다(회귀 2곳). 업무패널에선#pcDrop{font-size:13px !important}3줄로 자기 화면만 올렸다. ⚠게다가 그 파일은 사본이라(아래 항목) 고치면 다음 sync 에 날아간다 — 두 이유가 같은 결론을 가리킨다. [코디] - 🔴2026-09-14 QA Helper 는 «모든» HTML 응답에 after_request 로 주입된다 — 좁은 전용 화면은 명시적으로 빼야 한다. 400px 상주 패널(
/field/worklog-panel)에 위젯이 얹혀 화면을 덮고 12px 글자(qa-h-*)가 섞여 13px 하한 검사가 FAIL 했다. 총관리자 세션에서만 나므로 일반 계정 검증으로는 절대 안 잡힌다. ⛒web/__init__.py_inject_qa_helper()에 경로 제외 1줄. 새 전용·임베드 화면을 만들면 이 주입을 먼저 떠올려라. [코디] - 🔴2026-09-14 PFS 로그인은
next를 안 받는다 — 로그인 후 무조건/field/list로 간다. 주소창 없는 임베드(웹뷰 앱·키오스크)에선 돌아올 길이 없어 화면이 통째로 못 쓰게 된다(업무패널 실기 스크린샷에서 잡혔다, 자동검증은 통과했었다). ⛒임베드 쪽이 책임진다 —NavigationCompleted에서 「로그인도 내 화면도 아닌 곳」이면 제 URL 로 되돌린다. 🔑Playwright 검증은 로그인 «후 리다이렉트 도착지»를 안 본다 — 이런 건 실기 캡처가 잡는다. [코디] - 🔴2026-09-14
pfs\web\pncad\*는 사본이다 — 고치면 다음 sync 에 날아간다. 단일 출처 =C:\dev\pncad\, 나르는 도구 =cd C:\dev\pncad && PYTHONUTF8=1 python tools/sync_to_pfs.py(--check= 차이만 보고). 대상 13개(app.html·editor.js·droplist.js·schema.js·lindiv.js·spec.html·data/*.json). ⛒pncad 원천 커밋 + sync + pfs 커밋 = 세 걸음이 한 작업이다(리포 둘 다 push). 정적폴더(web/static/)에 두면 안 된다 — 로그인 없이 열린다. [코디] - 🔑2026-09-14 자체 드롭리스트 펼침 방향 = 옵트인
data-drop-side="right"(컨테이너에 1개). 표 칸에서 아래로 펼치면 화면 하단 행에서 위로 뒤집혀 다른 행 12개를 덮는다(페페 QA 지적). 우측이면 같은 행 옆 2열만 덮는다. ⛔전역 적용 금지 — pncad 좁은 사이드 패널·모달(400px)에선 팝업이 카드 밖으로 30px 나간다(web-designer 실측). 기하 =top=셀 top·left=셀 right+6· 우측 부족하면 좌측 반전 · 세로는 뒤집지 말고 화면 안으로 클램프. ⚠place()진입부에서style.bottom을 먼저 지운다 — top+bottom 동시 지정이면 팝업 높이가 늘어난다. 적용처 =#partGrid·#matGrid. 검사기 =scripts/_droplist_side_check.py [test|prod](28항목). [코디] - 🔴2026-09-14
production_*이름의 파일은 Edit 도구가 통째로 막힌다 — 브로커 오탐이다.api/pfs/field/service/production_status_service.py를 고치려다auto-deny blacklist (운영/위험 명령)로 거부당했다. 범인 =C:\dev\tools\cody-permission-broker\whitelist.py의OPS_FILE_PATTERNS에 맨패턴production이 들어 있어production_status_*.py가 전부 「운영 배포 파일」로 잡힌다(prod는\bprod\b로 묶여 있는데production만 안 묶였다). ⛒게이트와 싸우지 말고 데이터 주인 쪽 모듈에 붙여라 — 이번엔 미입고 보드 조회를orderpaper_controller/service/dao에 두고 화면에서fetch한 벌을 더 부르는 쪽으로 돌아갔다(오히려 자재분류 파생 규칙이 있는 곳과 같아져 결과가 낫다). 패턴 수정은 안전게이트 완화라 페페 승인 대상. [코디] - ⚠2026-09-14
pfs/scripts/_*.py는.gitignore대상이다 — 검사기는 커밋되지 않는다.scripts/.gitignore가_로 시작하는 파일을 전부 무시해_xxx_check.py91개가 로컬 전용이다. 「검사기를 커밋했다」고 보고하면 거짓말이 된다 — 다른 PC·서버엔 없다. 커밋해야 할 검사기는-f로 강제하거나 이름에서_를 떼야 한다. [코디] - 🔑2026-09-14 생산현황 출력물 메모칸 = 좌(사용자 메모) / 우(미입고 보드) 2분할. 우측은 손입력 칸이 아니다. 파이프라인(
\orderpaper_autosync30분 → orderpaper DB,\IntakeCheck-1630매일 16:30 →status='완료')이 채운 값을 출력물이 열릴 때마다 재조회한다(GET /api/orderpaper/board-wait). 범위 = 생산현황에 뜬 현장만(production_status.field_status='20'+ 출력 쿼리와 같은 상차연도 조건, 페페 결정 26.9.14). ⚠orderpaper.is_empty는 NULL 이 있다 →<> '1'만 쓰면 NULL 행이 조용히 빠진다,COALESCE(is_empty,'0')로 받아라. ⚠숨김현장(orderpaper_site_map.is_hidden)은 안 거른다 — 생산현황에 떠 있는데 발주서정리에서 숨겼다고 미입고가 사라지면 사고다(라인 VIP 가 실사례). 검사기 =scripts/_memo_split_check.py [test|prod](22항목). [코디] - 🔴2026-09-14 부재 그리드에서 «파생 상태»를 들고 있으면 그리드를 통째로 교체하는 5경로에서 전부 비워야 한다 — 안 비우면 새 부재가 조용히 빠진다. 「자재발주 완료 컬러」(
doneColors)가 그 사례. 부재 tbody 를innerHTML=''로 갈아엎는 곳은clearParts·loadSample·runPncad·runMomtong·loadJob다섯이고, 이 중colorTintMap.clear()가 원래 있던 곳은loadJob·newJob둘뿐이라 나머지 셋을 그냥 빠뜨리기 쉽다. 그러면 옛 잡에서 체크해 둔 컬러가 새 출처(PNCAD·몸통재·샘플) 부재를 에러 없이 네스팅에서 제외한다 = 발주 누락. ⛒규칙 = 「tbody 를 통째 비우는 곳 전부 clear,loadJob만 opts 로 복원」.newJob은clearParts를 타므로 별도 처리 불필요. [코디] - 🔑2026-09-14 표 옆에 패널을 붙일 때 줄바꿈 문턱은
flex-basis가 아니라min-width로 잡는다. basis 로 잡으면 둘 중 하나가 깨진다 — basis 1000 고정이면 1280px 에서 패널이 아래로 떨어지고(1000+240+gap > 가용폭, flex 는 shrink 보다 wrap 이 먼저다), basis 를 줄이고grow:1을 주면 1920px 에서 표 칸은max-width로 멈추는데.col만 남는 폭을 다 먹어 패널이 866px 저편으로 밀려난다. ⛒정답 =flex:1 1 0; min-width:min(900px,100%); max-width:1000px— grow 가 폭을 채우되max-width가 상한,min-width가 곧 wrap 문턱(900+패널240+gap18 → 1180px 밑에서 아래로 내려간다),100%를 섞어야 좁은 화면에서 그 900px 이 페이지 가로넘침을 안 만든다. 실측 1920/1440/1280 한 줄 · 1024/800 아래로 · 가로넘침 0. [코디] - ⚠2026-09-14
scripts/_nesting_grid_modal_check.py는 노후해 로컬 92/96 이 정상이다 — 회귀로 오해하지 마라. FAIL 4건(작은 화면 660px · ArrowRight 마지막 칸 · 최하단 Enter 같은 열 · 404 콘솔)은 26.9.14 3차(비고칸remark추가 ·ENTER_JUMP수량→가로)를 이 검사기가 아직 안 따라간 것이다. 판정법 =git show HEAD:web/nesting/nesting.html > 그 자리로 기준선을 떠서 같은 수가 나오는지 본다(⛔git stash금지, 26.9.11 항목). 현역 관문은_nesting_qa5_check.py(98/98)와_nesting_colorpanel_check.py(47/47). [코디] - 🔴2026-09-14 네스팅 부재 그리드에 컬럼을 추가할 땐 「맨 끝 +
newCol:1」 둘 다 지켜야 한다.setupPaste가 열 개수로 옛 9열/새 11열을 가르는데(oldCols = allCols.filter(c=>!c.newCol)), 새 칸에newCol을 안 붙이면 그 판정이 9→10 으로 틀어져 기존 엑셀·tread 붙여넣기가 에러 없이 한 칸씩 어긋난다. 중간에 끼우면 11열 붙여넣기의 결방향이 새 칸으로 밀린다. 비고(remark) 추가가 그 사례(26.9.14). 저장은nesting_job.parts LONGTEXT= 스키마리스 JSON 이라 DB 마이그레이션은 필요 없다. [코디] - 🔴2026-09-14 표 행 드래그 이동의 손잡이는 「입력 요소가 없는 칸」으로 잡는다 — 순번칸(
.seqcell). 거기선cellEl()이 null 이라 기존 범위선택 mousedown 이 원래 무동작 = 같은 마우스 제스처를 두고 싸울 일이 애초에 없다. ⛔HTML5draggable은setupRange가 글자 드래그 막으려고 걸어둔 전역dragstart차단과 부딪혀 못 쓴다. 필수 4종 = 4px 임계(그냥 클릭과 구분) · 드롭선은box-shadow:inset(border 면 행 높이가 들썩인다) · 가장자리 자동 스크롤 · 키보드 대체Alt+Shift+↑↓. ⚠raw mouse 라 터치에선 안 먹는다. [코디] - ⚠2026-09-14 그리드 컬럼폭의 진짜 바닥은
th가 아니라.grid-scroll input{min-width:…}다.th의min-width를 아무리 줄여도 입력칸 바닥이 이기고(두께 54 지정 → 실측 61), 반대로 넓은 화면에선 table auto-layout 이 남는 폭을 전 칼럼에 비례배분해 두께칸까지 170px 로 부푼다(1920px 실측) →#partGrid{max-width:1000px}로 막는다. [코디] - 🔴2026-09-14 자체 드롭리스트 「재클릭 닫기」 판정은 click 이 아니라 mousedown 시점(포커스 이동 전)에 해야 한다. click 때 「이미 포커스 + 이미 열림」을 재면 빈 칸 첫 클릭(포커스와 열기가 동시에 일어난 것)까지 「이미 열려 있었다」로 오인해 즉시 닫힌다. [코디]
- 🔴2026-09-14 자체 드롭리스트 팝업 검사를 「지금 보이나」로 재면 운영에서만 조용히 FAIL 한다 — 검사 방식이 틀린 것이지 화면이 깨진 게 아니다. 팝업이 열린 직후 다른 비동기 렌더(탭 전환이 부른 imos 조회 등)가 droplist 의
scroll/focus리스너를 건드려 곧바로 닫는다 → 200~300ms 뒤 판정 시점엔 이미display:none(열린 흔적인 항목 수·top좌표는 남아 있다).networkidle대기·scroll_into_view로는 안 풀린다. ⛒열린 「순간」을MutationObserver(attributeFilter:['style'])로 잡는다. 운영 파일이 테스트와 바이트까지 같은데 테스트만 통과하면 코드가 아니라 검사 타이밍을 의심해라. 페페 육안으로 실사용 정상 확인(자재표 세로규격 → 2440/2700/3000 표시) 후 종결, 운영 115/115. [코디] - ⛔2026-09-14 pfs 화면에도 「크롬 목록·자동완성 금지」가 적용된다 — 자체 드롭리스트 한 벌만 쓴다. 정본 =
C:\dev\CLAUDE.md§3 +projects/web-designer/kb/08-forms-inputs.md§1-0(ruleguardchrome-native-dropdown, block). pfs 실물 =web/nesting/nesting.html이/production/pncad/droplist.js를 import 해서 pncad 와 같이 쓴다(원천C:\dev\pncad\droplist.js→sync_to_pfs.py). 🔴그 파일의 공개 표면(providers·initDrops·dropOpen·dropClose)이 바뀌면 네스팅이 조용히 깨진다 — pncad 동기화 뒤엔scripts/_nesting_qa5_check.py --test를 한 번 돌린다(그 4개 export 를 직접 확인한다). 그리드에 물릴 때 함정 4개 = ①키 충돌은 그리드 핸들러에if(e.defaultPrevented) return;한 줄(droplist 는 캡처에서 소비할 때만 preventDefault) ②값이 찬 칸은 focusin 자동열기만 억제하고 click 은 건드리지 마라(stopImmediatePropagation하면 범위선택 Shift+클릭이 죽는다) ③new Event('change')는 버블 안 한다 → document 에서 받으려면 캡처 ④팝업 CSS 는body #pcDrop으로 특이도를 올려야 이긴다(droplist 가<style>을 런타임 append 한다). [코디] - 🔴2026-09-14 표 위 도구줄을 sticky 로 고정할 땐
.col안에 두면 안 된다. sticky 의 컨테이닝 블록이 그.col이라 표를 지나 결과·BOM 으로 내려가는 순간 같이 사라진다 — 그런데 네스팅에선 그게 주 스크롤 동작이다. ⛒.wrap직속으로 빼고, 탭 전용이면showTab()이 표시를 토글한다. 🔑고정 높이를 px 로 박지 마라 — 640px(200% 확대)에서 탭바·버튼줄이 둘 다 접혀 126→201px 이 된다.recalcStickyTop()이 실측해--stickyH로 내리고 표max-height가 그만큼 줄게 한다. 실물 =.sticky-top/.sticky-sub(nesting.html). 검사도 함정 = 「한 줄인가/제자리인가」를 자식top으로 재면 오탐한다(sticky 는 핀 지점까지 같이 올라간다) → 핀 뒤 두 지점의 좌표가 같은지로 잰다. [코디] - ⚠2026-09-14 그리드 셀에 값별 배경색을 줄 땐 인라인
background가 아니라td커스텀 프로퍼티 + 클래스로 준다. 인라인은 범위선택 하이라이트(.grid-scroll td.cellsel)를 덮어 선택이 안 보인다. ⛒td.style.setProperty('--tint', …)+.tint클래스, CSS 는.tint를.cellsel앞에 선언(같은 특이도면 나중 규칙이 이긴다). 파스텔 팔레트에서 하늘#dbeafe·회청#e2e8f0은 쓰지 마라 — 각각 범위선택색#d7e8ff와 사실상 동일(색거리 4.6), 헤더·민트와 녹색약에서 뭉갠다(5.1). 확정 8색 =#fde2e4 #d8f3dc #ffe8cc #e9dcf7 #cdf0ea #fdf6b2 #f5d0c5 #d4e09b. 갱신 지점은renumberParts()만으론 부족 —addMatRow()·붙여넣기 두 갈래(fillRow직접 대입) 를 빠뜨리면 자재 그리드엔 색이 영영 안 먹는다. [코디] - 🔴🔴2026-09-14 자동화의 «성공 경로»에만 걸리는 검사를 만들지 마라 — 요율감시 배포 관문이 그랬다. 감시는 값이 바뀌었을 때(
applied)만 계산부 자체검사를 돌리는데, 그 검사가 「DC_RATES[0].rate === 0.0176」 을 못 박고 있었다 = 실제 요율 변경이 오는 날에만 깨져서 배포가 영구히 막힌다(한 번도 안 돌아본 경로라 26.8.27~9.14 동안 아무도 몰랐다). ⛒append-only 이력에 값을 고정할 자리는 「맨 윗줄」이 아니라 「그 시점 줄」(rateFor('20260827')) — 옛 줄은 영원히 안 변한다. 🔑맨 윗줄 ≠ 오늘 값이다: 고시는 시행일 전에 나오므로 감시가 넣는 새 줄은 한동안 미래날짜이고, 날짜 없이 부르는driverDeduct(fee)와 어긋난다 → 오늘 기준 비교는rateFor(dcToday()). ⚠검사용 미래날짜에20270101같은 그럴싸한 날을 쓰면 그 날이 실제로 반영될 때 「같은 시행일은 다시 안 넣는다」에 걸린다 →29991231. 검증법 = 요율이 바뀐 상태를 흉내 낸 사본에서 관문을 돌려본다(평시 통과만으로는 못 잡는다). 실물 =scripts/test_dispatch_netpay.js·scripts/test_rate_watch.py. [코디] - 🔴2026-09-14 law.go.kr·moel.go.kr 등 정부 사이트는 해외 IP(GitHub Actions 러너)에서 timeout 된다 — 국내 전용으로 막혔다. 같은 순간 실측 = 러너 law 20s·moel 15s 전부 timeout(구글은 0.1s) / 미니PC 한국 IP 는 둘 다 0.1s. 26.9.7 실행까지는 됐고 9.14 부터 전부
PARSER_BROKEN. ⛒국내 사이트를 긁는 주기작업은 Actions 에 두지 마라 — 한국 IP 인 미니PC 예약작업으로. 옮길 때 ⚠페페 작업트리(C:\dev\pfs)에서reset --hard금지(미추적 파일 수십 개) → 전용 클론에서만 돈다(C:\dev\tools\dc-rate-watch\repo). 🔑외부 조회 실패는 타입명만 남기지 말고 사유·응답시간까지 남긴다 + 실패 시 연결확인 3줄(law·moel·google)을 붙이면 「우리가 막힌 건지 사이트가 죽은 건지」가 한 방에 갈린다. 현역 =tools\dc-rate-watch\run.py· 예약작업\Cody\pfs-rate-watch(월 09:00 KST) · 워크플로는 지우지 말고 API 로disabled_manually. [코디] - ⚠2026-09-14 아래 datalist 항목은 폐기됐다 — 같은 날 오후 페페 지시로 크롬 양식 자체를 안 쓰게 바뀌었다(위 ⛔ 항목). 「화살표가 안 먹는다」는 증상이었고 진짜 문제는 크롬 양식을 쓴 것이었다. 이력으로만 둔다. [코디]
- ~~🔴2026-09-14 네스팅 그리드에
<datalist>자동완성을 붙이면 화살표 키가 안 먹는다 — 값이 안 골라진 채 포커스만 다음 행으로 간다. 이 그리드는setupKeys가 ArrowUp/Down/Enter 를 전역으로 가로채 셀 이동에 쓴다 → 팝업이 떠 있어도 ArrowDown 은 셀 이동이 이긴다. 에러가 안 나서 「골랐다」고 착각하기 딱 좋다. ⛒컬러 후보는 타이핑 또는 마우스 클릭으로만 고른다(현행 유지 — 컬러칸만 예외 처리하면 「다른 칸과 손맛이 다르다」는 새 혼란이 생긴다). ⚠팝업 자체는 OS 네이티브 렌더라 스크린샷·접근성 트리 어디에도 안 잡힌다 → 폭·부분일치는 자동검사 불가, 사람 육안 1회가 유일한 검증. 🔑칼라 신규등록 기본값 =edge_color='' · poin_color_type_id=1(PP) · company_id=0— 운영 840행 중 449행·최근 수작업 5건이 이 조합이다.type=0같은 미사용 값으로 「미지정」 표식을 남기지 마라(조회·필터가 깨진다). 실물 =web/nesting/nesting.html·api/pfs/poin_color/controller/poin_color_controller.py· 검사기scripts/_nesting_qa5_check.py· 상세 = devplanpfs-nesting-qa5.html. [코디]~~ - ⚠2026-09-14 배포 완료를
/api/health의rev로 재지 마라 — 테스트·운영 둘 다REVISION이 비어 있다(rev="", 26.9.14 실측). 배포 대기·확인은 서빙된 HTML 안의 실제 마커 문자열로 판정한다(예html.count('class="toolrow"')==1). 같은 이유로 검사기의 「배포본 마커」 항목이 유일한 배포 증명이다. [코디] - ⚠2026-09-14 「한 줄에 들어가는가」를 자식 요소의
top으로 재면 오탐한다.align-items:center인 flex 줄은 높이가 다른 자식의top이 같은 줄에서도 다르다 — 첫 검사가 1024px 에서 「4줄」로 FAIL 을 냈는데 실제로는 한 줄이었다. ⛒줄 높이로 잰다(행 높이 <= 자식 최대높이 + 2) — 줄이 늘면 높이가 배로 뛴다. [코디] - 🔴2026-09-14 화면에 계산기·수식 입력을 붙일 땐 콤마를 먼저 지운다. 화이트리스트에
,를 넣고Function에 그대로 넘기면(1,200)이 쉼표 연산자로 200 이 되는 조용한 오답이다(에러 0). ⛒replace(/,/g,'')→^[0-9+\-*/().\s]+$검사 → 평가. 구현 =nesting.htmlrunCalc(). [코디] - 🔴2026-09-14 「에넥스 발주서 변환이 안 된다」 = 고장이 둘이다 — 하나만 고치면 여전히 안 된다. ①에넥스가
.xls(OLE2) →.xlsx(OOXML) 로 바꿔 보내기 시작했는데 xlrd 2.x 는 xlsx 지원을 아예 뺐다(1.x 는 읽었다) →detect_format이 예외를 삼키고 조용히unknown= 「미등록 양식입니다」. ②처리 사이드카(8791)가 26.8.10 20:39 이후 죽어 있었다 → 업로드가 영영pending(화면은 2분 뒤 「시간 초과」). ⛒파서는_enex_sheet(data)→(cv,nrows,datemode)로 리더만 분기(xls=xlrd·xlsx=openpyxl)하고 스캔 로직은 한 벌 그대로 둔다 — 셀 좌표 레이아웃은 두 형식이 완전히 같다. 매직바이트가 확장자보다 우선(.xls이름을 단 xlsx 가 온다). 🔑openpyxl→xlrd 정규화 3종이 진짜 위험구간 = 빈칸None→''(안 하면 품명이 문자열 「None」이 되고qty==''비교가 전부 어긋난다) · 날짜datetime→_xldate가 먼저 받게 · 1-based→0-based는cv()한 곳에서만.data_only=True필수(수식이면'=SUM(...)'이 수량칸에 박힌다). 표 안 안내문 필터_BOILERPLATE에'**'추가하되 규격·수량이 둘 다 없을 때만(실자재 오삭제 방지). 검증기 =scripts/_op_enex_parse_check.py(.xls 18/18) ·scripts/_op_enex_xlsx_check.py(.xlsx 20/20). 상세 = devplanpfs-enex-xlsx-convert.html. [코디] - 🔴2026-09-14 상주 사이드카가 35일간 죽어 있어도 아무도 몰랐다 — 범인은 「로그에 타임스탬프가 한 줄도 없었다」.
_sidecar.log는[upload] …만 찍어서 마지막 줄이 언제 것인지 파일 mtime 으로만 알 수 있었다. ⛒기동·claim·완료·에러 줄에[YYYY-MM-DD HH:MM:SS]. 되살리기 = 예약작업\Cody\pfs-sidecar-watch(10분,wscript숨김 — 시작프로그램은 로그온 때 한 번뿐이라 죽으면 끝이었다). ⚠중복기동 차단을 「포트가 잡혀 있으니 알아서 하나만 뜨겠지」에 맡기면 윈도우에선 안 먹는다 — 상세 =rules\tooling.md🆕allow_reuse_address. 포트 8791 은tools\portgate\PORTS.md에 등록했다(그동안 미등록). [코디] - ⚠2026-09-14
deploy-live는 별도 worktreeC:\dev\pfs_deploy_wt에 있다 — 본 리포에서git checkout deploy-live는fatal: already used by worktree로 거부된다. 운영 배포(체리픽·push)는 그 폴더에서 한다. 배포 전 경계 확인(git diff --stat origin/deploy-live <작업직전커밋> -- <대상파일>이 비어야 내 변경분뿐)과 체리픽 후 blob 해시 대조(git rev-parse <deploy커밋>:<파일>vs<master커밋>:<파일>)는 그대로 — working-tree md5 는 CRLF 때문에 다르게 나온다. [코디] - 🔑2026-09-14 배포 검증에 「서버 파이썬으로 자체검사 돌리기」를 넣는다. 로컬 openpyxl 3.1.5 / 서버 3.0.10 이라 로컬 PASS 가 서버 PASS 를 보장하지 않는다 —
scripts/ssm_run.py로 EC2 에서X._demo()를 돌리면 20초에 끝난다. 한 걸음 더 = 운영 실데이터 1건을 pymysql 로 읽어 생성기에 직접 넣어 본다(앱·로그인 없이 산출물을 잰다,scripts/_al_prod_real_excel.py). PFS 로그인이 500 이라 API 로는 못 재는 상황에서 이게 유일한 실물 검증이었다. [코디] - 🔴2026-09-14 openpyxl 로 시트를 지우면
workbookView.firstSheet가 남아 「엑셀이 파일을 아예 못 연다」. AL발주서 템플릿은firstSheet=1(두 번째 시트부터 보임)인데 시트를 하나 지우면 그 인덱스가 사라져 Excel 이 열기를 거부한다 — openpyxl 로는 멀쩡히 읽히고 zip 구조(workbook.xml·rels·definedNames)도 정상이라 파일만 봐서는 절대 안 걸린다. ⛒시트를 지웠으면for v in wb.views: v.firstSheet=0; v.activeTab=0. 판정법 = Excel COM 으로 실제 열어본다(C:\dev\_work\al-op\pagecheck.py,Workbooks.Open이com_error면 그거다). [코디] - ⚠2026-09-14 엑셀 양식 산출물은 「블록마다 page break」를 깔아야 한다 — 안 깔면 한 페이지에 발주서 두 장이 찍힌다. AL발주서 양식은 20행이 1장인데 자동나눔이 40행에 떨어져 수신처가 다른 두 업체 발주서가 같은 종이·같은 팩스로 나갔다.
fitToPage=True라도fitToHeight=0(높이 자동)이면 수동 브레이크가 정상 동작한다(실측 AL 3블록 → 3페이지). 브레이크 id 는delete_rows를 따라오지 않으니 행 삭제 뒤에 깐다. 같은 뿌리 = 날짜칸에str을 넣으면 셀 서식(mm" ."dd(aaa))이 안 먹어 양식과 다른 표기가 나간다 →datetime.date로 넣는다. [코디] - 🔴2026-09-11 기준선 비교용
git stash push -- <경로들>; … ; git stash pop금지 — 경로에 미추적 파일이 하나라도 섞이면 push 가 통째로 실패하고,;뒤pop이 남의 옛 stash 를 꺼낸다. 실측 = 검사기scripts/_nesting_grid_modal_check.py(미추적)를 섞었다가 push 실패 → pop 이pre-deploy stash pfs-2026-000006을 꺼내default_controller.py충돌(UU). stash 는 남아 있어git checkout HEAD -- <그 파일>로 원상복구. ⛒옛 코드와 비교하려면 stash 대신git show HEAD:<경로> > 임시파일로 떠서 돌린다. pfs 에는 옛 stash 가 쌓여 있으니stash pop은 언제나 남의 것을 꺼낼 수 있다(26.9.4reset --hard파이프 사고와 같은 뿌리 = 앞 명령 실패 뒤 파괴적 git 명령이 이어서 돈다). - 🔴2026-09-10 편집 그리드의 copy/paste 리스너를
<table>에 걸면 포커스가 빠진 순간 안 온다. 범위를 드래그한 뒤 순번 칸(포커스 못 받는 td)·헤더를 누르면 input 이 blur 돼activeElement=body→ Ctrl+C 이벤트 타깃이 body 라 표 리스너를 안 거친다. ⛒document에 걸고 「포커스가 표 안 · 또는 body 인데 이 표에 범위가 칠해져 있음」으로 게이트(gridOwns). 짝 규칙 = 칠한 범위는 포커스가 범위 밖 칸으로 옮겨가면 무효(pickedCells) — 남은 표시 때문에 엉뚱한 칸에 붙는 조용한 오기입을 막는다(검사기가focus()만으로 실제로 재현했다). 실물 =web/nesting/nesting.htmlsetupRange·setupPaste. - ⚠2026-09-10 드래그 뒤 click 은 mousedown·mouseup 타깃의 「공통 조상」에서 난다 — 시작한 토글 버튼으로 돌아와 놓으면 그 버튼이 토글된다. ⛒드래그 중 다른 칸을 지났으면
moved표시 → tbody 캡처 단계 click 에서stopPropagation(인라인 onclick 은 타깃 단계라 막힌다).moved리셋은 mouseup 뒤setTimeout— click 핸들러에서 리셋하면 표 밖에서 놓았을 때 click 이 tbody 를 안 거쳐 다음 정상 클릭(삭제 ✕ 등)을 한 번 먹는다. Shift+클릭 범위 확장은 인라인onclick="event.shiftKey||toggleCell(this)". - ⚠2026-09-10
navigator.clipboard는 테스트서버(http)에 없다 — 로컬(127.0.0.1)은 보안 컨텍스트라 되는데 테스트서버 실측에서undefined로 검사기가 죽었다. 앱 코드는ClipboardEvent.clipboardData만 써서 http 에서도 동작한다. ⛒검사기 클립보드 = 임시 textarea + 실 Ctrl+C(쓰기)/실 Ctrl+V(읽기) 후 포커스 복귀(_nesting_grid_modal_check.pyclip()). - 🔑2026-09-10 편집 그리드 키 처리의 한글 IME 가드는 「무시」가 아니라 「keyup 으로 미루기」다. 조합 중 누른 Enter·방향키는 keydown 이
keyCode 229·key='Process'로 온다(조합 확정이 먼저).isComposing이면 return 만 하면 한글 입력 뒤 Enter 를 두 번 쳐야 한다. ⛒e.code는 물리키 그대로(Enter·ArrowRight) 오므로 이동키만 기억 → 같은code의 keyup 에서 이동, 일반 keydown 이 오면 기억을 지운다(keydown 을 두 번 쏘는 브라우저에서 중복 방지). 실물 =web/nesting/nesting.htmlsetupKeys. ⚠Playwright 는 실 조합을 못 만든다 —KeyboardEvent('keydown',{isComposing:true,code:'Enter'})합성으로만 잰다(실기 육안 필요). - ⚠2026-09-10 「옛 버튼 문구가 HTML 에 없다」로 배포 반영을 재면 새 코드 주석에 걸려 오탐한다 — 「다른 이름」 이 내 JS 주석에 남아 테스트 실측이 FAIL. 기존 「클래스명 문자열 검증 = CSS 정의에 걸려 오탐」과 같은 뿌리. ⛒마커는 옛 코드의 호출부(
saveJob(true))나 새 요소 id 개수로 잰다. - 🔴🔴2026-09-09 행을 삭제하면 수식 안 「다른 행 참조」까지 전부 옮겨야 한다 — 안 옮기면 순환참조가 된다.
drop_hidden_rows가 「이 시트 수식은 전부 같은 행만 참조한다」는 전제로 같은 행 참조만 다시 썼다. 가격적용분엔 그 전제를 깨는 수식이 딱 둘 있었다 —S489=SUM(S5:S488)(총합) ·S842=SUBTOTAL(9,S480:S483). 숨김행 5~17(13행)을 지우자 셀은S476으로 내려왔는데 범위는S5:S488에 남아 자기 자신을 더하는 순환참조가 됐다(페페가 파일 열자마자 「순환 참조」 경고). ⛒build.shift_refs()로 수식 안 모든 행 참조에 매핑을 걸고, 범위 끝점이 삭제행이면 살아남은 쪽으로 좁힌다(엑셀이 행을 지울 때와 같은 동작). 앞에!가 오는 참조(다른 시트)는 잡지 않는다 — 그 행번호는 이 시트 삭제와 무관하다. 결과SUM(S5:S475)·SUBTOTAL(9,S467:S470). ⚠전제를 assert 로 박을 수 없는 자리면(일반 수식) 전제를 쓰지 말고 전부 처리해라 — 공유수식엔 assert 가 있어서 걸렸고, 일반 수식엔 없어서 안 걸렸다. - 🔴🔴2026-09-09 Excel probe 는 순환참조를 못 잡는다 — 「열렸다」가 「정상」이 아니다.
DisplayAlerts=False면 「순환 참조」 경고가 조용히 넘어가고 파일은mode=0으로 그냥 열린다. 정상화본 대조(수식/병합/도형 개수)도 통과한다. ⛒정적 사이클 탐지기를 검증에 넣어라 =C:\dev\_work\gs-wonju-dangu\cycles.py(수식 의존 그래프 DFS, 값 셀은 순환에 기여 못 하니 수식 셀끼리만 이어 범위 전개해도 노드가 안 터진다).verify.py에 항목으로 박아 뒀다(134항목). - ⚠2026-09-09 NAS 파일을 덮어쓰기 전에 기존본을 로컬로 먼저 받아 둬라. 순환참조 고친 본을 올릴 때 NAS 파일이 내가 올린 것과 크기가 달랐다(199,853 → 216,723) — 그 사이 페페가 열어 저장한 것이었다. 백업 없이 지웠고, NAS
Network Trashes Folder에서 회수해 대조하고서야 「차이 = 내가 고친 2칸뿐, 페페 수정분 없음」을 확인했다. 운이 좋았을 뿐이다. ⛒get→remove→put순서로 하고, 크기·해시가 내가 올린 것과 다르면 덮기 전에 멈추고 대조한다. - 🔴🔴2026-09-09 이진탐색 중간 산출물에도
finalize()(=calcChain폐기)를 걸어라 — 안 걸면 탐색이 자기 함정을 잰다.bisect_stage.py가finalize를 마지막 단계에서만 불렀다. 그래서 중간 단계 xlsx 가 전부 낡은xl/calcChain.xml(1,547항목) 을 달고 나갔고, 행이 밀리고 수식이 지워진 뒤라 계산체인이 옛 좌표를 가리켜 그 자체로 Excel 복구 트리거가 됐다. 그 결과 「범인 =fill_main/fill_sub」라는 틀린 결론이 나왔다(_z_copyonlyOK /_z_copyfill깨짐은 calcChain 손상량 차이였다). ⛒모든 단계를 실제 빌드와 같은 마감조건으로 만든 뒤 비교한다. 고치고 재측정하니 pack→rename→clone→lay1~4→cover→hidden 9단계 전부 OK. ⚠검증 순서도 이게 먼저다 —calcChain유무는 zip 목록 한 줄로 보인다('xl/calcChain.xml' in namelist()). - 🔑2026-09-09 Excel 이 「열렸다」고 끝내지 마라 — 조용히 고쳤을 수 있다.
DisplayAlerts=False면 복구 프롬프트 없이 열린다. ⛒판정 = 열어서SaveAs한 정상화본과 원본의 수식·병합·도형 개수를 대조한다(bisect_stage.shape()). 같으면 무수정, 다르면 Excel 이 손댄 것이다. 실측 = 최종본 수식1553/병합466/도형1 양쪽 동일. - 🔑2026-09-09 구조 결함 검사기
sfcheck.py추가(C:\dev\_work\gs-wonju-dangu\sfcheck.py) — 행/셀 정렬순서·중복, 공유수식 master 중복·고아·ref 범위 이탈, 병합 겹침/count,t="s"인덱스,inlineStr형식을 한 번에 본다._xlcheck·xldiag·openpyxl 사각지대를 메운다. 산출물 검증에 끼워라. - 🔴🔴2026-09-09 한샘 양식에 「단가 LOOKUP 이 한 칸도 없는 블록」이 있다 — 그 블록 금액이 조용히 0 이다. form 3 의 오픈장(25)·키큰 FIX(39)·공틀도어(46) 가 그렇다(원본 현장이 안 썼다).
_ensure_price_formula는 같은 블록 안에서만 베껴 오므로 아무 일 없이 지나가고 적용가 =빈칸 × 10MM기준= 0 이 된다. 서버 총액(hansem_calc)은 정상이라 목록으론 안 보인다 — GS 원주 단구 공틀도어 3탭 1,715,622원이 운영 엑셀에서 빠져 있었다. ⛒같은 단가군(hansem_furn_type.price_group)의 다른 블록에서 LOOKUP 을 베껴 오고 배수는 이 블록 것으로 다시 붙인다(공틀도어 = 키큰장 LOOKUP +*0.7). 단가군·배수는ctx['groups']로 넘긴다(master5a0c528→ 운영7cb6ea1). 회귀검사 =_work/gs-wonju-dangu/test_donor_price.py. ⛒새 양식을 등록하면 「쓸 블록에 단가 수식이 있나」를 먼저 재라 — 26.9.4 「칸 수만큼만 있다」의 극단값이 「0칸」이다. - 🔴2026-09-09
hansem_excel._find_total_row는SUM(I…)만 찾는다 — 페페 수작업 양식은SUMIF(B4:B45,"합계",I4:I45)라 못 찾고 53 으로 폴백한다. 그러면 총합행(46)과 그 아래 도면영역 행이 「보존 대상」 밖으로 떨어져 통째로 사라진다(실측: 총합행 서식·C=총 합라벨·A4:A46세로띠 병합이 전부 날아갔는데_xlcheck와 금액검증은 통과했다 — 값만 보는 검증은 이걸 못 잡는다). ⛒남의 손으로 만든 양식을 다룰 땐 총합행 탐색을SUMIF까지 넓히고, 검증에C{총합행}='총 합'·A4:A{총합행}병합·마지막 행을 넣어라. - 🔴2026-09-09 엑셀 행을 지우기 전에 공유수식(
<f t="shared">)을 명시 수식으로 전개하라. 마스터가 있는 행을 지우면 자식들이 고아가 되어 엑셀이 「복원하시겠습니까」를 띄운다(실측: 가격적용분R17·S17마스터의ref가R17:R125인데 17행이 삭제 대상 숨김행이었다 — 자식 108개 고아). ⚠get_formula는 자식(<f t="shared" si="N"/>)에서 None 을 돌려주므로 「수식 있나」 판정도 전개 전엔 전부 오판이다. ⛒전개 = 마스터 수식의 행번호를 자식 행번호로 치환(같은 열·같은 행 참조 전제를 assert 로 박아라). 정규식은 셀 단위로 잘라서 돌려라 — 전역<f …>(.*?)</f>는 자기닫힘 자식을 마스터로 오인해 셀 경계를 넘는다. - ⚠2026-09-09 행을 넣거나 뺄 때
<rowBreaks><brk id>도 같이 옮겨라. 안 옮기면 총합행은 60 인데 페이지 나누기는 46 에 남아 인쇄가 엉뚱한 데서 잘린다.remap_rows는 이걸 안 건드린다(수식·rowBreaks·VML·selection 이 사각지대). - 🔑2026-09-09 페페 수작업 한샘 파일(GS 원주 단구)의 구조 = 우리 엔진과 다르다. 블록 4/11/18/25/32/39 + 총합 46(SUMIF), 부자재는
<블록명> 부자재블록(단가/수량/적용가 미러열), 등급·배수를 혼용한다(상부장 D×0.7 · 하부장 A×1.65 또는 ×1.8 · 냉장고장 A×1.8 · 공틀도어 D×0.7·A×1.58 · 키큰장 A×1.65, 가전소물장 단가 5600 상수). 부호 A/B/C/D 는 도면 가구 부호지 한샘 단가군이 아니다. 새 평형을 끼울 땐 같은 평형 계열의 관례를 그대로 따른다(포인 금액에 억지로 맞추지 마라 — 이 파일은 한샘 실단가 기준이라 포인보다 작게 나오는 게 정상이다). - 🔴2026-09-07 CSS 주석 안에 Jinja 태그를 적으면 Jinja 가 그걸 실행한다 — 템플릿이 통째로 죽는다. Jinja 는
/* */를 모른다. 새@media블록 설명으로 적어 둔 조건문 한 줄 때문에if10 /endif9 가 돼/field/orderpaper·/omc/material·/field/company/<no>/material세 화면이 동시에 500 이 됐다(테스트서버 실측). ⛒가리려면{# … #}, 안 쓸 거면 태그 문법 자체를 안 적는다. 판정 =jinja2.Environment().parse(src)한 줄(자체검사에 넣어라). - 🔴2026-09-07 CSS 주석은 중첩이 안 된다 — 주석 안에 「주석 닫기 기호」를 적으면 바로 다음 규칙 하나가 통째로 사라진다. 위 함정을 설명하려고 주석에 닫기 기호를 적었더니 거기서 주석이 끝나고 남은 글자가 셀렉터로 먹혀
.op-mfbtn{display:none}(데스크톱 기본 숨김)이 증발, 데스크톱에도 모바일 전용 버튼이 떴다. 에러 0·CSS 문법 검사도 통과라 눈으로는 안 보인다. ⛒자체검사에 「/*·*/짝·중첩」 항목을 넣는다(scripts/_op_main_mobile_static.py). - 🔴2026-09-07 인라인
<style>은 그 뒤에 오는<link>외부 CSS 에 진다(같은 특이도면 나중 것이 이긴다).field_orderpaper.html은<style>다음에orderpaper_schedule.css를 부른다 → 거기 정의된.op-tabs·.op-tab·.op-inline-search·.op-mt-btn은 템플릿에서 접두를 붙여 특이도를 올려야 덮인다(#opMainWrap .op-inline-search꼴). ⚠@media는 특이도를 안 올린다 — 「미디어쿼리 안이니까 이기겠지」가 오답이다. - 🔴2026-09-07
.op-site-head처럼display:flex인데flex-wrap이 없는 헤더는 모바일에서 글자가 세로로 한 글자씩 쌓인다(web-designer 실측 = 현장명 w=21 h=203, 헤더 하나가 225px·데스크톱 46px). 같은 파일의.op-donevend-head도 같은 모양이다(문구가 짧아 아직 안 터졌을 뿐). ⛒모바일 블록에서flex-wrap:wrap+ 주값에flex:1 1 100%+word-break:keep-all. - ⚠2026-09-07 표를 그리드 카드로 바꾸면
colspan이 무시된다 — 「내용 없음(업체만 등록)」 같은 colspan 행은 배치가 깨진다. ⛒그 행만 클래스로 갈라display:flex한 줄로 뺀다(tr.op-item.op-empty). 배치 자체는nth-child가 아니라 클래스로 잡는다(오엠씨 블록만 품명이 전면/후면 2칸이라 순번이 어긋난다 — 26.9.4 자재관리 표에서 이미 낸 사고). - 🔑2026-09-07 같은 템플릿을 여러 라우트가 렌더할 때 화면별 CSS 는 Jinja 조건으로 통째로 가른다.
field_orderpaper.html은/field/orderpaper·/omc/material·/field/company/<no>/material셋이 공유하고#opMainWrap·#opList·.op-site·편집 칩이 전부 겹친다.{% if not omc_view %}로 감싸면 그 화면엔 아예 렌더되지 않아 셀렉터가 겹쳐도 샐 길이 없다(코덱스 상의 결론도 동일 —:not()·URL·JS 판별은 누락 시 기본 화면으로 오인). 검증도 쉽다 = 운영 HTML 에서 마커 문자열 개수를 세면 된다. - 🔑2026-09-07 페페 확정 — 한샘 내역서 블록은 「포인 항목 1개 = 블록 1개」, 블록명은 포인 품명이다. 「각 행블록 이름들도 포인 양식 항목별 이름이 기준. 단가만 해당 단가항목으로」. 한샘 단가군 블록(상부장/하부장/하부FIX/키큰장)을 여러 항목이 칸으로 공유하면 안 된다 — 이름을 못 붙인다. ⛒
build_ledger()가src(복제할 단가군 블록)에서 7행을 복제해 항목마다 블록을 깐다 → 단가 LOOKUP 범위가 항목별로 따라온다(주방 상하부$A$7:$A$54/ 키큰장군$A$59:$A$106, FIX 배수 0.5, 공틀 0.7).-추가블록도 그 항목 바로 아래. 실측 = 84A 14블록(본7+추가7) · 99A 12 · 104 12 · OT84A 7. ⚠세로 띠 병합A4:A<총합행>을 새 총합행까지 늘려야 한다(안 늘리면 평형명 칸이 깨진다). - 🔴🔴2026-09-07 포인 견적의
margin_total_amount(행총액)는 「최종 청구액」이고 sub-item 추가금액이 그 안에 이미 들어 있다. 한샘 양식에서 추가분을<블록명>-추가블록으로 따로 실으면 본항목(길이×단가)에서 그만큼 빼야 한다 — 안 빼면 이중 계상이다(1차 산출이 40,113,150 으로 부풀었다, 페페가 잡았다). ⛒target = margin_total_amount − Σsub.amount로 적용률을 역산한다. 실측 = 행총액 30,889,000 = 본항목 21,664,850 + 추가 9,224,150. ⚠본항목이 작아지면 적용률 4자리 반올림 잔차가 커진다(타입별 ±100원까지) — 갑지 절사(-MOD(…,10000))가 흡수하니 검증 허용오차를 타입당 100원으로 둔다. - 🔑2026-09-07 페페 지시 —
<블록명>-추가블록은 「단가 → 수량 → 비고 → 적용가」 순서다. 단가 행이 수량 행 바로 위, 행이름은단 가. 세트열(C~H)에는 품명만 두고 단가는 미러열(J~O) 로 간다(부모 블록의 길이→10MM기준→적용가 흐름과 같은 모양). 적용가 ==J{b+2}*J{b+3}= 마진포함 단가 × 수량. - 🔴2026-09-07 블록을 옮기는 함수(
_fix_block_formulas)는-추가블록을 본블록 모양으로 되돌려 놓는다.drop_empty_blocks가 빈 블록을 들어내며 살아남은 블록 수식을 다시 걸 때 추가 블록의 적용가를C16*J14(세트열 단가 × 미러열 수량)로 써 버려 단가 칸이 비어 금액이 0 이 된다. 겉보기엔 수식이 멀쩡해 더 위험하다. ⛒extra=True로 모양을 갈라 「단가 × 수량」을 유지한다. 블록 모양이 두 종류가 된 순간, 좌표를 다시 거는 모든 함수가 그 구분을 알아야 한다. - 🔴2026-09-07 엑셀이 「복원하시겠습니까」를 물으면 1순위 용의자는 「겹치는 병합」이다. 블록을 복제하며 병합을 같이 옮길 때 시작 행만 보고 고르면
A4:A53(평형명 세로 띠)처럼 블록 경계를 넘는 병합이 딸려와 복제본(A11:A60)이 원본과 겹친다. 엑셀은 XML 이 멀쩡해도 이걸로 복구 모드를 띄운다. ⛒양 끝이 모두 블록 안인 병합만 복제한다. 판정 도구 =C:\dev\_work\gs-ansan-seongpo\_xlcheck.py(행 순서·셀 중복/순서·병합 겹침·count 불일치·dimension·빈 dataValidations 를 한 번에 본다) — 산출물 검증에 항상 끼워라. - 🔴2026-09-07 갑지는
'<평형> 내역서-주'!O53으로 총합을 문다 — 블록을 넣거나 빼면 그 참조가 죽는다. 추가 블록 삽입/빈 블록 삭제로 총합 행이 67·60·60·46 으로 움직였는데 갑지는 53 을 그대로 봐서 탭별 합계가 통째로 빈 채 나갔다(페페가 잡았다). ⛒_retarget_total()이 행이 움직일 때마다 갑지 참조를 같이 옮긴다. 검증에 「갑지 O19~O22 가 실제 총합 행을 가리키는가」를 반드시 넣어라 — 금액 대조만 하면 내역서만 보고 갑지를 안 본다. - 🔑2026-09-07 페페 지시 — 내역서 탭은 「쓰는 블록만」 남긴다. 숨기기가 아니라 삭제다(숨김 행도 전부 해제).
drop_empty_blocks()가 세트 라벨 없는 블록을 7행씩 들어내고 총합 수식·도면 앵커·인쇄영역·갑지 참조를 같이 당긴다. 실측 = 84A 9블록 · 99A/104 8블록 · OT84A 6블록(오픈장·키큰FIX·공틀도어 삭제). - 🔴🔴2026-09-07 포인 견적의 sub-item(추가금액)은
/api/estimate/<no>에 안 들어 있다.estimate_detail_sub는/api/estimate/sub/list/<detail_no>로 상세마다 따로 받아야 한다(/save는 PFS-D007 로 410 잠금, 저장은 견적 메인 트랜잭션이 한다). 이걸 몰라서 GS 안산 성포 한샘 양식 1차 구현이 「sub-item 0건」으로 오판했다 — 실제는 46건 9,224,150원이고-추가블록이 통째로 빠진 견적서가 나갈 뻔했다(페페가 잡았다). ⛒「추가금액 없음」을 결론으로 쓰기 전에 반드시 이 API 를 찔러라. 금액 =amount = price × qty × (1+margin_percent/100)= 마진 포함 최종 합계 → 한샘 지정단가 =amount/qty. ⚠포인 견적서 출력은 sub 를 금액으로 안 싣는다 — 비고 텍스트로만 나오고 총액(37,689,000)에도 없다. 한샘 양식은 반대로 다 싣는다(페페 규칙). - 🔴2026-09-07
hansem_excel.set_num/set_str/set_formula는 「없는 칸」에 쓰면 조용히 아무 일도 안 한다.<c r="..">를 치환만 하는데 엑셀은 빈 칸을 아예 저장하지 않는다 → 원본 현장이 안 쓴 칸은 XML 에 없다(블록마다 칸 수가 다른 진짜 이유). 「단가·적용가 수식 누락」과 같은 뿌리다. ⛒ensure_cell()이 없으면 만들고 서식은 같은 행·같은 열그룹의 가장 가까운 칸에서 승계한다. master1fff4c1. - 🔑2026-09-07 페페 확정 3건(표기·분류·절사) — ①적용률 곱셈 표기 금지:
상부장 x1.9383같은 메모를 적용률 칸에 쓰지 마라(apply_note=""). ②냉장고장은 한샘 단가표에 자리가 없다 → 키큰장(주방_키큰장)에 편입한다(BLOCK_OF["R1"]=("K",32)). ③갑지 절사는 만원 단위 내림이다 — 페페가 손으로 넣은-9,012(30,889,012→30,880,000)가 그 규칙이라 수식=-MOD(SUM(W19:AD22),10000)으로 박았다(총액이 바뀌어도 자동). 운반비는 300,000 복원, 시공비는 0 유지(한샘 생산비 명세서엔 안 들어간다 — 별도 「2.시공비」 파일). - 🔴2026-09-07 NAS 산출물은 사람이 열어 고친다 — 덮어쓰기 전에 md5 를 대조하라. 내가 올린 견적서를 40분 뒤 갱신하려다 보니 크기가 450,075 → 566,200 으로 바뀌어 있었다(
calcChain존재 ·docProps/app.xmlApplication=Microsoft Excel = 사람이 Excel 로 저장). 그 편집이 곧 페페의 지시였다(절사 행 신설·운반비 복원). ⛒nasget.py put이 「대상이 있으면 거부」하는 게 이래서 옳다 — 강제로 덮어쓰지 말고 md5 가 내가 올린 그대로일 때만 교체하고, 아니면 버전명으로 따로 올리고 사람에게 알린다. - 🔴2026-09-07
-추가블록은 운영 옛 엔진으로 못 만든다 — 배포가 전제다.inject_ledger가 하드코딩된BLOCK_ROWS(4/11/18/25/32/39/46) 만 도는데, 추가 블록을 끼우면 본블록이 그 아래로 밀린다 → 옛 엔진은 엉뚱한 행에 쓰고 추가 블록을 지운다. 양식에 미리 구워 넣어도 11/25/39 에 앉은 추가 블록은 옛 엔진이 비운다. 「길이=10·적용률=수량」으로 DB 에 태우는 우회는 길이 칸에10이 찍혀 고객 문서를 버린다. ⛒결론 =api/pfs/hansem/배포 + sub-item 저장 자리(스키마) 가 있어야 운영이 같은 파일을 낸다. 그 전까지 산출물은 로컬 빌드(local_build.py)가 정본이다. - 🔴2026-09-07 「단가 LOOKUP 수식이 없으면 조용히 0」의 진짜 짝은 미러 열(J~O)의 「적용가」 행이다. 26.9.4 에 단가행(C~H)만 메우게 고쳤는데 그건 절반이었다 — 양식은 원본 현장이 쓰던 칸 수만큼만 수식을 갖고 있고, PFS 는 적용가 행(
J{b+5} = C{b+5}*J{b+3})을 손대지 않았다. 결과 ①col_index>=1인 줄은 적용가가 상수 0 → 금액이 조용히 사라진다 ②앞 현장이 박아 둔 상수 적용가(예K9=338000)가 남으면 길이가 없어도 블록 합계SUM(J..O)에 더해진다(유령금액). 서버 총액(hansem_calc)은 정상이라 목록으로는 절대 안 걸린다. 운영 실측(26.9.7) = 기존 한샘 견적 4건 전부 해당, 엑셀이 서버 총액과 −4,123,878 ~ +7,412,865 어긋난다. ⛒_ensure_price_formula가 단가·적용가를 같이 보장 +inject_ledger2패스(쓰는 칸 먼저 → 안 쓰는 칸은 단가·적용가까지 비움. 순서를 바꾸면 수식 복제 원본이 먼저 지워져 못 베낀다). master58daac4, 운영 미반영. 측정기 =C:\dev\_work\gs-ansan-seongpo\_prod_impact.py. - 🔑2026-09-07 한샘 양식 검증에 Excel COM 을 쓰지 마라 — 대신 수식 체인을 파이썬으로 따라간다. Bash 도구는 Session 0 이라
Workbooks.Open뒤 모든 COM 호출이피호출자가 호출을 거부했습니다(RPC_E_SERVERCALL_RETRYLATER)로 60초를 태우고 고아EXCEL.EXE만 남는다(26.9.7 실측 2개). ⛒단가기준 D:G → 백데이터 A/B/C(=단가기준!Dxx) → 내역서 LOOKUP(마감,등급)×배수 → 적용가 → 블록합 → O53을 그대로 재현한다(verify_form4.py[3]). 이쪽이 더 세다 — 마감명 문자열이 백데이터와 정확히 일치하는지까지 잡힌다. 실측 = 4평형 24줄 전량 목표금액 ±21원. - 🔑2026-09-07 양식 쪽에 「어느 칸을 쓰는지」까지 구워 넣으면 운영 엔진이 옛 코드여도 금액이 맞는다. 엔진 배포는 페페 승인이 필요하니, 새 양식을 만들 때
inject_ledger를 로컬 최신 엔진으로 미리 돌려 정답 레이아웃(쓰는 칸 = 단가·적용가 수식, 안 쓰는 칸 = 전부 비움)을 양식에 박아 올린다. 옛 엔진은 그 위에 같은 값을 다시 쓸 뿐이라 결과가 같다. 생성기 =C:\dev\_work\gs-ansan-seongpo\build_form4.py. - ⚠2026-09-07 xlsx 시트 복제 3종 함정 — ①
[Content_Types].xml에 Override 를+=로 붙이면</Types>뒤로 가서 엑셀이 「파일 형식이 잘못되었다」로 거부한다(replace('</Types>', …)로 넣는다) ②definedName의localSheetId는 탭 순서 인덱스라 탭이 하나 끼면 전부 밀린다 → 옛 순서→새 순서 표로 일괄 remap ③복제한 도면 파트의 rels 가 원본 미디어를 그대로 가리킨다 → 새media/imageN.png로 갈지 않으면 4번째 평형에 3번째 도면이 뜬다. - ⚠2026-09-07 한샘 도면 이미지는
twoCellAnchor라 상자 비율(가로/세로 1.6179)로 늘어난다. 원본 비율 그대로 넣으면 평면도가 옆으로 퍼진다(라인건설 원본이 실제로 그랬다 — 1.42 를 1.62 로 늘려 씀). ⛒넣기 전에 흰 여백을 대 상자 비율로 레터박스한다(build_form4.letterbox). 상자 비율은drawing2.xml의 캐시<a:ext cx cy>에서 읽는다. - ⚠2026-09-07
register_poin.py는 import 만 해도main()이 돈다 — 다른 스크립트에서session()만 빌리려고 import 하면 우리sys.argv를 물려받아REGISTER를 포인 견적 재등록으로 알아듣는다(중복등록 assert 에 걸려 겨우 막혔다). ⛒sys.argv = sys.argv[:1]로 자르고 import. - 🔑2026-09-07 한샘 양식 금액 맞추기 = 「등급 A 고정 + 적용률(곱하기 단가)」 이 페페 확정 규칙이다. 등급을 A~D 로 골라 맞추려 들지 마라 — 등급은 A 로 박고
J7 = J6/10*적용률의 적용률로 포인 금액에 붙인다. 적용률 = 포인 행총액 ÷ (A등급단가 × 블록배수 × 순길이/10), 소수 4자리(_trim)면 오차 ±수십원. 길이는 포인 순길이(Σ치수 − Σ코너) 를 쓴다(한샘엔 코너 개념이 없다). 실측 = GS 안산 성포 24줄 전량 오차 ±14원(합 30,889,014 vs 30,889,000). 산출기 =C:\dev\_work\gs-ansan-seongpo\hansem_plan.py. - 🔑2026-09-07 한샘 「추가금액」의 제자리는
추가분견적탭이 아니다(페페 정정). 그 탭은 본건 납품 후 별도 작성 건이다. 본건 견적의 항목별 sub-item 은 내역서 탭에서 그 블록 바로 하단에<블록명>-추가전용 블록을 새로 만들어 싣는다(예상부장-추가). 길이·단가와 무관하게 지정 금액(마진 포함 최종가) × 수량. 행 삽입이 필요하고 53행 총합 수식(SUM(I52,I45,…))과 도면 이미지 앵커(row 53~91)를 밀린 행수만큼 재계산해야 한다. - ⚠2026-09-07 한샘 양식에 넣을 도면은 모노크롬 2치화가 필수다(페페 지시). 컬러 그대로 넣으면 엑셀 인쇄에서 안 보인다. grayscale 변환만으로는 노랑·시안 선이 옅은 회색으로 날아간다 →
PIL .convert("L").point(v>=235 ? 255:0, mode="1")로 임계 이진화하고 최소 FHD(1920px) 이상. 생성기 =C:\dev\_work\gs-ansan-seongpo\_render_mono.py. - 🔑2026-09-07 「이 사람만 특수 허용」 =
user_permission오버라이드, 업체 쪽은 닫는다. 권한 우선순위 =user_permission>company_permission> 차단(permission_service.has_menu_permission). 박광은(enex5983) 오엠씨 권한이 실은company_permission(에넥스 4)= 업체 전체였다 — 에넥스 계정이 그 사람 하나뿐이라 「이 사람만」처럼 보였을 뿐, 계정이 늘면 전부 열린다. ⛒처방 = 업체 행을'0'으로 닫고user_permission(user_no, menu_key)='1'. ⚠「현재 허용을 유지」라는 지시는 먼저 실측하고 해석한다 — 이때 현재 허용은OMC_SHEET·OMC_BOARD둘뿐이었고OMC_MATERIAL은 원래 없었다(자재관리를 얹었으면 지시 위반). 권한 변경 뒤엔user.permissions_updated_at+company.permissions_updated_at을 건드려 재로그인시킨다. 검증은 DB 행이 아니라 계산 결과로 재라(「같은 업체 다른 계정이면 차단되나」까지). - 🔴2026-09-07 「A 를 골라 B 를 채우는」 폼은 필수값 검증보다 «채우는 쪽»을 먼저 돌려야 한다. admin/user 에 「이름 = 거래처 담당자 선택」을 붙였는데, 담당자만 주고 이름을 비운 요청이
name 필수로 거부됐다 —create()가 필수값 검증을 먼저 하고apply_member_link(담당자에서 이름·전화·업체를 채우는 함수)를 뒤에 불렀다. 화면은 이름을 채워 보내니 UI 로는 절대 안 보이고 API·자동화에서만 터진다(테스트서버 실측에서 잡혔다). ⛒순서 = 정규화·보충 → 검증 → 저장. - 🔑2026-09-07
user.member_no= 사용자 계정 ↔ 거래처 담당자(company_member) 연동. 화면에서 담당자를 고르면 이름·전화가 채워지고 업체(company_no)는 서버가 담당자 소속으로 강제한다(화면 값을 믿으면 담당자와 업체가 어긋난 계정이 생긴다). 이름·전화는 등록 당시 스냅샷(담당자가 나중에 바뀌어도 안 따라간다). ⛒예외가 하나 — 담당자가 다른 업체로 옮겨가면 연동 계정의company_no도 같이 옮기고permissions_updated_at을 건드려 재로그인시킨다. 자재관리 스코프·메뉴 권한이 전부user.company_no에 달려 있어, 안 옮기면 그 계정이 옛 업체 권한을 그대로 들고 남는다(코덱스 리뷰 지적). 담당자 삭제 시엔 연동만 끊고 계정은 살린다. 한 담당자 = 한 계정(DB UNIQUE 대신 저장 시점 검사 — 제약을 걸면 정당한 예외에 운영 DDL 을 또 쳐야 한다). - ⚠2026-09-07 담당자 자동완성 후보행에 «업체명»을 반드시 병기한다 — 동명이인이 20개 이름그룹, 최다 「사장님」이 8개사다(web-designer 실측 399건). 이름만 보여주면 남의 업체 담당자를 고른다. 검색은 서버 API 를 새로 만들지 않는다 —
GET /api/company/member383~399건을 첫 focus 때 1회 받아 클라에서 substring 필터(모듈 로드 시 받으면 모달을 안 열어도 매번 받는다). 이름 0건이면 업체명으로 재시도(「사장님」처럼 이름이 역할명인 레코드 대응). 구현 =company_autocomplete.js에 훅 5개(search·labelOf·valueOf·idOf·minWidth) + 한글 조합 가드를 더해 재사용 — 새 모듈을 만들지 않았다. - 🔴2026-09-07
.admin-modal-content는.admin-detail의 «형제»다 — 상세패널 스코프 CSS 가 모달엔 안 먹는다. 26.9.7 오전에.admin-detail .btn-sm{font-size:13px}로 13px 하한을 맞췄는데 신규 사용자·비밀번호 모달 버튼은 그대로 12px 였다(전역style.css:2045). ⛒모달은.admin-modal-content스코프로 따로 올린다. 같은 날user.html인라인.cp-bulk-btn11px 도 발견 — 인라인<style>은 CSS 파일 검사에서 안 잡힌다. - 🔴2026-09-07 URL→menu_key 매칭은 정확매치가 없으면 「최장 prefix」로 떨어진다 — 하위 경로 페이지는 라우트에서 메뉴 권한을 직접 확인해야 한다.
permission_service.match_url_to_menu_key규약이라,/field/company/452/material메뉴를 안 만든 상태에서 그 URL 을 직접 치면 상위/field/company/452(현장 페이지) 권한을 타고 그대로 열린다. before_request 게이트는 「매칭된 menu_key 의 권한」만 보므로 못 잡는다. ⛒새 하위 경로를 만들 땐 라우트 안에서has_menu_permission(user_no, <그 페이지 menu_key>)를 한 번 더 본다(자사는 company_type='1' 기본 허용이라 직원 영향 없음). - 🔴2026-09-07 PFS 페이지 제목(h3·breadcrumb)은 전역 CSS 로 «항상» 숨는다 — 제목에 정보를 넣으면 아무도 못 본다.
web/static/css/pfs_layout.css:55=.page-titles .text-themecolor, .page-titles .breadcrumb { display:none !important; }(전 페이지 공통·의도된 규칙). 「지금 어느 업체/현장 화면인지」 같은 표시는 그 화면에만 렌더되는 바(예:.op-omc-bar)에 배지로 넣는다. ⛒이 전역 규칙은 절대 건드리지 마라 — 풀면 전 페이지 제목이 한꺼번에 되살아나 레이아웃이 흔들린다. (web-designer 실측) - ⚠2026-09-07 템플릿 조건부 마크업을 「클래스명 문자열」로 검증하면 전부 오탐이다.
{% if not omc_view %}로 빠진 걸 확인하려고'op-filterbar' in html로 재면 같은 파일<style>의 CSS 정의에 걸려 항상 True 다(실측 4건 오탐). ⛒판정은'class="op-filterbar"'처럼 마크업 형태로 한다. JS 함수 정의(function openUpload(){)도 조건부 밖에 남으므로onclick="openUpload()"로 재라. - ⚠2026-09-07 Jinja
|tojson은 ensure_ascii 라 한글이\uXXXX로 나간다.var OP_VENDOR = {{ x|tojson }};→var OP_VENDOR = "글로벌…";. JS 는 정상 디코드하므로 기능은 맞고, HTML 문자열로 검증할 때만 실패한다 → 검증은json.dumps(name)(ascii) 과json.dumps(name, ensure_ascii=False)둘 다 인정한다. 값 주입 자체는|e(HTML 엔티티라 JS 문자열 안에서 깨진다)가 아니라|tojson이 맞다. - 🔑2026-09-07 업체별 페이지(현장·자재관리)는 menu_catalog 상태기계 하나가 모양을 정한다 —
permission_dao._reconcile_company_pages. 둘 다 ON = 업체명이 접이식 그룹(company_<no>, url NULL) 아래 「현장」·「자재관리」 / 하나만 ON = 업체명 직링크(parent=field_company) / 둘 다 OFF = 그룹행 삭제. 생성·삭제 양쪽이 이 함수 하나만 부른다(멱등). ⛒순서가 규칙 — 그룹화는 부모 먼저 만들고 자식을 옮기고, 해제는 자식을 먼저 떼고 그룹행을 마지막에 지운다(트랜잭션이 없어 중간 실패해도 고아 자식이 안 생긴다). 판단 기준은 현재 이름·부모가 아니라 행의 존재 여부. 그룹행icon_class는 일부러 NULL —base.html이 부모에만la la-folder-ofallback 을 주므로 비워야 부모/자식 아이콘이 갈린다(오엠씨는 부모에la-cubes를 박아 자식과 똑같아졌다). 오엠씨(OMC*)는 이 규약 밖이라 안 건드린다. - 🔴2026-09-07 거래처(외부 업체) 계정에 화면을 열어줄 땐 「읽기 API·쓰기 API·페이지」 셋을 다 막아야 한다.
/api/orderpaper/all-items는 원래 전 업체 발주서를 통째로 주고 화면(JS)에서만 걸렀다 — API 를 직접 부르면 다 보였다. 쓰기도orderpaper_no가 순차 정수라 URL 만 바꾸면 남의 행을 고친다. ⛒처방 = ①all-items 에 서버측 업체 스코프 ②쓰기는 허용목록(자재관리 화면이 실제 쓰는status·receive-date·note·qty·item-name5개 + 자기 발주처 행) ③페이지는 위 prefix 항목. 가시성 판정은orderpaper_service.item_visible_to단일 출처 — 읽기/쓰기가 갈라지면 「화면엔 보이는데 저장은 403」이 난다. - 🔑2026-09-07 거래처 계정 활동 감사 로그는
web/__init__.pybefore_request 한 곳에 모았다(기존audit_log재사용, 스키마 변경 없음). action_type =access_page·api_write·api_denied(거래처만) ·access_denied(전 계정) ·logout. 조회 = 관리자 > 감사 로그 액션 셀렉트. 비밀번호·토큰은audit_log_dao.redact가 마스킹. ⚠업체 판정(get_scope_company_no)은 지연 호출로 둔다 — 매 요청 무조건 부르면 API GET·정적까지 업체조회 쿼리가 한 번씩 더 붙는다. - 🔑2026-09-07 견적서 엑셀 다운로드 =
GET /api/estimate/print/excel/<no>다./api/estimate/report/<no>는 1차 raw 데이터 라우트라 엑셀이 아니고, 지금 운영에서 부르면 502가 난다(text/html182바이트). 네임스페이스 =api/config.py의add_namespace(estimate_print, path="/api/estimate/print")→data|excel|pdf/<no>. - ⚠2026-09-07 등록 후 대조에서
amount정확일치를 요구하면 오탐한다 — MySQL 텍스트 프로토콜이 유효 6자리로 잘라 보낸다(실측: 저장1,990,073→ 조회1,990,070, 763 도1,640,353→1,640,350). 금액 정본은margin_total_amount(천원 단위라 안 잘린다) — 대조는 그걸로 하고amount는 상대오차1e-5까지 허용한다. 같은 뿌리 = 이 파일의 「읽을 땐CAST(x AS DECIMAL(24,8))」 항목. - 🔑2026-09-07 포인 견적 API 등록은 서버가 금액을 재계산하지 않는다 —
estimate_service._normalize_detail/_normalize_master는 빠진 키 default 보정만 한다. 즉amount·margin_amount·margin_total_amount는 보내는 쪽이 정본이고, 계산식은estimate_regist_v2.js를 그대로 재현해야 한다.margin_percent는 master·detail 모두None(=SQL NULL, 자동) 으로 보낸다(_NULLABLE_KEYS라 None 이 통과한다). 단가표 연동은price_no로 — 마감재 13/판2 기준 W1=47 · B1=58 · T1=69 · R1=80 · G1=36. - 🔑2026-09-07 포인 견적서 엑셀은 DB·로그인 없이 만들 수 있다 —
estimate_print_service.build_excel(print_data)는 dict 만 먹는다.print_data = {"main":{"master":{...},"details":[...],"extras":[],"materials":[]},"sub_estimates":[]}. detail 필수키 =type_name·field_type·product_name·dimension1~5·corner1~5·qty·price·margin_total_amount·comment·size_display·display_order, master 필수키 =company_name·field_name·estimate_date·payment_condition·construction_cost·transportation_cost·company_member_name(+estimate_manager_field_yn). 금액칸은 수식(=SUMIF(...)·=H62* I62)이라 파일 안에 총액 리터럴이 없다 — 검증은 「단가(SET) 칸 = 우리 행총액」으로 한다. 계산식 정본은 그대로web/static/js/estimate_regist_v2.js((Σ치수−Σ코너)/300 × 자당단가 × 부위비율 × (1+마진)→Math.ceil(Math.round(x)/1000)*1000). - ⚠2026-09-07 한샘 양식(활성 form_no 2)은 내역서 쌍이 3개뿐 = 3평형이 상한이다. 4평형이면
FormCapacityError→ 파일을 쪼갠다(예: 아파트 3 + 오피스텔 1). 🔴남는 슬롯은 양식 원래 이름(84A·84B)으로 남아 우리 평형명과 겹쳐 보인다 — 슬롯 이름으로 「우리 평형」을 판별하면 오탐한다. 판별은A4셀 값으로(빈 슬롯은 A4 가 비어 있다). - 🔑2026-09-07 한샘 건립도면의 「견적 대상」은 도면이 직접 표시한다 — ①표제란 머리글
구분 : KIT. 건립 : 84㎡A, 99㎡A, 104㎡, OT84㎡A②모델스페이스에 글자높이 1500 짜리MH건립라벨 + 시트 묶음을 두른 큰 사각형(LWPOLYLINE, h≈14291). 도면 전체를 조감으로 한 장 렌더하면 눈으로 바로 잡힌다. GS 안산 성포 실측 = 타입별 Style Up 주방옵션 1종 + 냉장고장-옵션형만 건립(확장기본형·Style Up2/3·냉장고장 기본형1/2 는 제외). - 🔴2026-09-04 포인 견적 행총액은 세 조각이다 —
margin_total_amount−amount를 「옵션」이라고 부르면 마진이 섞여 든다. ①자당몫amount(Σ치수/300 × 자당단가 × 부위비율, 길이 기반) ②거래처 마진margin_amount(= ① ×estimateMaster.company_margin_rate, 763 은 15.5% · 가전소물장 3행만 30%, ①의 비율이라 역시 길이 기반) ③추가금액 = 행총액−①−②(유리도어·블럼 HK-TOP·리프트도어 등 치수와 무관한 수동입력, 행별 천원 올림 끝수가 <1,000원 섞인다). 763 실측 = 15,820,982 + 2,645,020 + 5,432,998 = 23,899,000. - 🔴2026-09-04 포인↔한샘 단가 비교의 본선 = 「길이로 측정된 단가」끼리 = ①+② vs 한샘 환산(페페 지시). ③은 길이와 무관하니 비교에 섞지 않고 양쪽에 동일하게 얹는 공통 가산항으로 따로 세운다 → 최종 =
(①+②+③) vs (한샘환산+③), 차액 = 한샘환산 − (①+②) 로 길이단가 차이 그대로다. 🔴①만 떼서 비교하면 마진만큼 포인을 과소평가한다 — 763 에서 그 오류가 D등급을+1,220,568(한샘이 비싸 보임)로 뒤집었다. 바른 값 = D−1,424,452(−7.7%), 추가금액 5,432,998. 🔑「행총액 대비 −28.7% 손실」 서사는 틀렸다 — 손실이 아니라 ③을 한샘 양식 어느 탭(자재비 / 추가분견적)에 실을지의 청구 경로 문제다. 산출 =C:\dev\order-automation\hansem_price_mapping.py(billed_sum·addon_sum·hansem_vs_billed·final_diff+①+②+③=행총액·공통가산이 차액을 안 바꾼다assert 2개 내장) → 페이지 =C:\dev\devplan\hansem-poin-price-map.html. ⚠표B·C(단가표 대 단가표)는 마진 전 축이라 표A 와 %가 다르다(페이지에 경고 박스 있음). - 🔑2026-09-04 「아이비젼글로벌」은 화면에 「아이비젼」으로 축약해 적는다(페페 기본규칙). 이걸로 8.10 의 「이름 순서 유지 +
<br>2줄 끊기」 규칙은 끝났다 — 축약하면 업체칸(8%)에 1줄로 들어간다. 구현은 표시하는 곳 한 군데만: 출력은compName()(/(?:글로벌)?\s*아이비[젼전]\s*(?:글로벌)?/ → '아이비젼'), 필터는COMPANY_FILTERS원소를 짧게(부분일치라 짧은 쪽이 운영아이비젼글로벌·테스트글로벌아이비젼둘 다 잡는다 → 테스트 0건 함정도 같이 해소). DB 거래처명·엑셀·발주서는 안 바꾼다. - 🔑2026-09-04
table-layout:fixed표는<thead>를 지워도 컬럼 폭이 안 흔들린다 — 폭은<colgroup>이 정한다. 생산현황 출력의 「상차일 미확정」 표가 헤더 없이도 위 AS 표와 픽셀 단위 동일([84,199,69,123,238,54]). 같은 양식이 연속되면 두 번째 표부터 헤더는 지면 낭비다. 반대로auto레이아웃 표에서 헤더를 지우면 폭이 데이터로 재계산된다 — 그때는 지우지 마라. - 🔴2026-09-04 한샘 견적 저장은 평형(type)을 전량 delete-insert 한다 →
type_no가 매번 새로 발급된다.GET /api/hansem/estimate/N을 그대로POST하면(API 왕복) 상세가 없어진 옛 type_no 를 물어 고아가 되고, 총액은 상세로 계산돼 목록은 멀쩡한데 엑셀 내역서만 텅 빈다(실측: 견적 2, 상세 13줄 전부 고아). ⛒코드는 고쳤다(_replace_children가 types 에 실려 온 옛 type_no 로 옛→새 다리를 놓는다, master0d37bec). 그래도 스크립트에서는type_index(types 배열 인덱스)로 보내는 게 정석이다. 복구 = GET 후 type_index 로 재저장. 검사 =python scripts/test_hansem_type_remap.py(가짜 tx 4케이스). - 🔑2026-09-04 한샘 양식 교체는
POST /api/hansem/form(multipart) 로 올리면 새 form_no 가 활성이 된다 — 기존 견적은 옛 form_no 를 계속 본다. 사업자등록증처럼 우리 회사 정보가 바뀐 것이면 기존 견적의master.form_no도 새 값으로 갱신해야 한다(안 하면 옛 사진이 계속 나온다). ⚠그때 상세를 그대로 되돌려 보내면 위 고아 함정을 밟는다. 양식 원본 = NAS2026현장\견적\넥서스\한샘 견적 양식(가구,시공).생산비 거래명세서_라인건설 아산테크노밸리_26년 06월.xlsx(시공비는 같은 폴더2.시공비…— PFS 미등록). 검증 = 출력 엑셀의xl/media/*md5 가 원본과 같은지. - 🔑2026-09-04 pfs 슬롯은 셋이다 — 운영 8000(
/home/ubuntu/pfs·deploy-live·RDS) / 테스트 8001(pfs-test·master·Docker MySQL 3307·실험 데이터, 쓰기 실험은 여기서) / 스테이징 8002(pfs-stage·deploy-live·Docker MySQL 3308·운영 사본, 배포 리허설 전용·매일 04:00 KST 덮어씀). 접속 = Tailscalehttp://100.73.87.48:8002(SG 의 사무실 IP 는 낡았을 수 있다). - 🔴2026-09-04 배포 반영 판정은
/api/health의rev로 한다.curl / → 200은 옛 워커가 살아 있어도 200 이라 아무것도 증명 못 한다(uWSGI 중복 인스턴스 선례). 배포 스크립트가REVISION(=git rev-parse HEAD)을 쓰고 health 가 그걸 돌려준다. 같은 이유로 종전deploy-production.sh는 앱이 죽어도 배포가 초록이었다(curl -s -o /dev/null -w는 500 이어도 curl 자체는 성공) → 헬스 판정 + 자동 롤백으로 개편. - 🔴2026-09-04 EC2 는 Ubuntu 18.04 라 Playwright 가 네이티브로 안 돈다(glibc 2.27 · python 3.6). 서버 화면검사는 컨테이너
pfs-smoke:1.47.0로만. ⚠MSplaywright/python이미지는 브라우저만 들었고 파이썬 패키지가 없다(실측) →pip install playwright를 얹어 구운 것이 그 태그다. - ⚠2026-09-04 브로커 게이트가 「auto-deny」를 계속 뱉으면 브로커 프로세스가 옛 규칙을 들고 있는지부터 본다.
whitelist.py를 고치고Start-ScheduledTask만 해서는 옛 프로세스가 7777 을 계속 잡고 있다(실측: 잡고 있던 PID 의 시작시각이 오전이었다). ⛒Get-NetTCPConnection -LocalPort 7777로 PID 를 확인해 그 PID 를 죽인 뒤 재시작. 규칙과 실제 응답 대조 =venv\Scripts\python.exe -c "import whitelist; print(whitelist.classify(...))". - 🔴2026-09-04 브랜치 전환·
reset --hard를 파이프(| tail)에 물리지 마라 — 로컬 변경이 날아간다.git checkout deploy-live 2>&1 | tail -3 && git reset --hard origin/deploy-live에서 checkout 이fatal(worktree 점유)로 죽었는데 파이프라인 종료코드는tail것이라&&가 성립, master 위에서 reset 이 돌았다(커밋 안 된pfs/CLAUDE.md수정 유실·복구 불가). ⛒브랜치 명령은 단독 실행 + 앞에git status. - 🔑2026-09-04 pfs
deploy-live는 worktreeC:\dev\pfs_deploy_wt가 점유한다 — 운영 배포는 거기서git checkout origin/master -- <파일>→ 커밋 → push. 메인 체크아웃(C:\dev\pfs)에선checkout deploy-live자체가 안 된다. - 🔴2026-09-04 업체 필터 버튼은 늘어날 때마다 상태탭을 덮는다 —
#view-manage .dataTables_filter절대배치라 겹침 = 뷰포트폭 − 1045(5버튼 기준), 992px 에서 「완료」탭 실종(4버튼 때부터 -10px 잠복). ⛒@media (max-width:1199px)흐름 복귀로 막았다. 6번째 업체를 추가하면 1280px 대에서도 좁아진다 → 그때는<select>전환을 검토. - ⚠2026-09-04 화면 검증에서 「타업체가 섞였나」를
tbody전체 텍스트로 보면 오탐한다(현장명에 남의 업체명이 든 현장). ⛒DataTable().rows().data()의 해당 필드만 본다. - 🔴2026-09-04 DataTable 화면에서 「필터가 걸렸나」를
tbody tr수로 재지 마라. 페이지 크기(10·20행)에 잘려 필터 전후가 같은 수로 나온다 → 멀쩡한 기능이 FAIL 로 뜬다(업체필터 봉적 검증에서 실제 2건 오탐). ⛒page.evaluate("(s)=>$(s).DataTable().rows().count()", "#table")로 센다. - ⚠2026-09-04 테스트 RDS 와 운영 RDS 는 거래처명이 다른 게 있다 — 아이비젼 = 테스트
글로벌아이비젼/ 운영아이비젼글로벌. 업체 필터는company_name부분일치라 테스트에서만 0건이 나온다. 화면 검증 때 「내 변경이 깨뜨렸나」로 오해하기 쉽다. - 🔑2026-09-04 업체 필터 버튼 추가 = 배열 원소 1개(
COMPANY_FILTERS,field_list.html·estimate_list_v2.html두 곳 동일 구조). 이름은 거래처 마스터 실명을 DB 로 먼저 확인한다(scripts/_bongjeok_company_probe.py패턴 ·estimate테이블엔company_no가 없어field조인으로 센다). - 🔴2026-09-04 행마다 grid 가 독립이라, 긴 셀 하나가 그 행의 열 폭을 늘려 「그 행만 줄이 밀린다」. 시트관리에서 비고 있는 행(6·7·11·12번)만 품명코드가 우측으로 쏠린 게 그것(비고를 col1 에 뒀더니 그 열이 비고 길이만큼 확장). ⛒처방 = 길이가 들쭉날쭉한 셀은 아래 줄 전체 폭(
grid-area:N/1/auto/5)으로 내리고, 열 폭은 짧고 고른 값(NO·수량)만으로 정해지게 둔다. 같은 사고가 보드에도 있었고(품목 col1~2 와 비고 col2 가 같은 칸을 밟고 있었다) 함께 고쳤다. - 🔴2026-09-04
auto열은 「글자수 다른 배지」에도 흔들린다. 비고를 뺀 뒤에도 시트 품명코드 시작점이 x=75/88 두 종류였다 — 범인은 거래처 칩(포인 2자 vs 에넥스 3자). ⛒열을 고정폭(60px) 으로 못 박고 칩에 말줄임을 준다. 🔑테스트 데이터엔 한 종류뿐이라 안 잡혔다 — 운영에서만 재현됐다(같은 날span.code이탈도 그랬다). 표 레이아웃은 운영 데이터로도 반드시 한 번 잰다. - 🔑2026-09-04 「줄 밀림」은 검사로 잡을 수 있다 — 1줄째 주값의
getBoundingClientRect().left를 전 행에서 모아 종류가 1개인지 본다(scripts/_omc_mobile_check.py「1줄째 주값이 모든 행에서 같은 x」).scrollWidth기반 가로넘침 검사로는 절대 안 잡힌다. - 🔴2026-09-04 2줄 압축표에서 「버튼을 키우면 주값 열이 깎인다」(web-designer 실측). 시트관리는 4열 트랙이 300px 를 나눠 쓰는데, 터치 타깃을 키우자 3줄째 액션이 col3 폭을 결정해 품명코드(1fr) 100→84px · 말줄임 5행→20행이 됐다. ⛒처방 = 액션을 col2~3 에 걸쳐 트랙 경쟁에서 빼면 col3 가 1줄째 숫자만으로 정해진다 → 실측 134px · 말줄임 0행(원래보다 넓어졌다). 터치 타깃은 세로만 키운다(가로 padding 을 늘리면 같은 예산을 또 깎는다).
- ⚠2026-09-04 td 에 말줄임을 걸어도 안쪽
span은 자기 폭대로 그려진다. 시트 품명코드가 셀 밖으로 나갔는데 테스트엔 그만큼 긴 코드가 없어 운영에서만 재현됐다(docW는 360 그대로라 「가로 넘침」 검사로는 안 잡힌다 — 요소별 rect 검사가 잡았다). 처방 = 그 span 에도display:block; max-width:100%; overflow:hidden; text-overflow:ellipsis. - 🔑2026-09-04 한샘 견적에서 「탭」 = 평형 슬롯(
hansem_estimate_type)이다.types를 2개 넣으면 엑셀에<이름> 내역서-주/-수가 두 벌 생기고 갑지(주방·수납)A19~A21이 각각을 참조한다 — 견적을 나눌 필요가 없다. 상세는type_index(저장 전) 또는hansem_type_no(저장 후)로 슬롯에 붙는다.col_index는 슬롯 안에서 세야 한다(합칠 때 같은 블록끼리 이어 세면 칸이 밀린다). ⚠마스터는 하나뿐이라 현장·견적일·담당자는 한 값이다 — 현장이 서로 다른 건(MH 1973 vs AS 2158)을 합치면 한쪽은 비고로만 남는다. 남는 슬롯은 양식 원래 이름(84B)으로 남고 금액 0. - 🔴2026-09-04 한샘 견적 엑셀 — 단가 LOOKUP 수식은 「양식 원본이 쓰던 칸 수」만큼만 있다. 블록마다 다르다(실측: 내역서-수 예비블록·붙박이장은 C 한 칸뿐, 어떤 블록은 CDEF). PFS 는 단가행을 안 건드리는 원칙이라, 그 다음 칸에 치수를 넣으면 적용가가
빈칸*10MM기준= 0 이 되어 그 줄 금액이 조용히 사라진다. 서버 총액(hansem_calc)은 정상이라 목록만 보면 절대 안 걸린다. ⛒처방 =hansem_excel._ensure_price_formula()— 값을 쓰는 칸에 수식이 없으면 같은 블록 수식을 열만 재투영해 복제한다($절대참조·*0.5배수 보존). 운영dfd1435. 검사 =python api/pfs/hansem/hansem_excel.py(자체검사) + 엑셀 zip 에서블록 값칸 ⊆ 단가수식칸확인. - 🔑2026-09-04 한샘 양식은 「평형 × 가구블록」 구조라 추가·AS 품목이 그대로 안 들어간다. 금액을 맞추는 손잡이는 적용률(
apply_rateDECIMAL(5,4), 0.0001~9.9999) 하나다 — 길이는 실치수로 두고 등급 A~D × 적용률(소수 4자리) 을 역산해 목표금액에 붙인다(반올림 잔차 ±수십원). 운임·물류비·부속처럼 가구가 아닌 줄은 「예비블록」(수납 S/블록46) 을 빌리고 길이 = 목표금액/400(STO_LOW·01·A 단가 4000 → 400×길이)·적용률 1.0 이면 정확히 일치한다. ⚠양식엔 「추가분견적(주방가구)·(일반가구)」 탭이 이미 있는데 모듈이 아직 안 쓴다(2차 미착수) — 추가건의 제자리는 거기다. - ⚠2026-09-04
POST /api/field/(현장생성)는 id 를 문자열로 보내야 한다.field_user_no·company_no·company_member_no를 int 로 주면FormValidator.validation_check가len()을 불러 500object of type 'int' has no len()이다. 필수 키도 전량 필요(shipping_comment·post_code·address1·address2·comment·construction_member_no·body_material_comment·finishing_material_comment) — 하나라도 빠지면 KeyError 500. - 🔑2026-09-04 현장명 사명 정규화는
field_service._normalize_field_name()이 한다 — 등록·수정으로 들어오는 것만넥서스→한샘(한샘넥서스를 먼저 치환해야한샘한샘이 안 된다). 기존 현장 7건(411·483·994·1486·1554·2057·2076)은 이력 보존으로 안 바꾼다(페페 결정). 같은 alias 가 발주서 파일명 쪽orderpaper-sync/orderpaper_parse.py _COMPANY_ALIAS에도 있다(사이드카라 import 불가 — 값만 맞춘다). 검사 =python scripts/test_field_name_alias.py. - 🔴2026-09-04 표를 모바일에서 2줄로 접을 땐
table·tbody까지display:block으로 바꿔야 한다.tr{display:grid}만 주면 테이블 익명 박스가 여전히 행을 배치하려 들어 열 폭이 글자 하나로 줄고 글씨가 세로로 찌그러진다(실측 1회). 같은 이유로 그 표 안의 구역 머리 행도display:block이어야 폭을 채운다(table-row로 두면 내용만큼만 잡히고 다음 행이 옆에 붙는다). - 🔴2026-09-04 셀 재배치를
nth-child로 걸면 rowspan 이 있는 표에서 깨진다. 자재관리는 그룹 첫 행에만 현장 rowspan 셀이 하나 더 붙어 td 순번이 1칸 밀린다 → 그 행만 배치가 통째로 어긋났다. ⛒처방 = 렌더 JS 에서 td 마다 클래스를 주고(op-c-front·op-c-status…) 클래스 셀렉터로 배치한다. 데스크톱엔 영향 0. - 📌2026-09-04 오엠씨 3페이지 모바일 표 = 「A안 · 2줄 압축표」(페페 목업 확인 후 확정). 1줄째 = 제일 많이 찾는 값 + 숫자, 2줄째 = 부속 정보, 3줄째 = 비고·액션. 마크업·JS 는 그대로 두고 행(tr)만 grid 로 재배치한다(데스크톱 무변화). 규칙 위치 =
web/static/css/omc.css의@media (max-width:767px)(시트#sheetTable·보드#boardTable) ·field_orderpaper.html안 같은 미디어 블록(자재table.op-omc-t). 목업 원본 =C:\dev\devplan\pfs-omc-mobile-mockup.html(안내 문구는 목업에만 둔다 — 페페 지시). 검사기 =scripts/_omc_mobile_check.py(360px·A안 구조·데스크톱 회귀). - ⚠2026-09-04 모바일 보조 글씨도 폰트 하한 13px·본문톤을 지킨다(페페 확정 규칙 +
pepe-ui-dark-font-preference). 2줄 압축표를 만들며 11~12px·연회색(--muted)을 썼다가 되돌렸다 — 위계는 크기·연한 색이 아니라 굵기(800 vs 600)로 준다. - 🔑2026-09-04 업체 로고를 「어떤 출력물에서만」 다르게 쓰려면 파일 오버라이드 규약을 쓴다 —
web/static/img/company_logo/pallet/{company_no}.{ext}. 있으면 파레트용지에서만 그걸 쓰고 없으면 기본company_logo/{company_no}.{ext}로 폴백한다(sticker_controller._logo_url(company_no, logo_dir)· 응답 필드pallet_logo_url·renderPallet()이f.pallet_logo_url || f.logo_url). ⛒원본 로고 파일을 고치면 안 된다 — 같은 파일을 스티커 헤더(.ss-logo)·현장 배지(#stk-field-badge)가 같이 쓴다. 롤백 = 오버라이드 파일 하나 삭제(코드 롤백 불필요). 첫 적용 = 한샘(1) 파레트용지에서 「HANSSEM NEXUS」→「HANSSEM」(페페 지시, 운영2fb811f). - 🔑2026-09-04 로고 이미지에서 단어를 뺄 땐 캔버스 비율을 그대로 두고 남는 글자를 가운데로 옮긴다(타이트 크롭 금지).
.ps-logo img{max-height:36mm;max-width:160mm;object-fit:contain}는 원본 비율 8.09 라 가로 160mm 기준으로 맞는다 → 잘라서 비율이 4.56 이 되면 세로가 19.8mm→35mm 로 치솟아(+77%) 옆 칸 「가구」(16mm)보다 무거워진다(web-designer 실측). 같은 캔버스(1376×170)에 글자만 재중앙하면 인쇄 크기가 원본과 완전히 같다. ⚠미리보기 실측은offsetWidth/Height로 한다 —.stk-pallet-preview-box에transform:scale(--ps)가 걸려 있어getBoundingClientRect는 화면 크기(1.1배 등)를 준다. 검증 =C:\dev\pfs\scripts\_pallet_logo_check.py [test|prod](17항목). - 🎨2026-09-04 pfs UI 작업은 착수 전
web-designer에이전트 상담이 필수다(페페 지시). 대상 = 템플릿·CSS·화면 JS·레이아웃·버튼·폰트·표·색·UI 문구·인쇄물. 면제 = 화면에 안 보이는 백엔드 변경·오탈자. 확정 규칙 = 폰트 크기 하한 13px(그 이하 신규 금지, 예외는 페페 승인) · BS5 문법 금지(이 리포는 Bootstrap 4.2.1 —data-bs-*·ms-*/me-*·.form-select·.btn-close안 먹는다). 그 에이전트는 진단까지(Playwright 프로브·실기 스크린샷·cody-vision) 하고 코드 수정은 코디가 한다. 매뉴얼 =C:\dev\seedchain\projects\web-designer\kb\(PFS 렌즈 =00-pfs-lens.md— 지금까지 터진 UI 사고 10건이 표로 들어 있다). - 🔑2026-09-04 크롬 자동완성 차단은 이제 공용 헬퍼 하나다 —
blockChromeAutofill(root)(web/static/js/pfs-component.js). 칸마다readonly를 손으로 박지 말고 화면 ready 에서blockChromeAutofill(document)한 줄 + 모달은$(document).on('shown.bs.modal', e => blockChromeAutofill(e.target))로 건다(팝업 DataTable 검색칸처럼 나중에 생기는 칸이 많다 — 모달 열린 뒤 ajax 로 그리는 행은 그 렌더 함수 끝에서 한 번 더). ⛒원래readonly인 칸은 제외한다(.not('[readonly]')) — 풀어 주면 팝업으로만 채우던 칸에 타이핑이 열린다. 적용 =/admin/user(26.9.3~4) ·/field/list+모달(26.9.4 운영53c4391). 검증 =scripts/_tbd_frame_check.py⑥~⑨. - 🔑2026-09-04 생산현황 출력의 「상차일 미확정」은 상차일 빈칸 하나로 판정한다 —
isTbd(f){return !f.date}(등록·수정 폼의 「미확정」 버튼도 같은 기준 = 별도 컬럼이 없다,initShippingTbd). 미확정 현장은 메인·AS 표에서 빠져 하단#tbd-grid로 간다(중복 아님 · 페페 「모아서 올린다」). 되돌리려면renderMain·renderAs의&& !isTbd(f)두 조각만 지운다. ⚠이 출력은 여전히 진행중(20)만 뽑으므로 대기 현장의 미확정 건은 새 표에도 안 뜬다(운영 실측 미확정 3건 중 2건만 대상) — 「미확정인데 왜 없냐」가 오면 그 현장 상태부터 본다. - 🔴2026-09-04 모바일 넘침 1순위 = grid 아이템의
min-width:auto.grid-template-columns:1fr로 1열을 만들어도 셀 안의 표·가로목록이 셀을 통째로 밀어낸다(노트20 실측: 보드관리.item-side가right=681px, 자재관리.op-omc-wrap도 같은 증상 — 둘 다 CSS 만 보면 1열이라 안 보인다). ⛒1열로 바꾸는 곳엔 항상> * { min-width:0 }을 같이 넣는다. - ⚠2026-09-04 폰 실기 검증은 웹 모바일뷰로 대체되지 않는다(페페 지시). Playwright 390px 에서 ALL PASS 였는데 노트20 에서 넘침 2건·버튼 어긋남 1건·탭 가림 1건이 더 나왔다. 실기 =
C:\dev\pfs\scripts\_omc_note20_shots.py(USB adb · 크롬 URL 인텐트 →screencap회수). ⛒함정 = ①앞 스크립트가awake()를 빠져나오며 화면을 끄므로 매번unlock()부터 ②keyevent 111(ESC)은 크롬을 통째로 닫는다 — 제출은keyevent 66(Enter) ③입력칸 이동은 좌표 두 번 탭보다 TAB(keyevent 61) 이 안전(키보드가 칸을 가린다). - ⚠2026-09-04 QA 헬퍼 토글은
position:fixed top:70 right:12라 모바일에서 페이지 최상단 요소를 덮는다(web/static/css/qa_helper.css). 오엠씨 상단 탭의 마지막 칸(자재관리)이 그렇게 가려졌다 → 그 자리(우측 52px)를 비워 두는 쪽으로 피한다. qa_helper 를 옮기지 않는다(전 페이지 QA 규약). - 🔴2026-09-04 크롬 자동완성은
autocomplete=off·type=search로 못 막는다 — 유일하게 확실한 건readonly다. 26.9.3 처방(/admin/user검색칸type=search+autocomplete=off)으로 재발했다(페페 QA: 첫 로드는 전체목록인데 작업 중poin01이 박혀 그 사람만 남는다). 크롬은 credential 후보 칸에서autocomplete=off를 의도적으로 무시하고,search도IsTextField()라 아이디 칸 후보에 그대로 들어간다. 처방 2겹 = ①칸을readonly로 두고focus때removeAttribute('readonly')(크롬은 readonly 를 안 채운다 · 확정적) ②같은 페이지 password input 전부autocomplete="new-password"(credential 채우기 자체를 끔 = 트리거 제거. 「현재 비밀번호」 칸도 new-password 로 적어야 안 채운다). ⛒readonly만 걸면 크롬이 옆의 다른 text input(여기선#detailCompanyName)으로 옮겨가 박으므로 ②가 진짜 뿌리다. ⚠헤드리스로는 재현이 안 된다(저장된 계정이 없다) → 검증은 「속성이 붙었나 + 검색이 여전히 되나」까지(C:\dev\pfs\scripts\_qa_autofill_check.py [test|prod]13항목). 운영98c4841. - 🔴2026-09-04 전역
pfs_layout.css의.topbar{display:none !important}는 이름이 같은 남의 요소까지 죽인다. 그 규칙은 테마 상단바를 없애려고 넣은 것인데, 오엠씨 페이지의 제목·액션 줄(.omc-scope .topbar)이 같은 클래스명을 써서 Excel 내보내기·품목 추가·확인서 출력·입출고 등록 버튼이 데스크톱에서도 통째로 안 보였다(실측getComputedStyle(topbar).display === 'none'). ⛒흔한 클래스명(.topbar·.card·.tools)을 스코프 안에서 쓸 땐 스코프 셀렉터 +!important로 되살려 둔다. 판정 1분 = 안 보이는 요소에offsetParent === null이면 조상까지 computed display 를 찍어 본다. - 🔑2026-09-04 모바일 UA 로 「별도 목업 HTML」을 서빙하면 실제 페이지와 영구히 갈라진다.
/omc/sheet·/omc/board는is_mobile_ua()면 와이어프레임 목업(sheet_mobile.html2,549줄)을 그대로 내줬다 — 사이드바·타이틀(「UI 와이어프레임」)·버튼이 전부 가짜였고,<button ... <!--주석--><button>처럼 태그가 깨져 빈 버튼까지 렌더됐다. 폐기하고 실제 페이지 하나 +@media로 통일(목업은C:\dev\pfs\mockups\omc\보관). 모바일 전용 이동 탭은web/templates/omc/_mobile_tabs.html한 조각을 세 페이지가 include 한다(자재관리는omc.css를 안 써서 스타일을 조각 안에 뒀다). - 🔑2026-09-04 사이드바 메뉴가 3단이 되면 권한 필터가 조상 체인을 끝까지 타야 한다.
permission_service는 허용된 leaf 의 직접 부모만 살렸다 → 「업체별 페이지 > 오엠씨 > 시트관리」에서 조부모(업체별 페이지)가 빠지면base.html이 최상위부터 훑으므로 그 가지가 통째로 사라진다. while 루프로 조상 전부 수집. - ⛒2026-09-04 같은 리포에서 다른 세션이 동시에 작업하면 내 staged 변경이 남의 커밋에 딸려 나간다.
git mv로 목업 2개를 옮겨 두고 다른 일을 하는 사이, PNCAD 작업 세션의 커밋(80a922c)이 그 rename 을 함께 커밋·push 했다 — 라우트 수정은 아직 로컬이라 운영/테스트가 「템플릿 없음」 직전 상태로 잠깐 갔다. ⛒git mv·git add는 그 자리에서 바로 커밋한다(스테이징을 걸쳐 두지 않는다). - 🔑2026-09-04 생산현황 출력(
/field/print/status)의 AS 섹션 판정은 유형 「이름」이 정확히AS인지 하나뿐이다 — 코드값(60)도, 상위 본건 유무도 안 본다(field_status_print.html의f.field_div !== 'AS'/=== 'AS', DAO 가d.value AS field_div로 라벨만 내려준다). ⛒유형관리 모달에서 AS 의 이름을 바꾸면 이 출력이 조용히 깨진다(AS 표가 비고 전부 본건 표로 간다). 또 이 출력은h.field_status='20'(진행중)만 뽑으므로 유형을 바꿔도 대기·완료 건은 애초에 안 나온다 — 「반영이 안 됐다」로 오판 말 것. - 🔑2026-09-04 AS(60)는 상위 본건 없이도 정상 유형이다(등록폼은 60 을 고르면 「상위 본건」 칸을 보여줄 뿐, 비워도 저장된다). 부모 없는 AS 의 대가는 내부 현장목록에서 ①행마다 「연결」 버튼 ②정렬상
asOrphan이라 목록 맨 뒤로 밀림뿐이고(field_list.htmlsortFieldResults), 업체 전용 목록(field_company_list)은 정렬·연결버튼 둘 다 없다(AS 배지만). 「본건이 없으니 AS 를 못 쓴다」는 오해다. - 📌2026-09-04 아이비젼글로벌(company_no=452) 현장의 정본 유형 = AS(60) — 입주완료 세대 AS 라 본건이 없다. 26.9.3 에 전수 내수(50)로 바꿨던 것을 페페 지시로 되돌렸다(운영 13행 UPDATE, 전체 15건 AS). 스크립트 =
C:\dev\pfs\scripts\_ivg_div_to_as.py(롤백 스냅샷 출력) · 검증 =_ivg_as_section_check.py. ⛒_ivg_div_rollback.py의 SNAP 13건은 낡았다(그 뒤 등록된 2100·2153 누락) — 이 업체는 「전체 중 60 아닌 것」으로 잡는다. - 🔑2026-09-04 공통코드 일괄저장(
save_common_code_group)은 「제출 안 된 기존 항목 = 삭제(use_yn=0)」다. 그래서 오래 열어 둔 화면이 저장하면 그 사이 남이 추가한 코드가 조용히 지워진다(코덱스 지적, 실사고 아님). 프론트에서 삭제 버튼을 잠그는 것만으로는 API 직접호출로도 뚫린다 → 서버(서비스 계층)에 가드를 둔다. 현장유형은_guard_field_div_delete()가 사용 중(현장 1건 이상)·AS(60) 를 거절한다(다른 그룹은 사용처 판정이 달라 미적용). 자체검사 =C:\dev\pfs\scripts\_fielddiv_guard_test.py(DB·서버 없이 7항목). - 🔑2026-09-04 셀렉트 옵션을 하드코딩하면 코드가 늘어난 걸 아무도 모른다.
/field/list유형 필터가MH/OT/SH/연고/내수/AS6개로 박혀 있어 운영에 있던개인건(70)이 검색조건에 아예 없었다(등록·수정 페이지는_field_div_list()로 이미 공통코드 렌더 중이었다). 공통코드가 있는 값은 서버 렌더 단일 출처로 통일한다. - ℹ2026-09-04 테스트(8001)와 운영 DB 는 별개다 — 같은 유형인데 사용 건수가 다르다(MH 489 vs 491, AS 1025 vs 1036). 「테스트에서 데이터로 확인했다」를 운영 근거로 쓰지 말 것.
- 🔑2026-09-03 모바일 사이드바는
.mini-sidebar가 붙은 채로 열린다. 테마가 화면폭 기준으로 body 에 붙이는 클래스라, 210px 로 펼쳐도 접힘용 압축 규칙(.mini-sidebar .sb-logo{display:none}·.sb-user-info{display:none})이 그대로 먹어 로고와 「비밀번호 변경·로그아웃」이 안 보였다. ⛒.mini-sidebar규칙을 쓸 땐 「접힘(데스크톱 70px)」과 「모바일 펼침」이 같은 클래스를 공유한다는 걸 전제한다 — 모바일용 되돌림은@media (max-width:767px)안에, 파일 맨 뒤에(같은 특이도라 순서로 이긴다). - 🔑2026-09-03 검색칸에 계정 아이디가 저절로 들어가면 서버·JS 를 파지 마라 — 크롬 자동완성이다.
/admin/user의#searchKeyword에poin01이 박혀 있던 건 같은 페이지에 password input 이 3개(신규 사용자·PW 재설정·내 PW 변경) 있어 크롬이 첫 text input 을 로그인 아이디 칸으로 읽은 것. 처방 =type=search+autocomplete=off(type=text만으로는 크롬이autocomplete=off를 무시할 수 있다). 판정 = 템플릿·컨트롤러에value=가 없는지 먼저 확인(1분). ⛔이 처방은 부족했다 — 26.9.4 재발(맨 위readonly+ passwordnew-password항목이 정본). - 🔑2026-09-03 오엠씨 화면(
/omc/material)에 새 렌더러를 붙일 땐 표를.op-site[data-cid=…]로 감싼다. 편집 헬퍼(opEditDate·opEditNote·opEditQty·opEditFace·setStatus)가 전부span.closest('.op-site').getAttribute('data-cid')로 cid 를 찾는다 → 감싸기만 하면 편집 로직을 한 줄도 복제하지 않고 새 UI를 얹을 수 있다. 반대로 안 감싸면 클릭해도 아무 일이 안 일어나고 에러도 안 난다. - 🔑2026-09-03
field_orderpaper.html의 저장상태 복원은 두 군데서 부른다 —$(document).ready(이미if(!OP_OMC))와loadAll()안(가드 없었다). localStorage 키가 메인/오엠씨 공용이라 뒤엣것 때문에 메인에서 「업체별」로 바꾸면 오엠씨 화면까지 업체별로 따라왔다(페페 지적). 오엠씨 전용 화면을 만들 땐 「저장상태를 읽는 지점이 몇 개인지」부터 센다. - 🔑2026-09-03
.op-wrap은max-width:1180px라 컬럼 많은 표는 무조건 잘린다. 자재관리 탭이body.op-mm-active .op-wrap{max-width:none}로 이미 푸는 것과 같은 처리를 해야 한다. 그리고 CSS Grid1fr은minmax(auto,1fr)이라 nowrap 표가 길면 칸이 늘어나 옆 사이드바를 화면 밖으로 민다 →minmax(0,1fr). 둘 다 실측으로만 드러난다(표 1349px, 사이드바 오른쪽끝 1735px > 뷰포트 1680px). - 🔑2026-09-03
onclick에 현장명 같은 문자열을 박지 마라.esc()가'를'로 바꾸고 브라우저가 속성값을 디코드하면서 되돌려 JS 문자열이 깨진다(뒤에.replace(/'/g,"\\'")를 붙여도 이미 늦다). 처방 = 렌더 때 배열에 담고 번호(index)로 넘긴다. - 🔑2026-09-03 발주서정리(
/field/orderpaper) 표는 블록마다 헤더 칸수가 다르다 — 오엠씨 블록만품명→전면/후면2칸이라 9칸, 나머지는 8칸(blockTable의split). ⛒컬럼을 추가·검증할 땐 반드시 「블록별 thead」로 대조한다. 첫 블록 헤더 하나로 카드 전체 행을 대조하면 오엠씨 헤더(9칸) vs 일반 행(8칸) 이 어긋나 앱이 멀쩡한데[FAIL] 컬럼수 불일치 / 셀값 1칸 밀림이 나온다(26.9.3 실측, 검증 스크립트 쪽 버그였다). 빈행colspan=opCols()-4와 완료 구분행colspan=opCols()는opCols만 고치면 자동 정합. - 🔑2026-09-03 「입고처」는 이미 DB→API 까지 다 와 있다.
orderpaper.receiver(patch46) · 파서_receiver_of(항목 아래 가장 가까운 입고처 행, 섹션 경계에서 끊김) · DAOselect_all_items가COALESCE(o.receiver,'')로 내려준다. 운영 1,869행 중 1,769행 적재(포인 1,497 · 나머지 20여 업체). 화면에 안 보이던 것뿐 — 새 컬럼 요청이 와도 백엔드부터 파지 말고it.<필드>가 이미 오는지 먼저 본다. - 🔑2026-08-31
ssm_run.py로 뽑는 조회는 stdout 24,000자에서 조용히 잘린다. orderpaper 현장×업체 집계 322행을 한 번에 찍었더니 247행에서--output truncated--로 끝났는데[ssm] Status: Success라 성공처럼 보인다 → 뒷 알파벳(현대·학동·전시관)이 통째로 빠진다. ⛒조회는field_no IN (...)으로 좁혀 나눠 뽑고, 찍은 행수(n=)와 받은 행수를 대조한다. - 🔑2026-08-31 카톡 게시글 ↔ PFS 발주 대조는 「게시글에 발주 얘기가 있는데 orderpaper 에 행이 0인 것」이 진짜 구멍이다. 상태가
대기인 건 최소한 등록은 돼 있어 추적되지만, 등록 자체가 없는 건(금강에코 그릴캡 알루피아 · SGC 라로데코 조명 · SGC 74 복도장 손잡이 · 아이비젼 양주옥정 키큰장)은 어느 화면에도 안 뜬다. 판정 = 게시글 품목 키워드로item_name|spec|note|vendor LIKE전량 검색 → 0건이면 미등록. - 🔑2026-08-31 카톡관리 「방↔현장 연결」을 배선했다 — dao 만 있고 부르는 데가 없었다.
kakao_board_dao.update_board_field는 26.8.19 부터 있었는데 service·controller·UI 가 전부 없어서 화면에서 연결할 방법이 0개였다(field_no는 SELECT 만 되고 있었다). 추가 =service.modify_board_field+PATCH /api/kakao-board/field+ 카톡관리 탭 「현장연결」 버튼. 현장 검색은 묶음 연결의wlSearchFields·wlFieldRowHtml·wlInitListHtml를 그대로 재사용한다(같은 화면에 현장 검색을 두 벌 두면 한쪽만 고쳐진다). ⛒현장 배지의 진실은field_no다 —site_name(로컬 데몬이 방 이름에서 만든 게시글 제목용 추정값)이 초록 배지로 떠서 연결된 것처럼 보이고 있었다 → 미연결은 회색 배지. 데몬 미러 upsert 는field_no칸을 안 건드리므로 30초 동기화에도 연결이 살아남는다(테스트 서버 왕복 실증). 커밋ada1ba1(master) → 운영배포348218b(26.8.31 12:35 KST, 페페 채팅 승인, 3파일 선별 포워드). 🔑「미배포 커밋 18개에 걸쳐 있다」는 배포를 막는 사유가 아니다 —field_worklog.html이 그랬는데git diff --stat origin/deploy-live ada1ba1^ -- <파일>이 0 이었다(그 18개는 이미 다른 해시로 체리픽돼 있었다). 선별 포워드 전제는 커밋 개수가 아니라 내 커밋 직전 시점의 파일 diff 가 0 인가로 판정한다(26.8.28 개수 함정과 같은 뿌리). ⛒배포 스테이징 = 워크트리C:\dev\pfs_deploy_wt(deploy-live 고정). 본체C:\dev\pfs는 미추적 임시파일이 74개라 거기서checkout deploy-live하면 그게 다 따라간다. - 🔑2026-08-31 몸통자재 발주서 = imos 주문번호에서 나온다(마감재는 treadwork, 몸통은 imos).
orderpaper-sync/imos_parts.py <PROADMIN ID…>→momtong_order.py→ 피앤데코·피앤알 탭. 쿼리 정본은nesting/serve.py의PARTS_SQL하나뿐이라 복사하지 말고 거기서 읽어 쓴다. ⛒몸통재는 결 세로 고정(V)·양면이다(원본 RDL 규칙) — 마감재(결무시)와 달라 매수가 바뀐다. ⚠팔레트에는 규칙이 하나 더 있다: 가로가 원장폭 1220 을 넘으면 결 무관하게 세로와 강제 스왑. - 🔑2026-08-31 몸통 3T(등판)는 피앤데코가 아니라 피앤알에서 나온다. 피앤데코는 LPM 생산이라 시트 발주가 없지만 3T 만은 시트를 떠서 피앤알로 보내고(입고처 별도·비고 「2분할 권취 필수 요청」), 피앤알이 3T 우라판 양면으로 만들어 포인 입고. → 피앤데코 탭은 입고처가 둘이다.
- ⚠발주서 탭은 29행 블록이라 행을 지우면 안 된다. NO 가
=ROW()-7-(29*(ROUNDUP(ROW()/29,0)))+29라 행 삭제 즉시 번호와 블록이 어긋난다 → 최하단 「입고처 포인」 행은 빈 품목행으로 덮는다 (_blank_item_row: A26:B26·D26:J26 병합 해제 → B:C·D:E·H:J 로 재병합 → NO 수식 복원). ⛒병합 셀에는 값을 못 쓴다(MergedCell read-only) → 지우기는 병합 해제 후·재병합 전에. 입고처 행을 만들 때는 서식까지 복사한다(_copy_row_style) — 값만 쓰면 테두리가 끊긴다. - 🔑2026-08-31 원부자재 초안 수량은 AcadTread 작업파일(.treadwork.json)로 채운다 —
orderpaper-sync/tread_to_draft.py <treadwork.json> <draft.json>→orderpaper_build.py. ⛒두 숫자가 존재하는데 기준은 네스팅이다(페페 2026-08-31 확정). PFS 정본board_qty(부재마다 +200/+250mm 여유·일반 +2매) = 86매 · 길로틴 네스팅 실배치 = 51매 ← 이게 발주 매수. 페페가 보는 화면이 AcadTread 팔레트 「자재소요」라 그 수와 같아야 한다 — GQ503-3U 가 정본 5매 / 소요량 2매로 갈려서 드러났다. 결(세로결 고정)은 51→52매라 차이 35매는 전부 여유 패딩이었다. 구현 =orderpaper-sync/nesting.py(Nesting.cs 포팅, 그룹키 = 두께|수종|컬러|양단면|세로규격). - 🔑2026-08-31 오엠씨 시트수량은 「자재넘버(컬러) 단위」로 합산하고 셀을 세로 병합한다(페페 지시).
양면·단면은 원장이 갈려 행은 따로(후면이 유사PP / 같은 컬러로 다르다) 가지만 시트는 같은 것을
쓴다 → 행마다 10M올림+여유10M 을 매기면 발주량이 부풀어 오른다(470M → 300M).
산식 = 컬러별 Σ(양면 매수×5M + 단면 매수×2.5M) → 10M 올림 + 여유 10M. 2440 원장 기준이고
일반화는
매수 × sheet_order_len_m(원장) × (양면이면 2). 병합은 I·J열, 값은 맨 윗행에만. - 🔑2026-08-31 treadwork 의
kf(K/F) = PFSfield_type.type_div(10=주방·20=일반) 그대로다.board_qty가지가 여기서 갈린다(주방은 두께 불문 +200, 일반 18T 는 +250·+2매). ⚠컬러 정본은 도면 사양표(ENEX FORM)이지 레이어명이 아니다 — 레이어S78104-8WD·NCAC1-1WA는 오타/표기차고 도면은S8104-8WD·NCACI-1WA다 →LAYER_ALIAS로 교정하고 발주서엔 도면 표기. - 🔑2026-08-31 오엠씨 탭 행은 (두께·수종·컬러·양단면) 단위로 쪼갠다 — 단면은 후면=유사PP, 양면은 후면=같은 컬러라 원장을 합칠 수 없다(매수도 행마다 올림, 시트 면수는 양면 ×2). ⚠1.0T22mm 엣지는 18T 전용* — 9T 쫄대·15T 은경판은 엣지 집계에서 빼고 따로 보고한다.
- 🔑2026-08-30 「눌렀는데 아무 일도 안 난다」는 먼저 서버 로그로 요청 유무부터 가른다.
자재탭 신규현장 모달에서 생성이 안 되던 건 —
journalctl -u pfs-live.uwsgi에 페페 세션 (20:25~20:31) 의POST /api/orderpaper/material-spec/가 0건이었다. 요청이 아예 안 나갔다 = 서버·API·DB 는 전부 무죄, 프론트에서 멈춘 것. 이 한 번의 조회가 재현 시도보다 빠르다. ⚠EC2 python 은 3.6 이라subprocess.run(capture_output=True)가 없다 →stdout=PIPE. - 🔑2026-08-30 자재탭 신규현장 모달 = 검색 목록이 「취소·생성」 버튼을 통째로 덮고 있었다.
.op-mat-ns-list는position:fixed · z-index:1500 · max-height:48vh라 입력칸 아래로 393px 펼쳐지는데, 모달(max-width:500px) 안에는 그 아래로 가구업체·도면첨부·푸터가 있다. 실측 = 목록 159~552 · 생성버튼 336~359 →elementFromPoint(생성버튼 중앙)이.ns-nm(목록 항목). 목록 항목의onmousedown="event.preventDefault();nsPick(i)"가 클릭을 삼켜 엉뚱한 현장이 다시 선택될 뿐 생성 요청은 안 나간다. 고침 =nsShowList()에서 목록maxHeight를 모달 푸터 상단 위에서 끊는다(CSS 48vh 는 상한 유지). 운영ba15934(master105d7c92파일 선별 포워드). 🔑판정 도구 =elementFromPoint(버튼중앙)이 그 버튼인가 — 스크린샷·스냅샷으로는 안 보인다. ℹ남은 무반응 경로 1개(안 고침) = 목록이 닫힌 상태의 Enter 는 아무 것도 안 한다(nsKey가preventDefault만 하고 끝). 실수 생성 방지 의도라 그대로 뒀다. - 🔑2026-08-30 자재탭에 수집본을 넣을 땐
/collect(draft) 가 정답이지만,spec_json_user에 types 가 있으면 draft 는 화면에 안 뜬다.apply_draft는 user 문서에 types 가 없을 때만 비운다 (_doc_has_types). 카드를 한 번이라도 열면 자동저장이 기본 doc(TYPE 1)을 user 슬롯에 써서 그 다음부터 draft 가 가려진다 → 이때는PUT /spec/<no>(base_updated_at 동반)로 권위본을 직접 쓴다. 문서 형태 ={schemaVersion:2, site, colors[], cores[], edges[], subMaterials[], types[]}, 칼라{id,name,faces:{front,back},brand,sheetVendor,category(PET|LPM|PP|PVC),color,note}· 코어{id,material(MDF|PB),thickness,note}(3·4.5·6·9·12·22T 는 MDF 고정) · 엣지{id,colorId,name,vendor,material,thickness,width,note}· 위치{id,name,drawingRef,cells:{"door.front":<refId>, …}}. - 🔑2026-08-30
GET /api/field/report/requirement/<field>는 부재 미등록 현장에서 500 이다. 몸통 쪽이 imos MSSQL 이라pyodbc.DataError 22018 — nvarchar 'None' → int로 죽는다. 자재소요량을 못 받는 게 아니라 아직 부재가 없다는 뜻. 발주 수량 산출은 부재 등록 이후. - 🔑2026-08-30 원부자재발주서 「타입/개수」는 「비 고」칸이 아니라 「타 입」칸(발주서
B28)에 쓴다. 페페가 말하는 「비고」 = 이 칸이다. 실제 「비 고」행(B29)은 담당자명 고정(이선영 사원/신동혁 대리). 형식 =84B,84C,120:주방3,일반3(선례 넥서스 롯데 화성향남) → 이번 건84A,59:주방3,일반2. 현장명B27· 가구업체E27이고, 오엠씨/복제 탭의 현장명·타입은 발주서 탭을 참조하는 수식이라 한 곳만 쓰면 된다. - 🔑2026-08-30 발주서 xlsx 쓰기 =
orderpaper-sync/orderpaper_build.py(신설). 읽기는 종전orderpaper_parse.py. NAS 표준양식(\\192.168.0.101\공유폴더\원부자재발주서.xlsx)을 복사해 값만 채운다. 업체 탭은 「발주서」 탭을copy_worksheet로 복제 후 rename(현장파일 관행) — 실측으로 병합 85개·서식·열폭 전부 보존됨을 확인했다. 탭 좌표 = 발주서형 품목행8~25(B:C 품명 · D:E 규격 · F 발주량 · G 입고일 · H:J 비고), 오엠씨 품목행9~26(B 두께 · C 수종 · D:E 전면 · F:G 후면 · H 발주량[매수] · I 시트수량 · J 시트업체), 날짜는I2/I3(옆H2/H3= 연도). 입고처는 탭 중간 행에 넣는다 — A:B 병합입고처· C 업체명 · D:J 병합 주소수식. 시트는 오엠씨로, 칼라샘플은 엣지업체(진영LDM)로 직송이 관행이라 한 탭에 입고처가 2~3개 갈린다. ⚠생성 파일을 NAS2026현장\원부자재발주서\<월>에 두면 자동 동기화가 PFS 로 적재한다(오엠씨=RAW_TAB). 초안은 확정 전까지 NAS 밖에 둔다. - 🔑2026-08-30 자재 소요량 정본 산식 =
api/pfs/field/dao/field_dao.pyselect_field_print_requirement_material/_body. 워커는 이걸 그대로 재현한다(화면과 숫자가 갈리면 안 된다).매수 = CEILING(SUM((W+pad)*(H+pad)) / 2,880,000) + 가산, 2,880,000 = 1200×2400(4×8 원장 유효면적). ⚠CASE 는 위에서부터 걸린다 —type_div='10'(주방)이 두께 불문 1번 가지(pad 200·가산 0)에서 끝난다. 2번 가지type_div='20' AND thk=18 OR thk=3 OR thk=4.5는 AND 가 먼저 묶여 주방이 아닌 3T·4.5T 를 전부 pad 250·+2매로 보낸다(일반 18T 도 여기).type_div='30' AND thk=15만 pad 200·+1매. 의도인지 버그인지는 미확인 — 워커는 원본 그대로 재현한다. 시트 M =2.5×면수 + 0.5→ 10M 올림 + 여유 10M(페페 규칙 2026-08-30). 실적 대조: 24매→70M/80M 급. - 🔑2026-08-30 에넥스 현장 발주 업체 배정(페페 지시). 마감재 보드 = 오엠씨 · 엣지 = 진영LDM · 시트 브랜드 = LX(구 LG)/현대/한솔이고 한솔은 대리점 「한솔아트」로 적는다. 몸통이 LPM 이면 그 LPM 업체에 보드째 전량 + 몸통피스마개 스티커 + 엣지를 같이 발주한다 (그래서 몸통 0.6T 엣지는 진영LDM 이 아니라 LPM 업체 몫이다). 피스마개는 표면재와 같은 로트로 찍어야 색이 맞아 함께 나가는 것이고, 소요는 몸통보드 1매당 약 160EA.
- 🔑2026-08-28 테스트 DB 는 더 이상 RDS 가 아니다 — EC2 안 로컬 MySQL 8.4.8
127.0.0.1:3307. 옛poin-dbinstance-test.cz62ldnopdup...는 삭제됐고(페페가 비용절감으로 이전, 코디 26.7 검토안 ② = 월 $21 절감) DNS 에서 아예 안 풀린다(getaddrinfo failed). env =/home/ubuntu/env/pfs-test.env, 운영 =pfs-live.env(RDS 3306). → DB 작업은 로컬 직결 말고pfs/scripts/ssm_run.py로 EC2 에서 돌린다(로컬 py → base64 → EC2 python3 실행 → 출력 회수). ⚠pfs/scripts/codi_phase*.py5개가 옛 테스트 RDS 호스트를 기본값으로 물고 있다 → 재사용 전 갱신 필요. ⚠EC2 시스템 python 은 3.6 —subprocess.run(capture_output=True)없다. ※tools/rdstunnel(13306) 은 엑셀 파워쿼리 전용 경로로 남는다. 코디 DB 작업엔 안 쓴다. - 🔑2026-08-28 발주서 엑셀에서 발주량을 고쳐 동기화해도
stock(재고량)은 옛값 그대로다.orderpaper_sync.py는 신규 항목 INSERT 때만stock=qty를 시드하고, 기존 항목은 UPDATE 경로라stock을 건드리지 않는다(사용자 재고입력 보존이 설계 의도). 그래서 발주량만 바뀐 현장은 자재관리 탭 재고가 수정 전 수량으로 남는다 —orderpaper_usage_history에 소비이력이 0건이면 그stock은 사용자 입력이 아니라 옛 시드값이므로 재시드해도 안전하다(그래도 페페 승인 후). 판정 =SELECT ... FROM orderpaper_usage_history WHERE orderpaper_no IN (...). 선례 = 봉적 전시관(field 2162) 더하임 탭 26항목 중 13항목 qty 변경, stock 미갱신. - 🔑2026-08-27 「엑셀에서 DB 연결이 갱신 안 된다」는 판정이 먼저다 —
Get-WinEvent -LogName OAlerts한 방. 엑셀이 사용자에게 띄운 대화상자 원문이 그대로 남는다.IM002=DSN/드라이버 없음 ·10060=보안그룹 DROP ·HY000 Access denied=파워쿼리 자격증명 — 증상은 같아 보여도 원인이 완전히 다르고, 세 개가 겹쳐 있을 수도 있다 (미니PC 원부자재발주서는 실제로 셋 다였다). ⚠COM(자동화)으로 띄운 엑셀에서는 파워쿼리 오류가 전부 「다운로드하지 못했습니다.」로 뭉개진다 → 진짜 사유는 OAlerts 나 저장 후 DataMashup 메타FillErrorMessage로 본다. ⚠파워쿼리는 DSN 의 UID/PWD 를 안 쓰고 자기 자격증명 저장소(%LOCALAPPDATA%\…\PowerQuery\User.zipDPAPI)를 쓴다. 🔑사무실 밖 PC 를 운영 RDS 에 붙일 땐 SG 를 넓히지 말고 EC2 를 SSM 포트포워딩으로 경유한다 (C:\dev\tools\rdstunnel,127.0.0.1:13306→ EC2172.31.35.211→ RDS). 회선이 바뀌어도 안 깨지고 노출이 0이다. 상세 =session-2026-08-27-rdstunnel-orderpaper-vendor-query.md. - 🔑2026-08-27 화이트리스트를 자동으로 따라가게 만들 땐 「추가 → 실연결 검증 → 그 다음 삭제」 순서가 설계의 전부다.
ipwatch워커(사무실PC 예약작업PFS-IPWatch, 5분) 규약 — ①공인IP는 두 소스 교차확인 일치 시에만 채택 (불일치·실패는 아무것도 안 하고 변경 횟수에도 안 센다) ②사설·CGNAT 대역이면 스킵 ③새 규칙 추가 ④3306 MySQL 핸드셰이크를 실제로 받아본다(최대 6회×5초, SG 전파를 즉시 성공으로 가정하지 않는다) ⑤검증 성공 후에만 옛 규칙 삭제 — 반대로 하면 자기 접속을 먼저 끊는다. 검증 실패면 옛 규칙을 그대로 둔다. 🔑AWS SG 가 진실의 원장이고state.json은 보조다 — 매 실행 reconcile 하면 「추가 성공 후 삭제 실패」가 다음 주기에 저절로 정리된다. 🔑삭제는 Description 이 아니라SecurityGroupRuleId로(Description 은 유일 식별자가 아니라 소유권 필터일 뿐). 소유 규칙이 3개 이상이면 자가 halt. 상세 =C:\dev\tools\ipwatch\README.md· devplanipwatch.html. - ⚠2026-08-27 예약작업을
pythonw로 걸어도 「창 없음」이 아니다 — 자식이 창을 만든다. ipwatch 가 부르는aws.exe·ssh.exe는 콘솔앱인데 부모(pythonw)에 콘솔이 없어 자기 콘솔을 새로 만든다 → 사무실PC 화면에 5분마다 cmd 창이 번쩍였다(페페 신고). 고침 =subprocess.run(..., creationflags=CREATE_NO_WINDOW)(두 호출 다capture_output=True라 대화형 실행에서도 출력 손실 0). 전역 처방은C:\dev\tools\nowin.py지만 사무실PC 엔C:\dev가 없어 여기선 플래그 직접. 전역 규칙 =CLAUDE.md§2-2. - ⚠2026-08-27 사무실PC 에서
aws.env를 읽는 것만으로는 최소권한이 안 된다. 그 PC 엔%USERPROFILE%\.aws에poinadmin(풀권한) 키가 이미 있다 → 전용 키를 환경변수로 넣어도 CLI 가 프로파일로 폴백할 수 있다.AWS_SHARED_CREDENTIALS_FILE·AWS_CONFIG_FILE을 없는 경로로 덮고AWS_PROFILE을 지워야 격리가 실제로 성립한다. - 🔑2026-08-27 SG 규칙에 설명을 붙일 땐 삭제·재추가 대신
modify-security-group-rules. 설명만 바꾸므로 접속이 끊기는 창이 0이다. 수동 규칙을 워커 소유로 이관할 때 이걸로 했다 (112.170.234.238→AUTO-ipwatch office,211.209.165.141→PEPE phone WAN - do not touch). - ⚠2026-08-27 기본 브랜치에 워크플로를 뒀으면
checkout의 ref 도 같이 정해야 한다.actions/checkout@v4기본값이 기본 브랜치(deploy-live) 라, master 에만 있는 스크립트를 부르면No such file or directory로 죽는다(실측 run 33046992757 실패 →with: ref: master로 33047147784 성공). 요율감시는 정본(DC_RATES)·감시 스크립트·검사가 전부 master 에 있고 제안 PR base 도 master 라 master 를 집는다 → 덕분에 운영 브랜치엔 워크플로 파일만 들어가고 스크립트를 안 밀어도 된다. - 🔑2026-08-27 고시 실수치는 국가법령정보 Open API(DRF)로 자동으로 읽힌다 — 「못 읽는다」는 결론은 틀렸다.
lawService.do?OC=test&target=admrul&LM=<고시명>&type=XML→ 시행일자·발령번호 + 별표 첨부 링크. 별표hwpx는 zip 이라Contents/section*.xml에서 태그만 지우면 직종 목록이 평문으로 나온다(pdf 첨부는 건너뛴다). ⚠화물차주는 직종명이 아니라 「산재보험법 시행령 제83조의5제14호」로 집는다 — 직종명·인용부호가 고시마다 다르다 (요율 고시는「…」, 경비율 고시는「…」. 이것 때문에 직종명 정규식이 한쪽에서만 실패했다). 🔴값 뒤에 다음 항목 번호가 공백 없이 붙는다(…말한다)1715. 「소프트웨어) → 항목번호 N 을 먼저 읽고값+(N+1)+'.'로 경계를 확정해야 한다. 안 그러면17을171로 읽는다(실측). 대조 결과 요율 17‰·경비율 49.9% = 수동 조사값과 일치. 도구 =scripts/dc_rate_watch.py, 검사 =scripts/test_rate_watch.py(13건). ❌막힌 곳 =law.go.kr웹페이지(JS 렌더),data.go.kr파일데이터, nodong.kr 검색. API 만 쓴다. - 🔑2026-08-27 요율 값은 정부 소스 2곳을 교차대조한다(페페 지시 「한쪽 파싱 오류·값 오류를 잡게」).
1차 = 국가법령정보 Open API, 2차 = moel.go.kr 고시 글(다른 도메인·다른 발행 경로, 서버렌더라 hwpx 첨부를 그대로 준다).
목록 GET 검색은 안 먹는다(POST 폼) →
pageUnit=100으로 앞 4페이지를 훑는다(실측 2페이지 안에 둘 다 나온다). ⚠제목 필터가 핵심 — 「(고용보험)노무제공자의 기준보수 및 보수액에서 제외하는 필요경비」는 산재용과 다른 고시다. '고용보험' 든 제목을 안 거르면 값 추출이 실패한다(그래서 한때 「moel 에 최신판이 없다」고 오판했다 — 실제로는 있었다). 판정 = MATCH / PARTIAL(한쪽만) / UNAVAILABLE(2차 전멸) / MISMATCH → 자동 반영 중단·사람 호출. 2차가 죽어도 예외로 안 터지고 「1차 단독 판단」임을 메시지에 남긴다. 값이 바뀌는 주에만 2차를 긁는다(평소 moel 요청 0). 실조회 대조 = 요율 17.0=17.0 · 경비율 49.9=49.9 MATCH. 검사scripts/test_rate_watch.py18건. - 🔴2026-08-27 Actions 에서
GITHUB_TOKEN으로 push 하면 다른 워크플로가 안 돈다. 요율감시가deploy-live에 밀어도deploy.yml이 안 깨어나 운영 서버에 실제 배포가 안 된다. → PAT 을PFS_PAT시크릿으로 등록하고checkout의token에 물린다. 등록된 시크릿 =TG_TOKEN·TG_CHAT_ID·PFS_PAT(+기존 AWS 2). PAT 은repo+workflow스코프가 있어야 워크플로 파일 push 가 된다. - 2026-08-27 요율 감시·반영·배포는 전부 무인이다(페페 지시 「요율 변경은 신경쓰고 싶지 않다」).
주 1회(KST 월 09:00) → 고시 대조 → 바뀌었으면
DC_RATES에 줄 추가 → 검사 2종 통과 → master + deploy-live 반영. 안 바뀌면 아무 데도 안 알린다. 실제 반영했을 때만 카톡 보고. 안전장치 = 값 범위·2배 급변·시행일 역행 검사, 두 고시 중 하나라도 못 읽으면 아무것도 안 고치고 PARSER_BROKEN 알림. ⚠카톡은 Actions 에서 못 보낸다 — 노트20 이 미니PC adb + Tailscale 안이라 러너가 못 부른다. → 신호는deploy-live의 「요율 자동반영」 커밋 자체이고, 미니PCtools/dc-rate-kakao(예약작업DcRateKakao, 매일 09:40)가 그걸 보고 노트20 → 카톡을 보낸다. 별도 큐 브랜치를 안 두는 이유 = 신호가 이미 커밋에 있는데 또 만들면 둘이 어긋난다. 이쪽이 죽어도 요율 반영은 정상으로 나간다(늦는 건 보고뿐 + 텔레그램 백업). state 없으면 첫 실행은 안 보낸다(과거 몰아보내기 방지). - ℹ2026-08-27 고시 실수치 자동 추출 타진 결과(완전 자동화 전제 조사).
✅산재보험료율 =
nodong.kr/instruction/2349883표에서 추출 가능 — HTML<tr>안에<td>14. 「화물자동차 운수사업법」…</td><td …>17</td>형태로 멀쩡히 있다. 「화물차주」 바로 뒤에 숫자가 오는 게 아니라 긴 조문 설명이 끼어 있어서 순진한화물차주\s*(\d+)는 실패한다 → 항목14.행의 마지막<td>를 집어야 한다. ❌필요경비 공제율은 아직 막혔다 —law.go.kr행정규칙 페이지는 JS 렌더라 본문에 「화물」도 「49.9」도 없고,data.go.kr파일데이터 페이지도 값이 없다(파일 별도 인증). moel.go.kr hwpx 첨부 경로만 남아 있다. - 🔴2026-08-27 pfs 기본 브랜치는
master가 아니라deploy-live다 — 스케줄 워크플로를 master 에 두면 영영 안 돈다. GitHub Actions 의schedule은 기본 브랜치의 워크플로만 실행하고workflow_dispatch도 기본 브랜치에 파일이 없으면HTTP 404 not found on the default branch다(실측). master 에 push 해도 Actions 목록에 등록조차 안 된다. → 주기 실행 워크플로는deploy-live에도 넣어야 하고, 그건 곧 운영배포 승인 대상이다. ℹdeploy.yml이 잘 도는 건 그게push트리거라 브랜치별로 각자 돌기 때문이다 — 헷갈리기 쉽다. - ⚠2026-08-27
run: |블록 안 heredoc 은 YAML 을 깬다. 종료 마커(BODY)가 줄 맨 앞에 와야 해서 블록 스칼라 들여쓰기를 벗어난다 →could not find expected ':'.<<-도 탭만 지워서 안 된다. → 긴 본문은 파일로 빼고--body-file로 넘긴다(.github/rate-pr-body.md). - ⚠2026-08-27 권한 브로커(7777)를 고쳤는데 안 먹으면 프로세스가 여러 개 살아 있는지 본다.
Stop-ScheduledTask는 프로세스를 안 죽인다 — 08-18부터 뜬 broker 6개가 SO_REUSEADDR 로 7777 을 동시에 리스닝하며 옛 코드로 판정하고 있었다(규칙을 고쳐도 계속 AUTO_DENY). 재기동 = CIM 으로*cody-permission-broker*프로세스를 전부 Stop-Process 한 뒤 예약작업 Start, 그리고netstat | grep 7777이 1줄인지 확인. - 2026-08-27
.github/workflows/쓰기는 brokerOPS_FILE_PATTERNS가 AUTO_DENY 했다(내용 무관·빈 워크플로도 차단). 페페 승인으로OPS_FILE_ASK_PATTERNS를 새로 두고 그 경로만 ASK_USER(텔레그램 승인) 로 내렸다 —deploy.yml수정도 여전히 사람이 본다.deploy-production.sh·.env.production은 AUTO_DENY 유지. 같이 바꾼 것 =approval_timeout_sec120→600(120초는 폰 켜서 누르기 전에 만료된다. 2회 실측: K8SE·9K35).default_on_timeout=deny는 그대로. - 🔴2026-08-27 「사무실 엑셀이 안 열린다」의 범인은 엑셀이 아니라 PFS 운영 RDS 보안그룹의 공인 IP 화이트리스트다.
운영 RDS
poin-dbinstance는PubliclyAccessible: true이고 3306 인바운드가 IP 3개로만 열려 있다 —172.31.35.211/32(PFS 운영 EC2) ·112.170.234.238/32(사무실 유선 공인IP) ·211.209.165.141/32. 사무실PC(POINIMOS01)에 ODBC DSNpfs(MySQL Connector 8.0 → 그 RDS/dbpfs)가 있고 업무용 워크북들이 그 DSN으로 붙는다. → 회선이 바뀌어 공인IP가112.170.234.238→223.38.228.130(폰 핫스팟 CGNAT) 이 되는 순간 보안그룹이 조용히 DROP → TCP 21초+ 타임아웃 → 파일 열 때 데이터 연결에서 엑셀이 멈춘다. 🔑증상이 「엑셀 고장」으로 위장한다 — LAN·NAS·DNS·인터넷은 전부 정상이라 네트워크를 파면 아무것도 안 나온다. 판정 =OAlerts이벤트 로그 한 방(Get-WinEvent -LogName OAlerts) →ODBC ... Can't connect to MySQL server on '...rds.amazonaws.com' (10060)원문이 그대로 있다. 🔑10060(타임아웃) = DROP = 보안그룹, 즉시 거부(RST)면 다른 원인이다. 대조군 = 다른 회선(집)에서도 3306 이 막히면 IP 화이트리스트 확정. 🔑고치는 자리는 파일이 아니라 DSN 한 곳(HKCU\SOFTWARE\ODBC\ODBC.INI\pfs) — 워크북을 한 개도 안 건드리고 전부 돌릴 수 있다. 🔑피해 범위 = 그 PC 에 ODBC DSNpfs가 있느냐로 갈린다 — QC1 은 MySQL 드라이버 자체가 없어(파일 DSN 3개뿐) 3306 이 똑같이 막혀도 증상이 0 이었다. 「같은 LAN 인데 저 PC 는 멀쩡하다」를 반증으로 쓰지 말 것. ⚠핫스팟 IP 를 보안그룹에 추가하는 건 사무실 「전 PC」를 한 줄로 고치는 유일한 방법(전부 같은 공인IP 로 나간다). DSN 우회는 PC 별이라 여러 대면 안 맞는다. 대신 외부 노출 = 위험작업 게이트(aws ec2 authorize-security-group-ingress= 텔레그램 승인 필수)이고 모바일 IP 라 재접속마다 바뀐다 → 회선 복구 즉시 원복. ⚠정정: 핫스팟 IP223.38.228.130은 CGNAT 사설(100.64/10)이 아니라 SKT 공인 IP 다 — 보안그룹 등록 자체는 된다. 남는 위험은 ①IP 변동 ②통신사 NAT 뒤 타 가입자와 공유. ⚠폰 핫스팟은 Tailscale 과 무관하다 — 폰에 Tailscale 이 깔려 있어도ExitNodeOption=False·PrimaryRoutes=None이고, 안드로이드 Tailscale 은 VpnService 라 테더링 트래픽이 터널 밖으로 나간다. 사무실 인터넷은 순수 LTE 데이터다. ✅2026-08-27 집행 = ②안(페페 승인) —sg-01ce3d8ed6eb999ac에223.38.228.130/32:3306추가(sgr-0b4c45937b7ba9cf5). 검증 = 사무실PC 21,740ms 타임아웃 → 710ms 연결 · MySQL 핸드셰이크 77바이트(8.4.8) 수신 · QC1 도 3,368ms 로 뚫림(전 PC 적용 실증) · 집 미니PC 는 계속 차단(과다개방 0). 🔴원복 =bash C:/dev/_backup/sg_revoke_hotspot.sh, 백업 =C:/dev/_backup/sg-01ce3d8ed6eb999ac_2026-08-27_before.json. ✅같은 날 14:5x 원복 완료 — 유선 복구로 사무실 공인IP 가112.170.234.238로 되돌아왔고(핫스팟 규칙 없이도 옛 화이트리스트가 그대로 적중),sgr-0b4c45937b7ba9cf5삭제 후 재검증 = 사무실PC 47ms 연결 · 핸드셰이크 77바이트(8.4.8) · OAlerts 신규 오류 0 · 집 미니PC 21.5초 타임아웃(차단 유지). 3306 인바운드 3줄로 복귀. 🔑원복 판정 순서 = ①공인IP 가 옛 값으로 돌아왔나 ②규칙 삭제 ③삭제 「후」 재프로브 — ①만 보고 지우면 통신사가 다른 IP 를 준 경우 사무실을 스스로 끊는다. 프로브는 TCP 연결만으로 부족하고 MySQL 핸드셰이크 바이트까지 받아야 확실하다(SG 통과 = 응답이 온다). - 🔴2026-08-27 화면에서만 계산하는 금액은 상수를 고치는 순간 과거 데이터까지 소급해서 바뀐다 — 요율은 「이력 배열 + 날짜 매칭」으로 둔다.
배차 실입금액은 DB에 없다(
dispatch에freight_cost뿐) → 산재 요율 상수를 갱신하면 과거 배차 표시금액도 전부 새 요율로 다시 그려진다. 처방 =dispatch_list.htmlDC_RATES=[{from,rate,expense}](최신이 위) +rateFor(주문일). DDL 0·백필 0. 기존 줄은 절대 수정·삭제 금지(고치면 과거가 소급된다), 고시가 바뀌면 맨 위에 한 줄 추가. 한쪽 고시만 바뀌어도 새 줄엔 두 값을 다 적는다(빠뜨리면undefined→ NaN). 🔑짝함정 = 날짜 형식이 두 가지다 — 목록r.order_date는YYYYMMDD, 모달<input type=date>는YYYY-MM-DD→rateFor가 둘 다 받는다. 🔑요율이 날짜 의존이 되는 순간 모달에 새 버그가 생긴다 —#dcOrderDate에onchange="syncNetPay()"를 안 걸면 날짜만 바꿀 때 옛 금액이 남는다. 🔑확인 못 한 과거 고시는 추정해 넣지 않는다 — 배열 최고참보다 이전 날짜는 그 값으로 폴백. 추정치를 넣으면 검증 안 된 숫자로 과거 금액이 바뀐다. - 2026-08-27 고시값 감시 = 달력 트리거가 1차, 스크레이핑은 2차. 고시 원문 숫자는 자동으로 못 읽는다 —
moel·law.go.kr 은 JS 렌더라 본문 HTML 에 숫자가 없고 실수치는 hwp/hwpx 첨부 안에만 있다(26.8.27 실측).
→
scripts/dc_rate_watch.py(GitHub Actions 주 1회) 3층 = ①시행일 달력(요율 1.1·경비율 7.1 고정이라 안 깨진다, 반영될 때까지 계속 부른다) ②구글뉴스 RSS(정부 사이트와 달리 안정적 XML) ③파싱 실패는 「값 변경 없음」과 다른 문구로 알린다(조용히 죽는 게 제일 위험). 연 2회짜리 값이라 오탐보다 미탐이 훨씬 비싸다 → 시끄러운 쪽으로 기운다. 감지 = 텔레그램 + 새 시행일 줄 PR 자동생성, 머지는 사람. 🔑값 확인 경로 = moel 첨부downloadFile.do?...file_ext=hwpx→ hwpx 는 zip →Contents/section*.xml태그 제거 → 「화물자동차 운수사업법」 항목 끝 숫자. 이 경로로 공고 2026-272호(2026.7.1 시행~2027.6.30) 화물차주 49.9% 유지를 대조했다. ⚠스케줄 워크플로는 기본 브랜치에서만 돈다 — pfs 기본 브랜치는master가 아니라deploy-live(git ls-remote --symref origin HEAD로 확인). ⚠.github/workflows파일 Write 는 broker 가 auto-deny 한다(whitelist.pyOPS_FILE_PATTERNS, 텔레그램 승인 경로도 없다) → 페페 승인 필요. - 🔴2026-08-27 자식 템플릿에서
{% block %}밖에 쓴<style>·<script>는 Jinja 상속이 통째로 버린다 — 조용히.dispatch_list.html은{% endblock content %}뒤에<style>블록을 두고 있어서 그 안의.dc-fr-hint등이 한 번도 적용된 적이 없다(에러 0·화면은 그냥 기본색). 실입금액 색 규칙을 거기 넣었다가 검증에서 운임비·실입금액이 같은 색(rgb 74,87,101) 으로 찍혀 잡혔다. → 규칙은 상단{% block css %}안에만 쓴다. ⚠짝함정 = 특이도..dc-table tbody td(0,1,2)가td.dc-num(0,1,1)을 이미 이기고 있어서 기존td.dc-num { color: var(--dc-ink) }도 죽은 규칙이다 → 셀 색을 바꾸려면 클래스 2개(td.dc-num.dc-net). 🔑색은 「소스에 썼다」가 아니라getComputedStyle실측으로 검증한다. 눈으로도, diff 로도 안 보인다. - 2026-08-27 배차목록 실입금액 = 운임비 + 부가세 10% − 산재보험 차주부담분(페페 QA).
🔑운임비는 부가세 별도(공급가액) — 부가세는 따로 얹고, 산재보험료 계산에서는 뺀다(페페 지시 26.8.27).
🔴산재보험료는 10원 단위 내림을 두 번 건다(정본 = NAS
\\192.168.0.101\공유폴더\산재보험계산식.xlsx, 직종 985 화물차주. 26.8.27 페페 지시로 대조). 공단이 「원단위 절사」로 부과해서 회계 정본이 그렇게 짜여 있다 — 반올림으로 두면 공단 고지서·매월 신고와 원 단위로 어긋나 대사가 깨진다.M 월보수액 = K − K×경비공제율→N 보험료 = ROUNDDOWN(M×요율,-1)→O 부담분 = ROUNDDOWN(N×부담율,-1)→P 경감액 = ROUNDDOWN(O×경감률,-1)→Q 공제액 = O−P→R 지급액 = K×1.1 − Q. 실측55,000 → 경비 27,445 · 월보수액 27,555 · 보험료 480 · 부담분 240 · 지급액 60,260(반올림이면 242 였다). ℹ엑셀 U열 주석은 옛날 값(1.8%·30.3%)이 그대로 남아 있다 — 정본은 V9/V10 셀(0.499 / 0.0176). 요율·경비율은DC_RATES이력 배열, 부담율(DC_SANJAE_SHARE0.5)·경감률(DC_SANJAE_RELIEF0)·부가세(DC_VAT_RATE0.10)는 전역 상수. 근거 = 노무제공자 직종별 산재보험료율 「화물차주」 17‰ + 출퇴근재해 0.6‰ = 17.6‰(2026 고시, 2025년과 동률) · 필요경비 공제율 49.9%(2025.7.1~2026.6.30 고시, 종전 30.3%에서 상향) · 사업주·차주 1/2 씩 부담 → 실효 공제율 0.44088%. 운임비를 부가세 제외(공급가액)로 보고, 3.3% 사업소득 원천징수는 안 뺀다. 감시 =scripts/dc_rate_watch.py(달력 트리거 + 뉴스 RSS). ⏳GitHub Actions 워크플로dc-rate-watch.yml미배치 — 훅이.github/workflows/쓰기를 차단한다(내용 무관, 빈 워크플로도 막힌다. 우회 안 함). 페페 승인/직접 배치 필요. 검사 =node scripts/test_dispatch_netpay.js(템플릿에서 계산부 추출) ·scripts/_dispatch_netpay_check.py(사이트 실측). ✅운영배포 완료 — master44499e7+309ef35→deploy-live8f66ce2, 부가세분5536f94→aeeda96(1파일 선별 포워드). 운영 검증 12/12 ALL PASS · 사이트맵 재빌드 미반영 0(그룹 8 · 페이지 57). ⚠운영 배포 판정을curl 200으로 하면 안 된다 — 구코드도 200 이라 배포 직후 검증이 옛 값으로 FAIL한다(26.8.27 실측: 운영이 부가세 없는 54,758 을 서빙). 판정은 바뀐 값 자체를 보는 검사 스크립트 재시도 루프로 한다. - 🔴2026-08-26 엑셀 탭 이름을 여러 개 바꿀 땐 반드시 「임시명 경유 2단계」로 한다 — 한 개씩 순서대로 바꾸면 교차된다.
한샘 양식 슬롯
77/84A/84B를 평형84A/84C/84C(F)로 rename 할 때, 첫 rename 이84A 내역서-주를 둘로 만들고 다음 rename 이.replace(...,1)로 엉뚱한(앞선) 탭을 집는다 → 탭 이름과 내용이 교차되고 갑지A19·A20이 같은 탭을 가리킨다. 평형명이 슬롯명과 같으면(아산 E2E) 안 걸려서 통과된 채 넘어간다. →hansem_excel.rename_slots()= 전 슬롯을 유니크 임시명으로 옮긴 뒤 최종명으로 다시 옮긴다. ⚠같이 고칠 것 =definedName본문. 인쇄영역 등 13개가 탭 이름을 문자열로 들고 있어<sheet name=...>과 워크시트 수식만 고치면 정의된 이름이 깨진 채 남는다(그 파일#REF2,044개는 원본부터 있던 유령이라 별개). - 🔴2026-08-26
MySQLUtil.execute는 파라미터가 비어도 항상 포맷을 돌린다 — SQL 안 리터럴%가 워커를 죽인다.fetchall/fetchone은len(param)==0이면cursor.execute(sql)로 포맷을 건너뛰는데execute만 그 분기가 없다 (cursor.execute(sql, tuple(param))고정). 그래서 DDL 에COMMENT '적용률 (1=100%)'같은 리터럴%가 있으면 pymysql 이 포맷 문자열로 읽어TypeError: not enough arguments for format string→ 500 이 아니라 화면상 502로 뜬다. → 파라미터 없는 문장을 execute 로 넘길 땐stmt.replace('%','%%'). pymysql 이 다시%로 되돌린다. 🔑증상 오독 주의 — 화면(Blueprint)은 200 인데 API 만 502 면 라우팅이 아니라 요청 처리 중 크래시다. 게다가web/__init__.py의 500 핸들러가str(error)에서 또 터져(__str__ returned non-string) 원인 메시지가 통째로 사라진다 — 이땐 무조건 EC2/var/log/syslog를 봐야 한다(내 변경 아님, 미수정). - ⚠2026-08-26 pymysql 의 DECIMAL 은
decimal.Decimal이라 flask jsonify 가 500 을 낸다.Object of type 'Decimal' is not JSON serializable. 화면으로 나가는 DECIMAL 컬럼은 DAO 에서 float 으로 끝낸다 (_defloat헬퍼). 이번에 걸린 건hansem_furn_type.multiplier·hansem_estimate_detail.apply_rate. 금액을 FLOAT 로 두는 기존 관행이 이걸 피해온 것뿐이고, DECIMAL 을 새로 쓰면 반드시 다시 만난다. - 🏗2026-08-26 한샘 견적은 기존
estimate계통과 완전 별도다(hansem_*8테이블 ·/estimate/hansem/*). 섞지 않은 이유 = 계산체계가 다르다. 포인 =자당(300mm) × 부위비율 × 마진, 한샘 =폭/10 × 단가 × 유형배수 × 적용률. 🔑금액을 PFS 가 엑셀에 안 쓴다 — 치수·마감·등급·적용률과 단가표만 주입하고 계산은 양식 안 LOOKUP 체인이 한다 (단가기준 → 백데이터 → 내역서 6탭 → 갑지). 목록 총액만hansem_calc가 같은 식으로 따로 낸다. 좌표(블록행 4/11/18/25/32/39/46 · 세트열 C~H/J~O · 오프셋)의 단일 출처 =api/pfs/hansem/hansem_excel.py상단 상수. 단가 판(book_no)은 등록 시 1회만 박힌다 — DAOupdate()의 SET 절에 아예 없다(과거 견적 금액 고정). - 🔴2026-08-26 「외부 데이터 연결」 경고의 진범은 수식도 링크정의도 아닌
x14 확장 데이터유효성일 수 있다. externalLink 파트·externalReferences·외부 definedName 을 전부 지웠는데도 엑셀이 계속 물었다. 범인은xl/worksheets/sheetN.xml의<extLst>안<x14:dataValidation>4건이 드롭다운 목록 원본을 절대경로로 다른 통합문서를 가리키고 있던 것('D:\0.박진수-pc\…\[201804 롯데 청량리_생산비(나움).xlsx]Sheet3'!#REF!,'G:\영업\…\[라인 광주일곡 MH 거래명세표 .xlsx]Sheet3'!#REF!). 전부 이미#REF!= 목록이 안 뜨는 죽은 유효성이다. 🔑openpyxl 로는 안 보인다 — 로드할 때 「Data Validation extension is not supported and will be removed」 경고만 찍고 통째로 무시한다. zip 안 XML 을 직접 grep 해야 나온다(\[[^\]]*\.xlsx\]·TargetMode="External"·[A-Z]:\세 패턴을 전 파트에 돌린다). ℹ️x15ac:absPath(마지막 저장 경로 힌트)는 링크가 아니다 — 경고를 안 띄우고 저장하면 갱신되니 그냥 둔다. - ⚠2026-08-26 xlsx 외부링크를 「없애려면」 수식만 고쳐선 안 된다 — definedName 까지 지워야 엑셀이 안 묻는다.
「링크 업데이트」 프롬프트를 띄우는 건 수식이 아니라 링크 정의의 존재다. 4곳을 같이 지운다 —
①
xl/externalLinks/**파트+rels ②workbook.xml의<externalReferences>③[Content_Types].xmlOverride ④외부파일을 가리키는definedName(이 파일은 2,344개 중 26개가 그랬다, 대부분 이미#REF!인 유물). 🔑definedName 은 지우기 전에 워크시트 수식·데이터유효성formula1에서 쓰이는지 반드시 확인한다 — 드롭다운 목록이 이름으로 걸려 있으면 지우는 순간 조용히 빈 목록이 된다. 수식의[n]은<externalReferences>순번이라 일부만 지우면 뒤 번호가 밀린다 → 전부 지우거나 하나도 안 지우거나 둘 중 하나로 간다. - 💡2026-08-26 한샘 거래명세서에서 자재비·운반비 탭은 현장 「전체」 합계다 — 주방 시트가 전액을 가져간다.
주방!O32=자재비!H124(5,654,155) ·주방!O33=운반비!I18(1,060,000). 수납 시트의 같은 칸에 연결하면 이중 계상이라 빈칸이 정답이다(수납!O33 이 원래 빈칸인 게 근거). 옛 양식의수납!O34=+[13]자재비!H3는 외부파일 잔재였다. 🔑수납·주방 대칭으로 보이는 칸이 비어 있으면 「빠뜨린 것」이 아니라 「일부러 비운 것」일 수 있다. - 📐2026-08-26 한샘 단가표 계산식 =
폭(mm)/10 × 단가(원/10mm) × 적용률. 포인 자당단가와 비교하려면자당 ÷ 30. 단가는단가기준탭의 가구유형 × 마감 12종 × 등급 A~D 매트릭스(등급 = 도어사양: A평판 / B투톤·매입손잡이·메지홈 / C자체손잡이·AL1면·휘모·후레임 / D AL4면엣지바·AL후레임·슬라이딩). 단가 배수 = FIX PANEL 50% · 공틀도어 70%, 적용률(수기) = 도어만 30% / 바디일부 50% / 도어+바디 70% / 전체 100%. 🔑단가 수준 자체는 포인과 거의 같다(763 실측, D등급 기준 −3.1%). 손실은 전부 추가금액(옵션) 에서 난다 — 한샘 생산비 단가표엔 옵션 자리가 없고 청구 경로는 자재비 탭(실비) 과 추가분견적 탭(사후 발생분) 뿐이며 하드웨어(플랩암·HK-TOP·레일)는 어느 탭에도 없다. 763 = 옵션 7,148,000(행총액의 29.4%). - ⚠2026-08-26 도형·이미지·메모·데이터유효성이 있는 xlsx 는 openpyxl 로 다시 쓰지 마라 — zip 안 XML 만 고친다.
load_workbook→save는 그림 10·도형 9·메모·x14확장(데이터유효성)을 조용히 떨군다(로드 시 경고만 뜬다). 수식만 고칠 땐zipfile로 열어xl/worksheets/sheetN.xml문자열 치환 후 되쓰는 게 안전하고 diff 도 작다. 수식을 새로 넣었으면calcChain.xml을 지우고(Content_Types Override·workbook rel 도 같이)calcPr에fullCalcOnLoad="1"을 달아야 엑셀이 다시 계산한다. 캐시<v>는 남겨두면 열기 전에도 값이 보인다. - ⚠2026-08-26 NAS 파일이
Device or resource busy(다른 PC 엑셀이 잡고 있음)면 임시명으로 쓴 뒤 rename 한다. 덮어쓰기(cp)는 SMB 잠금에 막히지만 같은 폴더 임시명 write → rename 은 통과한다(원자적 교체라 반쪽 파일도 안 남는다). 잠금 주인은~$파일명의 내용(UTF-16 사용자명)으로 확인한다 — 이번 건은poinpc. - 💰2026-08-26 견적 금액을 사람 손으로 만들 땐 계산식 단일 출처 =
estimate_regist_v2.js. DB 를 직접 고칠 땐 이 5줄을 그대로 재현하라. ①자당amount = (Σdimension1~5 − Σcorner1~5)/300 × price × qty②추가금액sub.amount = qty × price × (1 + sub.margin_percent/100)③행 최종margin_total_amount = ceil(round(자당 × 부위비율 × (1+마진%) + Σ추가금액)/1000) × 1000— 부위비율 all=1·body=0.65·door=0.35 ④margin_amount= 부위비율 미적용 총액 − (자당 + Σ추가금액) ⑤마스터total_amount = margin_total_amount = Σ행총액 + 시공비 + 운반비 + 별도항목🔑마진율 음수 = 할인이다(추가금액에서 이미 −20% 로 쓰고 있었다). 「전체 마진율 20%(할인)」 =estimate.margin_percent = -20, 행은margin_percent = NULL(자동=전체마진 추종) 로 두면 된다. 마스터 NULL = 자동(업체company.margin_rate추종, 에넥스=10). ⚠행 레벨 계수(마진율·part_mode)는 추가금액에 절대 안 닿는다 — ③에서 비율·마진은 자당 몫에만 곱한다(2026-08-24 항목의 재확인). 실증 = 견적 764 를 이 식으로 재현했더니 20행 전부·총액 24,058,000 원이 원 단위까지 일치했다. - ⚠2026-08-26
estimate_detail에는modify_user/modify_date컬럼이 없다(estimate에는 있다). UPDATE 에 습관적으로 붙이면1054 Unknown column. 그리고estimate.modify_date는 DATE 라NOW()를 넣으면1265 Data truncated경고가 뜬다(값은 날짜로 정상 저장). 금액 컬럼은 전부 FLOAT 이라 읽을 땐CAST(x AS DECIMAL(24,8))— 텍스트 프로토콜이 유효 6자리로 잘라 보낸다(견적 복사 함정과 같은 뿌리). - 📄2026-08-26 견적서 엑셀의 「단가(SET)」 I열 =
margin_total_amount / 수량이다 — 원본 단가도 마진율도 고객에겐 안 보인다. 그래서 할인은 단가 마스터를 건드릴 필요가 없다. 전체 마진율 한 칸만 바꾸면 엑셀 단가가 통째로 따라 움직인다. 운반비는 「본건」 시트 요약블록의 물류비 줄로 따로 찍히므로 마진 조정과 무관하게 고정된다. ⚠재작성으로 만든 차수는 다운로드 파일명에 「견적서」가 두 번 붙는다 — 이름이… 견적서 (2차)라_build_filename의 「견적서로 끝나면 접미사 생략」 규칙에 안 걸려… 견적서 (2차) 견적서.xlsx가 된다 (기존 quirk, 미수정). NAS 로 내보낼 땐 파일명을 직접 지어 저장한다. - ⚠2026-08-26 「클릭하면 값이 블록으로 잡히게」 =
focus만 걸면 안 된다.focus click둘 다 건다. 마우스 클릭 순서가mousedown → focus → mouseup → click이라 focus 에서select()해도 뒤따르는 mouseup 이 선택을 캐럿으로 푼다.click이 마지막이라 최종 승자 — 대신 키보드 이동(탭·방향키)은 click 이 안 나므로 focus 도 같이 남긴다. 🔑type=number는selectionStart를 못 읽지만(Chrome null)select()는 정상 동작한다 — "number 는 선택 API 가 안 된다"는 통설로 우회하지 말고 실측할 것. - ⚠2026-08-26 견적표 컬럼 폭(
columnSettings)은 사용자별 DB 값이다 — 셀 안 요소를 키우려면 폭 말고 패딩을 줄인다.defaultColumns()의column_width를 고쳐도 이미 설정이 저장된 사용자에겐 안 먹는다(기본값일 뿐). 폴딩 아이콘 18→26px 은 칸 폭 30px 을 그대로 두고 td 패딩 9/8 → 4/2 로 만든 자리에 넣었다. - ⚠2026-08-25 「상차일 미확정이 안 보인다」 = 정렬 아니라
WHERE부터 의심하라. 그리고LIMIT도 같이 본다. 상차일은NULL이 곧 「미확정」이라(별도 컬럼 없음) 기간 비교shipping_date >= %s에 걸리면 행이 통째로 사라진다. 세 번 반복된 함정 = ①생산현황 리포트 SQL ②현장목록 검색(field_dao) ③견적 현장검색(estimate_dao.select_site_search). → 상차일 기간조건은 항상(x.shipping_date IS NULL OR x.shipping_date >= %s)로 쓴다(date_to쪽도 같이). 🔑WHERE 만 고치면 반쪽이다 —ORDER BY shipping_date DESC에서 NULL 은 맨 뒤라LIMIT이 걸린 쿼리 (site-search =LIMIT 50)에선 그대로 잘려나간다 →ORDER BY (x.shipping_date IS NULL) DESC를 맨 앞에 세운다. 화면단도 같은 규칙 — 상차일 정렬은 빈 값을 정렬 방향과 무관하게 맨 위로 두고(오름/내림 토글해도 상단 유지), 빈 칸 대신 「미확정」으로 찍는다(맨 위에 빈 칸만 있으면 뭔지 안 보인다). - 🏷2026-08-25 거래처 「넥서스」 → 「한샘」 개명(구 넥서스). 출력 표기 =
(주)한샘.company_no=1한 행이라 현장 825건·담당자 22명은 FK(company_no)로 자동 추종한다. 🔑개명에서 바꾸는 것과 안 바꾸는 것을 가르는 기준 = 「실물이 같이 바뀌었나」다. - 바꾼다(마스터·화면 키) =
company.name·orderpaper.furniture_company·orderpaper_site_map.furniture_company·monthly_close.company_name(2026-08 이후만 — 과거 마감표는 당시 사명이 정본) ·al_orderpaper/assets/vendors.json(거래처에 없는 이름만 보충하는 폴백이라, 마스터만 바꾸면 옛 이름이 자동완성에 유령으로 남는다). - 안 바꾼다(실물 참조 키) = NAS 파일명(
orderpaper.source_file·door_order_file.dir_path/file_name) · 실제 카톡방명(kakao_room.room_name·kakao_board_op.room_name— 게시글 자동갱신 매칭키) · 녹음파일명(call_transcript.audio) · 템플릿 파일nexus_close_form.xlsx. 바꾸면 매칭이 깨진다. - 안 바꾼다(과거 사실) = 통화원문 ·
field.comment·estimate.estimate_name·worklog.site_name· archive/세션문서. ⚠DB 만 UPDATE 하면 되돌아간다 —orderpaper_sync가ON DUPLICATE KEY UPDATE furniture_company=VALUES(…)로 매 동기화마다 파일명 파싱값으로 덮는다(도어발주서company_hint는 H6 셀 재파싱). NAS 파일명은 옛 사명 그대로 들어온다. → 해법 =orderpaper_parse._COMPANY_ALIAS(단일 출처,door_order_parse가 import) 에서 적재 시점 정규화. 인식 목록_KNOWN_COMPANIES에는 옛 이름을 남겨둬야 옛 파일명이 계속 파싱된다. ⚠배포 순서 = 파서 먼저, DB 나중. 자동동기화는 사무실 PC 로컬 실행(orderpaper_autosync_hidden.vbs, 30분 주기)이라 파서 파일을 고치는 순간 발효된다 — 이 순서면 동기화를 멈출 필요가 없다(반대로 하면 되돌아갈 창이 생긴다). - ⚠2026-08-25 「정렬을 롤백해달라」 = 그 정렬이 겸하던 다른 효과까지 걷힌다. 롤백 전에 뭘 겸했는지 세라.
현장목록 정렬을 상태 그룹(미확정→대기중→진행중→완료)에서 상차일 내림차순 한 줄로 되돌렸더니,
그 정렬이 겸하던 「미확정 맨 위」가 같이 사라져 상차일 없는 현장이 마지막 페이지로 밀려 안 보였다
(운영 129행 = 7페이지째). 문자열 내림차순에서 빈 값은 항상 맨 뒤다.
🔑규칙 = 정렬·필터를 되돌릴 땐 되돌리는 부분과 남길 부분을 나눠 적고 남길 쪽을 검사로 고정한다.
지금
fieldListOrder는 미확정 여부만 보고 상태는 안 본다 — 상태는 표 위 필터 탭이 담당. - ⚠2026-08-25 상차일 NULL 을 만들었으면 「상차일 기간 검색」 전부를 열어야 한다 — 목록에서 또 걸렸다.
현장목록은 첫 진입에 기본 기간(과거 6개월~미래 1년)을 스스로 채워 검색한다.
select_field_search_list의AND a.shipping_date >= %s/<= %s가 NULL 을 걸러내 미확정 현장이 목록에 아예 안 나왔다 (같은 날 리포트 SQL 에서 이미 한 번 당한 함정의 2차 발현). 둘 다(a.shipping_date IS NULL OR …)로 고쳤다. 🔑아직 안 고친 같은 자리 =estimate_dao.select_estimate_field_search(견적 등록 현장검색 ±1개월),production_status_dao.select_production_status_search_list(생산현황 검색),select_calendar_print(월간계획표). 캘린더는 날짜가 없으면 놓을 칸이 없어 일부러 뒀다. 나머지 둘은 필요해지면 같은 방식으로 열면 된다. - 🔗2026-08-25 폼 저장이 다른 화면의 즉시저장 값을 덮지 않게 — 값이 안 오면 컬럼을 SET 하지 마라.
현장 수정 폼에 현장상태 토글을 달면서
UPDATE production_status … SET field_status = %s를 무조건 넣으면, 목록에서 상태만 바꿔 둔 현장을 나중에 폼에서 저장할 때 폼의 기본값이 조용히 덮는다.if args.get("field_status")로 SET 절을 조립하고, 서비스에서도 값이 있을 때만 검증한다. 같은 화이트리스트(ALLOWED_VALUES {10,20,30})를 서비스_valid_field_status와 PATCH DAO 가 나눠 쓴다. - ⚠2026-08-25
tabindex="-1"로 탭 순서를 정리할 땐 그 컨트롤이 유일한 조작 경로인지 먼저 봐라. 현장 수정 화면은 담당자를 팝업 버튼으로만 고른다(등록 화면과 달리 인라인 검색이 없다). 「현장담당자 → 업체담당자 → 시공담당자 직행」을 만들려면 그 팝업 버튼도 탭에서 빼야 하는데, 그냥 빼면 키보드로 담당자를 아예 못 고른다 → 이름칸 Enter 로 그 줄의 팝업을 여는 핸들러를 같이 넣었다. - ⚠2026-08-25
date_format(컬럼,…)을 WHERE 비교에 쓰면 NULL 행이 조용히 사라진다. 생산현황 출력 SQL 의WHERE date_format(a.shipping_date,'%Y') >= (date_format(curdate(),'%Y')-1)이 상차일 NULL(=미확정) 현장을 통째로 걸러냈다 —date_format(NULL,…)= NULL 이라 비교결과가 NULL(=거짓 취급). 「미확정 표기」를 화면에만 넣었으면 행 자체가 안 나와 영영 안 보였을 자리다. 🔑규칙 = 날짜 컬럼에 NULL 을 허용하기로 했으면 그 컬럼을 쓰는 WHERE 전부를(컬럼 IS NULL OR …)로 열어라. - ⚠2026-08-25 daterangepicker 에
startDate: null을 주면 달력이 통째로 안 그려진다. 플러그인이if (typeof options.startDate === 'object') this.startDate = moment(options.startDate)로 받는데typeof null === 'object'라moment(null)= Invalid date 가 된다. 에러도 안 나고td.available이 0개라 「달력이 안 열린다」로만 보인다(상차일 미확정 현장에서 날짜를 다시 고를 수 없었다). 🔑규칙 = 값이 없을 수 있는 날짜는startDate: 값 || moment().format('YYYY-MM-DD')로 넘기고 입력칸만 비운다. - 🔗2026-08-25 「미확정」 같은 3상태는 컬럼을 새로 만들지 말고 빈 값 하나로 정한다.
상차일 미확정 =
field.shipping_date IS NULL. 별도 플래그를 두면 「날짜 있는데 미확정」 같은 모순 상태가 생기고 화면·API·SQL 세 군데를 동기화해야 한다. 빈 값이 곧 상태면 토글 버튼은 그 값의 거울이라 로직이 한 줄이다 (initShippingTbdinpfs-component.js). 저장은 기존CONVERT_EMPTY_STRING_TO_NULL(%s)재사용 — DDL 0. - ⚠2026-08-25
/api/field/<no>/children은 하위 AS 가 0건이면 500 (미수정·별건).get_field_children의result.sort(...)가MySQLUtil.fetchall이 빈 결과에 주는 tuple 에 걸린다 ('tuple' object has no attribute 'sort'). 현장 수정 화면 콘솔에 500 이 하나 뜨는 원인 — 상차일과 무관하고 기존 현장(2134·2142)에서도 똑같이 난다. 검증 스크립트에서 이 500 은 걸러 쓴다. - ⚠2026-08-24 견적 행 마진율(margin_percent)도 자당에만 적용 — 추가금액은 절대 곱하지 마라(위 항목의 2차 발현).
calcItemTotal(unitAmount, subItemSum, marginPercent)가(unitAmount+subItemSum)을 합쳐 마진율을 곱해, 마진0%인 추가금액 항목까지 행 마진율만큼 부풀려졌다. 실사례(견적763, 마진15.5%, 추가금액 60,000원 단독 항목): 구공식 1,964,000원(페페 신고값과 정확히 일치, 역산으로 확인) → 신공식 1,955,000원. 추가금액은 이미calcSubItemAmount로 항목별 마진%를 반영하므로 행 마진율은 unitAmount 몫에만 곱하고 subItemSum은 그대로 더한다. 같은 함수가 3곳(calcItemTotal·calcPartAdjustedTotal·모달recalcModalTotal의 marginAmt)에서 각자 계산해 하나만 고치면 나머지가 계속 틀린다. 🔑"추가금액은 추가금액의 마진%로만 관리한다" 원칙 — 비율(part_mode)이든 마진율(margin_percent)이든, 행 레벨 계수는 절대 subItemSum에 닿으면 안 된다. - ⚠2026-08-24 견적 전체/몸통/도어 비율은 자당(unitAmount)에만 적용, 추가금액(subs)엔 절대 곱하지 마라.
estimate_regist_v2.js몸통(65%)/도어(35%) 버튼이applyPartRatio(fullTotal, mode)로 최종합계 전체(자당+추가금액)에 비율을 곱해 몸통/도어 설정 시 추가금액까지 같이 깎였다(견적 763 실사례 — 909,000원 표시, 정답은 1,127,000원). 추가금액은 항목별margin_percent로만 관리하는 게 설계 의도.calcPartAdjustedTotal(unitAmount, subItemSum, marginPercent, mode)로 교체해 비율을unitAmount몫에만 곱하고 subSum은 마진만 반영해 그대로 더한다. 계산 경로가 2곳 중복(행 렌더용recalcDetailRowSilent·계산기 위젯recalcCalc) — 하나만 고치면 다른 쪽이 계속 틀린다, 항상 같이 고칠 것. 모달(recalcModalTotal)은 애초에 비율 미적용이라 안전. - ⚠2026-08-24
SELECT h.*에 h 의 컬럼과 같은 이름으로 별칭을 달지 마라 — DictCursor 가 계산값을 버린다. 월마감 담당자 기본값을SELECT h.*, COALESCE(...) AS manager_name으로 냈더니 결과셋에manager_name이 두 개가 되고 앞의 원본(NULL)이 남아 폴백이 통째로 무시됐다. 저장값이 있는 행은 우연히 맞게 보여서 「일부만 안 된다」로 보인다(에넥스 7월만 빈칸). 🔑규칙 = 계산값은 다른 이름(manager_name_default)으로 받고 우선순위 판단은 서비스에서 한다. - 🔗2026-08-24 월마감 금액은 견적서가 정본이다 — 저장된 숫자는 캐시일 뿐이다.
monthly_close_item.main_amount/add_amounts/construction_amount/total_amount는 담을 때 복사한 값이고, 이제get_one·get_list가 읽을 때마다_relink_amounts()로 견적서에서 다시 계산한다 (되쓰기 없음 — 조회가 DB 를 안 건드린다). 그래서 금액을 기대값으로 박는 검사는 거짓 FAIL 을 낸다 (_monthly_close_ui_check의 합계 252,970,000 이 그 사고 → 자기정합 대조로 교체). 대조 기준 주의 =select_items의estimate_total은 추가금액에 견적번호가 있을 때만 「담은 것 합」이고, 없으면 그 본건의 자식 전부 합(레거시 폴백)이라 기준이 다르다. 연동 대조는 「추가건까지 번호로 연결된 행」만 해야 한다. - 🕘2026-08-24 PFS 시간대 = KST 전면 전환. 이제
NOW()·CURDATE()·datetime.now()가 그냥 KST 다. 뿌리였던 것 = EC2 OSEtc/UTC+ RDStime_zone=UTC. 바꾼 곳 3개 = ①RDS 커스텀 파라미터그룹pfs-custom-params-84의time_zone=Asia/Seoul(dynamic·무재부팅) ②EC2timedatectl set-timezone Asia/Seoul③테스트 도커 MySQLSET PERSIST time_zone='+09:00'. 앞으로 보정 코드를 새로 넣지 마라 — 넣으면 이중이동이다. 응답 직렬화도 바뀌었다 =KstJSONEncoder(web/__init__.py)가 datetime 을YYYY-MM-DD HH:MM:SS평문으로 낸다. 종전 Flask 기본은Sun, 15 Jun 2026 07:08:58 GMT였고 브라우저new Date()의 +9 에 얹혀 화면이 우연히 맞던 구조였다. JS 에서 그 값을 파싱할 땐pfsParseDateTime()(pfs-utils.js)을 써라 — 공백 구분 형식을new Date()에 그대로 넣으면 파싱이 엔진 구현정의다. 검사기 =scripts/_kst_timezone_check.py [test|prod]·scripts/_kst_db_check.py(SSM). - ⛔2026-08-24
scripts/ssm_run.py는 원격 스크립트에 명령행 인자를 넘기지 않는다.python3 <원격파일>만 실행한다. 이걸 모르고ssm_run.py kst_backfill.py --dry로 돌렸다가--dry가 무시돼 운영 DB 에 실집행됐다 (14개 테이블 +9h). 즉시 -9 로 원복했지만 그 사이 INSERT 된 행은 -9 만 맞아 따로 정정해야 했다. 🔑규칙 = 원격 스크립트의 위험 스위치는 인자가 아니라 파일 안 상수로 둔다(EXECUTE = False기본, 올리기 전에 sed 로 True).kst_backfill.py가 그 형태다. - ⚠2026-08-24 데이터 마이그레이션은 앱을 멈추고 해야 한다 — 「검증 실패」가 손상이 아니라 동시쓰기일 수 있다.
위 사고에서
kakao_board전후 MAX 검증이 실패했는데 원인은 데이터 손상이 아니라 앱·수집데몬이 계속 INSERT 하던 것이었다. 그리고 그 창에 들어온 행은 「+9 는 못 받고 -9 만 받은」 상태가 되어 9시간 과거로 남는다. 🔑복구 요령 = 시각 구간으로 고르면 정상 행까지 섞인다(audit_log 실측 126행 오탐). PK 로 골라야 한다 — 사고 행은 「PK 는 최댓값 근처인데 시각만 하루 전」이라ORDER BY pk DESC로 즉시 갈린다(실제 4행). - ⚠2026-08-24 타입 카운트(KF)는
field_type조인의ON절에서remove_yn을 걸러라.WHERE는 안 된다.production_status_dao.select_production_status_print()·select_calendar_print()의LEFT JOIN field_type AS f ON f.field_no = a.field_no에remove_yn조건이 없어 소프트딜리트한 타입까지 K/F/OT 개수에 들어갔다(생산현황 출력 · 월간계획표). 현장목록(field_dao.select_field_list)은 예전부터remove_yn < 1서브쿼리로 세고 있어 같은 현장이 화면마다 다른 숫자로 나오던 것이 증상. 운영 실데이터 영향 = 17개 현장. 🔑WHERE f.remove_yn < 1로 넣으면 LEFT JOIN 이 INNER 가 돼 타입 0개 현장이 표에서 통째로 사라진다 (리포트 9행 중 4행이 타입 0개). 반드시ON ... AND f.remove_yn < 1. 같은 표를 새로 만들 때 = 카운트는field_dao의 서브쿼리 패턴을 베끼는 게 안전하다. - ⚠2026-08-24 교차대조 검증은 「몇 행을 실제로 비교했나」를 먼저 못박아라 — 0건 대조가 ALL PASS 로 보인다.
위 수정을 「리포트 API 의 KF ↔ 현장목록 API 카운트」로 검증했는데, 캘린더 API(
/api/field/status/calendar)는 응답에field_no가 없다(select_calendar_print가 안 내려준다) → 조인 키가 없어 319행 전부continue, 구코드 운영에서도 ALL PASS 가 나왔다(무의미 통과). 현장명으로 잇고대조 커버리지 >= 100행체크를 먼저 넣자 구코드 운영이 즉시 FAIL(욕실 샘플장(F:3)vs 기대 공백). 절차 = ①대조 건수 자체를 검사항목으로 둔다 ②배포 전 운영에 돌려 FAIL 을 확인한다(대조군) ③배포 후 PASS. 검사기 =scripts/_kf_softdelete_page_check.py [test|prod]. - ⚠2026-08-21 자동 글자맞춤(fit-to-box) 판정은
scrollHeight <= clientHeight— 여유를 빼면 영영 안 맞는다. 안 넘치는 블록은scrollHeight가 콘텐츠 높이가 아니라 패딩박스 높이(=clientHeight)와 정확히 같다. 안전여유로scrollHeight <= clientHeight - 4를 쓰면 조건이 항상 거짓 → 이분탐색이 하한(1mm)에 박힌다 (스티커 파레트 상세칸 실측: 어떤 길이든 1.0mm). 등호 비교로 고쳤다. 같이 필요한 것 = flex 자식은overflow:hidden(또는min-height:0) — 안 주면min-height:auto때문에 칸이 글에 맞춰 늘어나scrollHeight == clientHeight가 되어 넘침 자체를 못 본다. ⚠가운데 정렬 flex(align-items:center)는scrollHeight자체를 못 쓴다 — 위로 넘친 만큼은 안 잡힌다. 그런 칸(파레트 현장명.ps-site)은 값을<span>으로 감싸 span 의offsetWidth/offsetHeight를 칸 안쪽 치수(clientW/H− padding)와 직접 대본다. 공용 이분탐색 =fitFont(el, fits)(sticker.html). ⚠검증 스크립트에서 이 페이지 함수를 못 부른다(스티커 JS 는 IIFE 라S·renderPallet이 전역이 아니다) → DOM 을 직접 바꾸고window.dispatchEvent(new Event('resize'))로 재계산을 발화시킨다. 파레트 인쇄는 여유가 필요 없다 — 웹 196mm(테두리 0.5mm×2, border-box)·인쇄 195mm(테두리 없음) = 내용 195mm 동일. - 2026-08-21 월마감 엑셀 = NAS 기준 파일을 다듬은 템플릿에 값 채우기(페페 지시, 운영 반영).
종전
Workbook()코드 드로잉(수식 0개) 폐기 →api/pfs/monthly_close/service/의nexus_close_form.xlsx(한샘형[구 넥서스형] 15열, 시공비 있음 — 파일명은 옛 사명 그대로, 실물이라 안 바꾼다) /general_close_form.xlsx(일반형 9열). ⚠견적서 템플릿과 규약이 정반대다.estimate_form.xlsx는 NAS 바이트동일이라 못 고치지만 (→ [견적서 A6 드롭다운] 항목처럼 코드로 스트립), 월마감 템플릿은 엑셀로 직접 고쳐 커밋한다. 양식 변경 = xlsx 를 열어 고치고 커밋. 코드는 안 건드린다. ⚠그래서 코드가 템플릿을 좌표로 안 읽는다 — 헤더 텍스트(업체명/본건금액/합 계, 자간 무시 대조)와{company}{year}{month}{manager}토큰으로 행·열을 찾는다. 「N월」 행에 금액을 넣을지도 템플릿이 그 열에 수식을 갖고 있는지로 정한다(일반형 「계 N월」=금액, 한샘형 「N월」=라벨뿐). ⚠openpyxlinsert_rows/insert_cols는 병합·서식·수식을 안 옮긴다. 삽입 전에 unmerge 하고 삽입 후 좌표를 밀어 remerge 해야 한다 — 삽입 뒤 unmerge 하면 내부 셀맵이 어긋나KeyError로 죽는다(실측). ⚠기준 파일에서 가져올 때 그 달 수작업 색칠을 걷어야 한다 — 7월 넥서스에 현장명 초록(6~14행)· 완료일 노랑(7·13·14행)·합계 하늘(6~15행)이 남아 있었다. 행 수가 달마다 달라 아무 상관없는 현장에 색이 찍힌다. ⚠엑셀 COM 검증 함정 2개: 셀을 한 칸씩 읽거나 한 인스턴스로 여러 통합문서를 연달아 열면RPC_E_CALL_REJECTED(피호출자가 호출을 거부) — 파일 손상이 아니다.UsedRange.Value로 한 번에 읽고, 파일마다 인스턴스를 새로 띄운다. 죽으면EXCEL.EXE고아가 남으니 taskkill. 재생성 =scripts/make_monthly_close_forms.py(NAS 원본 양식이 바뀔 때만). 검증 =scripts/test_monthly_close_excel.py(DB 불필요 7종). - 2026-08-21 견적서 엑셀 「종합」 A6 업체 드롭다운 폐기 — 템플릿
A6:C6에 list 데이터 유효성이 박혀 있어(넥서스,에넥스,한특,쿠스코,넵스,스탠다드A,더리버스) 목록 밖 업체를 손으로 못 적었다. 값은 코드가 채우므로 유효성은 아무 역할이 없다. ⚠템플릿estimate_form.xlsx는 NAS 「양식 견적서.xlsx」 와 바이트 동일이 규약이라 파일을 고치지 않는다 —_strip_template_images옆에서 생성 시점에 뗀다(_strip_template_validations, 전 시트 일괄). NAS 재동기화로도 안 되살아난다. 템플릿에서 뭘 빼야 할 땐 파일이 아니라 이 자리에 스트리퍼를 추가한다. 검증 =scripts/test_excel_no_validation.py(DB 불필요) + 서버 다운로드본 유효성 0건. - 2026-08-21 견적서 타입은 현장
field_type에 반영하지 않는다 — 연결 완전 폐기(페페 지시, 운영 반영 완료). 견적 저장이 현장을 건드리던 쓰기 3경로(_upsert_field_types_from_details/_rename_field_types/_reorder_field_types)를estimate_dao에서 삭제했다. 사고 = 이미 생산이 끝난 현장에 견적서(특히 추가건)를 붙이면 ①imos_type·마감재위치 없는 유령 타입 행이 생기고 ②타입명 수정이 같은 현장 모든 견적의estimate_detail.type_name까지 일괄 UPDATE 하고 ③드래그 순서가 현장 타입 표시순서를 덮어썼다. ⚠타입 순서 SSOT =estimate_detail.display_order(종전field_type.display_order). 마이그레이션 없다 — 탭 드래그가State.details를 그 순서로 재배열(applyTypeOrderToDetails)해 저장 시display_order가 순서를 싣고, 재열기(typeOrderFromDetails)·엑셀/HTML 출력(_type_order_key(details))이 같은 소스를 읽는다. ⚠읽기 방향도 함께 폐기 —GET /api/estimate/field-types는 404 가 정상이다(자동완성은/type-suggest폴백). ⚠이미 오염된field_type행은 페페 지시로 그대로 둔다(정리 안 함). 현장 타입은 이제 현장 관리 화면 (field_type_dao)에서 사람만 건드린다. 검증 =scripts/test_type_order_from_details.py. - 2026-08-21 생산현황 출력물 헤더 날짜 = 0~8시면 당일 · 9~23시면 익일(페페 지시). 저녁에 「내일 나갈 것」을
뽑고 새벽에 「오늘 나갈 것」을 뽑는 실사용 패턴이라 9시가 경계다. 구현 =
defaultHeaderDate()(field_status_print.html) 한 함수. 수동변경은 네이티브<input type="date">로 충분하다(라이브러리 0). ⚠인쇄용 화면에 조작 위젯을 넣을 땐class="no-print"— 그 페이지 인쇄 CSS가 이미.no-print{display:none!important}라 종이에는 안 나간다(위젯이 출력물에 찍히는 사고 방지). ⚠이 날짜는 표시 전용 —/api/field/status/report에 날짜 파라미터가 없어 표 내용은 안 바뀐다. 「날짜를 바꿨는데 데이터가 그대로다」는 버그가 아니라 사양이다(날짜별 필터는 API부터 별건). - 2026-08-20 월마감 시공비는 견적 master 칸에만 있는 게 아니다 — 「시공」 항목까지 합쳐야 한다.
추가건은 시공비를
estimate.construction_cost칸이 아니라 항목명으로 적는다(운영 견적 753 = 칸 0 인데 항목 「시공비」 2건 2,310,000) → 담기 모달이 그 금액을 추가금액에 그대로 넣어 본건 행 시공비로 안 넘어갔다. 규칙 = 견적 1건 시공비 =construction_cost+estimate_detail estimate_extra_cost중 항목명에 「시공」(DAO_CONS_EXPR, 본건·추가건 쿼리 공용). ⚠판정은 항목명만 — 비고엔 「2인시공 및 출장비」 가 흔해서(같은 견적의 운임 항목) comment 를 섞으면 운임까지 시공비로 딸려온다. ⚠금액은margin_total_amount(= total_amount 의 구성분)로 잡아야 총액 보존(본건+추가+시공 = 견적총액 합)이 성립한다. ⚠이미 저장된 마감행은 규칙 변경으로 안 고쳐진다 — 담기 모달은 이미 담긴 본건·추가건을 건너뛴다(if (nos.indexOf(...) >= 0) return) → 재담기해도 그대로다. 손으로 고치거나 행을 지우고 다시 담아야 한다(26.8.20 넥서스 8월 1행은 SSM UPDATE 로 재계산, 근거값은 견적 실측).- 2026-08-20 단위(mm)를 자동값에만 붙이면 수동입력값과 표기가 갈린다 — 보정은 입력 시점 1회.
견적 v2 치수칸: 출력 포매터
_spec_text(엑셀 C열)는dimension1~5조합에만mm를 붙이고 수동입력size_display는 그대로 내보냈다 → 운영 데이터가 「920mm」·「2771538」 로 섞였다 (페페가 손으로 mm 를 타이핑한 행만 단위가 있었다). 고칠 자리는 출력 3곳이 아니라 입력 1곳 —withSizeUnit()로 blur 에서 화면·size_display를 같은 값으로 맞추면 엑셀·PDF·HTML 이 자동으로 따라온다. ⚠부착 조건 = 숫자로 끝날 때만(「실측 후 협의」 류 문구·이미mm로 끝난 값에 붙이면 오염·중복). ⚠경로가 2개다* —.size-summaryblur 핸들러 + blur 없이 저장할 때 도는syncAllSizeDisplayToState(). 한쪽만 고치면 「저장했는데 단위가 없다」로 되돌아온다(기존 무단위 행은 다음 저장에서 함께 정리된다). - 2026-08-20 목록에 기능을 넣었으면 「1페이지에 뜨는가」까지 확인한다. 견적목록 추가건 폴딩을 넣고 E2E 도
통과했는데 페페는 못 찾았다 — 검증이
page.len(-1)로 전 페이지를 펼쳐놓고 셌기 때문이다. 실제 기본 화면은 최신순 10행이고 추가건 보유 본건 5건은 전부 지난달 건이라 1페이지에 ▶ 가 0개였다(실측). 교훈 2개 = ①E2E 에 기본 화면 그대로의 가시성 항목을 넣는다(:visible카운트) ②드문 대상엔 찾는 수단(필터 체크박스)을 같이 준다. 필터는 이미 받은 결과만 거르면 된다($.fn.dataTable.ext.search+datatable.draw(), 서버 왕복 0). - 2026-08-20 아이콘만 있는 토글 버튼은 안 눌린다 — 라벨에 내용을 박는다.
▶→▶ 추가 3. 펼침/접힘으로 화살표를 바꿀 때$btn.html('▶')로 통째 덮으면 건수가 날아간다 → 꼬리표를 떼서 다시 붙인다. - 2026-08-20 월마감 추가건은 「금액 배열」이 아니라 「번호+금액」으로 남긴다.
monthly_close_item.add_amounts를[912000,…]→[{"no":712,"amt":912000}]로 확장(숫자배열 하위호환 · 운영 DDL 0). 번호가 없으면 ①검증 배지가 무엇과 대조할지 모르고 ②추가건 입금여부를 마감표에서 파생할 수 없다 — 실제로 78행 중 12행만 견적에 붙어 있었고 추가건은 전부 끊겨 있었다. 파생 판정은JSON_CONTAINS(add_amounts, JSON_OBJECT('no', 견적번호))(RDS MySQL 8.4.8 확인). ⚠형식 바꿀 땐 옛 데이터용 폴백을 같이 넣는다 — 번호 없는 레거시 행은JSON_CONTAINS_PATH(...,'$[0].no')로 갈라 옛 기준(자식 전부 합)으로 대조한다. 안 그러면 배포만으로 기존 마감행이 전부 빨강이 된다. - 2026-08-20 검증 배지는 「담은 것」과 대조한다 — 확정금액은 검증 대상이 아니다. 확정금액(
confirm_amount)은 거래처와 네고한 수기값이라 규칙이 없다(페페). 대조는합계vs 담은 견적 합. 안 담은 추가건은 계산에서 빼고 배지 툴팁에 「미담김 N건」으로만 알린다. - 2026-08-20 「자동수집」류 버튼은 표에 직접 쏟지 말고 선택 모달을 연다. 구
/candidates는 거래처 연도 전체를 마감표에 통째로 넣어 지난달 마감건이 이번달에 또 딸려왔다. 새 규칙(미입금만)으로 바꾸니 넥서스 131건·에넥스 117건(미입금 본견적 608/629)이 한 번에 꽂히는 게 드러났다 → 같은 담기 모달을 「미입금만」 켠 채로 여는 것으로 통일. 🔑규칙만 바꾸고 건수를 안 재면 이 폭탄을 못 본다 — 바꾸기 전에 새 기준의 실제 행수를 SQL로 먼저 세라. - 2026-08-20 DataTables
row().child(html)는 className 을 안 주면 삽입한 tr 에 클래스가 안 붙는다.row.child(html, 'est-sub-row')로 줘야 선택·스타일이 먹는다. 안 주면 DOM 엔 들어갔는데 안 펼쳐진 것처럼 보인다. - 2026-08-20 일괄 상태변경은 기존 E2E 의 「고정 기대값」을 깬다. 6월까지 견적 609건을
is_deposit='2'로 일괄 마감처리하자_deposit_mark_check의 토글 검사가 시작상태'0'가정으로 3건 FAIL(동작은 정상). → 검증은 현재 상태에서 순환으로 기대값을 만들도록 고친다. 일괄 UPDATE 는 항상 롤백용 번호목록 백업 파일 선기록(/home/ubuntu/pfs-backup/is_deposit_*.json). - 2026-08-20 화면 채우는 미리보기 배율은 「폭 출처」와 「배율 대상」을 분리한다. 박스 폭을 배율로 계산하면서
그 폭에서 배율을 뽑으면 순환참조다 → 바깥 열만
flex:1 1 0;min-width:0로 두고 그clientWidth로 계산해--ps를 박는다(sticker.htmlfitPallet()). CSSmm은 96dpi 고정이라283mm=1069.6px로 결정론적. 숨김 탭에선 폭이 0 이라offsetParent로 스킵하고 탭 전환·resize에서 다시 부른다. - 2026-08-19 AL 규격W =
(장외경-10-큰손갭)/2 + 큰손마감— 큰손마감(설정 7열)이 빠져 있었다. 이 항 하나가 같은 장외경에서 작은손/큰손을 가르는 전부다(엑셀 원식((D-10-VLOOKUP(,6))/2)+VLOOKUP(,7)). 목대·유리는 정수 버림(370.5 → 370, 페페 확정 ASK-FA7446), 규격W·AL치수는 버리지 않는다(AL은 0.5 절단). - 2026-08-19 큰손/작은손 짝은 NO칸 🔗 로 사람이 묶는다(
al_orderpaper_item.pair_link= 윗행과 한 짝). 묶으면 ①장외경 공유 ②목대·유리를 첫 행 값으로 통일 ③원래 달랐던 행은 NO칸 노랑 경고. ⚠조용히 덮지 않는 게 핵심 — 값이 갈리는 건 설정 상수가 덜 채워졌다는 뜻이다(AA*494/495 는 목대뺄 0/0). 정상 짝은 상수가 이미 맞춰져 있다(AA-303/304 목대뺄 13/36, 차이 23 = 큰손마감) → 통일이 no-op 이다. - 2026-08-19 자동저장 화면은 저장 후
load()로 다시 그리면 안 된다 — 타이핑 중 커서·스크롤이 날아간다. 대신 저장 응답에 신규행 번호(nos)를 실어 보내 화면 행에 물린다. 안 물리면 다음 자동저장이 같은 행을 또 INSERT 한다. 안전장치 2개 = 품명 0행이면 저장 스킵(표 전멸 방지) · 숫자 아닌 값이면 저장 보류. 자동저장에선alert/confirm을 쓰지 않는다(타이핑 중 모달 폭탄) — 상태는#apDirty한 줄로. - 2026-08-19 인라인 드롭리스트는
web/static/js/al_combo.js공용(window.AlCombo.attach). CSS 도 그 파일이 주입한다 → 새 화면은 스크립트 1줄만 넣으면 같은 동작. 페이지 템플릿에.al-comboCSS 를 다시 쓰지 않는다. - 2026-08-19 통화 화자분리는 「전사가 준 발화 경계로 잘라 발화당 임베딩 1개」가 정답. sherpa-onnx 기본 파이프라인(자체 segmentation 10초 창)은 실측 76.2%, 발화단위 2-means 는 88.0%(정답 4통화 98세그먼트). 임베딩 = 3dspeaker campplus(voxceleb CAM++ 는 64.7%). ⛔고역 에너지로 「내 마이크 vs 통신망」 가르기는 62.2% = 다수결 수준이라 못 쓴다 — 통화녹음은 스트림 녹음이라 양쪽 다 전화 대역이고 모노 1채널이다.
- 2026-08-19 Groq whisper-large-v3-turbo = 5분 통화 1.4초(실시간 210배)·무료. 로컬 medium 대비 유사도 0.83~0.90. 단 폴백을 지운 채 갈아타면 안 된다 — 원본 m4a 는 적재 후 삭제라 전사에 실패한 통화는 영영 못 살린다.
- 2026-08-19 「30분마다 다시 만드는」 LLM 생성물엔 입력 변화 게이트가 필수다. 없으면 모델이 같은 사실을
조금 다르게 써서 지문(fp)이 바뀌고, 사람이 이미 「제외」한 행이 새 「대기」 행으로 되살아난다
(
put_fusion_post는 새 판에 없는 행을 지운다). 현장별 입력 sha1 을_fusion_state.json에 두고 같으면 안 부른다. - 2026-08-19
am broadcast는 받는 앱이 없어도result=0으로 성공한다. 폰을 두드려 놓고 「트리거 OK」라고 찍히는데 아무 일도 안 일어난다 →pm list packages <pkg>로 앱이 깔린 기기를 골라서 두드린다. (이 PC 에 USB 로 붙은 건 poin-agent 노트20 이고 talklog 앱은 거기 없다.) - 2026-08-19 폰↔PC 수집은
adb reverse가 IP·망 문제를 통째로 없앤다. 앱의 서버 후보는 Tailscale·하드코딩 로컬IP 뿐이라 PC IP 가 바뀌면 조용히 끊긴다(실측: 앱 후보 192.168.45.2 ↔ 실제 192.168.45.48 → 8/12부터 수집 0).adb reverse tcp:8792+setserver http://127.0.0.1:8792= 앱 수정 0, USB 만 붙으면 무조건 붙는다. - 2026-08-19 상주 수집서버가 죽으면 야간 배치는 「정상 실행 + 0건」으로 조용히 지나간다. 실패 로그가 안 남아
일주일을 못 봤다 → 주기 배치가 포트를 찔러 보고 죽어 있으면 살린다(
run_fusion30.ensure_server). - 2026-08-19 테스트 PFS 에 통화(source='call') 항목이 0건이라 전사 뷰어 경로는 늘 미검증이다.
scripts/_stt_seed_test.py로 테스트 RDS 에 1건 심고 검증한 뒤 지운다. 「0건이라 건너뜀」은 통과가 아니다. - 2026-08-19 완료 현장 회귀검사는
revived를 갈라 봐야 한다. 「완료 카드는 전부 filtered」 검사가 1-3차 살린 현장 규칙과 어긋나 운영에서 2건 FAIL 로 떠 있었다 — 규칙이 바뀌면 그 규칙을 검사하던 스크립트도 같이 고친다. - 2026-08-19 견적 v2 표에서 CSS 로 열 폭을 줄여도 안 줄면
ui_column_setting고정폭부터 본다.renderColumnHeader()가 DBcolumn_width를 th 인라인 style 로 박아 CSS 를 이긴다(act=105 가 그랬다). 이 값들은 옛saveColumnOrderToServer가offsetWidth로 덮어 자동폭 열이 고정폭으로 박제된 잔재다 → 고칠 땐!important로 싸우지 말고 그 행을 NULL(자동)로 되돌린다(DDL 0 · 1행 UPDATE, test·live 각각 SSM). 남는 폭을 특정 열에 몰아주려면 그 열의min-width(예td.col-note)를 올린다 — 고정폭을 새로 박으면 같은 박제가 반복된다. 선례 = 액션버튼 세로정렬(105→49px, 비고 143→200px) · 검증scripts/_actcol_vert_check.py. - 2026-08-19 AL발주서 업체 자동완성 정본 =
company+company_member실시간 조회. 번들assets/vendors.json(원부자재발주서 업체탭 스냅샷)은 거래처에 없는 이름만 보충하는 폴백이다. 스냅샷만 읽던 동안엔 거래처에서 담당자를 바꿔도 발주서엔 영원히 안 들어왔다(대진아트와이어 = 마스터 김정현대리 ↔ 화면 신동환차장). 프로세스 캐시도 두지 않는다 — 상주 uwsgi 가 옛 값을 붙들면 같은 증상 재발. 담당자 표기 =이름+직책(전화)(업체탭 형식 그대로). - 2026-08-19 유리 입고처(
al_orderpaper_item.receive_place) 기본값 = 그 행의 AL업체(없으면 첫 AL업체, 그것도 없으면 포인) — 유리는 AL업체로 입고돼 조립된다. 엑셀 입고처 주소칸은 거래처 마스터가 1순위 (발주서 헤더에 저장된 담당자는 그때 팩스 받을 사람이라 낡는다). 주소가 없는 업체면 주소칸은 빈칸 — 포인 주소로 폴백하면 업체명은 남의 것인데 주소만 포인이 되어 유리가 딴 데로 간다. - 2026-08-19 발주서 엑셀은 「블록 교체」다 — 행을 삽입하지 않는다. 유리시트는 업체+입고처로 묶고
묶음마다 양식의 블록(29행) 하나를 쓴다. 입고처 행은 블록마다 이미 있다(A
입고처· C 업체명 · D:끝주소/담당자) → 그 행에 값만 채운다. 블록이 모자라면 조용히 누락시키지 말고 ValueError(양식 5장 한도). - 2026-08-19 양식 항목행 판별은 캡노트 병합 시작열을 B·C 둘 다 본다. AL 캡노트는
B16:N16, 유리 캡노트는C24:H24— B만 보면 유리 24행을 항목행으로 세어 유리 17개 이상이면 병합셀 기입으로 엑셀 다운로드가 500이었다(MergedCell object attribute 'value' is read-only). 유리ipb= 17. - 2026-08-19 컬럼 많은 화면은
.al-wrap max-width:none+.page-wrapper > .container-fluid여백 25→16 (자재관리.op-wrap선례와 같은 방식). AL발주서 19컬럼 = 1,676px 로 1920 화면에서 가로 스크롤 0. - 2026-08-19 완료 현장도 「연결된 AS 가 진행중」이면 살린다(페페 지시) —
field.parent_field_no(AS→본건) + 그 AS 의field_status='20'. 26.8.7 「완료현장은 미출고만」 규칙을 살린 현장에만 면제한다(revived). 상태 플래그가 아니라 계산값이라 AS 가 끝나면 자동으로 다시 접힌다. 실측 사정거리 = AS 1,040건 중 parent 보유 30건뿐. - 2026-08-19 게시글 출고 완료분은 접힘 그룹(
fs-done) — 살린 완료현장은 게시글이 통째로 길어진다. - 2026-08-19 도어발주서 날짜 2개는 라벨이 없다 —
number_format이 유일한 식별자. J4=발주일 · M4=상차일 이고 「상차일:」 문구는 셀 값이 아니라 표시형식("상차일:"yy"."m"."d"("aaa")) 안에 있다. 값만 읽으면 두 날짜가 구분이 안 돼 상차일 그룹이 통째로 발주일로 뒤집힌다(양식 시트에도 라벨 텍스트 없음). - 2026-08-19 도어발주서 부품/완제품 판정 = 「숫자접두」가 아니라 「수식어 없는 바 부재명」. 타입접두 없는 완제품이
흔하고(
키큰도어·걸레받이·식기세척기 마이다) 부품은 늘 부재명 단독이다 —EP는 접고냉장고 키큰EP는 올린다. 위치 기반(GROUP 뒤 연속)도 못 쓴다(마지막 GROUP 뒤에 독립 도어가 이어지는 페이지가 있다). - 2026-08-19 동명 현장이 「본건 + AS」로 갈라진 곳은 추가건 field_no 를 발주서 폴더명으로 고르면 틀린다. 금강 부산에코 = AS 2163 / MH 본건 2142. 발주서는 AS 폴더인데 견적은 본건 2142 밑에 붙는다(부모 견적의 field_no 를 따른다).
- 2026-08-19 카톡 게시글 방은 동명 현장 중 「본건」에 붙인다(페페 지시).
_match_room_field동점 tie-break = AS(field_div='60') 제외를 active 판정보다 먼저. 규칙으로 못 붙는 건(점수 미달) 🔗 현장 연결 버튼으로 사람이 붙인다 — 롯데 향남처럼 방 이름에 「건설」이 끼면 부분일치가 깨져 64점이라 규칙으로는 영영 안 붙는다. - 2026-08-19 「본건 우선」은 완료 현장에 방을 붙일 수 있다 — 그러면 카드 생성 조건에서 탈락해 게시글이 화면에서 통째로 사라진다(🔗 버튼도 못 누른다). 방이 붙어 있으면 완료 현장도 카드는 만든다(내용 표시 규칙은 그대로).
- 2026-08-19 카톡방이 없어도 게시 승인을 미리 받는다(
kakao_state='승인') — 방이 연결되는 순간 승인분이 나간다. 게시글로 나가는 문은_publish_row하나 — 버튼도 방 연결도 그 함수를 지난다. - 2026-08-19
kakao_board.field_no수동 연결은 데몬 동기화에도 살아남는다 — 미러 upsert 컬럼 목록에field_no가 없다. - 2026-08-19 통합 게시글 = 「생성물은 늘 다시 만들고, 사람 결정만 지문으로 보존한다」.
fusion_row는 30분마다 현장 단위로 통째 교체되지만 upsert 가kakao_state·decide_*를 안 건드린다. 지문 = 섹션 + 첫 줄(구두점·공백· 「※수정」 표기 제거) — 데몬board_rows._ids와 같은 기준이라야 사람이 이미 판단한 행이 새 행으로 안 되살아난다. - 2026-08-19 게시글 「행 추가(add)」는
row_id칸에 섹션명이 온다 (고칠 행이 없어서). PFSadd_op는 그래서 add 만 「대기 타깃 중복검사」를 건너뛰고(안 그러면 같은 섹션 두 줄이 막힌다) 대신 같은 첫 줄을 막는다. 섹션 대조는 PFS·데몬 양쪽 다 「■」 유무를 무시한다 — 안 맞추면 같은 이름 섹션이 하나 더 생긴다. - 2026-08-19 flask_restplus 라우트 변수는 메서드 인자로 받아야 한다.
@api.route("/x/<int:no>")에def get(self)로 두면 500get() got an unexpected keyword argument. 테스트 데이터가 0건인 경로는 운영에서 처음 터진다 — 검증 스크립트가 "0건이라 건너뜀"을 찍으면 그건 통과가 아니라 미검증이다. - 2026-08-19 통화 근거(
worklog.excerpt)는 전사문의 인용이 아니라 「요약」이다. 통째로 찾는 형광은 실측 매치 0%. 겹치는 어구(구두점·공백 뺀 최장 공통 조각 6자 이상)만 짚어야 한다 → 운영 12건 전부 형광·근거의 91% 덮음. - 2026-08-19 카톡 게시글 방이 현장에 안 붙는 진짜 이유 = 동명 현장이 여럿(본건 + AS).
_match_room_field의 「유일 승자」 게이트에 걸린다(용두동 3건·에코 2건·용인 2건 실측). 현장 미등록 탓이라던 26.8.7 진단은 이제 틀렸다. - 2026-08-19 일괄치환으로 함수를 갈면 그 함수 본문을 눈으로 본다.
ensure_x()호출을_ensure()로 바꾸다 함수 안쪽 호출까지 먹어 자기 자신을 부르게 됐다 → API 500maximum recursion depth exceeded. -
2026-08-19 매치율·정확도를 잴 땐 만들어진 정규식을 한 건 찍어 본다.
SKIP + "*".join(...)이 연산자 우선순위로 「각 글자 0회 이상」이 돼 아무거나 매치, 92% 가짜 합격을 냈다(실제는 0%). -
⚠2026-08-19 파생값으로만 만든 상태는 「사람이 고를 자리」까지가 한 작업이다 (입금여부 마감 QA). 같은 날 list-v2 입금여부를 3상태(입금/마감/미입금)로 바꿨는데 「마감」은 월마감 초안에서 파생될 뿐이라 폼 라디오엔 입금/미입금 둘뿐이었다 → 페페가 마감을 지정할 수단이 없었다. ⚠같이 빠뜨리기 쉬운 자리 = 그 값을 바꾸는 다른 입구. 여기선 목록 토글이
1↔02상태라 폼에서 고른 마감을 한 번 클릭에 잃는다 →0→1→2→0순환으로 같이 고쳤다. 선례 =estimate_regist_v2.html·estimate_list_v2.htmldepositBtnHtml/토글 ·estimate_service.set_deposit. - ⚠2026-08-19 pfs 리포는 다른 세션이 같이 쓰고 있을 수 있다 — 이날 커밋·검증 도중
origin/master가 남의 커밋으로 앞서 나갔고 내 워킹트리의 미커밋 WIP 이 사라졌다.git add -A금지 규칙이 여기서 값을 한다 (파일 지정 add + deploy-live 선별 포워드라 남의 미완성 코드가 운영에 안 갔다). 배포 전git diff --stat origin/deploy-live master -- <대상파일>로 내 변경분만인지 눈으로 확인한다. - ⚠2026-08-19 부분 갱신 함수는 「그 값을 읽는 표시 전부」를 덮어야 한다 (견적 v2 그룹 소계 QA 근본원인).
recalcDetailRow()가 행 최종합계와 종합합계만 갱신하고 그룹 소계 행(.grouptot-row)은 안 건드려, 치수·단가·수량·마진을 고쳐도 소계가 마지막renderDetailTable()시점 값에 멈춰 있었다 (실측: 행합 420,000 인데 소계는 편집 전 값 그대로 — 페페 QA 보고 「310,000 이 실제 금액과 안 맞는다」).updateGroupSubtotal()은 있었지만 part_mode 토글 한 경로에서만 불렀다. 정답 = 갱신을 공통 지점 한 곳에 넣는다 — 호출자 4곳(치수/코너 입력·인라인 편집·단가 자동조회·SUB 재계산)이 전부recalcDetailRow를 지나므로 거기서 부른다. ⚠같은 사고를 반복하는 자리 = 새 합계/배지 행을 추가하면 「전체 재렌더 경로」에만 반영하고 부분 갱신 경로를 빼먹는다. 선례 =estimate_regist_v2.jsrecalcDetailRow· 검증scripts/_est_grouptot_manager_check.py. - ⚠2026-08-19 출력 담당자칸이 이름만 나오면 SQL SELECT 를 먼저 본다 (견적서 엑셀 QA 근본원인).
_compose_manager()는 참조인물(refer_man_name+refer_position)이 없으면 현장담당자 이름+직책으로 폴백하는데,estimate_dao.select_estimate가d.name만 뽑고d.position을 안 뽑아 pos 가 항상 빈값이었다 → 참조인물 보유 업체(전체 417명 중 33명)만 「이름 직책」이 나오고 나머지는 이름만. 폴백 경로가 다수라 티가 늦게 난다. 엑셀·PDF·HTML 이_build_common_header한 곳을 쓰므로 SELECT 한 줄로 전부 복구된다. ⚠별칭은AS position그대로 둘 것 —estimate·field·company어디에도 동명 컬럼이 없어a.*와 충돌하지 않는다(실측). - 2026-08-19 견적 목록(list-v2) 입금여부의 정본 = 월마감 초안(페페 지시). 3상태 = 마감행 확정금액 있음 → 입금 ·
마감행만 있음 → 「마감」 · 마감 초안에 없음 → 미입금. 수동 토글
is_deposit은 '1' 일 때만 우선(끄면 마감 기준으로 복귀). 마감행 매칭 =estimate_no일치 우선, 마감행에 견적번호가 없을 때만 같은field_no폴백 — 마감행이 다른 견적을 명시하면 현장으로 끌어오지 않는다. 서버는mc_confirm_cnt·mc_close_ym두 서브쿼리만 내려주고 판정은 화면depositBtnHtml()한 곳. ⚠옛 견적에 「마감」이 뜨는 건 정상 — 한샘 마감표는 미확정 현장을 매달 이월 반복 기재한다(분당파크뷰 2022년 견적이 2026-04 마감행에 있다). 선례 =estimate_dao.select_estimate_search_list·estimate_list_v2.html· 검증scripts/_deposit_mark_check.py. - ⚠2026-08-19 떠 있는 드롭리스트를
scroll캡처 리스너로 닫으면 안 된다 (AL발주서 QA 근본원인).window.addEventListener('scroll', closeCombo, true)는 캡처라 드롭리스트 자신의 스크롤까지 잡는다 → 휠 한 칸에 목록이 닫혀 「휠이 안 먹는다·조금만 움직여도 닫힌다」로 보고된다. 그리드overflow-x래퍼의 가로 스크롤도 같이 걸린다. 정답 = 닫지 말고 따라가게 —if (box.contains(e.target)) return;로 자기 스크롤만 무시하고 나머지는 재배치(placeCombo). 목록은position:fixed(뷰포트 기준)로 두고 아래 공간이 좁으면 위로 뒤집고 남는 공간만큼만 높이를 준다(하단 행에서 화면 밖으로 잘려 「프레임에 갇힌」 증상 제거). 목록mousedown은 무조건preventDefault— 스크롤바 드래그·여백 클릭에 포커스가 빠져 blur 로 닫히던 것까지 막힌다.overscroll-behavior:contain이면 목록 끝에서 페이지로 스크롤이 안 넘어간다. 선례 =al_orderpaper.jsplaceCombo(). - 2026-08-19 현장명 입력칸의 드롭리스트 정본 = 견적서 「현장 검색」과 같은 API (
GET estimate/site-search, 상차일 최근 30일 이후·키워드는 현장명 OR 업체명·최대 50건). 서버가 업체명으로 찾아준 현장이 화면 필터에서 걸러지지 않게 클라 필터는 보조설명(sub)까지 본다. 서버조회형 목록은attachCombo(input, getItems, fetchItems)3번째 인자로 붙인다(200ms 디바운스 · 도착 시 포커스가 남아있을 때만 재오픈). - ⚠2026-08-19 화면 라우트를 새로 만들면
menu_catalog행도 같이 만든다 — 사이드바에 안 띄울 페이지는show_in_sidebar='0'행으로. 카탈로그에 없으면 Patch#32dbefore_request가match_url_to_menu_key()None → 「unmapped」 403 으로 총관리자 외 전원 차단이고, 권한 매트릭스(/admin/user·/admin/company-permission)도 같은 카탈로그를 읽으니 켤 항목조차 없어 권한으로 못 푼다. 실측 =/field/al-profile(AL설정 관리) 26.7.29 신설 → 8.19 발견. 숨김 페이지 선례 =field_regist·field_modify·field_print_*. 카탈로그 행은 DB 1행이라 앱 배포 불필요(scripts/ssm_run.py로 test·live 각 INSERT, 멱등). 권한 허용/차단은 페페가 UI에서. - 2026-08-18
type=number휠 증감 차단 = document 위임 +blur()(preventDefault아님). 크롬은 document 에 붙은 wheel 리스너를 passive 로 잡아 preventDefault 가 안 먹고, 먹더라도 그 칸 위에서 페이지 스크롤까지 막힌다. → 포커스만 떼면 기본 증감이 사라지고 스크롤은 그대로 굴러간다. - ⚠2026-08-18 크롬에선 이 버그가 재현이 안 된다 — 재현 안 된다고 "버그 아님"으로 닫지 마라.
.qe-table .dim-cell::-webkit-inner-spin-button{-webkit-appearance:none}로 스핀버튼을 숨기면 크롬은 휠 증감 자체를 안 한다(파이어폭스 계열은 스핀버튼과 무관하게 증감). 헤드리스 크롬 검증에선 스핀버튼이 살아 있는 대조군 input 을 페이지에 심어 가드 발동을 증명했다(scripts/_dim_wheel_check.py). -
⚠2026-08-18 Playwright
mouse.wheel좌표는 스크롤이 끝나기 전에 쏘면 target 이 상위 DIV 로 샌다 —elementFromPoint로 조준해도 dispatch 시점엔 어긋나 값도 포커스도 안 변하는 「가짜 PASS」가 된다. 실제 휠이 꼭 필요한 자리만 좌표 재조준 루프로 쓰고, 나머지는 합성new WheelEvent(...)로 위임 가드만 본다. -
🔴 PFS 계정 비번을 바꾸면
C:\dev\.secrets\pfs_login.env도 같이 갱신한다. 자동화(E2E·_verify_deploy.py·배포 폴링)가 전부 그 파일의PFS_TEST_PW/PFS_PROD_PW로 로그인한다 → 비번만 바꾸면 다음 배포 검증이 통째로 막힌다(2026-08-18 실측, 로그인 500code 2000). 자격증명이 없을 때의 우회 = SSM 으로 서버 파일 해시를 로컬과 직접 대조(로그인 무관,scripts/_deploy_check.py패턴). 근본 해결은 자동화 전용 계정 분리. -
🔴 HTTPS 인증서가 만료되면 원인은 거의 항상 「ACME 챌린지가 앱으로 넘어가 404」다(2026-08-18 pfs-test 실측). vhost 의 80 블록이
return 301 https://...로 무조건 돌리는데 443 블록엔location /하나뿐이면,/.well-known/acme-challenge/…가 uWSGI(Flask)로 들어가 404 → certbot http-01 실패 → 조용히 만료된다. 80·443 양쪽에location ^~ /.well-known/acme-challenge/ { root /var/www/html; }를 둔다. - 🔴
sites-enabled안에 백업 파일을 두지 마라. nginx 는sites-enabled/*를 전부 include 한다 →pfs-test와pfs-test.bak.…이 같이 로드돼conflicting server name … ignored가 뜨고, certbot nginx 플러그인이 어느 블록을 고칠지 엉킨다. 백업은 디렉터리 밖으로. - 인증서 발급은
certonly --webroot로 한다 — EC2 certbot 0.27 의 nginx 플러그인은 alpha 라 중복 블록에서 오동작한다.certonly는 nginx 설정을 안 건드려서 운영 vhost 와 한 nginx 를 쓰는 지금 구조에 안전하다. 한 번 webroot 로 발급하면renewal/*.conf의authenticator가 webroot 로 영구 전환된다. 끝나고certbot renew --dry-run으로 다음 갱신까지 확인한다. - ⚠ 운영과 테스트가 같은 nginx·같은 EC2 다(
/home/ubuntu/pfs·/home/ubuntu/pfs-test). 설정을 고칠 땐 ①대상 파일만 ②변경 전 백업 ③nginx -t실패 시 자동 원복 ④systemctl reload(무중단) 순으로 짠다. - ⚠ EC2 원격 스크립트 함정 2개 — ①python3.6 이라
subprocess.run(capture_output=)이 없다(stdout=PIPE, stderr=STDOUT) ②bash -lc금지 — 로그인 셸이 stdout 에mesg: ttyname failed를 섞어 설정 파일을 오염시킨다(실제로nginx -t가unknown directive "mesg:"로 실패).bash -c를 쓰고, 파일은 파이썬 IO 로 직접 읽고 쓴다. -
계정 비밀번호 초기화 절차 = 앱과 같은 코드(
common.utils.string_util.StringUtil.hash_password)를 import 해 해시를 만든다. 직접 pbkdf2 를 짜면 반복수·프리픽스가 어긋나 로그인이 조용히 실패한다. 사용자 테이블은user(복수 아님)·이름 칼럼은name. 옛 해시는 서버에 600 파일로 백업하고, 평문 비번이 박힌 스크립트는 리포에 커밋하지 않는다(scripts/_*는 .gitignore 규약). -
2026-08-18 마감월 같은 「기간 이동」 UI 는
<input type=month>를 그대로 두고 좌우 버튼만 붙인다.mcShiftMonth(delta)= 값을new Date(y, m-1+delta, 1)로 굴려 연 넘김(2026-01 ◀ 2025-12) 이 공짜로 따라온다. 직접 자릿수 계산(±1 후 12 넘김 분기)은 쓰지 않는다. 검증에도 연 넘김 왕복 7회를 넣는다. -
⚠2026-08-18 UI 검증 스크립트의 「행수·금액」 단정은 운영/테스트를 갈라야 한다. 테스트 RDS 는 07-23 옛 시딩본(7월 넥서스 7행·176,113,000), 운영은 NAS 마감표 기준(9행·252,970,000). 안 가르면 변경과 무관한 FAIL 5건이 떠서 배포 판단이 흐려진다(
_monthly_close_ui_check.py의PROD분기). -
⚠2026-08-18 pfs REST API 성공 판정은
code == "0000"이다 —result == "success"가 아니다.RestResponse가{"code":"0000","message":<본문>}형태로 내려준다(생성 API 는message가 새 PK).result키로 보면 200 인데도 실패로 오판해 시딩이 중간에 멈춘다(월마감 운영 시딩 실측). - ⚠2026-08-18 월마감 페이지는
#mc-ym이 「현재월」로 시작한다 — 마감이 없는 달이면 목록이 비는 게 정상. 자동검증에서 "0행"을 렌더 버그로 오판하기 쉽다. 검증 스크립트는 데이터 있는 달로 바꾸고change를 쏴야 한다. 같은 표의 현장명은 편집용input.mc-sitevalue 라inner_text로는 안 잡힌다(evaluate_all로 value 읽기). 검증 =scripts/_monthly_close_ui_check.py [prod|test]. -
2026-08-18 NAS 월마감 엑셀을 읽을 땐 시트를 「이름」으로 고르면 틀린다. 넥서스 파일 전부에 2025년 잔재 시트(
10월 (2))가 붙어 있고, 6월 파일의 시트명이5월인 경우도 있다. 판정은 A1~A3 제목의2026년 N월/2026.N월로 한다. 같은 달에 파일이 2개면 시트 「계」가 정본과 맞는 쪽을 고른다. ⚠넥서스 1~4월 마감표는 그 달 매출이 아니라 이월 현황판이라 시트 계 = 행합 − 선지급/전월청구 조정행이다. -
2026-08-18 「로그인 유지」는 생산 인쇄 앱에서만 켠다 — 판별은
window.__PFS_PRINT_APP__. 앱이AddScriptToExecuteOnDocumentCreatedAsync로 모든 문서(로그인 화면 포함)에 심는 표식이라 로그인 페이지가 그걸keep_login으로 보내고, 서버는 그때만session.permanent = True(api/pfs/default/controller/auth_controller.py). 일반 브라우저는 종전대로 종료 시 로그아웃 = 사무실 공용 PC·외부 업체 계정 위험 없음. 비밀번호는 어디에도 저장하지 않는다(세션 쿠키 수명만 늘림). ⚠크로뮴은 쿠키 수명을 400일로 깎는다(3650일로 보내도 400일) — 요청마다 갱신되니 실사용상 무기한. 🔑앱 재설치 불요(웹만 고치면 된다). - ⚠2026-08-18 PFS 로그인 버튼은
type="button"+onclick이었다 → 엔터가 안 먹었다. 폼action="/login" method="post"는 살아 있는데 실제 로그인은 JS(login()→POST /api/auth/login) 라, 엔터로 기본 전송되면 엉뚱한 경로로 샌다. 그래서 submit 을 막아 뒀던 것. 고칠 땐form onsubmit="login(); return false;"+ 버튼type="submit"로 엔터·클릭을 한 경로에 모은다 (덤으로required브라우저 검증이 살아난다). 같은 모양 =auth/change_password.html(미수정, 지시 밖). - 🔴2026-08-18 PFS 안에서 인쇄하는 페이지는
@page{size}를 반드시 우리가 박는다 —— 부트스트랩 인쇄 CSS 에@page{size:a3}가 숨어 있다. 스티커가 100×100 라벨에 21×21mm 로 좌상단에 찍힌 진범이 이것. PrintSettings 로 용지를 100mm 라 지정해도 CSS 페이지 박스는 A3(297×420) 가 되고, 크로뮴이 그 페이지를 용지에 맞추려 0.238배(=100/420) 로 줄인다 → 90mm 시트가 21.4mm. 줄어든 A3 페이지가 용지 좌상단부터 그려져 "작게 좌측 상단" 증상이 된다.html,body{width:90mm}로는 절대 안 잡힌다(레이아웃 폭이 아니라 페이지 박스가 문제라서). ⚠@page에 margin 은 넣지 마라 — PrintSettings 여백을 덮어쓴다(2026-08-11 실측). size 만 박는다. - 🔑2026-08-18 "실물 크기가 이상하다"는 프린터 없이 판정한다 — 실페이지를 인쇄 직전 상태로 떠서
앱 엔진으로 PDF 화한다. 도구 =
scripts/_sticker_print_verify.py(로그인→인쇄상태 재현→CSS 인라인 단일 HTML 스냅샷) +PfsPrint.exe --pdfhtml <html|폴더> <출력폴더>(실인쇄와 같은 PrintSettings 로 PrintToPdfAsync) +scripts/_pdf_content_bbox.py --summary(mm 실측). 변형 실험은scripts/_sticker_print_bisect.py(CSS 를 빼가며 범인 이분탐색) — 이걸로 @page a3 를 찾았다. ⚠PDF 좌표 측정은q/Q스택을 지켜야 한다(안 지키면 cm 배율이 누적돼 768mm 같은 헛값). 라벨 후보는 테두리를 그린 사각형(re S) 만 — 배경칠·클립은 용지·컨테이너다. - ⚠2026-08-18 크로뮴 인쇄 축소를 Playwright
page.pdf()로 재현하려 하지 마라 — 안 나온다. Playwright 는 인쇄 시 레이아웃 뷰포트를 용지 크기로 재설정해서 축소가 사라진다(실측: 같은 페이지가 89.5mm 정상). WebView2 컨트롤 폭(1440px)도 범인이 아니었다(200px·1440px 모두 89.69mm 동일). 앱 엔진(PrintToPdfAsync)으로 찍어야 실물과 같은 결과가 나온다. - 🔴2026-08-12 외부 업체가 로그인해 보는 화면은 「업체 스코프」를 서버 한 곳에서 강제한다.
단일 출처 =
permission_service.get_scope_company_no(user_no)— 외부 업체 계정이면 그 업체번호, 자사(company_type='1')·총관리자면None(전체). 화면 라우트(/field/company/<no>)와 조회 서비스(field_service.get_field_search_list)가 둘 다 이 함수만 본다 → URL·요청 body 를 고쳐도 남의 현장이 안 나온다. ⚠화면에서 숨기는 것만으론 부족하다 —/api/*는before_request권한 훅이 면제(web/__init__.py)라 API 직타는 로그인만 통과하면 뚫린다. 조회 API 자체를 좁혀야 한다. - ⚠2026-08-12 super_admin 은
before_request페이지 차단을 설계상 통과한다(web/__init__.pyPatch #32d 1)). → 「URL 직접 입력 차단」을 super_admin 계정으로 검증하면 200 이 떠서 오진한다. 계정 없이 재현하려면GET /api/admin/menu/catalog로 실제 카탈로그를 받아 서버와 같은 규칙(정확매치 → 최장 prefix → None) 을 스크립트에서 돌린다 — 미등록 URL 이None(=403 unmapped)인지, 업체 권한 목록에 타업체 menu_key 가 없는지 확인. 검증본 =scripts/_company_field_page_check.py. - 2026-08-12 사이드 메뉴는 코드가 아니라 DB(
menu_catalog)다. 페이지를 늘리는 기능은 메뉴행 INSERT 로 만든다(운영 RDS 직접 접근 없이 앱이 멱등 부트스트랩 —ensure_vacation_menu·ensure_field_log_group선례). 최상위(parent NULL·url_path NULL) = 그룹 라벨, 그 자식 = 링크. 업체별 현장 그룹 =field_company(display_order 950). 🔑페이지 생성은 메뉴행 +company_permission권한 ON 을 한 번에 해야 "만들었는데 안 보인다"가 없다. - 🔴2026-08-11 웹 페이지를 PrintAsync 로 찍는데 「구석에 작게」 나오면 범인은 인쇄 레이아웃 폭이다.
실사출력 확인 = 100×100 라벨에 내용이 좌하단 1/4 크기로 찍혔다.
@media print로 다른 요소를 다 숨겨도 html/body 의 레이아웃 폭은 화면 기준 그대로라, 용지보다 넓으면 크로뮴이 페이지 전체를 축소해 맞춘다.ScaleFactor를 올려도 그 축소가 먼저라 크기가 안 변한다(페페 신고와 일치). → 인쇄 직전@media print{ html,body,#인쇄루트 { width:(용지−여백×2)mm !important; min-width:0 !important } }로 폭을 못 박는다. 앱 인쇄 경로는@page를 쓰지 않는다(용지·여백은 PrintSettings 단일 출처). - 🔴2026-08-11 CSS
@page{margin:0}은CoreWebView2PrintSettings의 여백을 덮어쓴다. 테스트 인쇄가 90mm 정확한데 여백 없이 모서리에 붙어 나온 원인. 여백은 한 곳에서만 정한다 = PrintSettings. - 2026-08-11 WinForms 대화상자 높이를 숫자로 박으면 항목이 늘 때 아래 버튼이 잘린다(실제로 잘렸다).
마지막 컨트롤의
Bottom에서ClientSize를 계산한다. 검사 =PfsPrint.exe --layout(창을 띄우지 않고DrawToBitmap으로 그림까지 떠 준다 — 화면 방해 없이 육안 확인). - 2026-08-11 PFS 레이아웃 사이드바는
body.mini-sidebar로 접힌다. ⚠custom.min.js의 resize 핸들러가 폭 1170 이상이면 그 클래스를 지운다 → 한 번 붙이고 끝내면 안 되고 load·resize 후 재적용해야 한다. -
⚠2026-08-11 배포 대기 루프의 grep 패턴이 안 맞으면 「배포가 느리다」로 오진한다(실제론 이미 끝나 있었다). 대기는 패턴 일치가 아니라 HEAD 해시 비교로 판정하고, 한 번은 실제 상태를 찍어 본다.
-
🔴2026-08-11 "고쳤는데 화면이 그대로다" = 먼저 「서버에 올라갔나」부터 본다. 스티커 인쇄가 계속 웹인쇄로 떨어진 원인은 코드가 아니라 미푸시였다(
origin/master가 로컬보다 2커밋 뒤). 판정 순서 = ①git log --oneline origin/master..HEAD(미푸시) ②git log origin/deploy-live..origin/master -- <파일>(운영 미반영) ③EC2 실파일에서 표식 grep(scripts/_sticker_deploy_probe.py). ⚠운영은 선별 포워드 이력이라 브랜치 비교로만 판단하면 틀린다 — 실파일을 봐야 확실하다. - 🔴2026-08-11
git checkout <브랜치>가 「Aborting」 으로 실패했는데 뒤에&&로git pull을 붙이면 현재 브랜치에 남의 브랜치를 머지한다. 실제로 master 에 deploy-live 가 머지되며 4파일 충돌(즉시merge --abort로 복구). → 브랜치 전환은&&체인으로 묶지 말고 한 줄씩, 전환 전git status --porcelain으로 워킹트리를 비운다(생성물tools/sitemap/menu_dump.json이 매번 걸린다 →git stash push <파일>). - 2026-08-11 운영에 파일 단위로 포워드할 땐 「그 파일에 쌓인 미반영 커밋 전부」가 같이 간다.
스티커 3파일에 13커밋이 걸려 있었다 → 배포 전
git log origin/deploy-live..origin/master -- <파일들>로 동승 목록을 뽑아 보고하고, DB 를 요구하는 게 섞였는지 스키마 프로브 먼저 (scripts/_sticker_prod_schema_probe.py, 읽기전용). 파일 일부만 배포할 방법은 없다. -
2026-08-11 pfs API 는 이제 인증 게이트가 있다 — 테스트도
/api/...직접 호출이 401. 검증 스크립트는POST /api/auth/login(C:\dev\.secrets\pfs_login.env) 후 쿠키를 물고 호출한다 (scripts/_sticker_fields_api_check.py가 그 형태). -
🔑2026-08-11 생산 인쇄는 「윈폼 창 + WebView2 +
PrintAsync」 가 정답이다 — 브라우저 인쇄도, 별도 렌더러도 아니다.CoreWebView2.PrintAsync(PrintSettings)는 인쇄 대화상자 없이PrinterName·PageWidth/Height(인치)·ScaleFactor·여백을 지정해 찍는다. 스티커=GT800, 파레트=A4 로 프린터를 나눠 보낼 수 있어--kiosk-printing(기본 프린터 1개 고정)이 못 풀던 A4 혼용 문제가 사라진다. 🔑렌더러를 따로 만들지 마라 — 화면의 HTML 을 그대로 찍으므로 미리보기=실물이 보장된다. 구print-agent(Pillow+GDI)는 라벨 모양을 웹 CSS 와 두 벌 유지해야 했고 그게 부채였다 → 폐기. 실측(같은 설정으로 PDF 뽑아 MediaBox 측정) = 스티커 99.82×99.82mm(기대 100) · 파레트 297.01×209.89mm. 검증 도구 =pfs/print-app/tools/verify_print_size.py(프린터 없이 크기 판정). - 🔴2026-08-11 WebView2 는 「보이지 않는 창」에서 안 뜬다 — 0x80070578(잘못된 창 핸들).
원인 둘 다 실측: ①WinForms 에서
Visible=false컨트롤은 HWND 자체가 안 생긴다(숨기려면 "안 보이게"가 아니라 화면 밖 좌표에 둔다) ②SSH·서비스 세션(Session 0) 에는 대화형 윈도우 스테이션이 없어 같은 오류가 난다 → 원격에서 GUI 앱을 띄울 땐schtasks /IT로 사용자 세션에. ⚠코디 Bash 도 Session 0 이라 GUI 앱 검증을 Bash 로 직접 하면 "코드 버그"로 오진한다(이번에 태웠다). - 2026-08-11 접두사 없는
Mutex는 세션마다 따로다 — 단일 인스턴스 가드가 Session 0/1 에 각각 뚫려 같은 앱이 둘 뜬다(실측). 머신 전체로 막으려면Global\접두사. - 2026-08-11 설치본이 자기를 복사할 때 「실행 중이라 복사 실패」를 조용히 넘기면 구버전이 계속 돈다. "고쳤는데 그대로"의 원인이 된다 → 복사 전에 돌고 있는 인스턴스를 내리고, 그래도 실패하면 사용자에게 알린다.
-
2026-08-11 원격 Windows 에서 UTF-8 파일을 읽을 땐 PowerShell 기본 인코딩(cp949)을 두 군데 다 바꿔야 한다.
[Console]::OutputEncoding=[Text.Encoding]::UTF8; Get-Content <path> -Encoding UTF8— 하나만 하면 한글이 깨진다. 경로는%LOCALAPPDATA%형태로 준다(PowerShell 작은따옴표 안의$env:는 안 펼쳐진다). -
2026-08-11 생산 스티커·파레트 인쇄는 브라우저로 정확도를 못 맞춘다 — 로컬 인쇄 프로그램(
PFSPrint.exe)이 정답이다. 브라우저 인쇄는 드라이버 배율을 타서 크기가 흔들리고(그래서 웹에 140% 보정이 있었다), 인쇄창 때문에 논스톱이 안 된다.--kiosk-printing은 기본 프린터 1개로 고정돼 스티커(GT800)+파레트(A4) 혼용과 충돌한다 — 이 경로는 쓰지 않는다. 구조 = 웹은 데이터만 POST(/print-sticker·/print-pallet) → 프로그램이 프린터 장치 DPI 로 렌더해 GDI 1:1 인쇄. ⚠웹 인쇄설정(용지·여백·배율)은 프로그램이 없을 때의 폴백 전용이다. 크기가 틀리다는 신고가 오면 배율을 만지기 전에 화면 상태표시(PFS 인쇄 프로그램 연결됨)부터 본다 — 십중팔구 프로그램이 안 떠서 브라우저로 나간 것이다. 실패는 조용히 폴백시키지 말고 사유를 상태줄에 남긴다(agentFail) — 안 그러면 "왜 크기가 틀리지"로 되돌아온다. - 🔴2026-08-11 Windows 의
SO_REUSEADDR는 Unix 와 뜻이 다르다 — 이미 바인드된 포트를 「가로채도록」 허용한다. 파이썬HTTPServer.allow_reuse_address기본값이 True 라 로컬 상주 서버가 중복 실행돼도 조용히 둘 다 뜬다(실측). 둘이 번갈아 받으면 낡은 설정으로 인쇄되는데 증상이 간헐적이라 찾기 어렵다 → 상주 프로그램은allow_reuse_address = False로 두고OSError로 스스로 종료시킨다. ⚠PyInstaller onefile 은 부팅로더+본체 = 프로세스 2개가 1인스턴스이므로 프로세스 개수로 중복을 판정하면 오진한다. - 2026-08-11 창 없이 도는 프로그램(
--noconsoleexe·pythonw)은sys.stdout이None이라print()가 터진다. → 실행파일 옆agent.log로 리다이렉트한다(원격 SSH 진단이 이 파일 하나로 끝난다). 반대로 콘솔에서 돌 때는 cp949 라—(em dash) 같은 글자에서UnicodeEncodeError로 죽는다(실측) →sys.stdout.reconfigure(encoding='utf-8', errors='replace')를 진입부에 같이 넣는다. 두 경로 다 막아야 한다. - 2026-08-10 발주서 항목이 화면에 없다 = 파싱 실패가 아니라
del_manual='1'(수동 삭제)일 수 있다. 먼저 그것부터 본다. 실측 = 현대 제기 MH 상원보드효산 HS2329(orderpaper_no 1631) — 파서는 정상 수집하는데 화면에 없었다.orderpaper_sync.py의 UPDATE 가remove_yn=IF(del_manual='1','1','0')라 수동삭제 행은 동기화해도 영영 안 돌아온다 (블록/항목 삭제 버튼이 "영구 삭제 — 동기화해도 복원되지 않음"인 게 설계 의도다). 진단 순서 = ①NAS 엑셀을orderpaper_parse.parse_file로 직접 파싱해 항목이 나오는지 ②DB 에서 그 행의remove_yn·del_manual·sync_round③최신sync_round와 같은데remove_yn='1'이면 범인은 수동삭제. 복구 =UPDATE orderpaper SET del_manual='0', remove_yn='0'후 상태·입고일은 API 로. ⚠운영에 「엑셀엔 살아 있는데 수동삭제된 행」이 135건 있다 — 미입고 일괄정리 때 이 집합을 먼저 봐야 한다. - 2026-08-10 분류·폴딩까지 바꾸는 「모드형」 토글은 localStorage 에 저장하지 않는다.
발주서 정리 「미입고」 토글이 그 예 — 켜면 업체별 분류 + 전량 펼침 + 숨김 현장 포함으로 화면을 통째로 바꾼다.
이걸
opSaveState()로 저장하면 다음 방문에 업체별·전량 펼침·숨김 섞임 상태로 갇히고, 사용자는 자기가 뭘 켰는지 모른다. 저장하는 건 일반 빠른필터까지. 모드는 새로고침하면 꺼지는 게 안전하다. ⚠켤 때OP_OPEN·OP_BLK_OPEN·OP_FOLD(개별 override)를 비워야 '기본 펼침'이 실제로 먹는다 — 사용자가 접어둔 카드가 남아 있으면 안 열린다. 끌 때는 켜기 직전 값을 통째로 복원한다. - 2026-08-10 「해제·복구가 그 화면에서만 되는 목록」에 날짜 필터를 새로 걸 땐 날짜 없는 행을 fail-open 시켜라.
숨김 현장 관리 모달이 그 예 — 상차일 기준 3개월 창을 걸었는데
field.shipping_date가 nullable 이라 그대로 두면 상차일 없는 숨김 현장이 검색에서 영영 사라져 활성화(해제)할 길이 없어진다(이 모달이 유일한 창구). → 날짜가 비면 기간과 무관하게 통과. 같은 판단의 선례 =orderpaper.receiver미기재 fail-open(오노출 < 누락). - 2026-08-10 숨김 현장 관리 모달은 열 때 한 번 받고 화면에서 필터·정렬한다(서버 왕복·신규 API 없음).
숨김 현장은 수십 행(운영 27)이라 GET
/api/orderpaper/hidden-sites한 번이면 끝 — 컨트롤러·서비스 무변경. 표 양식은 견적서estimate_regist_v2현장 검색과 동일(업체명/현장명/구분/상차일/최근입고일/항목 + 건수 + 헤더 정렬). DAOselect_hidden_sites에 field 컬럼을 더할 땐 LEFT JOIN 말고 서브쿼리 —GROUP BY site_name_raw, field_no라서 조인 컬럼을 SELECT 에 얹으면 ONLY_FULL_GROUP_BY 에 걸린다(기존field_name도 같은 이유). - 2026-08-10 발주서 탭 = 발주처, 「입고처」 행 = 목적지. 둘을 같은 것으로 보면 안 된다.
포인 발주서 표준 = A열(A:B 병합)
입고처· C열 업체 · D:J 주소, 그 위의 항목 묶음이 그 목적지로 간다. 한 탭에 여러 개면 각각이 자기 위 묶음을 끊는다(실측 엠제이실업 = 오엠씨/진영LDM/상원테크/세영M테크). → "시트는 오엠씨로 입고된다"는 전량 참이 아니다(운영 실측 시트 79건 중 오엠씨행 39건뿐, 나머지는 피앤알·바사디자인·헤브론·호선우드·포인 직입고). 업체 전용 화면은 발주처 OR 입고처로 데이터를 좁혀라. 파서 =orderpaper_parse._receivers/_receiver_of, 컬럼 =orderpaper.receiver. - 2026-08-10 orderpaper 에 컬럼을 더할 땐
item_key/block_key해시 인자에 넣지 마라. 키가sha1(파일,sheet,section,material_type[,idx])라서 인자를 늘리면 다음 동기화가 전 행을 신규로 만들어 사용자 편집(*_user)·상태·사용이력이 통째로 끊긴다. 값만 추가하고 키는 그대로 두면 동기화가 ins 0 / upd 전량으로 지나간다(실측 1769행). 컬럼 추가는 동기화 스크립트의 멱등 ALTER 목록에 넣고 DAO 가 SELECT 하기 전에(=배포 전에) 먼저 돌린다. - 2026-08-10 화면 검사(Playwright)를 「첫 카드」·「기본 펼침 상태」에 걸지 마라 — 데이터가 바뀌면 검사가 깨진다.
/omc/material은 완료 항목을 접으므로(foldcfg.done=false) 남은 항목이 전량 완료면 섹션·행이 아예 렌더되지 않는다 → 「시트 그룹이 첫 카드 맨 위」·「타업체 품명 편집 n/n」이 코드 무결한데 FAIL 이 났다. → 검사 전에opExpandCardAll로 전 카드를 펼치고, 해당 요소가 있는 카드 전부를 대상으로 단언한다. - 2026-08-10 QA 보고서는 두 세션에 동시 도착할 수 있다 — 파일 고치기 전에 「누가 이미 하고 있나」를 본다.
실측:
/omc/material입고처 QA(15:32:46)가 글자 그대로 같은 프롬프트로 두 세션에 들어왔고, 먼저 받은 세션이orderpaper_parse.py에_receivers/_receiver_of를 미커밋 상태로 이미 넣어둔 상태였다(mtime 15:40). 그대로 착수하면 같은 파일 편집 충돌 + 커밋 갈림 + deploy-live 이중 push 다(워크트리 점유 사고 전례 있음). → 착수 전 2줄 점검: ①git status의 미커밋 변경이 이 QA 내용인가(docstring·주석에 지시문 문구가 들어 있으면 타 세션 진행분) ②ls -la --time-style=+"%Y-%m-%d %H:%M:%S" ~/.claude/projects/c--dev/*.jsonl | sort -k6,7 | tail로 내 세션 외에 최근 갱신된 세션이 있는지(그 jsonl 첫 user 메시지를 보면 같은 QA인지 즉시 확인된다). 살아 있으면 손 떼고 페페에게 택1을 묻는다 — 중복 착수가 대기보다 비싸다. - 2026-08-10 특정 업체명만 손대는 화면 작업은 테스트 DB 표기로 검증하지 마라 — 운영과 다르다.
실측: 운영
아이비젼글로벌/ 테스트글로벌아이비젼(순서 반대). 업체명 고정 매칭은 한쪽에서 조용히 0건이 되고 화면은 멀쩡해 보인다. → 표기 변형을 정규식으로 다 받되((A)(B)|(B)(A)→$1$3<br>$2$4) 이름 순서는 절대 바꾸지 않는다. 검증은 양쪽 다 돌리고, 데이터에 없으면 페이지의 렌더 함수를 직접 호출해 임시 행을 주입·실측·제거한다(scripts/_field_status_print_ivision_check.py). - 2026-08-10
white-space: nowrap셀을 줄바꿈시키려면<br>를 넣는다 —<br>은 nowrap 을 무시하고 개행하므로 CSS(폭·nowrap)를 건드려 다른 칸을 밀 필요가 없다. 인쇄 레포트처럼 컬럼 폭이 확정된 표에서 가장 싼 수정이다. -
2026-08-10 부모-자식 복사의 대응키는
display_order가 아니라LAST_INSERT_ID()다.estimate_detail.display_order는 실데이터에 중복이 있다(테스트 실측: 견적 33·35·36·37·47 = 19행인데 distinct 6). display_order 로 짝지으면 그런 견적에서 sub 가 엉뚱한 항목에 붙는다. → 항목을 한 건씩INSERT .. SELECT하고 같은 커넥션·같은 트랜잭션에서SELECT LAST_INSERT_ID()로 새 PK 를 받아 그 자식만 붙인다(estimate_dao.copy_estimate_contents). 파이썬이 만지는 값은 정수 PK 뿐이라 FLOAT 함정도 같이 피한다. ⚠MySQLTransactionUtil은 lastrowid 를 노출하지 않지만 커넥션이 하나라LAST_INSERT_ID()로 읽으면 된다(유틸을 고칠 필요 없다). -
2026-08-10 SQL 문자열을
%포맷으로 조립할 때 「인자로 들어가는 조각」의 플레이스홀더는%s다.values = ", ".join("%%s" …)처럼 조각 안에%%s를 쓰면, 그 조각은 바깥%포맷의 인자라 다시 escape 되지 않아%%s가 그대로 남고 pymysql 이 파라미터를 못 채운다. 포맷 문자열 본문에 직접 쓰는 자리만%%s. (자체검사 = 완성된 SQL 을 print 해서%%가 없는지 눈으로 본다.) -
2026-08-10 견적 행 복사는 파이썬으로 읽어 되넣지 마라 — MySQL 안에서
INSERT .. SELECT로 한다.estimate_detail의price·amount·margin_*는 전부 FLOAT인데, MySQL 텍스트 프로토콜이 FLOAT 를 유효 6자리로 잘라 보낸다 → pymysql 로 읽은 값을 그대로 INSERT 하면 소수부가 사라진 다른 값이 저장된다 (708→747 실측: 행 단위로는 전부 같아 보이는데SUM(amount)가 33,419,453.09375 → 33,419,442.0 = 11.09 차이). 🔑대조도 파이썬에서 하면 못 잡는다 — 읽어오는 순간 양쪽 다 6자리로 뭉개져 "차이 0"이 나온다. 검증은 서버측NOT (a.col <=> b.col)조인으로 해야 비트 비교가 된다. 자식 행(estimate_detail_sub)의 새detail_no는display_order조인으로 매핑하면 값을 밖으로 안 꺼내도 된다 (estimate_no안에서display_order유일 — 실행 전COUNT(*)=COUNT(DISTINCT display_order)로 확인). -
2026-08-10 자재 입고 판정의 유일한 근거 = 카톡 「포인01」 명세서 사진이다.
orderpaper.status는 못 쓴다. 실측 = 지난주 관련 79건이 전부 '대기'(아무도 화면에서 안 바꾼다).item_receive_date도 발주서상 예정일이지 입고 확인이 아니다. → 자동화 =C:\dev\tools\intake-check\(스킬intake-check, 예약작업IntakeCheck-1630매일 16:30). 사진 수집→Gemini 판독→1:1 매칭→PATCH /api/orderpaper/<no>/status{완료}+/receive-date. 오탐 0이 재현율보다 우선이라 현장 2-gram 2개↑·코드 겹침·수량 충족·1:1 을 전부 만족할 때만 반영하고 나머지는 보고만 한다. ⚠입고일은item_receive_date_user에 들어간다(API가 사용자 override 컬럼에 쓴다) — 검증 쿼리에서 원본 컬럼만 보면 "반영 안 됐다"고 오진한다. ⚠업체명으로 매칭하지 마라 — 출고사가 다르다(pfs이연↔출고증다인시스템, pfs성지라미텍↔명세표 발행건창기업). 첫 실측 정확도 = 정밀도 100%(7/7) · 재현율 46.7%(헤펠레 8종 누락 — 한 프레임 전표 2장+회전으로 판독 실패). 정확도는runs/*.verdict.json+--accuracy로 매 회 누적 측정한다. 🆕2026-08-10 2차 — 카톡 본문은 「현장이 큰 그룹, 그 아래 [입고]/[확인]/[미입고]」로 현장을 가리지 않고 전개한다(페페 지시). 종전엔 오늘 명세서가 온 현장만 펼치고 나머지는 "그 밖에 46건(현장 12곳)" 한 줄로 뭉갰다 → 신규 사진이 0장인 날은 본문이 그 숫자 한 줄뿐이라 어느 현장을 재촉할지 아무도 모른다. 지금은 45일 창 안의 미입고를 현장별로 적고, 한 현장이 5건을 넘으면… 외 N건으로 건수를 남긴다(조용히 자르지 않는다). 실측 2,504자·79줄 = 읽히는 길이. ⚠미입고 집계에 숨김 현장도 포함된다(fetch_open_items는is_hidden을 안 거른다 — 페페 지시로 그게 맞다). 🆕3차 = 미입고는 「진행중 현장」만 보고한다(페페가 과거 현장 미입고를 PFS 에서 전부 정리한 뒤 지시). 판정 =POST /api/field/status/search {field_nos}→production_status.field_status='20'(10 대기/20 진행/30 완료). 🔴/api/sticker/fields(진행중 목록)로 판정하면 안 된다 — 상차일 3개월 창 + LIMIT 100 이라 오래된 진행중 현장이 조용히 빠진다. 현장 미연결·상태 조회 실패는 fail-open(안 좁힌다). 제외 건수는 각주로 남긴다(안 적으면 "미입고 0"과 "안 보여준 것"이 구분 안 된다). 실측 = 110건 → 진행중 33건(4현장)·776자 24줄(전 현장이면 122건·2,504자). -
2026-08-10 발주서 엑셀의 「병합 주석행」은 항목이 아니다 (
orderpaper_parse._is_merged_note_row). 포인 발주서 표준 = 품명B:C· 규격D:E로 각각 병합. 주석행은B:G처럼 한 칸으로 합쳐 쓴다 (실측에넥스 라인VIP 개인건피앤알 탭 행68 =B68:G68→시트 : 7.22(수) 13미터 1롤 차량 입고). 판별 = 품명칸의 병합 범위가 규격칸까지 덮으면 주석행. 페페 지시로 '시트'가 들어간 주석행만 수집 제외 (시트 입고 일정 메모라 자재분류 시트(T)에 자재처럼 뜬다) — 다른 주석행은 종전대로 유지(회귀 0). 이미 적재된 행은 DB 판별 = 품명에 '시트' + 규격 빈칸(DB엔 병합 정보가 없다) → soft delete. ✅orderpaper_sync.py는 사이드카가subprocess로 띄운다 → 파서 수정에 사이드카 재시작 불필요 (업로드 파서orderpaper_upload_parsers만sys.modules캐시라 재시작이 필요하다 — 둘을 헷갈리지 말 것). -
2026-08-10
deploy-live워크트리가 다른 세션에 잡혀 있으면--detach로 붙는다.git worktree add <경로> deploy-live는 그 브랜치를 쓰는 등록이 하나라도 있으면 거부한다 (이번엔 대상 폴더에.git조차 없는 깨진 등록이었는데도 묶여 있었다). 남의 등록을 지우지 말고git worktree add --detach <경로> origin/deploy-live→ 파일 포워드 → commit →git push origin HEAD:deploy-live. 동시에 다른 세션이 밀면 non-fast-forward 로 안전하게 실패한다. ⚠여러 세션이 같은 리포에 붙어 있다 — 배포 전git fetch로origin/deploy-live를 다시 보고, 내가 전에 올린 커밋이 조상인지(git merge-base --is-ancestor) 확인하고 포워드한다. -
2026-08-10 자재분류 시트(T)는 컬럼이 아니라 조회 시점 파생이다 (
orderpaper_dao._MT). 규칙 = 규격에 '시트'가 들어가면 무조건 T(페페: "시트는 규격에 항상 시트라는 단어가 포함된다"). 🔑저장값을 바꾸면 안 되는 이유 =item_key/block_key가sha1(파일,sheet,section,material_type)라material_type을 UPDATE 하면 다음 동기화가 전 행을 새 행으로 만들어 사용자 편집(*_user)·상태· 사용이력이 통째로 끊긴다. 파생이라 기존분·추가분이 자동으로 같은 규칙을 탄다(마이그레이션 0). 오분류는 규격(spec_user)을 고쳐서 푼다(블록 수동지정 R/E/S 는 T 를 못 이긴다 — 그래서 블록 토글에서 시트 제외). 🔴파생의 대가 = 한block_key에 시트/비시트가 섞인다(운영 실측 59블록 중 23) → 블록 단위 쓰기 (block-status·block-receive-date·block-receive-date-bulk·block-delete)에material_type을 같이 보내 그 섹션만 건드리게 좁혔다. 안 그러면 시트 섹션의 「상태 일괄」이 화면에 없는 부자재 행까지 바꾼다. ⚠저장된 필터 상태(localStorage)가S.mat를 통째로 덮어써 신규 분류가undefined=제외가 된다 → 복원 시 기본값과Object.assign병합 필수(입고일정·자재관리 탭 둘 다 해당). -
2026-08-10 오엠씨 전면/후면 편집은 합친 품명 1필드로 저장한다(
setItemFace).PATCH /api/orderpaper/{no}/item-name→item_name_user→ 기존 자재관리 탭·목록이 자동으로 같은 값을 본다. 재조립은 파서(omc_raw_fields)와 같은 규약 =전면 + (후면 있고 전면과 다르면 ' (후면)'). 🔑규격의 양면/단면도 같이 맞춰야 한다 — 후면을 비워도 규격이 '양면'이면opFaceParts가 후면=전면으로 되살려 편집이 안 먹은 것처럼 보인다. 분리·편집 적용 범위는 발주처가 오엠씨인 블록만 (오엠씨 화면·기존 발주서 정리 탭 공통 규칙 — 오엠씨 화면의 시트 블록은 타 업체라 품명 1컬럼). -
2026-08-10 오엠씨 화면의 시트는 오엠씨 발주가 아니다. 시트는 시트업체(조운IMT·데코밸리·미림· 엠제이실업…)에 발주해 오엠씨로 입고된다 — 실측 발주처가 오엠씨이면서 규격에 '시트'인 행 = 운영 0건. 그래서
/omc/material데이터 =발주처 오엠씨 OR 자재분류 T(오엠씨만 걸면 시트 그룹이 영원히 빈다). 화면은 시트 그룹을 현장 카드 맨 위에 두고 카드·블록을 기본 펼침(완료 항목만 접힘)으로 연다 — 목적이 오엠씨의 시트 입고 체크이기 때문. -
2026-08-10 관리자 전용 기능은 게이트를 2겹으로 — 템플릿
{% if is_super_admin %}+ 서버 재확인. 템플릿 가드만 두면 API 는 열려 있고, 서버만 두면 일반 사용자에게 눌리는 버튼이 보인다. 컨텍스트is_super_admin은web/__init__.py가 모든 템플릿에 이미 주입한다(따로 넘길 필요 없음), 서버는session.get("is_super_admin") != "1"→response.fail(...)(user_admin_controller 패턴). - 2026-08-10 기록을 지우는 버튼은 DELETE 가 아니라 소프트 딜리트로 만든다(스티커 출력 회수 초기화).
remove_yn='1'+removed_by·removed_at를 남기고 조회에remove_yn='0'필터를 건다 — 누가 언제 지웠는지가 남고 오조작을 DB 에서 되살릴 수 있다. 확인 대화상자에 대상 건수와 "복구 가능" 을 같이 적는다. -
2026-08-10 스티커 화면 E2E 는 「진행중 현장에 연결된 살아있는 도어발주서」가 필요하다 — 테스트 DB엔 0건.
door_order_file.remove_yn='1'인 삭제 파일에도 출력기록이 남아 있어 DB 만 보면 있는 것처럼 보이지만 화면엔 칩이 0개다(목록이remove_yn='0'필터). 검증은 살아있는 파일을 진행중 현장에 임시 연결 → 기록 시드 → 검증 → 연결 해제 + 시드 행 삭제 순으로 하고, 끝나고 SSM 으로 원복을 실측한다. -
2026-08-10 목록을 다시 그리는 클릭 핸들러는
stopPropagation을 붙인다 (스티커 현장 검색 정렬)..html()로 컨테이너를 재렌더하면e.target이 그 순간 DOM 에서 떨어져 나간다 → 뒤이어 도는 「패널/모달 밖 클릭이면 닫기」 핸들러의closest(...)가 0 을 반환해 밖 클릭으로 오판하고 닫아버린다 (정렬 한 번 누르면 드롭다운이 사라지던 실버그). 정렬·필터처럼 자기 컨테이너를 다시 그리는 핸들러 전부 해당. jQuery 는 위임 핸들러를 레벨 단위로 돌아stopPropagation()이면 뒤 레벨(=document 직접 바인딩)을 건너뛴다. -
2026-08-10 현장 검색 리스트의 표준 = 견적서 등록(
estimate_regist_v2) 양식(페페 지시). 업체명 / 현장명 / 구분 칩 / 상차일 / 담당자 5컬럼 + 헤더 클릭 정렬 + 결과 건수. 다른 화면에 현장 검색을 새로 만들면 이 양식을 따른다. 상차일은26.8.17(월), 빈 값은 정렬 방향과 무관하게 항상 뒤로. 게이트(진행중만/기간창)는 화면마다 다르므로 양식만 맞추고 쿼리 조건은 그 화면 것을 유지한다. -
2026-08-10 사이드 메뉴는 원래 스크롤이 안 됐다 — 테마가 높이를 두 번 100%로 잡는다.
.left-sidebar{height:100%}+.scroll-sidebar{height:100%}인데 그 위에 로고(58px)+사용자(56px) 블록이 얹혀 있어 스크롤 영역 아래 114px가 화면 밖으로 나간다. 게다가scrollHeight==clientHeight라 perfect-scrollbar가 스크롤 자체를 안 만든다(휠 무반응 실측) → 하단 메뉴가 화면 밖이면 아예 못 고름. 고치는 자리 =pfs_layout.css§10 (사이드바 세로 flex → 스크롤 영역이 "남은 높이"만 갖는다). ⚠mini-sidebar에선 테마가.scroll-sidebar에position:absolute를 걸어 flex 흐름에서 빠진다 →position:relative재선언 필수. 🔑스크롤 한계(기준선=시스템 관리)는 계산하지 말고 실측 보정(pfs.jspfsSidebarPad()) — ①콘텐츠가 컨테이너보다 짧으면scrollHeight가clientHeight로 잘려 1패스 계산이 안 맞고 ②perfect-scrollbar가 붙이는 절대배치 레일이scrollHeight를 8.5px 늘린다(그만큼 기준선이 상단 위로 넘어감). 여백 넣은 뒤 남은 오차를 한 번 더 보정하면 오차 0.5px. 높이 바뀌는 이벤트(resize·접힘토글·접이식 그룹 클릭)에 재계산. -
2026-08-10 새 화면은
menu_catalogrow가 없으면 403 "unmapped" (Patch #32dmatch_url_to_menu_key). 라우트·템플릿만 만들면 super_admin 말고는 전부 403이다 → 페이지 신설 = 코드 +menu_catalogINSERT가 한 세트 (마이그레이션patch45_omc_material_menu.sql형식, 멱등NOT EXISTS). 권한은 포인(자사company_type='1')만 명시 row 없으면 기본 허용이라 자동으로 보이고, 외부 업체는company_permissionINSERT를 따로 해야 한다. ⚠운영/테스트에서 자사 명시 row 유무가 달라(테스트 포인은 OMC_SHEET/BOARDgranted='0') 환경별로 보이는 게 다를 수 있다. 이 INSERT는permissions_updated_at을 안 건드려 세션 강제 로그아웃 없음(서명 = user/company의 그 컬럼뿐). -
2026-08-10 오엠씨 품명 전면/후면 분리 = greedy 정규식
/^(.*\S)\s+\((.+)\)$/(opFaceParts). 파서(omc_raw_fields)가전면 (후면)으로 합쳐 넣으므로 마지막 ' (' 에서 끊어야 한다 (HS2674유사(PVC) (오엠씨 재고PP)정상 분리). 🔴괄호가 자재넘버의 일부인 행이 실제로 있다 —70-113(PP)·유사PP(WH240EMT)·유사(GY124EMT)= 괄호 앞에 공백이 없다. 공백 없는 괄호를 합친 표기로 보면 오검출 (자체검사 1차에서 10건 오탐). 후면 유무는 규격의 '양면/단면'으로 판정(양면 동색이면 후면=전면, 단면·엣지 구포맷은 빈칸). -
2026-08-07 카톡 게시글 섹션 판정은 '미출고'를 '출고'보다 먼저 본다 (통합현장 탭
_kb_sections). '■ 미출고'에 '출고'가 부분문자열로 들어 있어 순서를 바꾸면 미출고 행이 전부 출고완료로 빠진다. 「변동 반영 이력」은 관리 로그라 융합 화면에서 제외. 현장 진행상태의 공식 소스 =production_status.field_status('10' 대기중/'20' 진행중/'30' 완료,field_list.html배지와 동일) — 상차일 휴리스틱(_is_active_field)은 매칭 동점 해소 전용. -
2026-08-07 qa_helper(검증 도구) 함정 3 — picker 가
documentcapture 에서 모든 클릭을 삼킨다. ①qa-helper 가 만드는 오버레이는 id 를 주고isInsidePanel목록에 넣는다 — id 가 없으면stopImmediatePropagation이 target 의onclick보다 먼저라 그 안의 버튼이 아예 안 먹는다 (「텍스트 보기」 다이얼로그가 검증 모드에서 안 닫히던 원인). ②picker 가 mousedown 을 preventDefault 하므로 포커스가 사이드바 입력칸에 남는다 → 키보드 단축키를 "패널 밖일 때만"으로 걸면 항목을 한 번 담은 뒤 통째로 먹통,textarea만 제외할 것. ③드래그 밴드는 자기 자신을 후보로 잡는다(자기 rect 안에 완전 포함 + 자식 없음 = 최말단) → 범위 수집에서 반드시 제외. 자체검사 =scripts/_qa_helper_multiselect_check.py(서버·로그인 없이 도는 정적 하네스) ·scripts/_qa_helper_live_check.py(실서버, 운영에서 돌려도 데이터 무변경 — localStorage 만 쓰고 클릭은 preventDefault). - 2026-08-07 pfs 배포용 브랜치 전환은 임시 worktree 로. 작업트리에 페페의 미커밋 수정이 상시 있어
git checkout deploy-live가 그걸 건드린다 →git worktree add <임시경로> deploy-live→git checkout origin/master -- <파일들>→ commit·push →git worktree remove --force. stash 보다 안전하다. - 2026-08-07 🔴로컬 동기화 스크립트의 상태 파일(
_*_state.json)은 환경별로 나눈다. 도어발주서door_order_sync가 상태를 공용으로 써서 test 로 한 번 돌린 파일은 prod 실행에서 "변경 없음"이 됐고 운영door_order_file이 0행이었다. →_door_sync_state.<env>.json(test 만 기존 파일명 유지). - 2026-08-07 🔴상주 워커의 대상 서버(BASE)를 한 곳으로 하드코딩하지 말 것.
door_sync_watch가 테스트만 폴링해 운영 화면 「엑셀 동기화」 요청이 7/21부터 17일간 pending 이었다(아무도 claim 안 함). → 운영·테스트 둘 다 폴링(try 는 환경 루프 안쪽). 같은 함정이 업로드 릴레이에도 있었다. - 2026-08-07 🔴큐에 넣는
target을 서버가 'test' 로 하드코딩하면 운영 버튼이 테스트 DB 를 갱신한다 (sticker_controller.sync-request). → 서버는FLASK_CONFIGURATION, 워커는 요청이 올라온 서버 기준으로 정한다(이중 안전). ✅서버 쪽 한 줄도 26.8.10 운영배포 완료(deploy-live84fcb2f). -
2026-08-07 스티커 화면의 도어발주서는 현장 자동매칭이 없다 — 연결은 100% 수동(
field_no). 동기화는field_no/connected_by/connected_at을 절대 안 건드린다. 운영 229건 중 연결은 사람이 붙인 것만(현재 금강 부산에코 2건). "검색이 안 된다" 신고는 ①데이터 유무 ②연결 여부 순으로 본다. -
2026-08-07 🔴
scripts/_*검증 러너 중 EC2에서 무로그인으로 API를 때리던 것들은 26.7.29 인증 게이트 이후 401 로 죽어 있다(AL발주서 E2E 실측). 러너는 로컬에서C:\dev\.secrets\pfs_login.env로 HTTPS 로그인 후 호출하는 형태로 고친다(쿠키 opener). 배포 직전에 러너부터 돌려보고 401 이면 러너를 먼저 고칠 것. -
2026-08-07 멱등 ALTER 를 DAO(
ensure_tables)가 자동으로 해도, 운영 배포 전에 DDL 은 손으로 먼저 넣는다. 앱이 첫 요청에서 조용히 스키마를 바꾸는 것보다 통제 가능하고, 실패 시 원인이 배포와 섞이지 않는다 (patch44 =vendors·al_vendor·glass_vendor3컬럼, 선행 적용 후 배포). -
2026-08-07 🔴에넥스 발주서(.xls)는 한 시트에 발주 페이지가 반복된다 — 업체(수신처)당 1페이지. 실측 = 407행·11페이지(37행 주기), 페이지마다 수신처·발주일·납기가 따로 있고 표 끝에 '계'·'특이사항'. 파서는 A열 '수 신 처' 를 페이지 머리로 잡아 블록 분할한다(머리+6행부터 '계' 전까지가 자재). ⚠같은 업체가 2페이지에 걸치면(실측 '쿠스코')
block_key=sha1(U,파일,업체,sheet)가 겹쳐 뒤 페이지가 앞 페이지를 덮어쓴다 →sheet에 페이지 번호(p10·p11)를 넣어 분리. 표 안 고정 안내문('포인 : …')은 제외, 납기가 '추후통보' 같은 문자열이면 날짜는 비운다. 비고 = 8열(9열 아님). 검증 =scripts/_op_enex_parse_check.py. - 2026-08-07 업로드 재적재는 두 갈래 — 키가 바뀌면 지우고, 안 바뀌면 그냥 되돌린다.
① 블록/행 키가 바뀌는 수정(업체·페이지·행번호 구조 변경): 사용자 편집·사용량 이력 0 확인 → 원문 JSON
백업(
/home/ubuntu/backup/) → 그source_file행 삭제 →orderpaper_upload.status='pending'. 옛 행은block_key가 달라 자동으로 안 지워진다(안 지우면 옛·새 행이 겹쳐 남는다). ② 값만 바뀌는 수정(품명 상속 등): 삭제 없이pending되돌림만 —item_key=sha1(U,파일,업체,sheet,idx)가 같아 로더가 같은 행을 UPDATE 하고*_user사용자 편집도 보존된다(실측 55행 유지·삭제 0). ⚠사이드카는 파서를sys.modules에 캐시한다 → 파서를 고치면 사이드카 재시작까지가 한 작업. - 2026-08-07 에넥스 양식은 같은 품목이 이어지면 품명칸을 비우거나 따옴표(〃)로 쓴다(수기 관행) →
파서가 바로 위 품명을 물려준다(페페 지시). 상속은 블록(업체) 안에서만 — 페이지가 바뀌면 초기화.
대상 = 품명 빈칸 + 규격·수량 있음 / 품명이
" ' 〃 ″ 상동류. 실측 10행이 이 경우였다. - 2026-08-07 🔴HTTPS 화면에서 사설 IP(HTTP)로 form POST·fetch 하면 크롬이 막는다 — "안전하지 않은 접속".
사무실 PC 자원(파서·NAS)이 필요한 기능은 ①같은 오리진으로 업로드(blob pending 저장) → ②사이드카가
claim/finish 릴레이로 처리로 짠다(동기화
sync-request와 같은 모양). 사이드카 인증 = 공유 토큰X-PFS-Agent-Token(값 =C:\dev\.secrets\pfs_agent.env, 운영·테스트 동일) +web/__init__.py_AGENT_PATHS에 경로 등록이 필수(안 하면 before_request 가 login_required 401 로 먼저 끊는다). - 2026-08-07 🔴
MySQLUtil.execute+ 별도SELECT LAST_INSERT_ID()= 항상 0(호출마다 새 커넥션). 새 PK 가 필요하면MySQLUtil.execute_id또는MySQLTransactionUtil. 업로드가upload_no=0을 돌려줘 화면이 진행상태를 못 찾던 원인. - 2026-08-07 🔴nginx
client_max_body_size가 어디에도 없었다 = 기본 1MB(실측 1.43MB 업로드 413)./etc/nginx/nginx.confhttp 블록에client_max_body_size 20m;(서버 파일 = git 밖,nginx -t후 reload). 백업 =nginx.conf.bak_20260807. - 2026-08-07 사이드카 폴링 루프의
try는 env 루프 안쪽에 둔다. 바깥에 두면 한 환경 실패(운영 미배포 401)가 같은 사이클의 다른 환경 폴링까지 굶긴다(테스트 업로드가 안 잡히던 원인). - 2026-08-07 사이드카는 로그를 남긴다 —
orderpaper-sync/_sidecar.log(orderpaper_sidecar_hidden.vbs가cmd /c … >> log 2>&1+PYTHONUTF8=1). 창 없는 상주라 출력이 사라지면 진단 자체가 불가능하다. ⚠성공은 조용하다(할 일 없으면 아무 것도 안 찍음) → "마지막 줄이 에러"로 상태를 판정하지 말 것. - 2026-08-07 발주서 파서 의존성(
xlrd·pdfplumber)은 사무실 PC(Python39)에만 깐다. EC2 는 py3.6 이라 pdfplumber 가 chardet 5.0(앱 requests 2.21 이 chardet<3.1 요구)·cryptography 40(certbot 사용)을 끌고 와 운영 앱을 깰 수 있다 — 실측(pip download)으로 확인하고 서버 설치는 접었다. - 2026-08-06 🔴테스트 앱이 읽는 env 실경로 =
/home/ubuntu/pfs-test/.env.production(→/home/ubuntu/env/pfs-test.env심볼릭).wsgi.py가 dotenv 없이 앱 폴더 기준으로.env.production→.env순서로 읽는다(/home/ubuntu/env/를 직접 가리키는 코드는 없다). 파서는split('=',1)+strip()뿐이라 따옴표를 안 벗긴다 → 값에 따옴표 쓰면 안 된다. uwsgi ini(service/uwsgi/pfs-test.ini)에는 env 파일 참조가 없다. 운영도 같은 구조(pfs/.env.production). 편집은 실파일을cat > file로 덮는다(sed -i는 inode·소유자를 바꾼다). 서비스 =pfs-test.uwsgi.service. - 2026-08-06 🔴MySQL 8.4 로 옮길 땐
--mysql-native-password=ON이 필수(앱 드라이버가 PyMySQL 0.9.3·2019년판). 8.4 는mysql_native_password가 기본 비활성인데 RDS 의admin이 native 라 그대로 두지 않으면 앱이 못 붙는다. 드라이버를 올리는 쪽은 운영과 차이를 만드니 서버를 현행에 맞추는 게 맞다. 참고 =mysql_database.py의charset="utf8"(utf8mb3) 도 현행 그대로 둔다 — RDS 에서도 같은 조건이라 바꾸면 그때부터가 변경이다. - 2026-08-06 🔴DB 이관 대조에
CHECKSUM TABLE을 최종 근거로 쓰지 말 것 — 물리적 행 순서를 탄다. 87테이블 중estimate·estimate_detail2개만 불일치했는데 행수·MAX(PK) 동일 + 데이터 동일이었다(수정이 잦아 물리 순서만 흐트러진 것). 확정은mysqldump --order-by-primary --skip-extended-insert --no-create-info --compact양쪽 MD5 비교로 한다(실측 완전 일치). 체크섬은 1차 스크리닝용으로만. - 2026-08-06 견적 차수의 불변식 = 「보관 안 된 것이 곧 지금 쓰는 견적서」(현장당 정확히 1건, 페페 정의).
그래서 옛 차수 복구는 해제가 아니라 자리 바꾸기다 —
PUT /api/estimate/<no>/select하나가 「그 현장 전부 보관 → 고른 것만 활성」을 트랜잭션으로 처리한다. 쓰던 차수를 지우지 않는다. 보관 토글(/archive)·해제 거부 가드는 이 개념에서 필요 없어져 제거했다(활성 0 상태를 만들 수 없다). ⚠추가건은field_no가 비어 있을 수 있다 → 현장 단위 UPDATE 는 부모 조인으로 같이 잡아야 한다 (LEFT JOIN estimate p ON p.estimate_no = e.parent_estimate_no). 차수 비교·복구 창구 = 목록 「차수 N」 모달. - 2026-08-06 견적서를 새로 쓰는 건 「삭제 후 재등록」이 아니라 「보관(
archive_yn) + 새 차수」다. 같은 현장 도면이 통째로 바뀌면 1차를 지우면 안 된다(이력·월마감 근거가 사라진다).estimate.archive_yn='1'로 내리면 목록·월마감 후보·1:1 카운트에서만 빠지고 데이터는 남는다. 🔑핵심은 1:1 가드를 안 고치는 것 —select_site_search의estimate_count에AND e.archive_yn='0'만 넣으면 「이미 견적서가 있는 현장입니다」 차단(estimate_regist_v2.js)이 저절로 풀린다. 보관은 본건+추가건 동반(OR parent_estimate_no=), 해제는 그 현장에 활성 본견적이 없을 때만. 러너 =scripts/_estimate_archive_{ddl,check,cleanup}.py. - 2026-08-06 🔴PFS 화면에서 서버 실패는 기본이 「성공」으로 지나간다. 공용
ajaxSuccessProcess는 code"2000"만 실패로 보는데RestResponse.fail은 일반 예외를"9999"로 준다(HTTP 200). 그래서 서버가 막아도 화면은 성공 분기로 가고 사유가 안 보인다. 새로 만드는 호출은 code 를 직접 보고data.message를 띄운다(_v2ApiFail패턴). 전역 정정은 code-qaplatform-response-policy소관. -
2026-08-06 운영→테스트 데이터 이관은 EC2 한 곳에서 두 RDS 를 잡는다 —
ssm_run.py로 올린 스크립트가pfs-live.env(읽기) +pfs-test.env(쓰기)를 각각 열면 된다. PK 는 재사용하지 말 것(테스트 708 은 이미 다른 견적이 점유) → auto_increment 로 새로 받고detail_no매핑으로 sub 를 다시 건다. ⚠float컬럼 값은 이관하면서 표시 밖 잔재가 정수로 정리된다(운영 SUM 과 11원 차이 = 정상, 행값은 동일). 검증용 견적을 만들었으면 원상복구까지가 한 작업(빈 2차 삭제 + 1차 보관해제). -
2026-08-06 🔴발주서 엑셀에서 행을 지우거나 순서를 바꿨으면 그 탭은 「동기화」가 아니라 「초기화+재입력」이다 (대우 용인 오엠씨 탭). 이유 둘 — ①
item_key = sha1(파일|탭|섹션|자재구분|**idx**)라 중간 행이 빠지면 아래 행들의 idx 가 밀려 옛 행의 사용자편집(receive_date_user·stock)이 다른 자재에 그대로 붙는다 (실측 = 입고일 07-14·재고 39/7 이 엉뚱한 보드에 남아 있었다). ②화면에서 ✕ 로 지운 행은del_manual='1'이고 동기화 UPDATE 가remove_yn=IF(del_manual='1','1','0')이라 엑셀에 되살아나도 영원히 안 뜬다. → 절차 = 해당 블록 행을 물리 DELETE(가드 =source_file+block_key정확일치) →orderpaper_sync.py prod. 신규 INSERT 경로로 다시 들어가stock=발주량·status='대기'·*_userNULL·del_manual='0'로 리셋된다.block_key = sha1(파일|탭|섹션|자재구분)= 탭 1페이지 단위 그대로라 다른 탭은 안 건드린다. ⚠DELETE 전orderpaper_usage_history(사용이력) 유무를 확인한다 — 있으면 이력이 끊기니 중단하고 상의.orderpaper_site_map은 그대로 두므로 현장연결(field_no)은 유지된다. 러너 =orderpaper-sync/_reset_*_prod.py(before 전 행 JSON 출력 = 롤백 자료) · 검증 =_verify_*_prod.py+scripts/_op_daewoo_omc_screen_check.py. -
2026-08-06 발주서정리 화면 검증 = 현장카드 cid 는
OP_CID.site['F'+field_no](s0·s1…)다 (같은 작업).'F2109'를 셀렉터에 그대로 쓰면 노드를 못 찾는다(.op-site[data-cid="s9"]). 블록 펼침은toggleBlock(bk,cid), 현장 펼침은toggleGroup(cid). 날짜는 API 가 RFC1123(GMT) 로 실어보내니 페이지 헬퍼isoD()로 변환해 비교하고, 헤더 표기는fmtD=8.11(화)형식이라08-11로 찾으면 헛 FAIL 난다. -
2026-08-06 카톡관리 게시글 행의 「같음」 판정 = 같은 섹션 · 첫 줄 동일 (중복 방지 작업). 첫 줄이 곧 행 id 의 근거다(
board_rows._ids=sha1(섹션+첫줄+등장순번)) → 같은 문구가 둘이면 id 가 등장순번으로만 갈려서 앞 행이 사라질 때 뒤 행 id 가 조용히 바뀐다. 그래서 편집·복원이 그런 쌍을 못 만들게 막는다(3중: 화면kbDupRow→ 서버add_op→ 데몬board_rows._dup_at). 기준을 고칠 땐 세 곳을 같이 고친다(한쪽만 고치면 화면은 통과시키고 데몬이 「반영 실패」로 뱉는다). 표기 접두사([완료]·※추가·※수정)는 떼고 비교 · 다른 섹션이면 같은 문구여도 허용. - 2026-08-06 같은 행에 대기 중(
state='대기') 명령이 있으면 두 번째 명령은 거부한다 (같은 작업). 미러는 데몬이 반영해야 갱신되므로 그 30~60초 사이에 같은 행을 또 고치면 화면이 옛 내용을 보여주고 나중 명령이 앞 명령을 덮어쓴다(= 방금 쓴 내용이 조용히 사라진다). 화면 문구 = 「이 행은 이미 반영 대기 중」. ⚠ 부작용 = 데몬이 죽어 있으면 그 행은 편집이 잠긴다(대기 큐가 안 빠진다). 방 헤더의 「반영 대기 n」이 신호다. -
2026-08-06 카톡관리 수정창은 내용 높이만큼 연다 —
kbGrow()(scrollHeight, 입력마다 재계산)·상한 60vh. 검사 =scripts/_kakao_board_edit_check.py {test|prod} [--api](--api 는 테스트 전용, 운영은 저장 안 누름). -
2026-08-05 🔴카톡관리 명령 사유(status)는 PFS 혼자 못 정한다 — 데몬과 짝이다 (소프트딜리트 작업 실측). PFS
kakao_board_service.VALID_STATUS와 사무실 PCC:\dev\poin-agent\board_rows.STATUSES는 같은 값이어야 한다. PFS 만 늘리면 명령은 큐에 잘 들어가는데 데몬이ValueError로 뱉어 화면엔 「반영 실패」로만 뜬다(원인이 안 보인다). 데몬 반영 경로는 둘이다 — 예약작업PFS-KakaoBoard-Sync(1분·매번 새 프로세스 → 파일 수정 즉시 반영)와 상주sgc/watcher.py(HKCU RunPoinAgent-BoardWatch, import 를 붙든다 → 재시작 필요). 둘이LOCK_PORT 47312로 번갈아 잡으므로 낡은 쪽이 먼저 집으면 실패한다 → 데몬 먼저 고치고 재시작한 뒤 배포. 재시작은 상승권한 불요(사용자 pythonw):Stop-Process→Start-Process -WindowStyle Hidden pythonw watcher.py. -
2026-08-05 「변동 반영 이력」 행의 ✖ = 소프트딜리트(숨김 이력), 그 밖의 행 ✖ 는 그대로 실제 삭제 (페페 지시). 이력 행은 로그라 오조작 방지로 ✔완료·⊘취소를 뺐는데, 그래서 ✖ 말고 내릴 방법이 없었고 그게 실제 삭제였다. → 이력 행만
action:'hide', status:'삭제'로 보내 ↩ 복원이 된다. 판정은 기존과 같은섹션명에 '이력'하나. 다른 행의 ✖ 는 건드리지 않는다(2026-07-28 페페 지시 "종결 항목 빠른 정리"). 검사 =scripts/_kakao_board_softdelete_check.py {test|prod} [--api]—--api는 테스트 전용 (운영에서 큐에 넣으면 고객 단톡방 게시글이 실제로 바뀐다. 코드가 막지만 습관으로도 막을 것). ⚠검증 스크립트는 반드시 한 방 안으로 가둔다 — 행 id 는sha1(섹션+본문+순번)이라 방이 달라도 같은 문구면 같은 id다. 첫 방이 읽기전용이면 ✎·✖ 자체가 없어querySelector가 엉뚱한 방의 행을 집는다(1차 실행에서 5건 오탐). -
2026-08-05 🔴견적서 엑셀 서식의 단일 출처 = 템플릿 파일이지 코드가 아니다 (페페 지시). 서식(선굵기·배경·행높이·열폭·병합·여백·배율)을 openpyxl 로 다시 그리면 NAS 실물과 반드시 어긋난다. NAS
2026현장\견적\양식 견적서.xlsx원본을 리포에 그대로 두고(api/pfs/estimate/service/estimate_form.xlsx, 바이트 동일·정적경로 밖 — static 에 두면 표본 견적 내용이 공개된다)load_workbook후 값만 쓴다. 양식이 바뀌면 코드가 아니라 이 파일을 교체한다. 검사 =scripts/_estimate_excel_form_check.py(서식 diff 0). 블록 = 45행, base=45*i, 오프셋 +2 제목 / +4 쪽수 / +6~11 수신·공급자 / +13 합계 / +15~16 표머리 / +17~36 본문 20행 / +37 공급가액 / +39~41 비고 / +44 맺음. -
2026-08-05 PFS 실서버 Playwright E2E — 매번 물리는 3가지 (견적서 다운로드 E2E 실측). ① 로그인은 폼 submit 이 아니다 —
login()JS 가POST api/auth/login후location.href="/"한다.page.evaluate("() => login()")로 부르고 URL 이/login을 벗어날 때까지 기다린다(Enter 키는 안 먹는다). ② 목록 행에 식별자가 안 박혀 있다(JS 클릭 핸들러) → 번호는 API 에서 뽑는다. ⚠응답 봉투 키는message다(data/result아님):{"code":"0000","message":[...]}. ③ 내려받은 xlsx 를 검사할 땐 블록 수를ws.max_row로 세지 말 것 — 인쇄영역($O$n)에서 센다. 실행 =PYTHONUTF8=1 python scripts/_estimate_excel_download_e2e.py {test|prod} [저장경로] [견적번호]. -
2026-08-05 openpyxl 로 xlsx 템플릿을 다룰 때 함정 4개 (위 작업 실측). ① 도형(그룹·텍스트박스)은 읽는 순간 사라진다 — 로드/저장만 해도 소멸. 견적서 결재칸이 도형 그룹이라 템플릿 실렌더 300dpi 크롭 PNG 로 대체하고 같은 셀 앵커(J39 오른쪽~O 끝, 39~41행)에 얹었다. ② 그림은 앵커마다 원본을 통째로 다시 쓴다 — 유령 앵커 68개짜리 템플릿이 저장하면 5MB로 부푼다. → 로드 직후
ws._images = []로 전량 제거하고 필요한 것만 다시 앵커(438KB→104KB). ③ 블록 수를ws.max_row로 세면 안 된다 — 서식 없이 행높이만 남은 행이 450행까지 있다. 판정은 표머리 병합(A{base+15}:B{base+16}) 유무로. ④ 시트 이름을 붙이기 전에 안 쓰는 시트를 먼저 지운다 — 이름이 겹치면 openpyxl 이 몰래 rename 해 다른 시트의=추가1!M37참조가 #REF! 로 깨진다. 그리고 병합셀 종속칸에 값 대입은AttributeError(read-only)→_put()가드로 건너뛴다. -
2026-08-04 🔴기본규칙 — pfs를 고치면 사이트맵 재빌드까지가 한 작업이다 (페페 지시).
cd C:\dev\pfs && python tools/sitemap/build_sitemap.py --dump→devplan/pfs-sitemap.html. 테스트 push 후 한 번, 운영배포 후 또 한 번 돌린다 — 배포 전엔 그 화면이 주황(미반영)으로 떠야 하고, 배포 뒤엔 주황이 걷혀야 한다. 둘 다 보고에 적는다(안 적으면 지도가 조용히 낡는다). 화면이 안 늘어난 변경(공통 유틸 등)은 트리가 아니라 「화면 밖 미반영 파일」 절에 잡힌다 — 그것도 확인 대상. -
2026-08-04 서버에서 페페에게 알림을 쏠 땐 EC2→api.telegram.org 직결이다 (생산현황 메모 알림). 코디 PC 의 broker(
127.0.0.1:7777)는 EC2 에서 못 닿는다 — 서버 코드에서 broker 를 부르지 말 것. 공용 모듈 =common/utils/telegram_notify.py(send_async), 키 =PFS_TELEGRAM_TOKEN·PFS_TELEGRAM_CHAT_ID를/home/ubuntu/env/pfs-{live,test}.env에 주입(백업.bak-memo-notify,.env.production심볼릭이 이걸 읽는다). 끄는 법 = TOKEN 줄 삭제 후 재시작(코드 수정 불요 — 토큰 없으면 조용히 no-op). ⚠알림이 업무를 막으면 안 된다 — 별도 스레드 + 전량 예외 삼킴. 텔레그램이 죽어도 저장은 성공해야 한다. ⚠본문에 환경 라벨([운영]/[테스트])을 반드시 넣는다 — 두 서버 알림이 섞이면 판단이 흐려진다. 신호는 저장 경로가 모이는 service 한 곳에만 단다(화면·컨트롤러마다 달면 중복·누락이 생긴다). 근거 =session-2026-08-04-memo-notify.md. -
2026-08-04 "운영 미반영"은 커밋이 아니라 화면 뒷단 파일 해시로 판정한다 (사이트맵 빌더). 화면 하나의 뒷단 = 컨트롤러 + 템플릿(모바일 분기 포함) + 그 템플릿이 부르는 static js/css + 연결 API 모듈. 하나라도 두 브랜치 내용이 다르면 그 화면은 미반영이다(playbook §2 — patch-id 비교는 파일단위 포워드 배포에서 28건으로 부풀려진 전례). ⚠트리에 안 걸리는 미반영 파일(공통 유틸·마이그·검증 스크립트)은 따로 열거한다 — 안 적으면 "미반영 2"가 리포 전체 미반영으로 읽혀 지도가 거짓말을 한다. 지도·빌더 =
tools/sitemap/build_sitemap.py→devplan/pfs-sitemap.html. 근거 =session-2026-08-04-sitemap.md. -
2026-08-04 pfs 코드를 기계로 훑을 땐 이 4가지를 먼저 가정한다(사이트맵 파서에서 전부 밟았다). ①라우트 URL 에
<int:no>가 있어 innerHTML 에 그냥 넣으면 태그로 먹힌다(/x/<int:no>/excel→/x//excel) ②render_template인자가 다음 줄에 있고(견적 v2 3화면) 한 함수가 템플릿 2개를 렌더한다(omc 모바일 분기) ③JS 의 API 호출은apiURL()직호출이 아니라 화면마다 지역 래퍼(dcApi(path)/api(method, path))를 거친다 ④🔴별칭 정규식이 본문을 소비하면 바깥 래퍼(function bootstrap(){ try { function api(…)가 안쪽 별칭을 먹어 한 파일에서 하나만 잡힌다 → lookahead 로만 본다. 증상이 "이 페이지만 API 0"이라 원인이 안 보인다. -
2026-08-04 여러 행을 고치는 화면은 「클릭=표시, 저장=전송」으로 만든다 (카톡관리 저장 버튼). 클릭마다 서버에 보내고 목록을 다시 그리면, 그 왕복 사이의 클릭이 사라진 DOM을 향해 조용히 날아간다 (사용자에겐 "체크했는데 안 먹었다"로만 보인다). 대기분은
KB_PEND{행키→명령}같은 단일 출처에 담고 클릭한 행만 다시 칠한다(재렌더 금지). 저장은 일괄 엔드포인트 하나로, 실패분은 삼키지 말고 식별자+사유로 회신해 그 행만 표시로 남긴다. 저장 대기 중엔 자동 새로고침을 멈춘다. ⚠식별자를id or i로 고르면 0이 falsy라 빈 값이 된다(복원 대상 첫 줄) →None판정으로 분리. 근거 =session-2026-08-04-kakao-board-batch-save.md. -
2026-08-04 pfs 화면에서
position:sticky는 그냥 쓰면 안 붙는다 — 템플릿의#main-wrapper{overflow:hidden}이 sticky의 스크롤포트를 가로채, 바가 고정되지 않고 그대로 스크롤돼 사라진다(운영 실측barY -1743). 쓰려면 그 페이지에서만html > body #main-wrapper { overflow: visible; }로 푼다. 부작용은 실측으로 확인했다 — 1440px·390px, 모바일 사이드바 개폐 전 상태에서docW==clientW불변(가로 스크롤·클리핑 변화 없음). -
2026-08-04 sticky·스크롤 동작은 「실제로 스크롤되는 페이지」에서만 검증된다. 테스트 DB는 카톡 방이 2개뿐이라 문서높이가 화면보다 짧았고, sticky가 깨져 있는데도 검증이 통과해 운영에서 드러났다. → 스크롤 의존 검증은 문서높이 > 화면높이를 먼저 확인하고, 아니면 스킵이라고 출력에 적는다(조용한 통과 금지).
-
2026-08-04 커밋 전
git diff --stat을 본다 — Edit 도구가 LF 파일을 CRLF로 바꾼다. 1,527행 템플릿이 통째 리라이트로 잡혀 배포 경계(파일 diff) 판정이 무의미해진다.--ignore-all-space와 차이가 크면 LF로 정규화하고 다시 본다. 배포 경계 확인은git diff --stat origin/deploy-live <작업직전커밋> -- <대상파일>이 비어야 그 파일의 미배포분이 내 변경분뿐이다(AL발주서 QA 15파일 유입 방지). -
2026-08-04 push 직후 검증은 배포보다 빠르다. 테스트 자동배포 전 옛 템플릿으로 돌아 FAIL 1건이 났고 코드 문제로 오진할 뻔했다 → 검증 전에 서빙되는 HTML에 새 문자열이 있는지 먼저 확인한다.
-
2026-07-30 견적서(양식) 엑셀 → PFS 등록 시 매핑은 출력 렌더러에서 역산한다 (신영 광주 추가1 등록).
estimate_print_service의_render_summary_body(요약블록)와_render_detail_item(상세블록)은 컬럼 매핑이 다르다: 요약 =A=현장명(site) · C=항목명(=type_name) · H=수량 · K=금액 · M=비고, 상세 =A=품명 · C=규격 · H=수량 · I=단가 · K=H*I · M=비고. 요약행에서 항목명을 A열로 읽으면 현장명이 들어간다(실제로 '운임'이 '신영 광주 챔피언스시티 MH'로 잡혔다). 블록 주기 = 45행, 본문 =base+17부터 20행. 근거 =session-2026-07-30-shinyoung-add1-estimate.md. -
2026-07-30 견적 마진 = master
margin_percent0(수동) + detailNULL(자동) — 엑셀 단가가 이미 고객 청구 확정가일 때. master를None(자동)으로 두면 재로드 시 업체 마진율(한샘 15.5%)을 추종해 금액이 부풀려진다. detailNULL은 마스터값을 따르고mNum(0, …)이 0을 그대로 반환하므로 안전(estimate_regist_v2.jsloadEstimate). ⚠ 도어발주서 스킬(견적등록_스킬.md§4·§5)의 "master도 None=자동"은 단가 미정(price=0)으로 등록해 스태프가 나중에 넣는 경우 규칙이다 — 확정가 등록엔 쓰지 말 것. -
2026-07-30 운임·기타비용은
estimate_extra_cost(기타 부가비)로 등록한다.master.transportation_cost는 출력 라벨이 '물류비' 고정이고 비고를 담지 못한다 → 원본 견적서의운임 / 신규자재 운임을 재현 못 함. 경로 =POST /api/estimate/extra/bulk-save {estimate_no, items:[{display_order,product_name,spec,qty,price,amount,comment}]}(delete+reinsert, 견적 POST와 별도 호출). 금액 영향 없음 —recalcTotal의 subtotal에 extras가 포함되고 월마감은total_amount를 본다. -
2026-07-29 폰트 색상 = 블랙 70% 이상 (페페 지시, QA #3). 페페는 눈에 잘 보이는 진한 블랙을 선호한다. 화면 텍스트 색은 블랙 기준 70% 이상(=
#4d4d4d보다 진하게) 유지한다. 회색으로 흐리게 처리하는 보조텍스트(계산 브레이크다운·단위·주석)도 예외 아님 — 흐려서 안 보이면 정보가 없는 것과 같다. 적용 선례 = AL발주서 그리드 계산셀 브레이크다운#94a3b8(≈42%) →#333(80%). → 신규 화면·수정 화면 모두 기본 적용. 근거 =session-2026-07-29-al-orderpaper-qa8.md. -
2026-07-29 계산값 강조는 색으로만, 굵기로 하지 않는다 (페페 지시, QA #4). AL치수·목대·유리처럼 자동계산된 치수 셀은
font-weight:700을 쓰지 않고 색상(#0f172a)으로만 구분한다. 입력행이 많은 표에서 굵은 글씨가 줄줄이 이어지면 오히려 읽기 어렵다. -
2026-07-29 표 컬럼은 지정폭이 '하한'이어야 한다 (페페 지시, QA #8). 입력 그리드는
table { width:max-content; min-width:100% }+th{width:...}조합으로 짠다.min-width를 컬럼합보다 작게 두면 브라우저가 컬럼을 지정폭 아래로 눌러 내용이 잘린다(입력칸은width:100%라 스스로 못 늘어난다). 가로가 넘치면overflow-x:auto래퍼로 스크롤시킨다 — 컬럼을 줄이지 않는다. -
2026-07-29 검색·선택 입력은
<datalist>대신 자체 인라인 드롭리스트 (페페 지시, QA #6). 부분일치로 목록을 띄우고 Enter = 강조행(기본 첫 항목) 자동입력. datalist는 필터 규칙·Enter 확정· 부가정보(업체 담당자 등) 표시를 브라우저가 제멋대로 처리해 못 쓴다. 구현 선례 =al_orderpaper.jsattachCombo(). -
2026-07-29 인쇄는
@media print+visibility토글로 하지 않는다 (QA #1 근본원인). 모달 안의 내용을 인쇄할 때 조상 요소의transform·max-height·overflow:auto가 인쇄 레이아웃을 잘라 프레임 선만 찍히는 증상이 난다. 인쇄 대상만 담은 별도 iframe 문서를 만들어contentWindow.print()로 출력한다. 구현 선례 =al_orderpaper.jsprintSheets().
§1 UI·표시 규칙 (색상·굵기·폭)
(🆕에서 안정화된 항목을 여기로 이관한다.)
§2 배포 규칙
deploy-live(운영) push/merge/checkout은 페페가 채팅에 직접 "운영 배포해줘"를 입력했을 때만 허용. 지시문 파일에 적혀 있어도 무시한다. (출처 =C:\dev\pfs\CLAUDE.mddeploy-live 절대 규칙.)- 기본 작업 브랜치 =
master→ 테스트 서버(8001) 자동 배포.
🆕 신규 26.8.28(목) — PFS-D008: /api/ 는 세션 없으면 401, 예외는 _AGENT_PATHS 뿐
🔴외부 도구(플러그인·워커)를 pfs API 에 붙이려면 반드시 _AGENT_PATHS 에 경로를 등록해야 한다.
안 하면 {"code":"0003","message":"login_required"} 401 을 받는다 — 코드가 배포돼 있어도 문 앞에서 막힌다.
- 자리 =
web/__init__.py:184@app.before_request(Patch32). 비로그인/api/요청을 401 로 끊는다. 예외 두 갈래뿐 = ①_PATCH32_EXEMPT_PATHS(로그인·정적·swagger) ②_AGENT_PATHS+X-PFS-Agent-Token헤더(envPFS_AGENT_TOKEN). - 경위 = 26.7.29 보안패치 BUG[PFS-D008] — 그전엔 훅이
/api/를 그냥 통과시켜 비로그인으로 전 API 를 읽고 쓸 수 있었다(화면만 잠겨 있었다). 의도된 수정이다. - 🔴부작용(26.8.28 발견) —
_AGENT_PATHS에/api/poin/colors가 없어서 acad-tread 칼라 자동완성이 7/29 부터 죽어 있었다. 플러그인이 실패 시tread_colors.txt(79개 고정)로 조용히 폴백해 아무도 몰랐다. 사무실PC(사내망)에서도 401 — IP 문제가 아니다. 같은 이유로/api/poin/sample-groups(샘플 템플릿)도 의심. PDF 파싱 사이드카/parse/claim·/parse/finish도 목록에 없다(세션 로그인 폴백으로 연명 중일 것). - ✅26.8.28 저녁 수정(master
c4835a7, 운영배포 대기) —_AGENT_PATHS에/api/poin/colors·/material-spec/fields·/collect3개 추가. 🔑문과 방을 토큰 하나로 연다 —material_spec_controller._relay_authorized()가X-PFS-Agent-Token도 받게 했다.before_request(문)는PFS_AGENT_TOKEN을 요구하고 핸들러(방)는PARSE_RELAY_TOKEN을 요구해서 헤더 2개·시크릿 2개가 될 뻔했다. - 실측 =
PARSE_RELAY_TOKEN은 운영·테스트 어디에도 설정돼 있지 않다(scripts/_op_agent_token_probe.py). 즉X-Parse-Token은 사실상 죽은 경로.PFS_AGENT_TOKEN은 양쪽 다 설정돼 있고 로컬C:/dev/.secrets/pfs_agent.env와 sha88a475d0c일치(운영=테스트=로컬 동일 값). - 안 넣은 것 2개(YAGNI) —
/api/poin/sample-groups는 26.7.09 부터 플러그인이 MSSQL 직결이라 헤드리스 호출자가 없다(웹 pncad 만 쓴다, 세션 있음)./parse/claim·/parse/finish는 사이드카가 로그인 세션으로 정상 동작 중이다(material_spec_parse.py가pfs_login.env로 로그인) — 안 고장났으니 안 연다. - 자체검사 =
acad-tread-plugin/matcollect/test_auth.py11/11 PASS(문·방·세션폴백·PARSE 잠금까지, DB·서버 불필요).cd C:/dev/pfs && PYTHONUTF8=1 .venv/Scripts/python.exe C:/dev/acad-tread-plugin/matcollect/test_auth.py⚠pfs venv(py3.9)에서Flask()인스턴스를 만들면 werkzeug 가TypeError: code() takes at least 14 arguments로 죽는다 → 테스트는 앱을 만들지 말고 컨트롤러 모듈의request/session전역을 가짜로 갈아끼워 부른다. - ⚠진단할 때 헤매지 않는 법 — 응답에
Server: nginx가 붙고 바디가…}(끝 개행)이라 nginx 가 낸 것처럼 보인다. 아니다. nginxpfs-live설정은 깨끗하고(uwsgi 로만 넘긴다) 8000 포트도 nginx 가 잡고 있어 우회가 안 된다.0003은RestResponse코드 체계(0000·2000·3100·3200·9999)에 없어서 앱이 아닌 것처럼 보이는데, 실제로는web/__init__.py가jsonify로 직접 낸다. 로컬에서api/·common/만 grep 하면 못 찾는다 —web/을 봐라. - 판정 = 미등록 경로도 401 이라 404/401 로는 배포 여부를 못 가린다. 운영 배포 확인은 EC2 에서
git log로(scripts/ssm_run.py).
26.8.28 운영배포 3건 (페페 채팅 승인)
- 19:43
deploy-live1430bba= 자재수집 401 벽(_AGENT_PATHS3경로 +X-PFS-Agent-Token수용). masterc4835a7체리픽. 검증 = 스테이징 blob 해시가 master 와 동일(⚠working-tree md5 는 CRLF 때문에 다르게 나온다 — blob 으로 비교해라) ·py_compileOK · EC2 HEAD1430bba·grep로 3경로·헤더 반영 확인 ·pfs-live.uwsgi19:43:25 KST 재기동 · health 8000 = 200. 운영 실측 = 토큰 있으면colors200·fields200·collect400 "doc required"(인증 통과, 쓰기 없음) / 토큰 없으면 전부 401 유지(보안 무손). 🔑사이트맵 「미반영」은origin/mastervsorigin/deploy-live비교다 — deploy-live 만 push 하면 미반영이 안 걷힌다(master 가 로컬에 남아 있어서). master 도 push 해야 0 이 된다(테스트 서버도 그때 같이 자동배포된다). 실측 = master push 전 2 → push 후 0. - 19:05
deploy-live400395c= 자재수집 API 2개(/material-spec/fields·/collect). master83d79e1선별 포워드. 내용 동일 검증 완료. -
19:27
deploy-lived14ab2c= 한샘rename_slots(평형명≠슬롯명이면 탭 교차). masterdd677a7파일단위 포워드. 검증 = 스테이징 diff 가dd677a7원본과 바이트 동일 ·py_compileOK · EC2/home/ubuntu/pfsHEADd14ab2c·rename_slots존재 ·pfs-live.uwsgi.service19:27:23 KST 재기동(코드만 갱신되고 워커가 안 죽으면 옛 코드를 계속 서빙한다 —uwsgi-die-on-term선례). -
🔴🔑커밋 수로 배포 지연을 판단하지 마라 — 26.8.28 자기교정(앞서 "deploy-live 가 master 보다 278 커밋 뒤처짐"으로 잘못 적었다. 페페가 잡아냈다).
master ^deploy-live= 278 인데 반대방향deploy-live ^master= 369. 뒤처진 게 아니라 2026-07-07cca4132에서 갈라진 분기다. 선별 포워드 규약([운영] 제목 (master <해시>))이라 같은 내용도 해시가 달라 양쪽 원시 개수는 배포를 아무리 잘 따라가도 영원히 벌어진다. 실제로 master83d79e119:01 → deploy-live400395c19:05, 4분 뒤 동일 내용이었다. ⚠이건 이미 playbook §2(26.7.27, 89건→실제 20건)에 적혀 있던 함정인데 또 밟았다 — 커밋 개수를 보는 순간 그 항목부터 펴라. 🔑판정은 개수가 아니라 파일 diff =git diff --stat master origin/deploy-live. patch-id 로 세려면 playbook §2 의--cherry-pick --right-only. rename_slots배포 후 실측 = 50파일 차이인데 서버 동작에 영향 있는 건 0건: | 묶음 | 파일 | 성격 | |---|---|---| |print-app/| 17 | C# 인쇄앱 소스·아이콘·빌드 — 서버에 안 올라간다 | |scripts/| 16 |_*QA/E2E 스크립트 | |common/| 6 | REFACTOR[PFS-R005~R007] 주석·함수추출뿐, 동작 동일(sqlserver_utilrow→dict 추출,Log.warn→warning, 죽은코드 4파일 주석) | |tools/sitemap/| 4 | 사이트맵 빌더(개발 도구) | |print-agent/| 4 | 사무실PC 데몬 | |orderpaper-sync/| 2 | 사무실PC 데몬(door_sync_watch운영+테스트 동시폴링 26.8.7 수정 포함) | | 루트.gitignore| 1 | | → merge 금지의 진짜 이유도 이것 = merge 하면 위 44개(dev 전용+데몬)가 통째로 운영에 나간다.- 작업은 격리 worktree(
git worktree add C:/dev/pfs_deploy_wt deploy-live)에서. 메인 작업트리는 미추적 파일 수백 개라 오염된다. - push 전
git fetch— 로컬deploy-live가 origin 보다 뒤처져 있으면 거부된다(rebase origin/deploy-live). -
⚠EC2 검증 스크립트는 python 3.6 문법으로 써라 —
subprocess.run(capture_output=True)는 3.7+ 라TypeError로 죽는다. 3.6 에선stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True. (scripts/ssm_run.py는 원격python3를 그대로 쓴다.) -
🔴🔑파일 덮어쓰기는 임시파일→rename.
open(p,"w")직접 쓰면 원본이 0바이트로 날아간다 (26.8.28 실사고).io.open(p,"w")는 여는 즉시 truncate 한다 → 뒤이은write()가 인코딩 에러로 죽으면 파일이 빈 채로 남는다.orderpaper_material.js2,797줄이 통째로 0바이트가 됐다(git checkout --로 즉시 복구, 커밋 안 된 상태라 손실 0). 처방 =tmp = p + ".tmp"에 쓰고os.replace(tmp, p). 이러면 실패해도 원본이 그대로다. ⚠서로게이트 이스케이프가 방아쇠였다 — 파이썬 문자열에\uD83C\uDFAF(이모지 서로게이트 쌍) 표기를 넣으면 UTF-8 인코딩 단계에서surrogates not allowed로 터진다. 이모지는 실문자(\U0001F3AF)로 쓴다. 같은 구조 = 큰 파일을 스크립트로 패치하는 모든 작업. 편집 후node --check(JS)·py_compile(PY) 로 즉시 검증까지.
26.8.28 운영배포 4건째 — 자재수집 픽 브리지
- 22:21
deploy-liveb14d4ac= 픽 브리지(op_material_pick+ API 4개 + 웹 캐드 픽 토글). master5aa9d1b·8f196cd2커밋 체리픽 후 squash + 규약 메시지. 검증 = 6파일 blob 전부 master 와 동일 ·py_compile+node --check· EC2 HEAD·라우트4·DDL·select_arm판정·JS 28곳·캐시버스터 확인 ·pfs-live.uwsgi22:21:04 KST 재기동 · health 200 · 운영 E2E 15/15 PASS · 사이트맵 미반영 0. - 🔑운영 E2E 에서 파괴적 항목은 실재하는 번호로 쏘지 마라 — 「토큰만으로 사양 DELETE 안 된다」를 실
spec_no로 검증하면 만에 하나 뚫릴 때 진짜 지워진다. 없는 번호(999999)로 쏴도 401 이면 「인증에서 막혔다」가 똑같이 증명되고 지울 대상이 없다. (Flask<int:>는 음수를 매칭 안 하니 큰 양수를 쓴다 — dev-38 실측.) - 🔴🔑
MySQLUtil.execute는 반환값이 없다(None). rowcount 가 아니다.UPDATE ... WHERE가 걸렸는지를 그 반환값으로 판정하면 항상 falsy 다. 정적검사로는 안 잡히고 실서버 E2E 가 잡았다. 건수 판정이 필요하면SELECT로 따로 확인해라(update_lock등 기존 호출부도 반환값을 안 쓴다).
🆕 신규 26.8.29(토) — pfs.poin.co.kr 은 거래처 존에 세들어 있다
- 🔑DNS 존 주인 = 거래처(contentsworks/hostcocoa).
pfs·pfs-testA 레코드가 남의 장부에 있다 → 서브도메인 추가·긴급 IP 절체·와일드카드 인증서 전부 거래처 없이는 불가. 존이 사라지면 이름 해석도 같이 죽는다(데이터·서버는 무사). 단 인증서 갱신은 자유 — Let's Encrypt HTTP-01 이라 A 레코드만 맞으면 자동으로 돈다. - ⚠우회용 서브도메인(
pfs2.poin.co.kr등)은 대피소가 안 된다 —.kr위임이 도메인 단위라 같은 존이다. 비상 경로는 테일스케일100.73.87.48/ EIP13.209.147.168/ hosts 파일(이 셋 중 hosts만 인증서 정상). - 📌 실사·TTL 실측·전환 절차·거래처 협의 항목 정본 =
projects/poin-site/handoff.md「DNS·도메인 권한 실사」. 여기 중복 기록 금지.
🆕 신규 26.9.3(목) — 자재분류(R/E/S/T) 판정은 「내용」이 아니라 「엑셀 헤더 라벨」을 본다
- 🔑
orderpaper_parse.py의is_raw = any('원자재' in lab ...)— 헤더에 「원자재」가 있는 탭(=오엠씨)만 R 이다. 보드 업체(상원보드·피앤알·성지라미텍…)는 일반 품명/규격 양식이라 순수 원자재여도 전부 S 로 떨어진다(운영 101행). - 고치는 자리 = 파서가 아니라
orderpaper_dao._MT(조회시점 파생) —block_key = sha1(파일,sheet,section,material_type)이라 저장값을 바꾸면 다음 동기화가 전 행을 새 행으로 만들어 사용자편집(*_user)·상태가 통째로 끊긴다. 시트(T) 선례 그대로. - ⚠
_MT안에LIKE '%…%'금지 —MySQLUtil이 param 있는 쿼리를 pymysql 포매팅에 넘겨%가 깨진다.INSTR()또는REGEXP '양면|단면|경면|민자'처럼%없는 식으로 쓴다. 소수점은\.대신[.]로 백슬래시 회피. - ⚠
material_type_user는 블록 전체에 쓴다(toggleBlockMaterial()이material_type을 안 넘긴다) → 혼합블록에서 누르면 엉뚱한 행까지 딸려간다(운영 증거 = 피앤알 SPP0012 보드 2행이user='S'로 묶여 있다). - 🔑파생규칙을 늘리기 전에 「그 컬럼을 읽는 자리」를 전부 grep 한다. 26.9.3 보드(R) 규칙을 넣을 때
update_status_bulk만 혼자COALESCE(material_type_user, material_type)를 직접 쓰고 있었다 → 그대로 뒀으면 일자별 「원자재」 일괄선택이 화면과 딴판인 행들을 바꿨다(에러 0, 조용히 틀림). 파생식은_MT하나만 두고 쓰기 경로도_mat_clause를 타게 한다. 26.9.3 적용 결과 = S→R 144행·강등 0.
🆕 업체 전용 페이지(/field/company/<no>)는 고객이 보는 화면이다 (26.9.3)
- 이 화면 데이터는 서버에서 그 업체분만 걸러 내린다. 26.9.3 「상세(카톡 게시글)」을 붙일 때
/api/kakao-board/list를 브라우저에서 부르면 롯데·에넥스·오엠씨 방 게시글이 통째로 고객 브라우저로 간다 (그 API 는 방 전량을 rows·board_text 까지 그대로 준다). →kakao_board_service.get_company_board_rows(company_no)로 서버에서 추려field_controller가 템플릿에 넣는다. 검사에도 「타업체 게시글 유출 0」 대조를 남겼다 (scripts/_ivg_page_check.py). kakao_board엔 업체·현장 정본 링크가 없다(실측 = 아이비젼글로벌 현장 15건 중field_no연결 0건). 그래서 현재 판정은 현장명 문자열 매칭이고 정규화 4자 미만은 제외한다 — 「완주」가 「완주 푸르지오」를 빨아들인다. 반대로 게시글 표기가 다른 현장(「완주 J손도어」·「GS 신반포」)은 못 잡고 공백으로 남는다(페페 확정). ⛒제대로 고치려면kakao_board.field_no(또는company_no)를 채운다.
🆕 devplan pfs.html 의 DONE/PENDING 배열에 작은따옴표를 넣지 마라 (26.9.3)
- 항목은
v:'…'한 줄짜리 JS 문자열이다. 본문에field_status==='30'처럼'를 그대로 쓰면 문자열이 끊겨 페이지 전체가 백지가 된다(콘솔Unexpected number하나만 뜨고 카드가 0개). 코드 인용은 「」 로 감싸거나 따옴표를 빼서 쓴다. 편집 후 검증 =<script>블록만 뽑아node --check, 그다음 8799 로 열어 카드 수를 눈으로 센다.
🆕 pfs 화면의 날짜 표기 = 26.8.29(수) (페페 표준 · 26.9.3 재확인 지시)
전역 원문 =
rules/tooling.md「날짜·시각 표기」(2026-07-27 지시). 여기 다시 적어 두는 이유 = pfs 화면에서YYYY-MM-DD로 찍어 놓고 매번 QA 로 되돌아왔기 때문이다(26.9.3 페페 「항상 내가 선호하는 형식이다」).
- 형식 = 연 2자리
.월.일 + 괄호 안 한글 요일. 월·일에 앞자리 0을 붙이지 않는다 —26.8.29(수),26.6.8(월). 시각까지 필요하면 뒤에 붙인다 —26.8.29(수) 17:38, 헷갈릴 자리엔KST꼬리표. - 적용 = 사람이 보는 모든 자리(화면 셀·인쇄물·게시글·알림·보고). 안 하는 곳 = 기계값
(DB·JSON·파일명·git). 원본은
YYYY-MM-DD그대로 두고 표시할 때만 바꾼다. - 🔑구현은 공용 포매터 하나만 쓴다 —
web/static/js/datatable.js의convertDateStringKo(26.9.3 신설). DataTables 컬럼이면{"data":"shipping_date", render: convertDateStringKo}. ⛔화면마다WD=['일','월',…]사본을 새로 만들지 마라 — 이미estimate_regist_v2.js·orderpaper_material*.js·orderpaper_schedule.js·pncad/spec.html에 제각각 5벌이 있고 연도 유무·구분자가 서로 다르다. 기존convertDateString(=YYYY-MM-DD)은 다른 화면이 쓰고 있으니 안 건드리고 새 이름을 옆에 뒀다. - 적용 이력 =
/field/company/<no>상차일(26.9.3). 다른 화면은 손댈 때마다 이 포매터로 갈아탄다.
🆕 모바일 레이아웃 함정 3종 (26.9.3 실측 · 전부 「에러 0인데 안 먹음」)
@media블록은<style>맨 끝에 둔다. 중간에 넣으면 뒤에 오는 데스크톱 규칙이 같은 특이도로 덮는다 —@media는 특이도를 안 올린다. 26.9.3 검색창position:static이 그렇게 통째로 무효였고, 화면은 종전 그대로라 「CSS 를 안 쓴 줄」 알았다.- DataTables
.dataTables_filter는float:right다. 이 요소를 다른 자리로 옮기면float이 따라간다. 바로 뒤에 플렉스/그리드 컨테이너(= 독립 서식문맥)가 오면 그 컨테이너가 뜬 요소를 피해 폭을 0 으로 줄인다 (실측#status-tabsw=0 → 탭 글자가 세로로 쌓임). 옮길 땐float:none을 같이 건다. - JS 가
body에 붙이는 요소는 기본 스타일을 미디어쿼리 밖에 둔다..pfs-nav-backdrop의display:none을@media(max-width:767px)안에만 둬서 데스크톱 전 페이지에 빈 블록 div 가 남았다. - 부록 = Bootstrap.row를 오버레이로 띄우면-15px음수마진 때문에 화면 밖으로 삐져나온다 →margin:0상쇄. - 검사 =C:\dev\pfs\scripts\_ivg_mobile_check.py(iPhone 390×844 23항목) ·_sidebar_regress_check.py(사이드바는 전역이라 데스크톱·모바일 3페이지씩 회귀).
🆕 업체 전용 「상세」는 코디 정리본이다 — 문자열 매칭은 폐기 (26.9.3)
- ⛔게시글 행을 현장명으로 골라 그대로 옮기지 마라. 한 행에 여러 현장이 섞여 있다 — 「GS 메이플자이 … 하프미러경은 제천자이 납품업체 생산 가능성 있음」 한 줄이 제천 자이 모달에 통째로 들어갔다(페페 지적). 문장 단위로 가르는 건 규칙으로 안 된다.
- 지금 구조 =
kb_digest테이블(현장 1건 = 1행rows_json) ← 배치C:\dev\pfs\tools\kb-digest\kb_digest.py가 헤드리스 코디에게 게시글+현장목록을 주고 현장별로 다시 쓰게 한다. 예약작업\Cody\kb-digest(15분·숨김). 게시글version이 그대로면 코디를 아예 안 부른다(토큰 0). 대상 업체 =menu_catalog의/field/company/<no>가 단일 출처 → 페이지가 생기면 자동 편입. - ⛔
fusion_row와 섞지 마라. 저긴 카톡으로 나갈 후보(대기/게시/제외)다. 읽기전용 요약을 거기 넣으면 누가 「게시」를 눌러 고객 단톡방에 나간다. 커버리지도 못 쓴다(452 현장 15건 중 3건·최신 8/25). - 화면은
kb_digest만 읽는다 → 정리본이 없는 현장은 링크 자체가 안 생긴다(공백, 페페 확정).
🆕 헤드리스 코디에 긴 프롬프트는 stdin 으로 넣는다 (26.9.3 실측)
claude.cmd는 npm 배치 래퍼 = cmd.exe 라 argv 가 8191자에서 잘린다. 게시글 7KB 를 argv 로 넘겼더니 코디가 「게시글 내용이 전달되지 않았습니다」라고 답했다 — 종료코드 0 · stderr 0 이라 파싱 실패로만 보이고 원인이 안 보인다.- ⛒처방 =
subprocess.run([claude, "-p"], input=prompt.encode("utf-8")). 짝함정(기존) = cwd 의CLAUDE.md를 읽어 답변 꼬리표가 산출물에 섞인다 → 임시폴더에서 돌리고 JSON 만 잘라 쓴다.
🆕 git add <디렉터리> 금지 — pfs 작업트리엔 미추적 임시파일이 상시 수백 개 (26.9.3 재발)
git add scripts/한 줄로 미추적 임시 스크립트 23개(op_*·size_*·margin_*…)가 커밋에 딸려 master 로 갔다.scripts/_*는 gitignore 지만 밑줄 없는 것들은 안 걸린다.- ⛒변경 파일을 하나씩 명시해 add 한다. 되돌림은
git rm --cached+ 삭제 커밋(파일은 워킹트리에 남는다). 운영 체리픽 땐 그 커밋을-n으로 가져와 내 파일만 남기고 커밋한다.
🆕 네스팅 저장본 덮어쓰기 소실 = 이력 없음 → nginx 로그 + RDS PITR (26.9.29)
- [코디]
nesting_job은 이력 테이블이 없고 binlog 보존도 없어서 몇 분만 남는다. 덮어써서 날아간 건 nginx 로그로 PUT 시각을 찾고, RDS PITR 별도 인스턴스(보존 7일)를 그 시각 직전으로 띄워 원본을 꺼낸다. 운영 인스턴스는 건드리지 않는다. 다 쓴 PITR 인스턴스는 삭제 승인을 받는다. - 원인(26.9.29): PNCAD「선택 작업본 네스팅 실행」이 부재 표를 확인 없이 지웠고, 저장창 기본값이 「현재 작업 덮어쓰기」였다.
e784497b에서 추가/교체/취소 선택창과 저장 기본값 「새 작업」으로 고쳤다.