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 보정
- R0 확정표 그대로 쓴다(H1 · doors 최상위 키 · door_rows 16키 · 장 uid 없음 · scene 56 매칭 · door.html 부작용 · L161 몸통/도어 경계).
- 보정 ① R0 「door_rows 밖 키 = PATCH 400」 → 실제는 HTTP 200 + 본문
code 9999. RestResponse.fail() 은 status 를 안 바꾼다(C:\dev\pfs\common\response\rest_response.py BUG[PFS-D002] 주석). door.html api()(L155-163)·app.html apiCall()(L4713-4737)은 code !== '0000' 로 판정한다. 새 검증도 같은 관례로 낸다 — HTTP 400 으로 바꾸면 app.html 은 메시지 대신 「서버 400」만 보인다.
- 보정 ② 몸통 PUT 은
dao.update() 에서 예외가 나면 컨트롤러가 안 잡아 500 이 된다(Scene.put L160-168). 새 모양 검사 실패는 컨트롤러에서 ValueError 를 잡아 9999 로 낸다(R2).
- 엔진 부재 상자 규약(실측
C:\Users\delix\AppData\Local\Temp\claude\c--dev\daf387c8-f84c-42f5-881e-fcb5cdcffdda\scratchpad\r1_doorpos.mjs): box = {x,y,z,w,d,h} · x = 장 왼쪽 0 → 오른쪽 · y = 몸통 앞면 0 → 뒤(벽 쪽) +D · z = 장 바닥 0 → 위. 도어 = y −18~0(몸통 앞). app.html mesh(b, mat, dim)(L1753)이 이걸 three 로 옮긴다(앞 = +Z, 벽 = world Z 0 · BACK_PLANE L3391).
- 운영판 차이(
origin/deploy-live vs pncad HEAD fa5d7f2): app.html 6066 vs 6728줄(rebuild·storeScene·loadScene 본문 다름 · delInsts/udUndo 없음 · 복제 = structuredClone 후 selInst+1 삽입 L4249) · schema.js 3702 vs 3876(도어 블록은 동일) · door.html·dao·controller·door_sheet 는 동일. 비교 스크립트 …\scratchpad\r1_cmp_live.py.
- 두 저장 경로는 이미 행 잠금(
SELECT … FOR UPDATE) + 한 트랜잭션이다(dao.update · dao.update_doors). 새 처리도 그 안에서만 한다(코덱스 구멍 1).
2. 데이터 계약
2-1. items[].uid (장 고유 ID)
- 형식
i + 8자 [0-9a-z]. 만들기 = crypto.getRandomValues. ⛔crypto.randomUUID 금지 — 테스트·운영 주소(http)는 보안 컨텍스트가 아닐 수 있다(tooling L50 함정).
- 부여 지점(master app.html): 카탈로그 배치 L2232 · 복제 L4656(클론마다 새 uid) · 열기
loadScene L5096 · 복구(localStorage) · 안전망 = rebuild() 첫 줄 ensureItemUids(SCENE.items) — 없는 것 부여 + 같은 uid 의 두 번째부터 재발급(앞 것 유지). 복제는 언제나 원본 뒤에 끼우므로(master at = 마지막+1 · live selInst+1) 원본 uid 가 살아남는다. live 판은 안전망 훅만으로 충분하다.
- 삭제 되돌리기(L3773)는 같은 객체를 다시 끼우므로 uid 그대로 → 부속이 제자리로 돌아온다.
- 부여만으로 dirty 를 켜지 않는다(열자마자 「● 변경됨」 금지) — 다음 💾 때 같이 올라간다. 확정한 배치가 가리키는 uid 는 그 배치와 같은 PUT 으로 저장되므로 끊어질 틈이 없다.
- 장 객체를 새로 만들어 갈아 끼우는 경로는 master 에 없다(grep
SCENE.items[…] = 0건 · 편집은 제자리) → 옛 판도 uid 를 그대로 돌려보낸다. R3b·R6b 가 live 판에서도 같은 grep 으로 확인한다.
- 서버는 items 를 안 본다.
nestRows()(키 = 부재 속성) · spec.html digest(명시 칸) · 규격표 OV 키(배치 번호)는 uid 를 안 읽는다 → 산출 불변.
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 가 각 리포에 그대로 저장)
- pncad
C:\dev\pncad\tools\fixtures\acc_cases.json · pfs C:\dev\pfs\tests_cody\fixtures\pncad_acc_cases.json
{ "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 를 바꿨을 때만 다시 눕힌다
- 컨트롤러
Scene.put: try: … except ValueError as e: response.fail(e) (보정 ②).
3-3. PATCH /doors 재연결 (door_place.relink) — 행 잠금 안
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
- 2단계가 3단계보다 먼저다: PO 를 다시 가져와 gid 가 밀려도(앞에 1줄 끼움·삭제) 내용이 1:1 이면 따라간다. 쌍둥이(같은 내용 둘)는 내용으로 못 가르므로 gid 로만(코덱스 구멍 2 「양방향 유일 1:1 만 자동」).
- 남는 위험 = «같은 gid 자리에 전혀 다른 품목이 새로 들어온» 경우 → 3단계가 붙인다 → 화면이 ck·kd 다름으로 «사양/품목 바뀜» 빨강(조용히 틀리지 않음). door.html 은 newGid 재사용 금지(§7)로 이 경우를 만들지 않는다.
- 🆕26.10.16 R5 I9e 보정 — 3단계에서 확정 때 ck 의 구분(div 첫 칸)과 그 gid 를 차지한 새 묶음 구분이 다르면 붙이지 않고 버림 +
relink.why[gid] = '구분 바뀜(20→10)'(도어 div 10 이 차지하면 화면·door.html 이 부속 아닌 묶음을 걸러 조용히 사라지던 것 · '|' 없는 추상 키는 비교 안 함). 화면 방어: 도어 묶음 gid 에 붙은 배치는 orphanGids 고아 줄(«부속 아닌 묶음(도어)에 붙어 있다»). 사례표 i9e-* 3건 · pfs cd9933be · pncad f578683.
- PATCH 응답 =
{scene_no, n, placements: out, relink: rep}.
3-4. GET /doors 추가
placements = data.placements or {} · item_uids = [it.uid for it in data.items if it.uid] — door.html 배지(기준 장 존재) 용. 몸통 items 는 여전히 안 싣는다.
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. 장 기하
- 장마다
{uid, wall, x0=x, x1=x+W, z0=z, z1=z+H, D, Wd=D+(y??0) 벽면 y, F=도어 앞면 y(엔진 부재 상자 최소 y · 도어 없으면 0), rot} — F 는 expandInst 결과(앱이 이미 계산)에서 받는다.
- 회전 장(
rot ≠ 0)은 run 에서 뺀다(제안 없음 · 수동 배치는 가능).
- 층: 상부 = z0 ≥ 1200 · 하부(기저) = 그 밖.
- 기둥 = 같은 벽·같은 층에서 x 범위가 50% 넘게 겹치는 장들(쌓은 장 L4 + L4-2) → x0=최소 · x1=최대 · 위 = 최대 z1 · 바닥 = 최소 z0 · 대표 = 위 장(T·F 기준) / 아래 장(B 기준).
- run = x 순으로 이웃 기둥 간격이 ±2mm 안인 최대 연속열. 부분 run = run 의 모든 연속 조각(i..j) · 폭 = x1(j) − x0(i).
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. 좌표계
- 장 기준 = 그 장의 엔진 부재 상자 규약(§1) — 템플릿이
{x,y,z,w,d,h} 를 내면 app.html 의 기존 mesh(b, mat, r.dim) + rebuild() 의 tf() 와 같은 변환(위치·회전) 그룹에 붙인다 → 장을 옮기고 돌리면 같이 간다.
- 벽 기준(
w) = 가상 장 {wall, x:0, z:0, W:0, D:0} → 그룹 위치 (wallX(w), 0, 0) · y 음수 = 벽에서 방 쪽. 같은 mesh() 로 그린다.
- 기호: W·D·H = 기준 장 치수 · z0 = 기준 장 바닥 높이(바닥 = −z0) · F = 도어 앞면 y(보통 −18) · Wd = 벽면 y · t = 부품 두께 · C = 현장
Ceiling_H(없으면 벽 h) · 주 = role main · 리 = return · 플 = flange · 모든 상자에 o 를 더한다.
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. 색 · 층 · 표시
- 색 = (부품 color 또는 그룹 color) 의
/ 앞 코드 → cmHex()·colorMat()(도어 재질 쪽). 자동색 풀은 비몸통 풀(kc) 맨 뒤에 붙인다 — 기존 도어·등판 코드의 자동 배정색이 밀리지 않게(규칙 26.10.13 「풀 판정 = 부재 역할」). 재질 교체는 cmPaint 규약(발광 중 base 교체).
- 층: 새 그룹
gAccEp(div 20 → EP 칩) · gAccDoor(div 30·40 → 도어 칩). 칩 이름 「EP·휠라·목찬넬」 유지(기본값 「부속」 칩 = 이 칩). 실사 모드는 보이는 층을 그대로 쓰되 재질 교체를 실기 확인(R3b).
- 제안 = 같은 색 35% 투명 + 점선 모서리(
LineDashedMaterial · computeLineDistances) · 확정 = 불투명 + 모서리선 · 불일치 = 빨강(#d93025) 모서리 + 패널 사유 · 보관대 = 마지막 벽 오른쪽(wallX(끝)+len+WALL_GAP) 바닥에 줄 세움 + 이름표. 도어 기호(gMark)는 안 그린다.
- 피킹 =
pointerdown(L3451) 앞단에서 부속 메시 먼저(userData.acc = {gid, k, sug}) → 부속 선택(장 선택과 별도 상태) · Esc 해제.
6. 화면 동작
6-1. app.html — 부속 패널(#doorList 자리 재사용)
- 머리: 「부속 15 · 확정 n · 제안 n · 모호 n · 미배치 n · 불일치 n」 + [제안 모두 확정] [확정분 저장] [표 열기].
- 줄: 품목 칩 · 제목(
doorTitle) · 상태 칩 · 동작 = 확정 / 해제 / 보관 / 후보 ▸(모호) / 위치(Δx·Δy·Δz 숫자 칸 13px) / 표에서 보기(door.html#gid=).
- 보관대 부속 클릭 → 「놓을 면 클릭」 모드 → 장 면(또는 벽) 클릭 → 그 품목 템플릿·면으로 놓음(
m:1).
- 규칙: 글자 ≥ 13px · 네이티브
<select>·<datalist> 금지(목록이 필요하면 droplist.js) · 오른쪽 위 64×60px 에 단추 금지(QA 헬퍼) · 모달 안 알림 = toast(text, host). web-designer 상담 권장(R3c).
- 미저장 알림: 확정·해제 = dirty → 기존 「● 변경됨」 + 패널에 「확정 n건 — 💾 저장 전」.
6-2. door.html
- 행 배지(13px): 확정 n/qty · 미배치 · 보관 · 불일치(사유 title). 계산 =
acc.js groupStatus(g, entry, {itemUids})(장 바뀜 제외).
- 행 번호 클릭 → BC
focus → app.html 강조. app.html 에서 「표에서 보기」 = 기존 openDoorSheet(gid).
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)
- 데이터: 테스트 8001 에 scene 56 덤프(
C:\dev\_work\daewoo-ulsan-munjuro\after_1016c\scene_56.json)를 POST 로 사본 1개(테스트 DB 쓰기만 · 운영 쓰기 0). 기준선 = 사본을 앱으로 열어 배치 없이 💾 1회(같은 엔진 기준 맞춤).
| # | 검사 | 합격 |
|---|---|---|
| I1 | 기준선 vs 배치 12건 확정·💾 뒤 GET /scene/<n>/doors/xlsx 전 시트 전 셀 값 | 동일 |
| I2 | 두 엑셀을 orderpaper-sync/door_order_parse.py parse_poin · api/pfs/estimate/service/door_order_parse.py parse_pages 로 읽은 결과 | 동일 |
| I3 | nest_rows(테스트 DB SELECT · _pncad_nest18_probe_LIVE.py 방식) | 동일 |
| I4 | door_rows 컬럼(PUT 전후) | 동일 |
| I5 | data 에서 items[].uid·placements 를 뺀 JSON | 동일 |
| I6 | door.html 에서 치수 1칸 고쳐 저장 → placements 그대로 · 엑셀엔 고친 값만 | 통과 |
| I7 | door.html 복제 → 새 gid 미배치 · 원본 배치 유지 | 통과 |
| I8 | 교차 탭: door.html 수정 ↔ app.html 확정·💾 ↔ door.html 다시 저장 | 둘 다 남음 |
| I9 | 외부 도구 흉내 PATCH(앞에 1묶음 끼우고 gid 재번호 · 1묶음 칼라 변경) | 내용 같은 것 이동 · 칼라 바뀐 것 미배치 · 응답 보고 일치 |
| I10 | 장 삭제 → 불일치 «기준 장 없음» · 되돌리기 → 제자리 | 통과 |
| I11 | 장 복제 → 새 uid · 부속은 원본에 남음 · 장 이동 → 부속 따라감 | 통과 |
| I12 | 구버전 흉내: placements 없는 PUT → 저장본 유지 · {} PUT → 비움 · 모양 오류 PUT → 9999 메시지 · 몸통 미저장 | 통과 |
| I13 | 회귀 | _e2e_3dui local 147/147 · _e2e_doors · _e2e_enterbest · _e2e_droplist · photo 145/145 · node schema.js 전 PASS |
- 명령:
node C:\dev\pncad\tools\_test_acc.mjs · cd C:\dev\pfs && python -m pytest tests_cody/test_pncad_placements.py tests_cody/test_pncad_door_sheet.py -q · python C:\dev\pncad\tools\_e2e_acc.py [local|test|prod] [scene] · python C:\dev\_work\pncad-ep3d\inv_check.py test <사본번호> · 헤드리스 렌더는 --use-angle=d3d11 --enable-gpu --ignore-gpu-blocklist(rules 26.10.14) · 실제 주소 http://pfs-test.poin.co.kr:8001 로 검사(localhost 금지) · Playwright 끝에 닫기.
- 반박: 코덱스 1회(diff 기준 «조용한 소실·오연결» 질문).
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)
- 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).
- 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.
- R5 통과 + 페페 테스트 확인 → 운영은 페페 원문 「운영배포」가 있는 세션에서(채팅 승인 = broker 텔레그램 재승인 면제 · 지시문에 옮겨 적은 승인은 무효).
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).
- 로컬 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.
- 되돌림 = 포워드 복원(이전 판 파일 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" } }
rule = §16-2 규칙 이름 · at = 대상(연속 기둥의 장 uid 목록 · 면 · 높이 모드 low|up|full · 벽 · 나눔 조각이면 set·i) · ov = 입력 칸 값(null = 규칙값) · sig = 마지막으로 묶음에 반영한 «계산 결과» 요약(이름·dim·qty·부품 name:thk:w:h:qty).
- 유효 연결 = doors 에 그 gid 묶음이 있고
src.id === gen.id. 아니면 «3D 기록 끊김»(도어규격표에서 지웠을 수 있다 · 자동 갱신 안 함 · 패널 [다시 만들기]=새 gid·새 id 로 생성 / [기록 지우기]). 예외 = 오픈장(§19-4: 장이 route='door' 인 동안 묶음이 없으면 화면이 다시 만든다).
- 3D 추가는 «미리보기 → 확정» 한 번에 묶음 + 배치(
st ok) + 기록을 같이 만든다 — 제안 상태로 저장되는 3D 묶음은 없다.
- 묶음을 바꿀 때마다
ck 를 같이 새로 쓴다(자동 갱신 뒤 «사양 바뀜» 오판 방지).
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 } ] }
door_ops 는 요청 본문 칸(data 밖) — 저장되지 않는다. data.doors 는 지금처럼 버린다(저장본 보존) — 3D 묶음은 door_ops 로만 들어간다.
- 저장본에
doors 키가 없을 때(옛 작업본)·POST(새 작업본) = 기존대로 들어온 doors·door_rows 통째(3D 묶음 포함 · rv 는 들어온 값) · door_ops 무시.
| 경우 |
처리 |
보고 |
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 묶음 보호 추가
- 본문 새 칸
del: [{gid, id, rv}] = door.html 이 이번 세션에 지운 3D 묶음(없으면 빈 목록 = 옛 door.html·외부 도구).
- 저장본 3D 묶음 S(gid 기준)마다:
| 들어온 배열 |
조건 |
처리 |
보고 |
| 사본 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 에 같이 넣고 ②·④ 까지 휴면)
door3d = data.door3d or null(④ 도어 대조 배지) · route_uids = items[].route=='door' 인 uid 목록(② door.html 오픈장 배지).
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. 공통
- 기호 = §5-1(x 장 왼쪽 0→ · y 몸통 앞면 0 → 벽 + · z 장 바닥 0 → 위 ·
Wd 벽면 y · F 도어 앞면 y · z0 장 바닥 높이 · C = Ceiling_H · M = Molding · 전고 위 = C − M(scene 56 = 2390)). 기둥·run = §4-1(runsOf).
- 사양 복사: color·back·부품 kind·side = 같은 pos 의 div 10 묶음 중 최다 칼라 묶음 → 없으면 div 10 전체 최다 → 없으면
SCENE.color + 패널 「칼라 확인」. 입력으로 바꿈(gen.ov.color).
- 품명 = PO 관례(§2-4 사전 첫 이름): 좌E/P·우E/P · 좌휠라·우휠라 · 상부휠라·상천판 · 목찬넬·목찬넬(플랜지) · 걸레받이 · 벽판넬 · 하부휠라 · 천장휠라 · 마감판 · 오픈장 → 월드 변환(
poin2world 큰제목·하위 품명 규칙)·견적 파서가 PO 행과 똑같이 다룬다. 비고 기본 = 빈칸(규격표 비고는 외주가 읽는다 — 내부 표식 금지 · 3D 여부는 화면 배지) · 예외 = 걸레받이 원장 비고(PO 관례).
- pos: 상부층 =
상부 · 하부층 = 하부(벽 이름에 「창」 = 창하부) · 전고·벽 기준·오픈장 = 주방 라인이면 주방 · 일반가구 라인이면 빈칸. 입력으로 바꿈.
- gid =
<접두>-3D<2자리> · 접두 = 기존 gid 최다 접두(^(.*)-(?:G|3D)\d+$) → 없으면 PN<작업본 번호>(새 작업본 = PNNEW) · 번호 = doors gid · placements 키 · 세션에서 쓴 3D 번호의 최대 + 1. src.id = g+8자.
- 겹침 사전 검사: 같은 자리(같은 기준 장·면 또는 상자 부피 50% 넘게 겹침)에 이미 묶음(PO·수동·3D)이 확정·제안돼 있으면 확정 전에 「G01 좌휠라가 이미 이 자리 — [그 묶음 확정] [그래도 새로]」(이중 생산 방지 · §18-3).
- 입력 칸 = 규칙 표의 «입력» 열만(근거 약한 것) · 나머지는 계산값을 회색 글자로 보인다.
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 을 조용히 붙이는 길을 생성 단계에서 막는다)
- 한계: 한 판 = 짧은 변 ≤ 1220 · 긴 변 ≤ 2420(실사용 — 효산 원장 2440 · furniture-expert 「2420~2440」) · 원장 그대로 쓰는 걸레받이 부품만 2440. 불변식 = 생성 부품 긴 변 > 2440 = 0(§23 G7).
- 길이 방향(상부휠라·상천판·목찬넬+플랜지·천장휠라): 구간 폭 L > 2420 → 조각 수 k = ⌈L/2420⌉ 부터, 그 구간의 기둥 경계 조합 중 모든 조각 ≤ 2420 이고 가장 긴 조각이 가장 짧은 것 → 없으면 k+1 → 기둥 하나가 2420 넘으면 균등 분할 + 경고. 조각마다 별도 묶음(gid 각자 ·
gen.at.set 같은 값 · gen.at.i 순번 · 배치 u = 조각 첫 기둥). 다시 계산 때 조각 수가 바뀌면 묶음을 늘리거나 지운다(비수동만 · 수동이 섞이면 그 집합 전체 경고).
- 원장 품목(걸레받이): 조각마다 +5(재단 여유) · 2440 에 First-Fit-Decreasing(조각 사이 커프 4) → 장수 +
걸레받이여유장 = qty. scene 56 = 2255 / 900+615 → 2 + 1 = 3(PO G30 와 같음).
- 판이 1220×2440 에 안 드는 높이·폭(천장 2450 넘는 현장의 전고 EP·벽판넬 폭 1220 초과 등): 미리보기 빨강 + 확정 막음 → 사람이 고른다: [기존 선에서 이음으로 나누기 — 상판선(하부 기둥 위) / 상부장 하단선 / 세로 이음] · 9자(2700) 원장 표기 — 비고 「2700 원장」 · 18T 만 · 장당 비용 증가. 이음 = 기존 선에 숨긴다(furniture-expert 「장 경계·줄눈·선반라인 우선」).
16-4. 벽 관통 금지 = 앞 돌출 (Q13 · 일반 원칙)
acc.js template() 끝: 기준이 장(회전 0)이고 어떤 상자든 y + d > Wd + 0.5 이면 그 배치(pl)의 모든 상자 y 를 최대 넘침만큼 앞(−y)으로 민다 — ㄴ자·ㄱ자 조립체가 깨지지 않게 통째. 반환 상자에 fwd(mm) → 3D 이름표 「앞 돌출 282」. pl.o 에는 굽지 않는다(계산값 · 장 깊이가 바뀌면 다시 계산 · 데이터 변경 0).
- 1차 §5-2 «벽 관통» 표시는 없앤다 — 이미 확정된 1차 배치(scene 56 G05·G06 상천판 590)도 같은 규칙으로 그려진다(앞 282 돌출).
- 돌출 > 100 = 패널 «확인» 표지(경고 아님 · furniture-expert: 상부장 위 280 돌출판은 한국 주방 표준 부속이 아님 → 도면 확인 권장. 페페 결정 Q13 은 그대로 따른다).
- 벽 기준 배치(
w)는 이미 y ≤ 0(방 쪽) 규약 · 회전 장은 벽면이 없어 적용 안 함.
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) · 삭제
- 계산 =
rebuild() 끝 ACC.rebuild() 안에서 accgen.regen(scene, geoms) → 생성 기록마다 «기대 묶음·배치·sig».
| 묶음 |
기대 = 지금 |
다름 · 이번 세션 편집 때문(직전 계산값 ≠ 지금 계산값) |
다름 · 열 때부터(첫 계산값 = 기준) |
| 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}]
- 대상 = 벽·층별 run 의 첫·끝 기둥 측면(회전 장 제외).
gap = 벽 시작 → 첫 기둥 x0 / 끝 기둥 x1 → 벽 len.
| 조건 |
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)
- 두 묶음(출처 무관)의 배치 상자가 작은 쪽 부피의 50% 넘게 겹치면 = 필수 「겹침 — G01 ↔ 3D01 이중 생산 의심」. 3D 추가 확정 전 사전 검사(§16-1)와 같은 함수.
- 같은 묶음의 조각(나눔 집합)끼리는 빼고, 도어 묶음(div 10)은 대상 아님.
19. 오픈장 (단계 ② · Q8)
19-1. 표지 · 3D
items[i].route = 'door' = 이 장은 도어규격표로 생산(없음 = 몸통). 포인 자체 오픈장 아티클(C:\dev\pncad\data\poin-articles.json OPN_CORNER_400·OPN_BASIC_400)에 "route": "door" 기본값 → 카탈로그 배치 때 장에 복사 · 장 팝업 토글 「오픈장 — 도어규격표로 생산」(다른 장에도 켤 수 있다).
- 3D = 그대로 장 엔진(칸·선반·뒤판) · 재질 = 도어색(
colorMat(hex, door)) · 몸통두께 18 강제(토글이 inst.bodyThk = 18 를 같이 켬 · 끌 때 원래 값 복원 · bodyForce 경로 재사용 — rules 26.10.2).
19-2. 몸통 쪽 제외 — 판정 한 곳 schema.js isDoorRoute(inst)
- 빼는 곳: app.html
nestRows()·matUse()·규격표 bom()/renderBom·renderSum(부재 수) · spec.html digest() → 대신 「오픈장(도어규격표) n개 — 장 이름」 한 줄. 3D·이름표·치수·선택·AI 연출은 그대로.
- R13 이
allParts() 호출처를 전부 grep 해 «몸통 산출»만 거른다(목록을 보고서에 남긴다). 새 산출 경로를 만들면 이 판정을 탄다(rules 신규 줄로 남길 것).
19-3. 묶음 생성 (accgen.openGroup(inst, parts, ctx) · gen3d · src.kd 'open' · src.u = 장 uid)
- 묶음: div
40 · join D · shape 빈칸 · name 오픈장(월드 변환 KEEP_KEYWORDS = 블록 유지 · 견적 파서 제목 형식) · dim W*D*H · qty = inst.qty · pos §16-1 · 칼라 = 장 칼라(inst.color) → 없으면 도어 최다 칼라 · back·kind·side 복사.
- 부품 = 장 전개 부재(emit 부재 전부 · 앞면 없음)를 «역할·두께·치수»로 묶음: 품명 = 역할 표시명(
roleLabel — 측판(좌)·측판(우)·천판·지판·고정선반·이동선반·뒤판·칸막이·좌대) · 세로판(측판·칸막이·뒤판) = w 깊이(뒤판은 폭) × h 높이 · grain 세로 · 가로판(천·지·선반·좌대) = w 폭 × h 깊이 · grain 가로 + 비고 「결 가로」(포인탭엔 결 칸이 없다 → 비고로 전달) · qty = 같은 치수 개수 · thk = 부재 두께 · 엣지 칸 = 빈칸(칸 의미 확인 전 · 도장 마감).
- 치수 = 엔진 전개값(3D 와 같은 값). furniture-expert 표준식(측판 H×D · 천·지·고정 (W−36)×(D−뒤판) · 이동 (W−38)×(D−뒤판−20))은 아티클 설계 검토용 — 엔진이 정본.
- 검사(권장): 선반 내경 ≥ 800 = 「23T 또는 전면 보강 확인」(furniture-expert).
- 자동 갱신(§17): 장 치수·칸·선반이 바뀌면 다시 묶음(비수동) · 수동이면 경고. 장 삭제 = 그 묶음 pending del(오픈장은 장이 곧 제품).
19-4. 생산 경로 정확히 하나 (코덱스 구멍 3)
- 서버(PUT·POST 병합 뒤 ·
door_place.check_open_route(items, doors)): ① route='door' 장마다 src.kd='open'·src.u=uid 묶음이 정확히 1개 ② open 묶음마다 그 uid 장이 있고 route='door'. 어기면 9999 · 몸통도 저장 안 함(메시지 = 장 이름·gid).
- 화면이 저장 전에 같은 검사로 맞춘다: 토글 켬 = 묶음 생성(pending up) · 끔 = pending del(수동 묶음이면 「도어규격표에서 고친 오픈장 묶음도 지운다」 확인 →
force) · 장 삭제 = del force · 복제 = 새 uid → 새 묶음.
- PATCH(door.html)는 그 장이
route='door' 인 동안 open 묶음을 못 지운다(§15-4 open_kept).
19-5. 왕복·외주·네스팅 영향
- 도어규격표 엑셀 = G 「오픈장 WDH=n」 + B 부품행 →
orderpaper-sync/door_order_parse.py parse_poin·api/pfs/estimate/service/door_order_parse.py parse_pages 로 그대로 읽힌다(§23 G6) · poin2world = 「오픈장」 KEEP(블록 유지) · 스티커 동기화(door_order_sync) = 일반 행.
- 몸통 네스팅: 그 장 부재만 nest_rows 에서 빠진다(§23 G5). door_rows 의 네스팅 소비처 = 0(26.10.16 재확인 — pfs 안 door_rows 사용처 = pncad dao·controller 뿐) → 도어(협력사·월드) 쪽은 도어발주서 경로로만 받는다 → 영향 = 발주서에 오픈장 블록이 생김.
- ⚠ 이미 몸통으로 저장된 오픈장 장(26.10.9 이후 OPN_ 배치본)은 토글을 켜고 💾 해야 nest_rows 에서 빠진다(저장 시점 nest_rows · rules 26.10.2) — 배포 보고에 «영향 작업본 목록»(운영 SELECT · OPN_ 참조) 첨부.
20. 마감재 판재류 (단계 ③)
- 규칙 = §16-2 ③ 줄(
ceil_fil 천장휠라 · side_pnl 측면 마감판 · cornice 몰딩판). 템플릿 = acc.js fil_top(T) 틀 재사용 + panel L/R 판(§5-2 «나머지·고른 면»을 L/R 측면 판으로 굳힘).
- 천장휠라 = 상부휠라와 같은 틀이지만 대상이 «아무 기둥 위»(키큰장·붙박이장 포함) · 높이 가변(50·60 관행 · 계산값) · 리턴 = 몸통+20(제작 ≤ 400 후 현장 재단).
- 측면 마감판(덧방) = 노출 측판 위에 붙이는 18T 판(9T 선택) · 노출면 경고(§18)의 덮개로 인정.
- 몰딩판 = 정의가 근거 없음(furniture-expert:
Molding 변수는 천장 몰딩 공제값이지 부재가 아니고 「몰딩판」 PO 사례 없음) → §27 Q19 답 뒤 규칙 확정. 사전 cornice(코니스·몰딩)는 1차 그대로.
21. 도어 대조(Q15) · 경첩 · 손잡이 (단계 ④)
21-1. 도어 대조 — 전부 표시 · 불일치 색
- 3D 도어 = 전개 부재 중
emit === false(도어·서랍앞판·인출망도어) · 치수 = 앞면 가로 × 세로(반올림 mm) · 층 · 경첩측(p.hinge).
- 표 도어 = div 10 묶음 부품 · 매수 =
parts.qty × qty · 좌·우 칸.
- 맞춤: ① 같은 치수(±1) → 수 비교 → 일치(초록 #1e8e3e) / 수량 다름(주황 #e8710a) ② 남은 것끼리 ±5 → 치수 근접(노랑 #b88400) ③ 나머지 = 3D 에만(보라 #8e24aa) / 표에만(빨강 #d93025 · 3D 물체가 없어 패널·door.html 에만).
- 화면: 앱 = 「도어 대조」 보기 칩(켜면 도어 메시 모서리 상태색 · 강조는 색으로만) + 패널 표(치수·3D 수·표 수·상태 · 전부) · door.html = div 10 행 배지(저장 시점
data.door3d 기준 · 「💾 시점」 표기).
data.door3d = 💾 때 앱이 갱신(§15-7) · GET /doors 가 싣는다(§15-5).
- scene 56 기대 = R18 이 픽스처로 고정(도어 12묶음 · 20매).
21-2. 경첩 방향
- 3D 정본 = 엔진
hinge(imos DOORHISIDE > CP 이름 꼬리표 > 좌 · 더블 = 짝 위치 — schema.js hingeOf) + 장별 덮어쓰기 items[i].doorOpt[pkey].hi(키 = 부재 pkey · k = CP 이름이 다르면 무시 + 고아 보고 — 라운드 own 과 같은 규약 rules 26.10.9). schema.js expand(opts.doorOpt) 가 덮는다 → gMark(대각선)·도어 대조·규격표 채우기가 같은 값을 읽는다. 새 전개 경로를 만들면 doorOpt 도 넘긴다.
- 3D: 도어 클릭 → [경첩 좌↔우] · 확인 표지(권장) = furniture-expert 관행(직각 코너 인접 = 코너 반대 · 벽 끝 = 벽 쪽 · 키큰장·냉장고 옆 = 반대)과 엔진값이 다르면 «경첩 확인»(자동 변경 없음).
- 규격표: 「좌」「우」 칸 = 경첩 방향별 매수(합 = 좌+우) → door.html 3D 로 좌·우 채우기.
21-3. 손잡이 위치
doorOpt[pkey].hd = T(도어 위쪽) · B(아래쪽) · M(높이 ho) · -(없음) · ht = bar(기본 128) · al(포인 AL 손잡이 = 도어 전체 길이). 기본 = 하부 T · 상부 B · 키큰장 M(바닥에서 1050 — 부속 변수 손잡이높이 추가) · 찬넬 CP(noHandle)·비고 push·J/G = -.
- 3D = 도어 앞 작은 막대(gMark 층 · 칩 「도어」).
- 규격표 표기 = §27 Q20 답 뒤(추천 = 비고 관례).
22. 화면 요약 (web-designer 상담 = R9c·R14·R19 착수 전)
- 앱 부속 패널 머리: [+ 추가] · 「노출면 n」 · 「갱신 필요 n」 · (④) 「도어 대조」 칩. 추가 = 품목 칩 → 3D 클릭(1차 «놓을 면 클릭» 모드 재사용) → 점선 미리보기 + 입력 칸 → [확정]. 줄 배지 = 「3D」·「3D·수동」·「기록 끊김」·「앞 돌출 n」.
- door.html: 묶음 배지 「3D」·「3D·수동」(고치는 순간 토스트 「3D 자동 갱신을 껐다」) · 3D 묶음 [3D 자동 갱신 다시 켜기] · 삭제 = del 목록 · 오픈장 묶음 배지 「오픈장(3D 장)」 · (④) div 10 대조 배지 · [3D 로 좌·우 채우기] · gidPrefix 정규식
-(?:G|3D)\d+$.
- 규칙: 글자 ≥ 13px · 네이티브
<select>·<datalist> 금지(목록 = droplist.js) · 오른쪽 위 64×60px 단추 금지(QA 헬퍼) · 모달 안 알림 = toast(text, host) · 3D 위 단추는 pointerdown stopPropagation(rules 26.10.8).
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 |