98.4 KB · 수정 2026-10-16 16:43
목차

PNCAD 3D 부속(EP·휠라·걸레받이 등) 배치 — 1차·2차 설계·계약 (design-ep3d)

🆕 [코디] 26.10.16(금) KST 2차 설계·계약 = §13(질문 → 결정 표) · §14~§27(§14 = 한 장 요약 · §24 = 런 분할). 1차 절(§0~§12)은 운영 반영된 계약 그대로다.

[코디] 26.10.16(목) KST · R1 설계·계약 런(코드 수정·배포 0). 입력 = R0 C:\dev\_work\pncad-ep3d\R0_facts.md · 그릴링 확정([페페] 「1차 질문 모두 추천대로」 Q1~Q7 + 기본값) · 코덱스 1회(§12). [페페] 목표 원문: 「도어·EP를 pncad에 포함하는 건 3D 화면으로 작업내용을 확인하려는 것. 도어는 보이는데 EP류가 안 보인다. 보이게 + 시스템 개편 구축. 큰 목표 = pncad 에서 도어·EP류·마감재·오픈장 모두 작업. 운영배포」

0. 결론 (계약 한 장)

항목 결정
배치 저장 위치 pncad_scene.data.placements — 최상위 키, gid → {ck, kd, pl[]} 맵. doors 그룹 안에 넣지 않는다(§3-7 비교 · 코덱스 대안 채택)
장 식별 items[].uid 신설(i+8자). app.html 이 부여, 서버는 안 본다
배치 1건 pl = {u 장 uid | w 벽 id, f 면, o 보정, len 원장 구간, st ok|hold, m 수동} — 기준 장의 부재 좌표계 상대값이라 장을 옮기면 따라간다
저장 경로 배치·장 uid = 몸통 PUT(💾) 한 길(app.html 이 유일한 작성자). 도어 PATCH = 사양만. 새 PATCH 경로 없음
서버 변경 ① PUT: placements 키가 없으면 저장본 유지 + 모양 검사 ② PATCH /doors: 같은 트랜잭션에서 배치 재연결(gid 유지 → 내용 키 1:1 이동 → 제자리 → 버림) ③ GET /doors 에 placements·item_uids. DDL 0 · door_rows·도어 엑셀·네스팅 무변경
재연결 키(Q5) ck = div | 이름압축 | 치수 | 칼라 — 옛 목록·새 목록 둘 다 유일할 때만 내용 따라 이동
자동 제안(Q4) 벽별 기둥(쌓은 장 합침) → 연속 구간(run) → 품목 규칙 13종 + 비고 숫자열·키워드 + 위치(pos). 1등이 100점 이상·2등과 30점 이상 차이 = 점선 제안, 아니면 모호/미배치. scene 56 = 제안 12 · 없음 3 · 누락 0
렌더(Q3) 템플릿이 기준 장의 엔진 부재 상자 규약(x 좌→우 · y 몸통 앞면 0→벽 + · z 바닥→위)으로 상자를 내고 기존 mesh() 로 그린다. 실물 색 · 제안 = 반투명+점선 · 불일치 = 빨강 · 미배치 = 장면 옆 보관대
모듈 새 acc.js(순수 엔진 · node 검사) + acc3d.js(three·패널). app.html 은 훅 ~9곳만 — 운영판(deploy-live) app.html 이 950줄 뒤라 이식을 작게
door.html 배치 배지 · PATCH 응답 반영 · BroadcastChannel 양방향 · 행 → 3D 강조 · newGid 재사용 금지 · flattenDoors 부품 칸 우선(색 접미·단면 소실 수정)
불변식 같은 엔진에서 «배치 전 저장 vs 배치 후 저장»: 포인탭 엑셀 전 셀 · 엑셀 파서 2벌 · nest_rows · door_rows(PUT) · data(uid·placements 제외) diff 0

1. 사실 근거 · R0 보정

2. 데이터 계약

2-1. items[].uid (장 고유 ID)

2-2. data.placements

"placements": {
  "DWU-PO-G05": {
    "ck": "20|상부휠라|2100*590*60|iv00149gm",
    "kd": "fil_top",
    "pl": [ { "u": "i3k9x2ab", "f": "T", "o": [0, 0, 0], "st": "ok" } ]
  },
  "DWU-PO-G30": {
    "ck": "20|걸레받이|152*2440|iv00149gm", "kd": "kick",
    "pl": [ { "u": "i0aa11bb", "f": "B", "len": 2250, "st": "ok" },
            { "u": "i9zz88yy", "f": "B", "len": 610, "st": "ok" },
            { "u": "i5cc33dd", "f": "B", "len": 895, "st": "ok" } ]
  }
}
칸 뜻 규칙
맵 키 묶음 gid doors 에 같은 gid 가 둘 이상이면 첫 묶음에만 붙이고 «gid 중복» 경고(배치 불가)
ck 확정 때 묶음 내용 키(§2-3) 지금 내용과 다르면 «사양 바뀜» 불일치 · 재연결의 근거
kd 확정 때 품목 종류(§2-4) 지금 종류와 다르면 «품목 바뀜» 불일치
pl[] 한 벌(group.qty 중 1)마다 1항목 · 원장 현장재단 품목(kick 이고 부품 w ≥ 2400 또는 비고 「원장」)은 구간마다 1항목 + len 확정(ok)·보관(hold)만 저장. 제안은 저장 안 함
u / w 기준 장 uid / 기준 벽 id(커텐박스·벽판넬처럼 벽에 붙는 것) 둘 중 하나. hold 는 둘 다 없어도 됨
f 면 L·R·T·B·F(장) · W(벽) 렌더 템플릿 선택(§5)
o [x, y, z] mm 보정(엔진 상자 규약 · y + = 벽 쪽) · 0 이면 생략 제안 때 run 앞면 보정(깊이 다른 장 섞임)을 여기에 굽는다
len 구간 길이(원장 품목만)
st ok 확정 · hold 보관대 고정(일부러 안 놓음 — 제안을 끈다)
m 1 = 사람이 위치를 고침 «장 바뀜» 검사 생략
- {} = 전부 해제(명시). 키 자체가 없음 = «모름» → 서버가 저장본 유지(코덱스 구멍 3 「누락=유지 · []=삭제」).
- 크기 상한(서버 모양 검사): 키 ≤ 3000 · pl ≤ 500/묶음 · 전체 JSON ≤ 1,000,000자 · 모르는 칸은 통과(앞으로 칸 추가 대비 · 화면이 안 읽을 뿐).

2-3. 내용 키 ck (재연결 키 · JS·Python 같은 규칙)

ck(g) = div | name | dim | color - div = trim. name = 묶음 name(비면 첫 부품 name) → 공백·/·-·_·(·)·. 제거 + 소문자(「좌 E/P」→좌ep). - dim = 숫자만 뽑아 정수면 정수, 아니면 소수 그대로 → * 로 이음("2100 * 590*60.0" → 2100*590*60). - color = 그룹 color 의 / 앞 → 공백 제거 + 소문자(IV00149 GM → iv00149gm · colKey 관례와 같은 대소문자 무시). - 공용 사례표 = 아래 §2-6 JSON — R2(pytest)·R3a(node) 가 같은 내용을 각 리포에 둔다. R5 가 두 사본 해시 일치를 확인.

2-4. 품목 사전 kd (scene 56 의 12종 이름 + 다른 현장 이름 · 그룹 이름 기준, 비면 첫 부품 이름)

kd 이름 정규식(공백 제거 뒤) 자동 제안 기본 면
fil_side ^(좌|우|세로|측면)휠라 벽 시작/끝 빈칸 = 주 부품 w L(시작)·R(끝)
fil_top 상부휠라|천장휠라|상휠라 상부 run 폭 = 주 부품 w T
fil_low 하부휠라 그 벽(pos) 하부 run 끝 장 · 이름 좌/우 L·R
top_pnl ^상천판|^천판 (단독) 없음 T
ep E\/?P|이피|엔드판넬 (좌·우·측면·냉장고·숨은) run 끝 + 이름 좌/우 + 높이로 하부/상부/전고 L·R
chan 목?찬넬 하부 run 폭 = 길이 · 비고 「서랍 사이」 = 서랍장 앞면 T(F)
kick 걸레받이|굽도리 다리 있는 하부 기둥의 연속 구간(바닥에 닿는 기둥에서 끊음) B
wall_pnl 벽판넬|벽패널|마감판넬 ㄱ자(리턴 EP 있음)만: 벽 끝 남은 길이 r ≤ w ≤ r+60 R·L
curtain 커[튼텐]박스 벽 길이 = 주 부품 w · 이름에 「창」 있는 벽 W
cable 전선가림 없음(수동) W·F
cornice 코니스|몰딩 없음(2차) T
fix ^FIX|픽스 없음(2차) F
panel 노출판|마블판|등판|마감판 없음(수동) F·W
other 그 밖 · div 30(도장)·40(오픈장) 전부 없음(수동 · 상자로 근사) 고른 면
- 대상 = div ≠ 10(Q3 「도어 외 전 품목」). div 10(도어·서랍앞판·인출망도어)은 장 템플릿이 이미 그린다(H1) → 배치 대상 아님(목록엔 «장 앞면»으로 표시). 층: div 20 = EP 칩, 30·40 = 도어 칩(doorLayer 그대로).
- 사전은 acc.js 한 곳. 현장마다 이름이 늘면 정규식 1줄 추가(R0: 에넥스 84B/84C 이름 전부 위 표에 들어간다).

2-5. 상태

상태 판정 어디 보이나
확정 ok 항목 수 = qty(원장 품목은 Σlen ≤ qty × 원장 길이) app·door
일부 ok 수 < qty app·door («확정 1/2»)
제안 저장 없음 · 엔진 1등 후보 app
모호 후보 둘 이상 동점권 app (후보 넘기기)
미배치 저장 없음 · 후보 없음 → 보관대 app·door
보관 hold app·door
불일치 기준 장 없음(uid 못 찾음) · 벽 없음 · 수량 초과 · 원장 부족 · 사양 바뀜(ck) · 품목 바뀜(kd) · 장 바뀜(같은 기준으로 다시 제안하면 길이/위치가 5mm 넘게 다름 · m 이면 생략) · 묶음 없음(맵 키 gid 가 doors 에 없음) · gid 중복 app(전부) · door(장 바뀜 제외)

2-6. 공용 사례표 (R2·R3a 가 각 리포에 그대로 저장)

{ "ck": [
    { "g": {"div":"20","name":"좌 E/P","dim":"720*2330","color":"IV00149 GM/전면유사PP"}, "ck": "20|좌ep|720*2330|iv00149gm" },
    { "g": {"div":" 20","name":"","dim":"2100 * 590*60.0","color":"iv00149 gm","parts":[{"name":"상부휠라"}]}, "ck": "20|상부휠라|2100*590*60|iv00149gm" } ],
  "relink": [
    { "id": "same", "old": [["G05","K1"],["G06","K2"]], "new": [["G05","K1"],["G06","K2"]], "pl": ["G05"], "want": {"G05":"G05"}, "dropped": [] },
    { "id": "inplace-edit", "old": [["G05","K1"]], "new": [["G05","K1b"]], "pl": ["G05"], "want": {"G05":"G05"}, "dropped": [] },
    { "id": "shift-insert", "old": [["G05","K1"],["G06","K2"]], "new": [["G05","K0"],["G06","K1"],["G07","K2"]], "pl": ["G05","G06"], "want": {"G05":"G06","G06":"G07"}, "dropped": [] },
    { "id": "shift-delete", "old": [["G05","K5"],["G06","K6"]], "new": [["G05","K6"]], "pl": ["G05","G06"], "want": {"G06":"G05"}, "dropped": ["G05"] },
    { "id": "twin-edit", "old": [["G16","K"],["G18","K"]], "new": [["G16","Kb"],["G18","K"]], "pl": ["G16"], "want": {"G16":"G16"}, "dropped": [] },
    { "id": "twin-ambiguous", "old": [["G16","K"],["G18","K"]], "new": [["G20","K"],["G21","K"]], "pl": ["G16","G18"], "want": {}, "dropped": ["G16","G18"] },
    { "id": "deleted", "old": [["G30","K"]], "new": [], "pl": ["G30"], "want": {}, "dropped": ["G30"] },
    { "id": "orphan-rescue", "old": [], "new": [["G09","K"]], "pl": ["G01"], "plck": {"G01":"K"}, "want": {"G01":"G09"}, "dropped": [] },
    { "id": "new-prefix", "old": [["A-G01","K1"],["A-G02","K2"]], "new": [["B-G01","K1"],["B-G02","K2"]], "pl": ["A-G01","A-G02"], "want": {"A-G01":"B-G01","A-G02":"B-G02"}, "dropped": [] } ] }

(old·new = [gid, 내용키] · pl = 배치 있던 gid · want = 옛 gid → 새 gid · plck = 저장본에 없는 gid 의 배치 ck)

3. 저장 경로 · 소유

3-1. 소유표 (화면 간 덮어쓰기를 «소유 분리»로 0 으로)

데이터 쓰는 쪽 경로 서버
items(+uid) · placements · 그 밖 몸통 app.html PUT /api/pncad/scene/<no>(💾 · Ctrl+S) doors·door_rows 저장본 유지(기존) + placements 없으면 저장본 유지 · 모양 검사
doors(사양) · door_rows door.html · 외부 도구(PO 가져오기 스크립트) PATCH /api/pncad/scene/<no>/doors 같은 트랜잭션에서 placements 재연결(§3-3) · modify_date 불변(기존)
새 작업본(다른 이름 저장) app.html POST /scene 그대로(uid·placements 도 같이 복사 — 작업본 안에서만 유일하면 된다)
- 「확정」 = 편집(● 변경됨) → 💾 로 저장(몸통 저장과 같은 길 · 기존 UX 그대로). 패널에 「확정분 저장」 단추를 둔다(26.10.16 「확정하고 저장」에서 개명 — 제안까지 확정되는 걸로 오해)(storeScene(false) 호출).
- 💾 는 nest_rows 를 지금 엔진으로 다시 계산해 같이 올린다(기존 동작). 엔진이 그 작업본 저장 뒤 바뀌었으면 nest_rows 가 달라질 수 있다 — 기존 몸통 저장과 같은 성질(규칙 L164 «저장 시점 nest_rows»). 배치만의 영향은 0(§8 I3).

3-2. PUT (dao.update) — 추가분만

# 행 잠금으로 읽은 old(저장본 data dict) 기준 · 기존 doors 보존 분기는 그대로
d = _obj(data)
if old is not None and d is not None and "placements" not in d and "placements" in old:
    d["placements"] = old["placements"]          # 모르면 유지(구버전 캐시 app.html · 배치 전 열린 탭)
if d is not None and "placements" in d:
    door_place.validate_placements(d["placements"])   # 모양만 · 실패 = ValueError → 컨트롤러 9999
data = json.dumps(d, ensure_ascii=False)          # d 를 바꿨을 때만 다시 눕힌다
def relink(old_doors, new_doors, placements):
    """옛 doors → 새 doors 로 바뀔 때 배치 맵 키(gid)를 옮긴다. 반환 (새 맵, 보고)."""
    oldk = {gid: ck(g)}  # 첫 등장만 · newk 동일
    cnt_old = Counter(oldk.values()); holders_new = {K: [gid…]}   # 새 목록에서 K 를 가진 gid 들
    out, taken, rep = {}, set(), {"kept": 0, "moved": [], "edited": 0, "dropped": []}
    # 1) 그대로: 같은 gid · 같은 내용
    for gid, P in placements.items():
        if gid in oldk and gid in newk and oldk[gid] == newk[gid]: out[gid] = P; taken.add(gid); rep.kept += 1
    # 2) 내용 따라 이동: 옛 내용 K 가 옛 목록에서 유일(옛 목록에 없던 gid 는 P.ck) + 새 목록에서도 유일 + 대상이 비어 있음
    for gid in sorted(rest):
        K = oldk.get(gid) or P.get("ck")
        if (gid in oldk and cnt_old[K] != 1) or len(holders_new.get(K, [])) != 1: continue
        h = holders_new[K][0]
        if h in taken: continue
        out[h] = P; taken.add(h); rep.moved.append([gid, h])
    # 3) 제자리: gid 가 새 목록에 남아 있고 비어 있음 = 그 자리 사양 고침(ck 는 옛 값 → 화면 «사양 바뀜»)
    for gid in sorted(rest2):
        if gid in newk and gid not in taken: out[gid] = P; taken.add(gid); rep.edited += 1
    # 4) 나머지 = 버림(묶음 삭제 · 쌍둥이 모호) → 보고에 gid
    return out, rep

3-4. GET /doors 추가

3-5. 동시성 · 구버전 시나리오

상황 결과
door.html 열린 채 app.html 이 배치 💾 → door.html 다음 자동저장 door.html 은 placements 를 안 보낸다 → 서버가 재연결만 → 배치 유지 ✅
app.html 💾 사이 door.html 이 사양 고침 PUT 이 doors 저장본 유지(기존) → 사양 유지 ✅
외부 도구 PO 재가져오기(gid 밀림) 내용 1:1 이동 ✅ · 모호·삭제 = 버림 + 응답 보고
앱 두 탭 몸통 💾 기존처럼 마지막 저장 승(몸통과 같은 성질 · 낙관적 잠금 안 함)
캐시된 옛 app.html(배포 전 판) 💾 열 때 받은 placements·items[].uid 를 Object.assign 으로 그대로 들고 있다가 돌려보낸다 · 배치 전 열린 탭이면 키 없음 → 서버 유지 ✅ · 옛 판이 새로 놓은 장 = uid 없음 → 새 판이 열 때 부여
캐시된 옛 door.html placements 를 모른다 → 영향 없음 ✅
재연결 직후 옛 키로 app.html 💾(드묾) 옛 gid 키가 다시 써질 수 있다 → 화면이 «묶음 없음/사양 바뀜» 빨강 → 제안 다시 확정

3-6. 응답·방송(BroadcastChannel pncad-doors)

방향 메시지 받는 쪽 처리
door → app(기존 확장) {scene_no, doors, placements, src:'door'} — PATCH 성공 뒤(placements = 서버 응답) old = SCENE.doors → SCENE.doors = doors → SCENE.placements = relink(old, doors, SCENE.placements)(같은 함수 JS판 · 미저장 확정 보존) · dirty 안 켬
app → door(신규) {scene_no, placements, item_uids, src:'app'} — PUT 성공 뒤 배지만 다시 그림 · dirty 안 켬
door → app(신규) {scene_no, focus: gid, src:'door'} — 행 클릭 그 묶음 부속 강조 + 층이 꺼져 있으면 켬
- 옛 app.html 은 모르는 칸을 무시한다(기존 처리 SCENE.doors = d.doors 만). 순서 역전은 «표시만 잠깐 옛것» — 각 데이터의 작성자가 하나뿐이라 저장 소실로 이어지지 않는다(코덱스 구멍 4 · 서버 revision 은 2차 후보).

3-7. 왜 doors 안(doors[g].uid·pl)이 아니라 placements 인가

A doors 안 B placements(채택)
PUT 서버가 들어온 pl 만 골라 저장본 doors 에 병합(새 로직) 기존 그대로 + 「없으면 유지」 3줄
PATCH 들어온 pl 무시 + 저장본 pl 을 uid/gid/키로 옮김 맵 키 재연결
그룹 uid 필요(서버 발급 → 요청별 임시 ID 로 클라 수신 · 코덱스 지적) 불필요(gid + 내용 키)
door.html 복제(bDup 통째 복사) uid·pl 복사 → 초기화 필수 해당 없음(그룹에 배치가 없다)
응답·방송 반영 로컬 pl 우선 병합 필요 doors 는 그대로 교체, 배치는 재연결 함수 한 번
엑셀·door_rows·네스팅 섞임 그룹 객체에 키가 늘어난다(지금은 무해) 0
2차 3D 생성 부재(src=3d) 그룹에 같이 둠 그룹(doors) + 배치(placements) 따로 — 2차에 app.html 의 그룹 생성 경로를 별도 설계

4. 자동 제안 (acc.js suggest)

4-1. 장 기하

4-2. 점수

근거 점수
길이 일치 |폭 − L| ≤ 5 +100 (≤ 20 = +60)
비고 숫자열(600+1050+600)이 부분 run 기둥 폭 순서와 같음 +50
pos 일치: 상부↔상부층 · 하부↔하부층 · 창하부/커텐박스↔이름에 「창」 있는 벽 · 주방 = 0 +30 · 어긋남 = 후보 제외
비고 키워드(서랍·싱크·후드·쿡탑·인출·식세기·가스·창하부)가 기준 장 이름에 있음 개당 +10
- 판정: 1등 ≥ 100 이고 (2등 없음 또는 1등 − 2등 ≥ 30) = 제안 · 1등 ≥ 100 이지만 근소 = 모호(후보 목록) · 그 밖 = 미배치.

4-3. 품목별 후보 생성

kd 후보 기준 장 · 면 · o
fil_top 상부 부분 run, L = 주 부품 w 첫 기둥 위 장 · T · o = [0, run 최전면 F − 기준 F, run 최고 z1 − 기준 z1]
chan 하부 부분 run(기둥 위 높이 ±5 일치), L = 주 부품 w 첫 기둥 위 장 · T · o = [0, run 최전면 몸통앞 − 기준 0, 0]
chan 「서랍 사이」 하부 단일 기둥, W ≈ L, 이름에 「서랍」 그 장 · F · o.z = 그 장 도어 부재 사이 가장 큰 틈 가운데(없으면 H/2) — 확신 낮음
kick 각 벽 하부 run 을 «바닥 닿는 기둥(z0 ≤ 5)»에서 자른 구간 = 구간마다 pl 1개(len = 구간 폭) · 묶음이 하나일 때만(둘 이상 = 모호) 구간 첫 기둥 아래 장 · B · o = [0, 구간 최전면 몸통앞 − 기준 0, 0]
fil_side 벽 시작 빈칸 x0(첫 기둥) · 끝 빈칸 len − x1(끝 기둥) ≈ 주 부품 w(±5) · 이름 좌=시작/우=끝 그 끝의 하부 기둥 아래 장(없으면 상부) · L/R
ep run 끝 + 이름 좌/우(없으면 양끝 = 모호) · 높이: h ≥ 2000 전고 / 하부 run 위 ±20 하부 / 상부 장 높이 ±20 상부 끝 장 · L/R
wall_pnl(ㄱ자 · 리턴 EP) 벽 끝 남은 길이 r: r ≤ 주 w ≤ r + 60 끝 하부 장 · R(시작 쪽이면 L)
fil_low pos 벽의 하부 run 끝 기둥 · 이름 좌=첫/우=끝 · h ≈ 하부 run 위(±20) 그 장 · L/R
curtain 벽 len ≈ 주 w(±5) · 이름 「창」 벽 우선 벽 · W
나머지 없음 보관대

4-4. scene 56 기대값 (R3a 검사의 정답표 · div 20 15묶음)

gid 품목 결과 기준 · 면 근거
G01 좌휠라+좌E/P 제안 L1 · L 벽 A 시작 빈칸 152
G05 상부휠라+상천판 2100 제안 U1 · T U1+U2+U3 = 2100(유일)
G06 상부휠라+상천판 1360 제안 U4 · T U4+U5 = 1360
G16 목찬넬 2250 제안 L1 · T 후보 2(L1~L3 · L2~L4) → 비고 600+1050+600 +50
G17 목찬넬 1210 제안 L4 · T L4기둥+L5+L6 · 비고 600+200+410
G18 목찬넬 600 서랍 사이 제안(확신 낮음) L1 · F 「서랍 사이」 + 「2단 서랍장」
G19 목찬넬 895 제안 W1 · T 벽 B · pos 창하부
G23 벽판넬 840 + 우E/P 제안 L6 · R 벽 A 남은 785 ≤ 840 ≤ 845
G24 벽판넬 890×2390 미배치 — 단품 벽판넬 규칙 없음(사람 확인)
G25 벽판넬 960×330 미배치 — 〃
G26 하부휠라(면 30) 좌 제안 W1 · L 창하부 · 좌
G27 하부휠라 ㄱ자 우 제안 W1 · R 창하부 · 우
G28 커텐박스 1000 제안 벽 B · W 벽 B len 1000 · 「창」
G29 전선가림목 미배치 — 위치 정보 없음
G30 걸레받이 2440×3 제안(구간 3) L1·B 2250 · L5·B 610 · W1·B 895 L4-2 바닥 기둥에서 끊음 · Σ3755 ≤ 7320
- 도어 12묶음(div 10)은 배치 대상 아님(목록 «장 앞면»). 합계 = 제안 12 · 모호 0 · 미배치 3 · 누락 0.

5. 렌더 규약

5-1. 좌표계

5-2. 품목 × 면 → 상자 (x · y · z 범위)

kd · 면 주(main) 리턴(return) / 플랜지 확신
fil_top · T x 0~w · y F~F+t · z H~H+h (앞 띠) 리: x 0~w · y F~F+h(깊이) · z H+주h−t~H+주h (수평 상천판) 중 — scene 56 상천판 깊이 590 은 벽을 뚫고 보인다(그대로 그림 · «벽 관통» 표시 · 2차 Q13)
chan · T x 0~L · y 0~h(39) · z H−t~H (수평) 플: x 0~L · y 주h−t~주h · z H−t−h(51)~H−t (수직) 중
chan · F 같은 모양 · z 가운데 = o.z 낮
kick · B x 0~len · y 0~t (들임 0 = 몸통 앞면) · z −z0~−z0+h(152) 중(들임 = 2차 Q11)
fil_side · L x −w~0 · y Wd−리w~Wd−리w+t · z −z0~−z0+h (EP 앞끝에 맞춘 앞 띠) 리: x −t~0 · y Wd−w~Wd · z −z0~−z0+h (장 옆에 붙은 숨은 EP) 중
fil_side · R x W~W+w · 나머지 같음 리: x W~W+t 중
ep · L/R x −t~0 / W~W+t · y Wd−w~Wd · z 전고 −z0~−z0+h · 하부 −z0~ · 상부 0~h 중(깊이 = 2차 Q12)
wall_pnl · R(ㄱ자) x W~W+w · y Wd−t~Wd · z −z0~−z0+h (벽면 판) 리: x W~W+t · y Wd−w~Wd · z −z0~−z0+h (EP) 중
fil_low · L/R 단품(면 t): x −t~0 / W~W+t · y F~F+w · z −z0~−z0+h · ㄱ자: 주 x 밖으로 w · y F~F+t ㄱ자 리: 밖 끝에서 y F~F+리w 중
curtain · W x 0~w · y −리w~−리w+t · z C−h~C 리 ×qty: x 0~t · x w−t~w · y −w~0 · z C−h~C 중(높이 = 천장 가정)
cable · W/F ㄷ자 주 x 0~w · y −리w~−리w+t · z 0~h 리 ×2 양끝 수동
나머지 · 고른 면 dim 3수 = 상자 W×D×H 를 면 바깥으로 · 2수 = 판 w×h 두께 t(F·W = y 두께 · L·R = x 두께 · T·B = z 두께) 수동
- 부품 qty > 1 이고 위 표에 자리가 없는 것은 한 번만 그리고 «×n» 표지. 깊이가 다른 장이 섞인 run 은 제안 때 o.y 로 최전면에 맞춘다.

5-3. 색 · 층 · 표시

6. 화면 동작

6-1. app.html — 부속 패널(#doorList 자리 재사용)

6-2. door.html

7. door.html 부작용 수정 범위 (R4)

# 현재 고침
1 저장 때 flattenDoors 가 door_rows 를 다시 만들며 color 접미(/전면유사PP)·kind·side·grain 을 버림(R0 #5) schema.js flattenDoors L378: color = p.color || (g.back ? g.color + '/' + g.back : g.color) · kind/side/grain = p.kind/p.side/p.grain || ''. scene 56 = color·kind(MDF)·side(단면) 유지 · grain 은 부품에 없어 '' = 네스팅 기본값 «세로»와 같은 뜻(rows_sync L160) · remark 의 「gid role · 」 접두는 grp·role 칸과 중복이라 안 살린다(허용 차이)
2 newGid = 남은 것 중 최대+1 → 마지막 묶음 지우고 추가하면 같은 gid 재사용 세션 동안 본 gid 집합(불러온 것 + 만든 것)의 최대 + 1
3 저장 후 BC 로 요청 사본만 보냄 서버 응답 placements 를 META 에 반영 · BC 에 placements·src:'door' · relink.moved/dropped 가 있으면 토스트 「배치 이동 n · 미배치로 n」
4 app.html 소식을 안 들음 BC src:'app' 수신 = 배지만 갱신(dirty 안 켬)
- 복제(bDup)가 그룹을 통째 복사 계약 B 에선 그룹에 배치가 없어 고칠 것 없음(새 gid 로 미배치)
- 범위 밖(기록만): door_rows grain 기본값 결정 · door.html 에 kind/side/grain 편집 칸(2차 후보).

8. 불변식 · 검증 계획 (R5)

9. 런 분할

런 내용 대상(전체 경로:함수) 완료 조건 등급 · worker
R2 서버 재연결·모양검사 모듈 · PUT 유지 · PATCH 재연결 · GET 추가 · 테스트 신규 C:\dev\pfs\api\pfs\pncad\door_place.py(content_key·relink·validate_placements · py3.6 문법 = f-string·walrus 금지) · C:\dev\pfs\api\pfs\pncad\dao\pncad_scene_dao.py(update·update_doors 가 (placements, report) 반환 · select_doors) · C:\dev\pfs\api\pfs\pncad\controller\pncad_scene_controller.py(Scene.put ValueError→9999 · SceneDoors.get +placements·item_uids · SceneDoors.patch 응답) · 신규 C:\dev\pfs\tests_cody\test_pncad_placements.py(+fixtures\pncad_acc_cases.json · 기존 _FakeTx 방식) 사례표 전부 · keep-if-absent · {} · 9999 · PATCH 가 door_rows·modify_date 그대로 · 기존 test_pncad_door_sheet PASS L2 worker-sonnet-high + 코덱스 diff 검토 1회 (1회 실패 = L3)
R3a 엔진 순수 엔진 + 검사 신규 C:\dev\pncad\acc.js(KIND 사전·kindOf·contentKey·newUid·ensureItemUids·geomOf·runsOf·suggest·template·groupStatus·relink) · 신규 C:\dev\pncad\tools\_test_acc.mjs · tools\fixtures\acc_cases.json §4-4 정답표 15/15 · 사례표 전부 · node -e "import('./acc.js')" L3 worker-opus-high(기하·첫 사례)
R3b 3D 통합 메시·제안·보관대·피킹·층·uid·BC 신규 C:\dev\pncad\acc3d.js(ctx 주입: THREE·scene·gBody·mesh·colorMat·cmHex·msg·SCENE 접근자) · C:\dev\pncad\app.html 훅: import(L1133 근처) · rebuild() L1867 첫 줄 ensureItemUids + 끝 ACC.rebuild() · L2232 · L4656 · loadScene L5096 · storeScene L4758 성공 뒤 BC · applyLayers L5389 · renderDoorList L5454 → 패널 위임 · BC L5477 relink · pointerdown L3451 앞단 피킹 · 신규 C:\dev\pncad\tools\_e2e_acc.py 사본 = 제안 12 · 미배치 3 · 누락 0 · 확정→💾→다시 열기 같은 자리 · I10·I11 · pageerror 0 · 3dui 147/147 L3 worker-opus-high
R3c 패널 §6-1 패널·보관대 놓기·위치 칸 C:\dev\pncad\acc3d.js(panel · CSS 모듈 주입 · app.html CSS 무변경) 13px · select/datalist 0 · QA 헬퍼 자리 회피 · E2E 패널 단계 L2 worker-sonnet-high + web-designer 상담
R4 door.html §7 전부 + 배지·focus C:\dev\pncad\door.html(load L528 · save L385 · newGid L421 · 클릭 핸들러) · C:\dev\pncad\schema.js flattenDoors L378 배치 유지·이동 토스트 · scene 56 door_rows color·side 유지 · node schema.js · _e2e_doors L2 worker-sonnet-high
R6a 테스트 배포 pncad → pfs master → 8001 C:\dev\pncad\tools\sync_to_pfs.py·_deploy_hash.py·_verify_deploy.py FILES 에 acc.js·acc3d.js(+_verify_deploy 에 door.html) · app.html = git apply --directory=web/pncad(pfs 쪽 AI 연출 줄 보존) Actions success · _deploy_hash.py test 일치 · 사이트맵 재빌드 L2 worker-sonnet-high
R5 검증 §8 I1~I13 + 반박 신규 C:\dev\_work\pncad-ep3d\inv_check.py 전부 통과 · 페페 확인 링크(테스트 사본 번호) L2 worker-sonnet-high + 코덱스 반박
R6b 운영 deploy-live 선별 포워드 C:\dev\pfs_deploy_wt · 신규 C:\dev\_work\pncad-ep3d\mk_live_acc.py 운영 해시 일치 · prod 읽기 E2E(쓰기 0) · 사이트맵 주황 0 L2 worker-sonnet-high(앵커 실패 = L3)
- 순서: R2 ∥ R3a → R3b ∥ R4 → R3c → R6a → R5 → (페페 확인) R6b.
- 위임 지시문에 넣을 것: 이 문서 경로 + 해당 절 번호 · «읽지 마라: HANDOFF·BRIEF·archive·다른 design 문서» · 커밋은 자기 리포 변경 파일만 명시 add · push·배포는 R6 만.

10. 배포 절차 (R6)

  1. pncad 커밋들 → diff --strip-trailing-cr 로 pfs web/pncad 쪽에만 있는 줄 확인(규칙 26.10.15) → app.html 은 git apply --directory=web/pncad --ignore-whitespace · acc.js·acc3d.js 신규 복사 · door.html·schema.js 는 pfs 판과 pncad 직전 판이 같은지 확인 후 복사. 협력사 droplist 사본은 droplist.js 를 안 건드리면 재빌드 불필요(건드리면 tools/partner_nesting/build.py).
  2. pfs master 커밋(서버 + web/pncad) → push → gh run watch <id> --exit-status → _deploy_hash.py test → 사이트맵 cd C:\dev\pfs && python tools/sitemap/build_sitemap.py --dump.
  3. R5 통과 + 페페 테스트 확인 → 운영은 페페 원문 「운영배포」가 있는 세션에서(채팅 승인 = broker 텔레그램 재승인 면제 · 지시문에 옮겨 적은 승인은 무효).
  4. C:\dev\pfs_deploy_wt: fetch · merge --ff-only origin/deploy-live · 서버 4파일 = git checkout origin/master -- …(먼저 git log origin/deploy-live..origin/master -- <파일> 로 딸려갈 커밋 0 확인) · web: acc.js·acc3d.js·door.html(동일 기반) = master 판 · schema.js = flattenDoors 덩어리만(live 도어 블록 동일 → git apply) · app.html = mk_live_acc.py(앵커 치환 · 앵커마다 정확히 1회 assert · master 전용 참조 photoOn·phRef·hideErr·udShift·delInsts 증가 0 assert · 선례 C:\dev\_work\enex-gyeryong-1015\doors_deploy\mk_live_doors.py).
  5. 로컬 E2E on 후보: PNCAD_ROOT=C:\dev\pfs_deploy_wt\web\pncad python tools/_e2e_acc.py local · _e2e_3dui.py local(기준선 141/148) → 커밋 → push 1회 → Actions → PNCAD_HASH_LOCAL=C:\dev\pfs_deploy_wt\web\pncad python tools/_deploy_hash.py prod → prod 읽기 E2E(scene 56 열어 제안 12·미배치 3·pageerror 0 · PUT 0) → _e2e_cache_reval prod → 사이트맵 재빌드(주황 걷힘 확인) → C:\dev\devplan\pfs.html PENDING/DONE → 페페 안내 Ctrl+Shift+R.
  6. 되돌림 = 포워드 복원(이전 판 파일 checkout → 일반 커밋). 데이터는 덧붙은 키뿐(옛 코드는 무시·보존)이라 되돌려도 안전.

11. 위험 · 대책

위험 대책 검증
재연결이 엉뚱한 묶음에 붙음 내용 1:1 만 이동 · 쌍둥이 = gid · ck/kd 스냅샷으로 «사양/품목 바뀜» 빨강 · newGid 재사용 금지 사례표 · I9
💾 가 nest_rows 를 다시 씀 기존 몸통 저장과 같은 성질 · 배치 자체 영향 0 · 엔진 바뀐 옛 작업본은 nest_rows 가 바뀔 수 있음을 패널에 안내 I3
운영판 app.html 이식 실패 로직은 새 모듈 · 훅 ~9곳 · 앵커 assert · 후보 로컬 E2E R6b 5
3D 자동색이 밀림 부속 코드는 kc 맨 뒤 3dui 칼라 단계
제안이 틀린 자리에 굳음 자동 확정 없음(Q4) · 모호는 후보 넘기기 · 확신 낮음 표시 §4-4
실사 모드에서 부속 재질 photo.js 재질 교체 대상 확인 · 안 되면 실사 중 부속 숨김 photo 145/145
옛 캐시 화면 모르는 키 보존 · 「없으면 유지」 I12
헤드리스 렌더 오탐 GPU 플래그 · 실기 1회 R5
몸통/도어 경계(L161) 위반 부속은 items 에 안 넣는다(nest_rows 합산 0) I3

12. 코덱스 검토 반영 (26.10.16 · C:\Users\delix\AppData\Local\Temp\claude\c--dev\daf387c8-f84c-42f5-881e-fcb5cdcffdda\scratchpad\r1_codex.out.txt)

코덱스 로컬 실행기가 파일을 못 읽어 «계약 문구 기준» 검토였다. | 지적 | 반영 | |---|---| | 1 서버 병합도 동시성 충돌 → 같은 행 잠근 뒤 읽기→병합→저장 | 두 경로 모두 이미 FOR UPDATE 트랜잭션 — 새 처리도 그 안에서만(§1) | | 2 fallback 오연결 → uid 있으면 uid 만 · 양방향 유일 1:1 만 자동 · 모호는 보존 · 복제 새 uid | 그룹 uid 대신 «내용 키 양방향 유일 1:1 이동 + 쌍둥이 gid» (§3-3) · 모호·삭제는 버리고 응답 보고(보존함은 2차 후보) · 장 복제 = 새 uid | | 3 구버전 PUT 이 장 uid 지움 → 계약 버전 검사·거절 | 옛 판은 모르는 키를 돌려보내므로 uid 가 안 지워짐을 코드로 확인 · 「누락=유지·{}=삭제」 채택 · 버전 거절은 안 함(옛 탭의 미저장 작업을 막는다) | | 4 응답·방송의 옛 상태 재유입 → 요청별 임시 ID·revision | 계약 B 로 그룹 uid 수신 자체를 없앰 · 배치는 작성자 하나라 역순은 표시만 · revision 은 2차 후보 | | 대안: 배치를 doors 밖 placements 로 | 채택(§3-7) |

13. 2차 질문 → ✅ 26.10.16 [페페] 결정 (아래 «1차 때 추천» 표는 기록 · 결정 표 = 바로 아래 · 본문 = §14~§27)

# ✅ 결정(26.10.16) 반영 절
Q0 2차 순서 = ① 3D 에서 EP·휠라·걸레받이 새로 추가 + 노출면 EP 누락 경고 → ② 오픈장 → ③ 마감재(천장휠라·몰딩판·측면 마감판 · 판재류) → ④ 도어 경첩 방향·손잡이 위치 §24 단계별 배포
Q8 오픈장 = 3D 에선 장으로 모델링(칸·선반 = 장 엔진 재사용) · 생산은 오픈장 전체 세트를 도어규격표(doors/door_rows)에 작성 · 몸통 nest_rows 로 안 보냄(L161 «도어 동일마감 판 몸통 원장 금지»와 같은 방향) §19
Q9 장 치수 변경 시 3D 에서 만든 부속(src=3d) = 자동 갱신 · 엑셀·수동 행 = 불일치 경고만 §17
Q10 3D 추가 기본 치수 = R0 §10 역산 초안 + 현장 변수(vars) 연동 · 근거 약한 항목만 입력 칸 §16
Q11 걸레받이 들임 = 현장 변수(배리어블) 걸레받이들임 기본 90mm(추후 변경 가능) §15-6
Q12 측면 EP 깊이 = 현장 변수 EP깊이 기본 밑장깊이 + 60 §15-6
Q13 상천판 590 = 앞으로 돌출 · 일반 원칙 = 벽을 뚫는 제품은 없다 → 벽 쪽으로 넘치면 앞으로 돌출(벽 관통 금지) §16-4
Q14 3D 생성 부속을 door.html 에서 고칠 수 있음 → 고치면 src=manual §15-4 · §17
Q15 도어 행 ↔ 3D 도어 대조 = 전부 표시 · 불일치는 별도 색 §21-1
Q16 재연결 실패 = 버림 + 보고 유지 (1차 그대로)
Q17 저장 = 💾 몸통 저장과 같이 유지 §15-3(3D 생성 묶음도 💾 한 번에)

(기록) 1차 때 질문 표: | # | 질문 | 선택지 | 추천 · 이유 | |---|---|---|---| | Q8 | 오픈장을 어떻게 모델링할까 | ① 장 종류(몸통재 · nest_rows) ② 도어재 묶음(door_rows) ③ 장으로 모델링 + 부재별 재질(도어재 부재만 door_rows) | ③ — 칸·선반 형상은 장 엔진 재사용, 생산 경로는 재질이 정한다 | | Q9 | 장 치수가 바뀌면 붙은 부속은 | ① 자동 갱신 ② 불일치 경고만 ③ 고정 | 3D 에서 만든 것 ① · 엑셀·수동 행 ② — 사람이 정한 발주 치수를 몰래 바꾸지 않는다(1차는 전부 ②) | | Q10 | 3D 에서 새로 추가할 때 기본 치수 | ① R0 §10 역산 초안 승인(vars 연동) ② 직접 지정 | ① — 확신 낮은 항목(EP 깊이·들임)만 입력 칸 | | Q11 | 걸레받이 들임(앞면에서 얼마나 들어가나) | ① 몸통 앞면과 같음(0) ② 도어 앞면에서 N mm ③ 현장 변수 걸레받이들임 | ③(기본값은 페페가 한 번 정함) — scene 56 데이터에 근거 없음 | | Q12 | 측면 EP 깊이 기본 | ① 밑장깊이 + 60 ② run 최대 깊이 + 60 ③ 현장 변수 EP깊이 | ③(기본 = 밑장깊이+60 · 2현장 확인 후 확정) — 570+150 도 맞아 1현장으론 못 가른다 | | Q13 | scene 56 상천판 깊이 590 의 뜻 | ① 벽 쪽으로 590(지금 그림 = 벽 관통) ② 앞으로 돌출 ③ 다른 부재 | 페페 도면 확인 — 1차는 치수 그대로 그리고 «벽 관통» 표시 | | Q14 | 3D 에서 만든 부속(src=3d)을 door.html 에서 고치면 | ① 읽기 전용 ② 고칠 수 있고 고치면 src=manual | ② — 현장 수정이 막히면 안 된다 | | Q15 | 도어(div 10) 행 ↔ 3D 도어 대조 표시 | ① 불일치만 ② 전부 | ① — 확인 화면이 시끄러워지지 않게 | | Q16 | 재연결 실패 배치 보존함 · doors revision | ① 버림 + 보고(1차) ② 보존함에 두고 다시 붙이기 | ① 유지 → 운영 실측에서 버림이 잦으면 ② | | Q17 | 확정 단위 저장 | ① 💾(몸통 저장)과 같이(1차) ② 배치 전용 PATCH(nest_rows·수정시각 안 건드림) | ① — 엔진 바뀐 옛 작업본에서 nest_rows 변화가 문제되면 ②(guarded uid 스탬프 필요) |


2차 설계·계약 (26.10.16(금) KST · [코디] 설계 런 · 코드 수정·배포 0)

입력 = 1차 운영 반영(deploy-live 7e7e1dc8 · 실사 deffdb7d) · [페페] 2차 결정(§13 결정 표) · 코덱스 1회(§26-1) · furniture-expert 1회(§26-2). 코드 기준 pncad 7c95bba · pfs master cd9933be.

14. 2차 결론 (계약 한 장)

항목 결정
3D 생성 묶음 doors 에 src.gen='3d' 묶음 + placements[gid].gen(생성 기록). gid = <접두>-3D01(door.html 의 -G01 과 이름공간 분리) · 연결 증표 src.id(= gen.id · 불변) · 묶음별 판 src.rv(서버만 올린다)
저장(Q17) 💾 PUT 본문(data 밖)에 door_ops {up, del} → 서버가 같은 행 잠금 안에서 묶음 단위 병합(새 = 추가 · 같은 id·같은 rv·비수동 = 교체 · 그 밖 = 거절+보고 · 거절 gid 의 placements 칸은 저장본으로 되돌림) · door_rows 는 최종 doors 순서로 grp 별 재조립(손 안 댄 묶음 행 = 바이트 그대로)
door.html 수정(Q14) 3D 묶음을 고치면 src.manual=true(서버도 내용 차이로 직접 찍는다 — 옛 door.html 이어도) → 앱 자동 갱신 중지·경고만 · 삭제 = PATCH del:[{gid,id,rv}] · 복제 = 일반 수동 묶음
옛 탭·외부 도구 보호 PATCH(배열 통째)에서 저장본 3D 묶음은 들어온 사본 rv = 저장본 rv 일 때만 바뀐다 · 배열에 없으면 같은 rv 의 del 이 있을 때만 지운다 · 아니면 저장본 유지 + 응답 doors·보고 → PO 재가져오기·열려 있던 옛 탭이 3D 묶음을 지우거나 옛 치수로 되돌리지 못한다
자동 갱신(Q9) 이번 세션 편집(장 이동·크기·칸·현장 변수)으로 다시 계산이 달라지면 비수동 3D 묶음 = 자동 갱신(● 변경됨 → 💾) · 열 때 차이(엔진·코드 변경)는 「갱신 필요 n」 표시만 · 수동·엑셀 행 = 경고만
현장 변수(Q10~Q12) 새 소제목 「부속」: 걸레받이들임=90mm · EP깊이=$밑장깊이+60mm · 상부EP깊이 · 걸레받이여유장=1 · 휠라최대 · 벽판넬여유 — 처음 생성 때 SCENE.vars 에 굳힌다(코드 기본값이 바뀌어도 그 작업본 불변)
원장 2440 길이 방향 = 장 경계에서 나눔(조각 ≤ 2420 실사용 · 조각 최소·균형) · 걸레받이 = 2440 원장 패킹 + 여유장 · 1220×2440 에 안 드는 판(높이·폭) = 생성 막고 사람이 고름([이음으로 나누기] / [2700 원장 표기]) · 네스팅이 1220×2700 을 조용히 붙이는 길을 생성 단계에서 막는다
벽 관통 금지(Q13) 기준 장(회전 0)의 벽면 y = Wd 를 넘는 상자가 있으면 그 배치 전체를 앞으로 민다(조립체가 깨지지 않게 통째) · 「앞 돌출 n」 표지 · 1차 «벽 관통» 표시 없앰 · 데이터 변경 0(계산값)
노출면 경고 벽·층별 run 끝: 벽 끝 ≤ 5 = 막힘 · ≤ 휠라최대 = 휠라 필요 · 그 이상 = EP 필요 · 덮개 = 확정 배치 상자가 그 측면을 실제로 덮음(기하 판정) 또는 장 템플릿 EP · [EP 추가]·[휠라 추가]·[무시] · 같은 자리 두 묶음 = 겹침(이중 생산 의심) 경고
오픈장(Q8) items[].route='door'(포인 오픈장 아티클 기본) → nest_rows·몸통규격표·자재소요에서 뺌 · 전개 부재 전부 → div 40 「오픈장」 조합 묶음(gen3d · src.u = 장 uid) · 장마다 생산 경로 정확히 1개를 서버가 검사(어기면 9999)
마감재(③) 천장휠라(가변 높이) · 몰딩판(cornice) · 측면 마감판(panel L/R) 생성 규칙·템플릿 추가 — ①의 틀 재사용
도어 대조(Q15) · 경첩·손잡이(④) 3D 도어(장 템플릿 앞면) ↔ div 10 행 = 치수 키로 맞춤 → 전부 표시 + 상태색 5 · 💾 때 data.door3d 요약 → GET /doors → door.html 배지 · 경첩 = 엔진값 + 장별 덮어쓰기 items[].doorOpt · 규격표 「좌·우」 칸 = 3D 로 채우기(사람 확정) · 손잡이 = 3D 막대 + 규격표 표기는 §27 질문
서버 변경 R7 한 번에 다 넣는다(②~④ 는 화면만 배포): door_ops 병합 · PATCH 3D 보호 · 생산 경로 검사(오픈장 없으면 휴면) · GET /doors door3d·route_uids. DDL 0
불변식 door_ops 없는 PUT·3D 묶음 없는 PATCH = 1차와 바이트 동일 · 3D 묶음 추가 = 엑셀 파싱 행 «기준선 + 추가분» · nest_rows = ①③④ 동일 / ② 오픈장 장 부재만 빠짐 · 생성 부재 1220×2440 밖 0 · 벽 관통 0

15. 2차 데이터 계약

15-1. 3D 생성 묶음 (doors 원소 · 기존 그룹 형식 그대로 + src)

{ "gid": "DWU-PO-3D01", "sec": "D", "div": "20", "join": "S", "shape": "", "name": "우E/P", "dim": "720*852", "qty": 1,
  "pos": "하부", "type": "", "color": "IV00149 GM", "back": "전면유사PP",
  "src": { "gen": "3d", "id": "g7k2m9x0a", "rv": 3, "kd": "ep" },
  "parts": [ { "pid": "1", "role": "main", "name": "우E/P", "thk": 18, "w": 720, "h": 852, "left": "", "right": "", "edge": "", "qty": 1, "remark": "", "kind": "MDF", "side": "단면", "grain": "" } ] }
src 칸 뜻 누가 쓰나
gen '3d' = 3D 에서 생성 앱(생성 때)
id 연결 증표 g+8자(acc.js newUid 와 같은 방식 · 접두만 g) · 생성 때 1회 · 불변 앱
rv 묶음 판 — 추가 = 1, 바뀔 때마다 +1 서버만(화면은 받은 값을 들고 다닐 뿐)
kd 품목(§2-4 사전 + open·ceil_fil·side_pnl) 앱
u 기준 장 uid — 오픈장만 필수(생산 경로 검사 §19-4) 앱
manual true = 사람이 도어규격표에서 고침 → 자동 갱신 끔 door.html · 서버(내용 차이로 직접)
- 기존 src 관례와 공존: PO 가져오기 {sheet, rid} · door.html 새 묶음 {manual:true} · 복제 {copyOf}. 판정 isGen3d(g) = g.src?.gen === '3d' && /^g[0-9a-z]{8}$/.test(g.src?.id) — 3D 기록 없는 3D 묶음(복제·되살림)은 «수동»으로 다룬다(자동 갱신·자동 삭제 안 함).
- door_rows 행 = flattenDoors([g], SCENE) 그대로(16키 · sec 'D' · grp = gid · role main/return/flange). 새 행 키 0.
- ck·kd 스냅샷(placements)과 엑셀(door_sheet)은 src 를 안 읽는다 → 1차 재연결·엑셀 규칙 그대로.

15-2. 생성 기록 placements[gid].gen (앱 소유 · 💾 PUT · 서버 모양검사는 «모르는 칸 통과»라 변경 0)

"DWU-PO-3D01": { "ck": "20|우ep|720*852|iv00149gm", "kd": "ep",
  "pl": [ { "u": "i3k9x2ab", "f": "R", "st": "ok" } ],
  "gen": { "id": "g7k2m9x0a", "rule": "ep", "at": { "u": ["i3k9x2ab"], "f": "R", "lev": "low", "wall": "A" },
           "ov": { "w": null, "h": null, "color": null, "pos": null }, "sig": "우E/P|720*852|1|우E/P:18:720:852:1" } }

15-3. 💾 PUT door_ops — 서버 묶음 단위 병합 (dao.update 행 잠금·한 트랜잭션 안)

"door_ops": { "v": 1,
  "up":  [ { "g": { 묶음 }, "rows": [ door_rows 행 ], "base": 0 } ],
  "del": [ { "gid": "DWU-PO-3D02", "id": "g…", "rv": 2, "force": false } ] }
경우 처리 보고
up · 저장본에 그 gid 없음 · base 0 추가(rv 1) added
up · 같은 gid · 같은 src.id · rv = base · 비수동 교체(rv+1) updated
up · 저장본이 수동 거절 manual
up · rv ≠ base 거절 stale
up · src.id 다름 / 저장본이 3D 묶음 아님 / 새 gid 인데 base ≠ 0 거절 clash
del · 같은 id·rv · 비수동(또는 force — 오픈장 장 삭제·경로 끔 확인 뒤만) 묶음 + 그 grp 행 삭제 deleted
del · 그 밖 거절 manual·stale·clash
- 판정 순서 = clash → manual → stale. base = 앱이 마지막으로 받은 그 묶음의 서버 rv(열 때·PUT 응답 doors) · 새 묶음 = 0.
- 거절된 gid 의 placements[gid] 는 저장본 값으로 되돌린다(저장본에 없었으면 뺀다) — 묶음·배치·생성 기록이 같이 바뀌거나 같이 안 바뀐다(코덱스 구멍 2).
- 검사(9999 = 몸통도 저장 안 됨): up·del ≤ 500 · g 객체 · gid ^[0-9A-Za-z가-힣._\-]{1,60}$ · src.gen '3d' · src.id 형식 · parts 객체 배열 · rows 키 ⊆ 16키 · rows[].grp = gid.
- door_rows 재조립 = 최종 doors 순서로 grp 별: 이번에 바뀐 묶음 = 들어온 rows · 나머지 = 저장본 rows 그대로(순서·바이트) · grp 가 어느 묶음에도 없는 옛 행 = 저장본 순서대로 끝에.
- 병합 뒤 생산 경로 검사(§19-4 · 오픈장 없으면 아무 일 없음).
- 응답 = {scene_no, doors(병합 결과), door_ops: {added, updated, deleted, rejected:[{gid, why}]}} → 앱이 SCENE.doors = res.doors 후 다시 계산(§17) · 거절은 토스트 + 상태 재판정 · clash 는 새 gid 로 바꿔 다음 💾 에 다시.
- 구현 = door_place.apply_ops(saved_doors, saved_rows, ops) → (doors, rows, report) 순수 함수(pytest 사례표 · §23).

15-4. PATCH /doors (door.html·외부 도구 · 배열 통째) — 3D 묶음 보호 추가

들어온 배열 조건 처리 보고
사본 I 있음 I.src.id = S.src.id · I.src.rv = S.src.rv · 내용(src 뺀 정규 JSON) 다름 I 채택 · rv+1 · manual=true 를 서버가 찍는다 —
〃 같은 rv · 내용 같고 manual 만 풀림 해제 반영(rv+1) — door.html 「3D 자동 갱신 다시 켜기」 —
〃 같은 rv · 내용 같음 S 그대로 —
〃 rv 다름(옛 사본) · id 다름 S 유지(들어온 사본의 수정은 버림) stale3d
없음 del 에 같은 id·rv 삭제 — 단 오픈장 묶음(kd open)은 그 장이 아직 route='door' 면 유지 — / open_kept
없음 그 밖 S 를 끝에 붙여 유지(행은 저장본 rows) kept3d
- 들어온 배열의 3D 묶음 중 저장본에 없는 것(지워진 것을 옛 탭이 되살림) = 버림 + dropped3d.
- door_rows = 최종 doors 순서 · 들어온 사본을 채택한 묶음 = 들어온 rows · 유지한 3D 묶음 = 저장본 rows. 3D 묶음이 없는 작업본은 들어온 rows 그대로(1차와 바이트 동일). 들어온 door_rows 가 없으면(옛 외부 도구) 기존대로 NULL 저장 + 보고 rows_missing.
- 재연결(relink)은 병합 결과 기준(유지된 3D 묶음 = 같은 gid·같은 내용 → 1단계 그대로).
- 응답 추가 = doors(병합 결과) · doors3d: {kept, stale, dropped, open_kept} → door.html 은 «보낸 뒤 더 안 고쳤으면» DOORS 를 응답으로 교체 + 토스트(자기가 고친 3D 묶음이 stale 이면 「다른 화면에서 바뀐 3D 묶음 n — 그 수정은 반영 안 됨」).
- 일반(PO·수동) 묶음의 «door.html 두 탭 마지막 저장 승»은 1차와 같다(기존 성질 · §26-1 대안 보류).
- 구현 = door_place.merge_patch(saved_doors, saved_rows, in_doors, in_rows, dels, route_uids) → (doors, rows, report).

15-5. GET /doors 추가 (R7 에 같이 넣고 ②·④ 까지 휴면)

15-6. 부속 현장 변수 (accgen.js ACC_VARS 한 곳 · 현장 설정 모달 「현장 변수」 아래 소제목 「부속(EP·휠라·걸레받이)」)

변수 기본 쓰는 곳 근거
걸레받이들임 90mm 걸레받이 앞면 = 도어 앞면(가구 최전면)에서 안쪽으로 90(= 몸통 앞면에서 70) Q11 · 기준면 = furniture-expert 권장(넥서스 KB 「몸통라인 안쪽 70」과 일치) · ⏳§27 Q18 페페 1회 확인
EP깊이 $밑장깊이+60mm 하부·전고 측면 EP · 키큰장·냉장고 EP(하부 EP 와 같은 앞선) · 측면 휠라 리턴(숨은 EP) · 벽판넬 리턴 Q12 · scene 56 720
상부EP깊이 $벽장깊이+20mm 상부 측면 EP(도어 앞면과 일치 = 310) · 상부휠라 리턴(상천판) 기본 깊이 furniture-expert [표준]
걸레받이여유장 1 걸레받이 원장 수량 = 패킹 장수 + 여유 scene 56 PO 「2440 2 + 여유 1」 · furniture-expert 같은 값
휠라최대 200mm 노출면 판정: 벽 끝 빈칸 20 초과 ~ 이 값 = 휠라(60 넘으면 대형 휠라+숨은 EP) · 넘으면 노출(EP·벽판넬) furniture-expert(200 = 가정 · 현장 조정)
벽판넬여유 20mm 벽판넬 폭 = 벽 끝 남은 길이 + 여유(현장 맞춤) scene 56 G24 +20(G23 +55 는 입력)
- 값 형식 = 기존 리니어 식(90mm · $밑장깊이+60mm) — renderVars 해석·힌트 그대로. app.html 훅 = VAR_GROUPS 뒤에 ACC_VARS 그룹 1개를 붙이는 줄 1개.
- 굳힘: 3D 생성(또는 [EP 추가]) 때 그 묶음이 쓴 부속 변수가 SCENE.vars 에 없으면 기본값 원문을 그대로 적는다(dirty) → 코드 기본값이 바뀌어도 그 작업본의 3D 묶음은 안 바뀐다. 열기만 해서는 안 적는다(열자마자 ● 변경됨 금지).

15-7. 그 밖 앱 소유 새 키 (서버 무검사 · PUT 통째 · 옛 화면은 Object.assign 으로 모르는 키 보존)

키 모양 단계
accChk { ign: ['A|low|e|<끝 장 uid>'] } — 노출면 경고 [무시] ①
walls[].ends { s?: 'open', e?: 'open' } — 열린 벽 끝(아일랜드·반도형 · 기본 = 막힘) ①
items[].route 'door' = 도어규격표로 생산(오픈장) · 없음 = 몸통 ②
door3d { v:1, at:'YYYY-MM-DD HH:MM', rows:[{w, h, n, L, R, lay}] } — 3D 도어 요약(💾 때 갱신) ④
items[].doorOpt { <pkey>: { hi:'L'|'R', hd:'T'|'B'|'M'|'-', ho: mm, k: CP이름 } } — 경첩·손잡이 덮어쓰기 ④

16. 생성 규칙 (3D 에서 새로 추가 · Q10~Q13)

16-1. 공통

16-2. 규칙 표 (단계 ①: ep·fil_side·fil_top·chan·kick·wall_pnl·fil_low · ③: ceil_fil·side_pnl·cornice · ②: open)

rule 대상 지정 부품(role 품명 = w × h · thk 18) 상자(기준 장 좌표) 확신 · 입력
ep 하부 run 끝 장 측면 L/R 클릭(또는 노출면 [EP 추가]) main 좌/우E/P = EP깊이 × 바닥~기둥 위(scene 56 852) x −t~0/W~W+t · y Wd−w ~ Wd · z −z0 ~ 기둥 위 중 · 깊이
ep 전고 하부·상부가 같은 끝(±5)에서 끝나거나 키큰장(H ≥ 1800) main = EP깊이 × C−M (냉장고·키큰장도 하부 EP 와 같은 앞선 · 이웃 하부가 없으면 몸통+20) z −z0 ~ C−M−z0 높음(높이) · 깊이
ep 상부 상부 run 끝 main = 상부EP깊이(310) × 상부 기둥 높이 z 상부 기둥 바닥~위 중 · 깊이
fil_side 벽 끝 빈칸 g(20 < g ≤ 휠라최대) main 좌/우휠라 = g × (상부도 그 끝까지면 C−M · 아니면 기둥 위) · g > 60 이면 + return 숨은 좌/우E/P = EP깊이 × 상부 위(없으면 하부 위) · join D ㄱ자 §5-2 fil_side(주 = EP 앞끝 띠 · 리턴 없으면 띠 y = F) 중 · 숨은 EP 높이
fil_top 상부 연속 기둥(클릭 = 그 run 전체 · Shift+클릭 = 구간) main 상부휠라 = 구간 폭 × (C−M − 구간 최고 위) · [상천판 같이] return 상천판 = 구간 폭(+여유 입력 0) × 깊이 상부EP깊이(입력) · join D ㄴ자 §5-2 fil_top · 벽 넘침 = 통째 앞 돌출(§16-4) 높음(휠라) · 상천판 깊이·여유
chan 하부 연속 기둥 main 목찬넬 = 구간 폭 × (CH깊이+2 = 39) · flange 목찬넬(플랜지) = 구간 폭 × (CH높이 − t − 1 = 51) · join D ㄴ자 (변수가 작업본에 없으면 앱 기본 CH높이 70·CH깊이 37 · 옛 작업본의 찬넬높이 가 있으면 그것) §5-2 chan T 중 · —
chan 서랍 사이 서랍장 앞면 F 클릭 같은 부품 · 폭 = 장 W §5-2 chan F(z = 도어 틈 가운데) 낮음 · z
kick 하부 장 클릭 = 그 벽 하부 run(구간 지정 가능) 구간 = run 을 바닥 닿는 기둥(z0 ≤ 5)·run 끝에서 끊은 조각 · main 걸레받이 = 2440(원장) × (구간 최소 z0 − 8 = 152) · qty = 패킹(§16-3) + 걸레받이여유장 · 비고 「2440 원장 · 현장 재단 · 필요 2255+615+900=3770 → 2장 + 여유 1」 · pl 구간마다 len x 0~len · y F+걸레받이들임 ~ +t · z −z0 ~ −z0+h 중 · 높이
wall_pnl 벽 끝 남은 길이 r > 휠라최대 main 벽판넬 = (r + 벽판넬여유) × C−M · return 좌/우E/P = EP깊이 × C−M · join D ㄱ자 §5-2 wall_pnl 중 · 여유·높이(창 아래)
fil_low 하부 run 끝(창하부) 면형 하부휠라 = 입력 폭 × 기둥 위(852) · ㄱ자 = 입력 폭 × 기둥 위 + return §5-2 fil_low 낮음 · 폭·형태
③ ceil_fil 장(키큰·붙박이·상부) 위 ~ 천장 main 천장휠라 = 구간 폭 × (C−M − 위) · return = 몸통+20(≤ 400) fil_top 과 같은 틀 중 · 리턴 깊이
③ side_pnl 장 측면 L/R main 마감판 = (장 몸통 깊이 + 20) × 장 높이 · thk 18(입력 9) x 밖 t · y Wd−w ~ Wd · z 장 바닥~위 중 · 두께
③ cornice (정의 = §27 Q19 답 뒤) — — —
② open 장 route='door' §19-3 장 자체(부속 상자 없음) —
- 높이 −8(걸레받이)·CH깊이+2·CH높이−19 는 scene 56 한 현장 역산(R0 §10) — 2현장째에서 어긋나면 그 칸을 입력으로 돌린다.
- 키큰장·냉장고 EP 는 하부 EP 와 같은 앞선(EP깊이) — 이웃 하부가 없으면 «장 몸통 + 20». 깊이가 «장 몸통 + 도어»(Wd − F)보다 얕으면 생성 막음(§18-2 필수 경고와 같은 판정).

16-3. 원장 2440 — 나눔·패킹·막음 (네스팅이 1220×2700 을 조용히 붙이는 길을 생성 단계에서 막는다)

16-4. 벽 관통 금지 = 앞 돌출 (Q13 · 일반 원칙)

16-5. scene 56 재현표 (R8 생성 엔진 정답 · C:\dev\_work\daewoo-ulsan-munjuro\after_1016c\scene_56.json)

생성(대상) 기대 PO 대조
fil_side 벽 A 시작(빈칸 152 · 상부 U1 도 x 152) 좌휠라 152×2390 + 숨은 좌E/P 720×2330 · ㄱ자 G01 같음
wall_pnl 벽 A 끝(남은 785) 벽판넬 805×2390 + 우E/P 720×2390 G23 840(+55) — 여유 입력
fil_top U1~U3 / U4~U5 상부휠라 2100×60 / 1360×60 (+상천판 선택 시 ×310) G05·G06 휠라 같음 · 상천판 PO 2120·1360×590 = 입력
chan L1~L3 / L4~L6 / W1 목찬넬 2250·1210·895 × 39 + 플랜지 × 51 G16·G17·G19 같음
kick 벽 A·B 하부 조각 2250(L1~L3) · 610(L5~L6) · 895(W1) → 2440 × 152 · qty 3 · pl 3개 G30 qty 3
ep L6 오른쪽(하부만 · 가정) 우E/P 720×852 —
fil_top 가상 3000 구간(시험용 합성) 기둥 경계에서 2조각(≤ 2420) —
fil_top U1~U3 + 상천판 590 입력 상자 통째 282 앞 돌출 · 표지 «확인» G05 배치(1차)도 같은 그림

17. 자동 갱신(Q9) · 수동(Q14) · 삭제

묶음 기대 = 지금 다름 · 이번 세션 편집 때문(직전 계산값 ≠ 지금 계산값) 다름 · 열 때부터(첫 계산값 = 기준)
3D · 비수동 할 일 없음 자동 갱신 — 묶음 교체(gid·id·칼라·비고 유지) · ck·sig 갱신 · 나눔 조각 수가 바뀌면 묶음 추가/삭제 · pending up/del · ● 변경됨 + 패널 「3D 부속 n 갱신 — 💾 전」 「갱신 필요 n」(노랑) + [모두 갱신] — 엔진·코드·변수 기본값 차이를 몰래 적용하지 않는다
3D · 수동 — 주황 «수동 — 3D 와 다름(가로 2100→2250)» · 자동 갱신 없음 같음
PO·수동 행 1차 규칙(«장 바뀜» 불일치) 〃 〃
- 기준 장 삭제 → «기준 장 없음» 빨강 · 자동 삭제 안 함(삭제 되돌리기로 장이 돌아오면 그대로 붙음) · [삭제] 단추. 예외 = 오픈장(§19): 장 삭제 = 그 묶음 삭제(pending del).
- 3D 묶음 삭제(앱) = 3D 부속 고름 → 삭제 → doors·placements 에서 빼고 pending del.
- PUT 응답 뒤 SCENE.doors = res.doors → 다시 계산 → 거절(stale·manual)은 그 상태로 다시 판정(비수동 stale = 새 base 로 pending 재등록) · clash = 새 gid.
- door.html 「3D·수동」 묶음에 [3D 자동 갱신 다시 켜기] → manual 해제 PATCH(§15-4) → 앱은 다음 열 때 「갱신 필요」로 보인다.

18. 노출면 EP 누락 경고 · 겹침 경고 (단계 ①)

18-1. 판정 accgen.exposedSides(scene, geoms, cover, opts) → [{wall, lay, end:'s'|'e', u, f:'L'|'R', gap, need, level, by, rule}]

조건 need level(하부·키큰) level(상부)
옆(같은 층) 기둥이 2mm 안 · 키큰장 기둥이 붙음 — — —
gap ≤ 5 이고 그 벽 끝이 열린 끝(walls[].ends) 아님 — (벽에 붙음) — —
5 < gap ≤ 20 — (벽맞춤 · furniture-expert) 정보만 정보만
20 < gap ≤ 60 fil 휠라 필수 권장
60 < gap ≤ 휠라최대(200) fil 대형 휠라 + 숨은 EP 필수 권장
gap > 휠라최대 · 열린 끝 ep EP 또는 벽판넬 필수 권장(같은 끝에 전고 EP 가 있으면 면제)
키큰장(H ≥ 1800) 측면이 옆 하부 기둥 위로 100 넘게 드러남 ep 필수 —
- 덮개 인정(기하 판정): 확정(ok) 배치 상자를 벽 좌표로 옮겨(기둥 x0 + 상자 x) 그 측면 평면(±(2+t))에 닿고 그 기둥 높이의 80% 이상을 덮으면 덮임 — 하부 장에 붙은 전고 EP 가 상부 끝도 덮는다(scene 56 G23). 장 템플릿 자체 EP(전개 부재 중 그 면의 PRODCODE 33.1 · 역할명 /EP/)도 덮개. 제안(점선)으로만 덮이면 «권장: 제안 확정».
- 추가 검사(furniture-expert): 확정 EP 깊이 < 그 장 «몸통 + 도어»(Wd − F) = 필수 · 이웃 EP 앞선 차이 > 5 = 권장.
- [무시] = data.accChk.ign 에 벽|층|끝|끝 장 uid — 장이 바뀌면 키가 안 맞아 다시 뜬다.
- 표시: 3D = 그 측면에 빨강(필수)·노랑(권장) 세로 띠 + 「EP 없음」 이름표(부속 칩이 켜져 있을 때) · 패널 「노출면 n」 목록 줄 = [EP 추가]/[휠라 추가]/벽판넬 추가 · [무시].
- scene 56 기대: 1차 제안 12건 확정 상태 = 경고 0(벽 A 시작 152 = G01 · 끝 785 = G23 전고 · 벽 B W1 양끝 = G26·G27) · 배치 없음 상태 = 필수 4 + 권장 2(정확한 목록은 R8 이 픽스처로 고정).

18-2. 겹침(이중 생산 의심) — accgen.overlaps(cover)

19. 오픈장 (단계 ② · Q8)

19-1. 표지 · 3D

19-2. 몸통 쪽 제외 — 판정 한 곳 schema.js isDoorRoute(inst)

19-3. 묶음 생성 (accgen.openGroup(inst, parts, ctx) · gen3d · src.kd 'open' · src.u = 장 uid)

19-4. 생산 경로 정확히 하나 (코덱스 구멍 3)

19-5. 왕복·외주·네스팅 영향

20. 마감재 판재류 (단계 ③)

21. 도어 대조(Q15) · 경첩 · 손잡이 (단계 ④)

21-1. 도어 대조 — 전부 표시 · 불일치 색

21-2. 경첩 방향

21-3. 손잡이 위치

22. 화면 요약 (web-designer 상담 = R9c·R14·R19 착수 전)

23. 불변식 · 검증 (G 시리즈 · 1차 I1~I13 회귀 포함)

# 검사 합격
G1 door_ops 없는 PUT · 3D 묶음 없는 작업본의 PATCH 저장 data·door_rows·응답(새 칸 제외) 1차와 바이트 동일 · 재연결 결과 동일
G2 door_ops 사례표(추가·교체·manual·stale·clash·del·force·검사 9999) · PATCH 사례표(채택+서버 manual · 해제 · stale3d · kept3d · dropped3d · open_kept · rows_missing) 전부(pytest · JS 사본 해시 일치)
G3 거절 gid 의 placements·gen 저장본 값 그대로
G4 3D 묶음 추가·💾 → 엑셀 parse_poin 2벌 결과 = 기준선 행 멀티셋 + 추가 행(쪽 번호·바코드 칸 제외 — 쪽이 밀린다)
G5 nest_rows ①③④ = 동일 · ② = 오픈장 장 부재 집계만 빠짐
G6 오픈장 엑셀 왕복 · poin2world 계획 G 제목 + 부품행 그대로 · 오픈장 블록 유지
G7 생성 부품 긴 변 > 2440 = 0 · 2420 초과 = 걸레받이 원장(2440)뿐 · 짧은 변 > 1220 = 0(사람이 고른 2700 표기 제외)
G8 벽 관통 회전 0 장 기준 y + d > Wd + 0.5 상자 = 0 · scene 56 G05 = 앞 282
G9 옛 탭 door.html A(옛) 열어 둔 채 앱이 3D 묶음 추가·자동 갱신·💾 → A 가 다른 줄 고쳐 저장 → 3D 묶음 남음·최신 치수 · A 화면이 응답으로 갱신
G10 수동 door.html 에서 3D 묶음 고침 → 서버 manual · 앱 장 폭 바꿔도 묶음 불변 + 주황 경고 · [다시 켜기] → 「갱신 필요」
G11 생산 경로 오픈장 켬/끔·장 삭제·복제·되돌리기 → 검사 통과 · 어긋난 PUT = 9999 · 몸통 미저장
G12 노출면 scene 56 = 확정 상태 0 · 배치 없음 상태 = 픽스처 목록 · 겹침 = 3D 좌휠라를 G01 자리에 만들면 필수 경고
G13 변수 굳힘 3D 생성 전 열기·💾 = vars 불변 · 생성 뒤 = 쓴 부속 변수만 추가
- 명령(1차 + 추가): node C:\dev\pncad\tools\_test_accgen.mjs · node C:\dev\pncad\tools\_test_acc.mjs · node C:\dev\pncad\schema.js · cd C:\dev\pfs && python -m pytest tests_cody/test_pncad_gen3d.py tests_cody/test_pncad_placements.py tests_cody/test_pncad_door_sheet.py -q · python C:\dev\pncad\tools\_e2e_gen.py [local|test|prod] [scene] · python C:\dev\_work\pncad-ep3d\inv_check.py test <사본>(G 항목 추가) · 실제 주소 http://pfs-test.poin.co.kr:8001 · 헤드리스 GPU 플래그 · Playwright 끝에 닫기.

24. 런 분할 (단계마다 운영배포 가능 · 서버는 R7 한 번)

단계 런 내용 · 대상(전체 경로) 완료 조건 등급 · worker
① R7 서버 C:\dev\pfs\api\pfs\pncad\door_place.py(+apply_ops·merge_patch·rows_rebuild·check_open_route · py3.6 문법) · C:\dev\pfs\api\pfs\pncad\dao\pncad_scene_dao.py(update door_ops·placements 되돌림·경로 검사 / update_doors del·병합·응답 doors / select_doors door3d·route_uids) · C:\dev\pfs\api\pfs\pncad\controller\pncad_scene_controller.py(PUT door_ops 받기·검사·응답 · POST 경로 검사 · PATCH del·응답 · GET 추가) · 신규 C:\dev\pfs\tests_cody\test_pncad_gen3d.py + fixtures\pncad_gen_cases.json(pncad tools\fixtures\gen_cases.json 과 같은 내용) G1·G2·G3·G11(서버 몫) · 기존 pytest 66 PASS L2 worker-sonnet-high + 코덱스 diff 1회(1회 실패 = L3)
① R8 생성 엔진 신규 C:\dev\pncad\accgen.js(ACC_VARS·규칙 §16-2 ①·사양 복사·gid/id·2440 나눔·패킹·regen·exposedSides·overlaps·cover) · C:\dev\pncad\acc.js(template 앞 돌출·fwd · kick 들임 env) · 신규 C:\dev\pncad\tools\_test_accgen.mjs + tools\fixtures\gen_cases.json §16-5 재현표 전부 · G7·G8·G12·G13(엔진 몫) · _test_acc 57/57 회귀 · node schema.js L3 worker-opus-high(기하·첫 사례)
① R9 3D 통합 C:\dev\pncad\acc3d.js(추가 모드·미리보기·확정·삭제·pending ops·ops()·afterStore(res)·노출면 띠·갱신 필요·앞 돌출 이름표 · BC app→door 에 doors) · C:\dev\pncad\app.html 훅 4곳(scenePayload door_ops 1줄 · storeScene 응답 ACC.afterStore · renderVars 부속 그룹 · BC 수신 doors) · 신규 C:\dev\pncad\tools\_e2e_gen.py 사본에서 EP·휠라·걸레받이 추가 → 💾 → 다시 열기 같은 자리 · 장 폭 바꾸면 자동 갱신 · G9·G10 · pageerror 0 · _e2e_acc·_e2e_3dui 회귀 L3 worker-opus-high
① R9c 패널 UI C:\dev\pncad\acc3d.js 패널(추가 칩·입력 칸·노출면 목록·[무시]·3D 배지) 13px · select/datalist 0 · QA 헬퍼 자리 · E2E 패널 단계 L2 worker-sonnet-high + web-designer
① R10 door.html C:\dev\pncad\door.html(3D 배지 · onCell 3D 묶음 manual 토스트 · 삭제 del · bDup = 수동 복사(gen 지움) · PATCH 응답 doors 반영 · BC doors 병합 · [다시 켜기] · gidPrefix) G9·G10 E2E · _e2e_doors 회귀 L2 worker-sonnet-high
① R11 테스트 배포·검증 1차 R6a 절차(sync·FILES 에 accgen.js 추가 3곳·Actions·8001·사이트맵) + inv_check.py G 항목 G1~G13(① 몫) + I1~I13 · 코덱스 반박 1회 · 페페 확인 링크 L2 worker-sonnet-high
① R12 운영 1차 R6b 절차 · 신규 C:\dev\_work\pncad-ep3d\mk_live_gen.py(app.html 앵커 4곳 · 앵커마다 정확히 1회 assert) · 서버 3파일(door_place·dao·controller) checkout · web = accgen.js 신규 + acc.js·acc3d.js·door.html master 판 운영 해시 · prod 읽기 E2E(쓰기 0) · 사이트맵 주황 0 L2(앵커 실패 = L3)
② R13 오픈장 엔진·경로 C:\dev\pncad\schema.js(isDoorRoute) · accgen.js(openGroup) · C:\dev\pncad\app.html(몸통 산출 거름 · 장 팝업 토글 · 18T) · C:\dev\pncad\spec.html(digest 거름 + 안내 줄) · C:\dev\pncad\data\poin-articles.json(route) G5·G6·G11 · node schema.js · 엑셀 왕복 L3 worker-opus-high(몸통/도어 경계 = 이중 생산 위험)
② R14 오픈장 UI·door.html 토글·도어색·door.html 오픈장 배지(route_uids) E2E · web-designer L2
② R15 배포 테스트 → 검증 → 운영(화면만 · 서버 변경 0) + 영향 작업본 목록 L2
③ R16 마감재 accgen.js ③ 규칙 · acc.js 템플릿 · 패널 칩 (Q19 답 반영) 규칙 사례 · E2E L2 worker-sonnet-high(①의 틀 재사용)
③ R17 배포 L2
④ R18 도어 대조 accgen.js doorMatch · acc3d.js 대조 보기·표 · app.html scenePayload door3d 1줄 · door.html div 10 배지 scene 56 정답표 · 전부 표시 L2 worker-sonnet-high
④ R19 경첩·손잡이 schema.js expand(opts.doorOpt)(hinge 덮어쓰기 · 고아 보고) · app.html/acc3d.js 경첩 토글·손잡이 막대 · door.html [3D 로 좌·우 채우기] · (Q20 답 반영) node schema.js 경첩 사례 · E2E L3 worker-opus-high(엔진)
④ R20 배포 L2
- 순서: R7 ∥ R8 → R9 ∥ R10 → R9c → R11 → (페페 확인) R12 → R13 → R14 → R15 → R16 → R17 → R18 → R19 → R20. ②~④ 는 서버 변경 0(R7 이 미리 넣음) = 화면만 배포.
- 위임 지시문에 넣을 것(장부 §0-1.8): 이 문서 경로 + 절 번호 · 대상 파일·함수 · 검증 명령 · «읽지 마라: HANDOFF·BRIEF·archive·다른 design 문서·1차 절(§1~§12) 중 지정 외» · 커밋 = 자기 리포 변경 파일만 명시 add · push·배포는 R11·R12 류만.

25. 위험 · 대책 (2차)

위험 대책 검증
doors 작성자가 둘(앱·door.html) 3D 묶음만 앱이 쓰고 묶음별 rv + 서버 병합 · 옛 사본은 못 이긴다 · 거절 = 보고 + 화면 재계산 G2·G9
앱이 몰래 발주 치수를 바꿈 자동 갱신은 «이번 세션 편집»에서만 · 열 때 차이 = 「갱신 필요」 표시 · 수동·PO 행은 안 씀 · 변수 기본값 굳힘 G10·G13
이중 생산 오픈장 생산 경로 1개 서버 검사 · 겹침 경고 · 3D 추가 전 같은 자리 묶음 확인 G11·G12
1220×2700 원장 조용히 붙음 생성 단계 나눔·막음 · 걸레받이 패킹 G7
운영판 app.html 이식 ① 훅 4곳 · ② 몸통 산출 거름 몇 곳(앵커 assert) · 로직은 accgen.js·acc3d.js R12 후보 로컬 E2E
기본 치수 근거 1현장 확신 낮음 = 입력 칸 · 2현장째 실측 후 조정 · 변수로 현장마다 §16-5
엑셀 쪽 밀림 비교는 파싱 행 단위 G4
오픈장 옛 저장본(몸통 경로) 토글 + 💾 전엔 몸통 nest_rows 그대로 → 영향 목록 보고 R15
일반 묶음 door.html 두 탭 경쟁 1차와 같은 성질(마지막 저장 승) — 이번 범위 밖 · §26-1 대안으로 후속 —

26. 상담 반영

26-1. 코덱스 (26.10.16 · C:\Users\delix\AppData\Local\Temp\claude\c--dev\daf387c8-f84c-42f5-881e-fcb5cdcffdda\scratchpad\r2_codex.out.txt)

지적 반영
1 수동본도 옛 탭 PATCH·del 에 덮인다 → 묶음별 rev · 기대 rev 불일치면 거절 채택 — src.rv(서버만 올림) · PATCH 는 rv 같을 때만 3D 묶음을 바꾸고 del 도 같은 rv 만(수동 여부 무관)
2 gid 재사용·부분 성공 오연결 → 불변 UUID 연결 · 묶음 단위 같이 반영/거절 채택 — src.id = gen.id 연결 증표 · 다르면 clash 거절 · 거절 gid 의 placements·gen 은 저장본으로 되돌림
3 오픈장 전환 중 이중 생산·누락 → 같은 트랜잭션에서 장마다 생산 경로 하나 강제 채택 — check_open_route PUT·POST 9999 + 화면 사전 맞춤 + PATCH open_kept
대안: 배열 통째 PATCH 폐지 · 두 화면 rev 기반 up/del 통일 · door_rows 서버 재생성 보류(후속 후보) — door.html 저장 방식 재작성 + JS·Python flatten 이중 구현이 1차 운영 직후 위험을 키운다. 이번엔 새 위험(3D 묶음)만 rev 로 막고, 일반 묶음 두 탭 경쟁은 1차와 같은 성질로 둔다

26-2. furniture-expert (26.10.16 · 근거 C:\dev\seedchain\projects\furniture-expert\kb\ 01·02·03·05·07)

자문 반영
걸레받이 들임 기준면 = 도어 앞면 권장(몸통 기준이면 70 · 넥서스 KB 「몸통라인 안쪽 70」) §15-6 기준면 = 도어 앞면 · ⏳Q18 페페 1회 확인
상부 EP = 벽장깊이 + 20(도어면 일치) · 키큰·냉장고 = 하부 EP 앞선에 통일(없으면 몸통+20) 상부EP깊이 변수 · §16-2
누락 경고 단계: 하부·키큰 = 필수 · 상부 = 권장(전고 EP 있으면 면제) · 틈 ≤20 벽맞춤 · 20~60 휠라 · 60~200 대형 휠라+숨은 EP · >200 노출 · EP 깊이 < 몸통+도어 = 필수 · 이웃 앞선 ±5 = 권장 §18-1 그대로 · 휠라최대 200(가정 · 변수)
실사용 원장 2420~2440 · 이음 = 장 경계·줄눈 우선 · 걸레받이 = 구간+5 패킹(커프 4) + 여유 1 · 전고 EP 2440 초과 = 기존 선 가로 이음 / 9자 / 도장 특주 §16-3(한계 2420 · 막고 고름)
오픈장 부재·조립·결(측판 세로·가로판 가로)·노출 모서리 엣지·내경 800↑ 선반 보강 §19-3(치수는 엔진 정본 · 결 = grain+비고 · 선반 보강 권장 경고) · 18T 뒤판 비관행 지적은 페페 아티클(26.10.9) 그대로
천장휠라 h = 천장−몰딩−상부 위 · 리턴 몸통+20(≤400) · 숨은 EP = 상부 위까지 · 「몰딩판」 근거 없음 · 덧방 18T(9T) §16-2 ③ · §20 · Q19
경첩 관행(코너 반대·벽 끝 벽 쪽·키큰 옆 반대) · 손잡이 하부 위/상부 아래/키큰 1000~1100 · 좌·우 칸 = 경첩별 매수 · 손잡이는 비고 관행(추정) §21-2·§21-3 · Q20
상천판 590 = 앞 280 돌출 — 한국 주방 표준 부속 아님(냉장고장 천판·천장 단내림 값 혼입 의심) Q13 결정대로 앞 돌출로 그리되 돌출 > 100 = «확인» 표지(§16-4)

27. 페페 질문 (서로 독립 · 지금 답하면 바로 반영 · 답 전에도 ① 진행 가능)

# 질문 선택지 추천 · 이유 막는 런
Q18 걸레받이 들임 90mm 는 어느 면에서 잰 값인가 ① 도어 앞면에서 90(몸통에서 70) ② 몸통 앞면에서 90 ① — 관행(furniture-expert)·넥서스 KB 「몸통라인 안쪽 70」과 같다. 3D 위치만 바뀌고 규격 치수 영향 0 없음(기본 ① 로 진행 · 답이 ②면 변수 뜻만 바꿈)
Q19 ③ 「몰딩판」은 무엇인가 ① 상부장(키큰장) 위 ~ 천장을 막는 판(= 천장휠라 · 높이 큰 것) ② 천장 장식 몰딩(코니스) ③ 기타(도면 예시) ① — PO·KB 에 「몰딩판」 부재 사례가 없고 Molding 은 공제값. ①이면 천장휠라 규칙 하나로 끝난다 R16
Q20 ④ 손잡이 위치를 도어규격표에 어떻게 적나 ① 비고 관례(「손잡이 상」·「손잡이 하」·「손잡이 H1050」·「푸시」) ② 3D 표시만(규격표엔 안 적음) ③ 새 칸(포인탭 양식 변경 — 파서 2벌·월드 변환·스티커 영향) ① — 양식·파서 무변경 · 외주가 이미 비고를 읽는다 R19