# 용접선 케이스북 (CASEBOOK v1)

> 목적: "어떤 상황일 때 어떻게 푸는가"를 카드로 쌓는 정답지.
> 새 부품 만날 때 이 카드를 먼저 꺼내 대조한다.
> 추가 규칙: 검증됨 = 커밋 해시 명시. 미검증·부분은 정직하게 표시.

---

## 카드 양식
```
## 케이스 [번호]: [상황 이름 — 현장 말로]
- 증상: 화면/콘솔에서 뭐가 잘못 보이나 (한 줄)
- 원인: 왜 그러나 — 물리적/구조적 이유
- 해결: 어떻게 풀었나 (한 줄)
- 판별법: 어떤 상황이면 이 케이스인가
- 쓰는 함수/위치: 코드 어디서 처리되나
- 상태: 검증됨(커밋 해시) / 미검증 / 부분
```

---

# [검증됨] 케이스

---

## 케이스 01: 조립 모델에서 단품(블럭)이 섞임 — g태그로 부품 구분
- 증상: 판A(리브)를 클릭했는데 옆 부품(뒷판·다른 리브)까지 영역이 번져 용접선이 엉뚱하게 잡힘
- 원인: OBJ 파일에 그룹태그(g)가 있지만 메시 로드 시 버려짐 → BFS가 그룹 경계를 무시하고 인접 삼각형으로 계속 확장
- 해결: OBJ 파싱 시 g태그 순서를 `_faceGroupId` 배열로 보존 → 클릭 시 같은 그룹ID 면만 선택(selectFaceRegion BFS 중단 조건 추가)
- 판별법: 클릭 1번에 붙지 않은 여러 부품이 한꺼번에 빨간색으로 칠해짐. 콘솔에 "BFS 그룹 경계 중단" 로그 없으면 이 케이스.
- 쓰는 함수/위치:
  - `dozikworks_engine_v5.js` → OBJ 파서에서 `_faceGroupId` 배열 생성
  - `selectFaceRegion` (engine_v5) → 그룹ID 다르면 BFS 중단
  - `_seamAGroup` (engine_v5:614) → 판A 그룹ID 전체를 grpA로 확장
- 상태: **검증됨** (3495e90 g태그 보존, e606269 _seamAGroup g태그 교체, 14d6631 BFS 중단)

---

## 케이스 02: 리브(보강판) 바닥 둘레 용접선
- 증상: 리브를 클릭하면 측면·윗면까지 포함돼 용접선이 리브 전체 둘레로 나옴(바닥선만 나와야 함)
- 원인: 리브의 오픈 경계 엣지는 바닥(하판에 닿는 면)과 측면 모두 포함됨. 측면 엣지도 오픈 경계이므로 구분이 안 됨.
- 해결: `_seamAGroup` — 판A(리브) 그룹 중 판B(하판)에 가장 가까운 면(바닥면)만 ribFloor로 선별 → 그 면의 오픈 경계만 seam으로 추출
- 판별법: 판A=리브(세워진 판), 판B=하판(눕혀진 판). 두 판이 T자로 만나되 메시상 공유 모서리 없음(별도 단품). 콘솔 `[그룹경계seam]` 로그로 확인.
- 쓰는 함수/위치:
  - `_seamAGroup` (engine_v5:614) — 그룹 BFS + 바닥면 선별 + 오픈경계 추출
  - `handlePointerDown` (edgepath_v5:1002~) → `sharedBoundary` 없으면 `gapDiagnose` fallback
- 상태: **검증됨** (1b3bef7 _seamAGroup 추가, e606269 g태그 방식으로 교체)

---

## 케이스 03: 삼각 보강재 안에 홀(구멍)이 생겨 용접선이 끊기거나 구멍 둘레가 잡힘
- 증상: 삼각 보강재를 클릭하면 파란 용접선이 외곽만 아니라 안쪽 구멍 둘레도 함께 표시됨
- 원인: 삼각 보강재 메시에 경량화 홀이 있어 바닥면 오픈경계가 외곽 1개 + 홀 N개로 분리됨. 전부 opaen 경계이므로 필터 없이는 홀도 포함.
- 해결: seam 엣지를 체인으로 묶어 XZ 바운딩박스 면적이 가장 큰 체인만 반환. 홀=작은 닫힌 루프, 외곽=큰 루프이므로 자동 분리.
- 판별법: 파란 용접선이 외곽선과 안쪽 원형/다각형 선이 동시에 나옴. 콘솔에 "홀제거" 또는 "외곽N개" 로그 확인.
- 쓰는 함수/위치:
  - `_seamAGroup` 안 홀제거 블럭 (engine_v5, 9214d30에서 41줄 추가)
  - 체인 분리: ptKey 인접 맵 → DFS → XZ bbox 면적 비교 → best 반환
- 상태: **검증됨** (9214d30 홀제거, XZ 바운딩박스 최대 체인만 반환)

---

## 케이스 04: 체인이 끊겨 용접선이 여러 토막으로 쪼개짐
- 증상: 파란 용접선이 한 줄로 이어지지 않고 짧은 선 여러 개로 끊겨 보임. 콘솔에 구간 5개 이상 표시.
- 원인: 스캔 메시(또는 CAD→메시 변환) 과정에서 인접 정점이 수 mm 어긋남 → `_splitChains`에서 끝점이 연결로 인식 안 됨.
- 해결: `_mergeChains` — 체인 끝점 거리 ≤ 8mm 이면 이어붙이기(RoboDK Join Curve Tolerance 원리). 8mm는 `TUNING.CHAIN_MERGE_MM`으로 설정.
- 판별법: 콘솔에 구간 개수가 실제 용접선 개수보다 훨씬 많음. 구간10·12 등 짧은 끊김(5mm 이내)이 있으면 이 케이스. 25mm 이상 끊김은 물리적 별개 구간(다른 대응 필요).
- 쓰는 함수/위치:
  - `_mergeChains` (engine_v5:408~) — toleranceMm=TUNING.CHAIN_MERGE_MM(8)
  - `_seamToPoints` (edgepath_v5) → _splitChains 후 _mergeChains 호출
- 상태: **검증됨** (b96d14d — 구간10·12 끊김 해결 확인)

---

## 케이스 05: 보스(원통 돌기) 360도 측면 용접선
- 증상: 보스 측면을 클릭하면 윗면(캡)이나 바닥면까지 포함돼 원통 전체가 잡힘. 또는 너무 좁게 잡혀 측면 일부만 선택됨.
- 원인: `growSmoothRegion`은 법선 차이로 확장하는데, 원통 측면은 법선이 연속적으로 돌아가서 캡면과의 경계 구분이 어려움. `selectFaceRegion`은 평면 기준이라 곡면에 부적합.
- 해결: `_selectAutoRegion` — 클릭 면 인근의 교차축(cross product 누적)으로 실린더 감지. 실린더로 판정되면 축에 수직인 면(측면)만 BFS. 비실린더면 `growSmoothRegion` fallback.
- 판별법: 보스(원형 돌기)를 클릭할 때. 콘솔에 `[자동선택] 실린더 감지 → 축(x,y,z)` 로그가 뜨면 이 케이스.
- 쓰는 함수/위치:
  - `_selectAutoRegion` (edgepath_v5) — axisCount≥2이면 실린더 BFS, 아니면 fallback
  - `handlePointerDown` mode 1/2 판A 클릭 → `_selectAutoRegion` 호출
- 상태: **검증됨** (b87bed1 _selectAutoRegion, 46195b6 정리, fef286c cross product 부호 통일)

---

## 케이스 06: 여러 면을 판A/판B로 누적 지정 — N클릭 방식
- 증상: 보스가 여러 개이거나 리브가 띄엄띄엄 있을 때 1번 클릭으로 전체를 못 잡음
- 원인: 1클릭 = 1면 선택이라 물리적으로 연결 안 된 여러 단품을 한 번에 지정 불가
- 해결: 판A/판B 버튼을 각각 N번 클릭해 누적 선택. 클릭마다 영역 추가(union). "용접선 실행" 버튼으로 최종 실행.
- 판별법: 같은 역할의 단품이 2개 이상 흩어져 있을 때. 또는 면이 복잡해 한 번에 안 잡힐 때.
- 쓰는 함수/위치:
  - `handlePointerDown` (edgepath_v5) — mode 1~4 누적 union 구조
  - a2c5ed2 참고
- 상태: **검증됨** (a2c5ed2 N클릭 방식 개편)

---

# [미해결] 케이스

---

## 케이스 07: 뒷판에 붙은 변이 용접선에 섞임
- 증상: 삼각 보강재 3변 중 뒷 큰 판에 붙은 1변까지 파란 용접선에 포함돼 L자·T자가 아닌 V자 또는 삼각형 전체 선이 나옴
- 원인: 현재 오픈경계+판B근접 방식은 숫자(거리·각도)로 필터링 → 부품 크기·각도마다 수치 다름 → 한 부품 맞추면 다른 부품 깨짐 (두더지잡기)
- 해결: 미해결. 올바른 방향: "판A 오픈경계 엣지 중 판B(플레이트)와 좌표 공유 → 용접선, 제3 부품(뒷판)과 좌표 공유 → 제외" (그룹 좌표 공유 판별법 — 구현 안 됨)
- 판별법: 삼각형·다각형 보강재를 판A로 지정할 때. 뒷판(다른 부품)과 한 변을 공유함.
- 쓰는 함수/위치: 미정 (그룹좌표 공유 판별 함수 필요 — engine_v5에 없음)
- 상태: **미해결**

---

## 케이스 08: 클릭한 면이 과도하게 잡힘 (면이 번짐)
- 증상: 판A를 1번 클릭했는데 의도한 부품보다 훨씬 큰 영역(옆 면, 연결된 다른 부품)이 빨간색으로 칠해짐
- 원인: 그룹태그가 없거나 OBJ 구조상 그룹 경계가 모호한 모델에서 BFS가 멈추지 않음. 또는 `growSmoothRegion` 임계값이 해당 부품 곡률에 맞지 않음.
- 해결: 미해결. g태그 보존(케이스01)으로 일부 해결됐으나, g태그 없는 모델이나 동일 그룹 내 여러 면이 연결된 경우는 여전히 번짐.
- 판별법: 클릭 후 의도한 면보다 2배 이상 큰 영역이 선택됨.
- 쓰는 함수/위치: `selectFaceRegion`, `growSmoothRegion` (engine_v5)
- 상태: **미해결 (부분 — g태그 있는 모델은 해결됨)**

---

## 케이스 09: 사선 판·큰 갭 용접선
- 증상: 45°로 기운 사선 판이나 두 판 사이 갭이 10mm 이상인 경우 용접선이 엉뚱한 위치에 잡힘
- 원인: `gapDiagnose`가 판A 법선을 주축(X/Y/Z)으로 강제 스냅 → 45° 사선 판에서 실제 법선과 다른 방향으로 스냅되어 판B 경계 거리 필터 오작동
- 해결: 미해결. 올바른 방향: `pickFacePlane+planeIntersectLine+clipLineToMesh` 평면교차 방식 (engine_v5:156~221에 구현돼 있으나 라이브 연결 안 됨 — A단계 초록점 표시까지만 구현)
- 판별법: 판A가 수직/수평이 아닌 대각선 방향. 또는 두 판이 물리적으로 떨어져 있음(용접 준비 상태).
- 쓰는 함수/위치:
  - 미연결 함수: `pickFacePlane`, `planeIntersectLine`, `clipLineToMesh` (engine_v5:156~221)
  - 현재 A단계: 초록점 표시만 (edgepath_v5 9be7c91)
- 상태: **미해결 (A단계 표시만 구현됨 — 경로 등록 미연결)**

---

## ⚠️ 케이스 10: 아크 trim 후 판B 모드 유지 — 다음 클릭이 판B로 처리됨 [재발 절대 금지]
- 증상: 아크 ON/OFF 찍고 trim 완료 후, 화면 아무 데나 클릭하면 판B 클릭으로 처리되어 2번·3번 구간이 의도 없이 자동 생성됨
- 원인: `_applyArcTrim`이 완료된 후 `_faceMode`를 리셋하지 않아 mode=3(판B 대기)이 그대로 유지됨. trim은 경로만 자를 뿐 선택 모드를 건드리지 않음.
- 해결 방향: `_applyArcTrim` 완료 시점에 `_faceMode=0` 또는 작업 완료 상태로 리셋 필요
- 판별법: 아크 trim 완료 로그 직후 판A 선택 없이 `[자동모드] 판B 누적:` 로그가 찍힘
- 쓰는 함수/위치: `_applyArcTrim` (edgepath_v5:858) — trim 완료 후 모드 리셋 누락
- 상태: **미해결 — 재발 절대 금지 (2026-07-01 확인)**

---

## ⚠️ 케이스 11: 판B 누적 클릭으로 체인 엣지가 뒤섞여 용접선이 지렁이처럼 꼬임 [재발 절대 금지]
- 증상: 판B를 여러 번 클릭할수록 공유경계 엣지가 누적(150→599개)되고, 이 엣지들이 `_splitChains`→`_mergeChains`를 거치면서 서로 다른 위치의 점들이 하나의 체인에 뒤섞임. 결과적으로 용접 경로가 앞뒤로 튀는 지렁이 형태가 됨.
- 원인: `_faceBRegion`이 클릭마다 누적(union)되는 구조에서, 공유경계 엣지도 누적되는데 이 엣지들의 위상(연결 순서)이 보장되지 않음. 실제 확인된 사례: 체인 범위 x=1740~1873인데 중간 점이 x=10.2로 튐.
- 해결 방향: 판B 클릭마다 `_faceBRegion`을 누적하지 말고, 클릭한 면만으로 경계를 계산하거나 — 누적이 필요하면 엣지 순서 정렬 로직 추가 필요
- 판별법: 파란 용접선이 직선이 아닌 지그재그·루프·꼬임 형태. 콘솔에서 아크 OFF 마커 좌표가 체인 start/end 범위를 벗어남.
- 쓰는 함수/위치: `_seamToPoints` (edgepath_v5) → `_splitChains` → `_mergeChains` — 체인 내부 점 순서 미보장
- 상태: **미해결 — 재발 절대 금지 (2026-07-01 확인)**

---

## ⚠️ 케이스 12: 용접선이 지그재그(톱니) 형태로 꼬임 — 체인 탐색 방향 오류 [재발 절대 금지]
- 증상: 파란 용접선이 직선이 아닌 톱니/사인파 형태로 지그재그함. 곡면(라운드) 경계에서 특히 발생.
- 원인: `_splitChains`에서 다음 탐색 점을 `find(첫 번째 미방문 이웃)`으로 고름. 한 꼭짓점에 미방문 이웃이 여러 개일 때 이동 방향과 반대 방향 점이 선택되어 앞뒤로 튀는 탐색 경로가 만들어짐.
- 해결: 다음 점 선택 시 직전 이동 벡터와 내적(dot product)이 가장 큰 이웃을 선택 — 방향 연속성 유지. (`_splitChains` engine_v5.js 수정)
- 판별법: 용접선 튜브가 지그재그·톱니·사인파 형태. 곡면/라운드 경계에서 발생 빈도 높음.
- 쓰는 함수/위치: `_splitChains` (engine_v5.js:381) — 다음 이웃 선택 로직
- 상태: **수정됨 (캡처 확인 전)**

---

# [참고] 현재 정상 작동 흐름 요약

```
판A 클릭
  └─ _selectAutoRegion → 실린더? → 축BFS 측면선택
                       → 비실린더? → growSmoothRegion
  └─ _planeAHit 저장 (평면교차용)
  └─ 빨간 영역 표시

판B 클릭
  └─ _selectAutoRegion (동일)
  └─ sharedBoundary → 공유 모서리 있으면 직접 사용
  └─ 없으면 gapDiagnose → _seamAGroup
       └─ g태그 그룹ID 기반 ribFloor 선별
       └─ 오픈경계 추출
       └─ 홀제거 (XZ bbox 최대 체인)
  └─ _registerSeamAsPath → _splitChains → _mergeChains(8mm) → _teachSegments 등록
  └─ _drawSeamWeld → 파란 튜브 표시
  └─ 평면교차 초록점 표시 (A단계, 별도)
```

---

---

## ⚠️ 케이스 13: 화면 시작 시 JBI 파일이 자동으로 로드돼 로봇이 임의 자세로 시작됨 [재발 절대 금지]
- 증상: DOZIKWORKS OS 첫 화면에서 JBI 선택란이 "12500(IN2222).JBI"로 표시되며 로봇이 JBI의 첫 자세로 이미 위치해 있음. 사용자가 아무것도 선택하지 않았는데 JBI가 활성화된 상태.
- 원인: `componentDidMount`에서 `fetch('jbi.json')` 1줄이 앱 초기화 시 항상 실행됨 → jbi.json(12500 IN2222 데이터)이 자동 파싱·로드.
- 해결: `componentDidMount` 안 `fetch('jbi.json')...` 1줄 삭제. JBI는 상단 피커에서 사용자가 직접 선택할 때만 로드됨.
- 복원 방법 (이 동작이 필요해질 때): `DOZIKWORKS_OS_v5_locked.dc.html` 1101번 줄 아래에 아래 1줄 추가:
  ```js
  fetch('jbi.json').then(r=>r.json()).then(d=>{ this.jbi=d; this.buildWeldFromJbi(); this.setJoints(this.poseAt(0)); this.forceUpdate(); this.log('INFO','SYSTEM','JBI 로드: '+d.n+' 스텝 · 용접 '+d.blocks.filter(b=>b.cat==='weld').length+'구간'); }).catch(()=>{});
  ```
  연결 JBI: `E:\도진팩토리\3D스캔및티칭시스템\jbi.json` (12500 너클 IN2222 데이터)
- 판별법: 새로고침 직후 상단 JBI 선택란에 파일명이 표시되어 있고 콘솔에 "JBI 로드: N 스텝" 로그가 보이면 이 케이스.
- 쓰는 함수/위치: `componentDidMount` (v5_locked.dc.html 약 1101번 줄)
- 상태: **수정됨 (커밋 f05957c, 2026-07-02)**

---

## ⚠️ 케이스 14: 정반 위에 용접 경로선(4색 튜브)이 첫 화면부터 표시됨 [재발 절대 금지]
- 증상: OS 첫 화면에서 아무것도 선택하지 않았는데 3D 정반(jeonban) 위에 빨강·파랑·초록·주황 용접 경로 튜브와 진행방향 원뿔이 미리 그려져 있음.
- 원인: 3D 씬 초기화 함수(`_initThree`) 안에서 `this.loadWeldPath()` 가 호출됨 → `weld_path.json`(12500 너클 IN2222 경로 데이터)을 자동 fetch·렌더링.
- 해결: `_initThree` 안 `this.loadWeldPath();` 1줄 제거. 주석으로 위치 보존(케이스북 참조 표시).
- 복원 방법 (이 동작이 필요해질 때): `DOZIKWORKS_OS_v5_locked.dc.html`에서 아래 주석 해제:
  ```js
  // this.loadWeldPath();
  ```
  위치: `_initThree` 내부, `this._initTransformControls(...)` 바로 위 (약 1518번 줄).
  연결 데이터: `E:\도진팩토리\3D스캔및티칭시스템\weld_path.json` (12500 너클 IN2222 경로, 4개 SEAM 구간)
- 판별법: 새로고침 직후 정반 위에 색깔 튜브가 자동으로 나타나면 이 케이스. `loadWeldPath` 실행 여부는 콘솔 경고(`경로 JSON 로드 실패`)로 역으로 확인 가능.
- 쓰는 함수/위치: `_initThree` → `loadWeldPath()` (v5_locked.dc.html 약 1518번 줄) / 실제 렌더: `loadWeldPath` 함수 (1647~1703번 줄)
- 상태: **수정됨 (2026-07-02)**

---

---

## ⚠️ 케이스 15: JBI 피커에서 폴더 이동이 안 됨 — 포트 분리 문제 [재발 절대 금지]
- 증상: JBI 선택 버튼 누르면 E:\ 폴더가 뜨지 않고, 폴더를 눌러도 아무 반응 없음. 콘솔에 404 오류.
- 원인: HTML이 8090 포트에서 서빙되는데, `fetch('/api/browse')` 는 상대경로라 8090으로 요청됨. 그런데 API(dzw_server.py)는 9090 포트에서만 실행되고 있어 8090에는 `/api/browse` 핸들러가 없음 → 404.
- 해결: `dzw_server.py` 기본 포트를 9090 → **8090으로 변경**. HTML+API를 같은 8090 포트로 통합. `start_servers.bat`의 중복실행 체크도 8090으로 수정.
- 재발 방지: 서버 구조 변경 시 HTML 서빙 포트와 `dzw_server.py` 포트를 반드시 동일하게 유지. 포트 다르면 `fetch('/api/...')` 상대경로가 작동 안 함.
- 적용 후 재시작 방법:
  1. 현재 실행 중인 8090 정적 서버 종료 (작업관리자에서 python 프로세스 확인)
  2. `start_servers.bat` 실행 (또는 `python dzw_server.py` 직접 실행)
  3. `http://localhost:8090/` 접속 → JBI 피커 → E:\ 폴더 이동 확인
- 쓰는 함수/위치: `dzw_server.py` 199~203번 줄 (port 기본값) / `start_servers.bat` 6번 줄 (포트 체크)
- 상태: **수정됨 (커밋 81ed0a9, 2026-07-02) — 재시작 후 확인 필요**

---

---

## ⚠️ 케이스 16: JBI 로드 제거 시 로봇이 기계적 원점(쭉 뻗은 자세)으로 돌아가는 버그 [자세 기준값 보존]

### 자세 정의 (3종)

**[자세 A] 기계적 원점 (Mechanical Zero — 쭉 뻗은 자세)**
- 정의: 모든 관절 펄스 = 0. 소프트웨어 기준 0점. 팔이 위로 쭉 뻗음.
- 펄스값: `S=0, L=0, U=0, R=0, B=0, T=0`
- 코드: `goHome()` / 초기 state `joints:{S:0,...}` (v5_locked.dc.html 982·1942번 줄)
- JBI 미로드 시 → `poseAt(0)`이 이 값 반환 → 화면에 쭉 뻗은 자세 표시

**[자세 B] 홈 자세 (Home Pose — 실제 작업 대기 자세)**
- 정의: 12500 IN2222 JBI 파일의 첫 번째 스텝. 굽힌 대기 자세.
- 펄스값:
  - S = -2,568 pulse (= -0.7132°)
  - L = -86,934 pulse (= -24.1482°)
  - U = -177,139 pulse (= -49.2054°)
  - R = -4,632 pulse (= -2.5731°)
  - B = -149,197 pulse (= -82.8872°)
  - T = 116,744 pulse (= 64.8579°)
- 파일: `jbi.json` (source: tcp_path.csv, RoboDK AR2010 기반)
- 전체 스텝: 120개
- 펄스↔도° 변환: S/L/U = 3600 pulse/°, R/B/T = 1800 pulse/°

### 실제 로봇 ABS DATA (기계적 원점에서의 절대 엔코더값 — 발주도면 확인)

**로봇 1** (SER.NO K19724-802-01 / ST.NO A-01)
| 축 | ABS DATA |
|----|----------|
| S  | -1,054   |
| L  | -86,957  |
| U  | 72,437   |
| R  | 5,088    |
| B  | -2,085   |
| T  | 47,554   |
| S1 | -144,265 |

**로봇 2** (SER.NO K19718-912-03 / ST.NO A-03)
| 축 | ABS DATA |
|----|----------|
| S  | 211      |
| L  | -86,494  |
| U  | 71,055   |
| R  | 2,663    |
| B  | -4,583   |
| T  | -29,978  |

> [CEO확인] 위 ABS DATA는 발주도면(라벨 사진) 확인값. 실제 로봇 본체와 대조 필요.

### 버그 발생 조건
`jbi.json` 자동로드 제거 시 → `this.jbi = null` → `poseAt(0)`이 내부 기본값(기계적 원점 A) 반환 → 화면에 팔이 쭉 뻗음.

### 재발 방지
- JBI 제거 시 반드시 대체 초기 자세 지정 필요 (케이스북 13번 복원 코드 참조)
- **기계적 원점(A) ≠ 홈 자세(B)** — 혼동 금지. 화면 팔 형태로 구분.

- 상태: **기록 완료 (2026-07-02 CEO 정정 반영)**

---

---

## ⚠️ 케이스 17: weld_path.json 로드가 홈 자세를 덮어쓰는 버그 [재발 절대 금지]
- 증상: 첫 화면에서 JBI 로드로 홈 자세(S=-2568...)가 설정됐는데, weld_path.json 로드가 나중에 완료되면서 로봇이 전혀 다른 자세(L=-63°)로 튀어버림.
- 원인: `loadWeldPath()` 마지막 줄 `if(data.poses.length) this.poseRobot(data.poses[0])` — 경로 데이터의 첫 자세(poses[0]: L=-63.8°)로 로봇을 강제 이동. fetch 완료 타이밍이 jbi.json보다 늦으면 홈 자세를 덮어씀.
- 해결: `loadWeldPath` 안 `this.poseRobot(data.poses[0])` 1줄 제거. `_csvPoses` 배열은 유지(ICP용).
- 재발 방지: `loadWeldPath`는 경로 시각화만 담당. 로봇 자세는 JBI 로드가 단독 결정. 두 fetch의 완료 순서가 보장되지 않으므로 자세 설정 코드는 한 곳에만 있어야 함.
- 쓰는 함수/위치: `loadWeldPath` (v5_locked.dc.html 1699번 줄) — 삭제됨
- 상태: **수정됨 (커밋 692c0b0, 2026-07-02)**

---

## 케이스 18: "기계적원점으로 돌려" 명령 — 자세 이동 기능
- 기능: ARIA 채팅에 "기계적원점으로 돌려" 입력 시 로봇1 ABS DATA 기준 자세로 이동
- 구현: `goMechZero()` 함수 + `localIntent()` 키워드 인식 ("기계적원점", "기계적 원점", "원점+기계")
- 이동 목표 (로봇1 K19724-802-01 ABS DATA → 도° 변환):
  - S = -1054 pulse → -0.2928°
  - L = -86957 pulse → -24.1547°
  - U = 72437 pulse → 20.1214°
  - R = 5088 pulse → 2.8267°
  - B = -2085 pulse → -1.1583°
  - T = 47554 pulse → 26.4189°
- 홈자세 명령: "홈자세로 돌아가" → `goWorkHome()` → S=-2568/3600°, L=-86934/3600°... (12500 IN2222 JBI 첫스텝)
- action 코드: `go_mech_zero` / `go_work_home` (runAction switch에 등록)
- 쓰는 함수/위치: `goMechZero`, `goWorkHome` (v5_locked.dc.html 1942번 줄 이후), `localIntent` (3432번 줄)
- 상태: **구현됨 (커밋 692c0b0, 2026-07-02) — 새로고침 후 ARIA 채팅 확인 필요**

---

---

## 케이스 19: 토치/TCP 진실좌표 어긋남 — 하이브리드 기구학 (팔=V1 / 계산=V4)
- 증상: 뷰어의 토치·TCP·용접경로가 진실좌표(weld_path.json, RoboDK FK)와 최대 568mm 어긋남. tcpOffset 값(19.5mm)만이 아니라 6번축(T) 회전방향이 반대라 자세마다 어긋남 커짐.
- 근본 원인: 뷰어 fk가 **제조사 URDF(V1) 체인**만 사용. V1은 팔 STL 메시 제작 규약이라 화면 팔은 맞지만, 절대좌표(TCP/경로)는 틀림. 진실은 **V4 체인(VERIFIED_LOCK §7)**: U/R/B/T 회전부호 −1, R·T 오프셋 부호반전, 베이스 −505.
- 함정: 체인을 통째로 V4로 바꾸면 팔 STL 메시가 홈 외 자세에서 최대 수백mm 깨짐(메시 보정변환이 자세의존, 상수 아님 — 수치 확인). → 체인 통일 불가.
- 해결 (하이브리드, **의도된 분리 — 통일 금지**):
  - `urdf`(V1) = 팔 STL 메시 표시 전용 (buildRobot). 절대 수정 금지.
  - `urdfCalc`(V4)+`calcBase`(−505) = TCP/경로/IK 계산 전용 (fk/solveIK).
  - 토치·TCP마커는 V1 팔프레임에서 떼어 `_truthTool`(진실추종 노드)에 부착, poseRobot에서 매 자세 `flangeMat`(V4)로 갱신 → 마커 진실 0.002mm, T회전 실물방향.
  - `buildWeldFromJbi`를 `_grp`(진실 mm)에 배치, `rg.position.z=−505`[시각 전용].
- **재발 방지 (핵심)**:
  1. **기구학 체인의 유일 기준은 VERIFIED_LOCK §7 (V4).** 뷰어·백엔드 어느 쪽이든 체인 수정 시 **4자세 오차 0.01mm 검사(weld_path.json 진실 기준) 필수.** 검증 하네스: `_verify_marker.py`(렌더 마커 월드좌표), `_verify_tcp.py`(엔진 fk).
  2. **`urdf`(V1)와 `urdfCalc`(V4)는 의도된 분리. 절대 "하나로 통일" 하지 말 것.** V1=메시, V4=계산. 통일하면 팔이 깨지거나 좌표가 틀림.
  3. `rg.position.z`, buildRobot 체인은 시각 전용 — 계산에 사용 금지.
- 검증: 렌더 TCP마커 4자세 최대 0.002mm · fk(weld_path포즈)↔오버레이 0.002mm(274mm→0) · headless 실엔진 캡처.
- 쓰는 함수/위치: `fk`/`flangeMat`/`solveIK`(dozikworks_engine_v5.js), `poseRobot`/`buildWeldFromJbi`/`buildRobot`(v5_locked.dc.html), `urdfCalc`/`calcBase`(dozikworks_data_v5.js)
- 잠금: v5_locked.dc.html + engine_v5.js + data_v5.js = lock_guard 등록 (system_lock.py LOCK_TARGETS)
- 상태: **해결·검증·잠금 완료 (커밋 7097327+1f4b883, 2026-07-02)**

#### 19-B: 토치 OBJ 부착 "통과" 오인 — 점(마커)만 보고 메시 자세를 안 봄 [2026-07-03]
- 사건: 신규 토치(welding_torch_TCP540.obj) 부착을 "노즐끝↔마커 0.00mm"로 통과 보고했으나, CEO 화면 판정에서 ①플랜지 원판이 손목에서 떠 있음 ②(초기엔) 꺾임 방향 반대 결함 발견.
- **근본 원인**: 검증 스크립트(_verify_torch.py)가 **노즐끝↔마커 거리 한 점만** 측정. 그런데 그 점은 부착 코드가 `me.position=tcp`로 **구조상 마커에 고정**한 값이라 항상 0.00mm — 즉 "검증이 곧 자기충족(자기가 맞춘 걸 자기가 확인)". 장착부↔플랜지 간격도, 꺾임(롤) 방향도, 메시 실제 자세도 검증하지 않음.
- 이는 케이스19 본편의 교훈(위치 vs 방향)과 같은 계열의 실수가 "점 vs 메시전체"로 재발한 것.
- **재발 방지**: 토치·공구 부착 검증은 **양끝(장착면↔플랜지, 노즐끝↔마커) + 방향(공구Z vs torch_dirs) + 롤(꺾임 평면)** 을 모두 수치화하고, 구조상 고정된(자기충족) 지표는 검증에서 제외한다. 최종은 반드시 CEO 화면 판정. 검증 하네스: `_verify_flush.py`(장착면·노즐 양끝), `_verify_wire.py`(용접점), `_verify_orient.py`(방향).
- 해결: 회전 유지·평행이동으로 플랜지 밀착(공백0), 노즐↔마커 19.5mm는 모델 치수차로 수용(OPEN_ISSUES IS-010). 와이어끝(용접점) 빨강 구 + 티칭 fkWire 도입.
- 상태: **부착 밀착·와이어끝 완료 (커밋 e303908 + Phase2, 2026-07-03)**

#### 19-C: 시각 결과물은 "이미지 직접 판독"이 검증의 일부 — 요약 텍스트 판정 금지 [2026-07-03]
- 사건: "검증 통과" 선언이 두 번 뒤집힘(19-B). 원인 2가지: ①숫자(마커 좌표)만 검증하고 화면(토치 메시)을 안 봄, ②CEO 화면 판정이 요약 텍스트로만 오가서 실제 결함(비례·간격·기울기)이 코드 담당에게 정확히 전달 안 됨.
- **규약(영구)**: 시각 작업은 (1) 수정 후 headless 캡처를 `shots/ceo_feedback/`에 `fix_{항목}_{n}.png`로 저장, (2) **담당이 그 이미지를 직접 열어** 판정 기준·실물 사진과 눈으로 대조, (3) 통과 시에만 보고, (4) CEO 판정 스크린샷도 같은 폴더에 저장돼 원본을 직접 판독. 실물 대조는 `real_*.jpg`와 같은 구도 캡처로 나란히 비교.
- 근거 문서: `shots/ceo_feedback/TORCH_VISUAL_SPEC_v1.md` (CEO 명세) + 참조 이미지 12장.
- 상태: **규약 확정·적용 (2026-07-03)**

#### 19-D: 토치 끝 구(球) 표시 금지 — CEO 확정, 어떤 작업·롤백 후에도 구가 다시 보이면 결함 [2026-07-03]
- 사건: CEO가 10회 지시했는데도 토치 끝 구가 계속 다시 보임.
- **근본 원인 규명(전수 추적)**:
  1. `weld` 빨강 구(SphereGeometry, buildRobot) — 생성 시 `visible=false`가 **없어 항상 보임**. 롤백으로 이 커밋(5d7bd8c 등) 복원 때마다 재출현 = 10회 재발의 주범.
  2. `arcDot` 빨강 구 — 초기 `visible=false`지만 `updateWeld()`가 재생 중 `visible=anyOn`으로 **다시 켬**.
  3. `tcpMarker` 주황 구 — `visible=false`, 토글 없음 → 숨김 유지(정상).
- **처리**: weld 구 `visible=false` 고정, updateWeld의 arcDot 토글 제거(항상 false). 좌표·계산 기준점(`_weldPoint` 위치, XYZ AxesHelper)은 유지. 아크 표시는 구 아닌 색변화 방식으로 CEO 승인 후 구현.
- **규약(영구)**: 토치 끝 부근에 구 형태를 그리는 코드 금지. 새 구 추가 시 반드시 `visible=false` 또는 미생성. **어떤 작업·롤백 후에도 토치 끝에 구가 보이면 결함으로 간주.** 검증: `_verify_sphere_flange.py`(토치끝 40mm내 가시 SphereGeometry 카운트=0) + 재로드2회·자세변경 재확인.
- 상태: **해결·검증 (플랜지간극0·와이어끝0.07mm·구0개, 2026-07-03)**

#### 19-E: 로컬좌표 불변 측정을 "위치 검증"으로 오용 — 끝점 134.2mm 이탈을 검증들이 전부 통과시킴 [2026-07-04]
- 사건: 서로 다른 두 자세(T=-5.5/64.9)에서 "토치 끝점" 콘솔 측정이 동일값 출력 — CEO가 물증으로 적발. truthTool-로컬 좌표는 자세와 무관하므로 로봇이 움직여도 불변 = 위치를 재지 않는 자기충족.
- **근본 원인(산술 규명)**: 토치 부착이 수식이 아니라 **손 정렬 박제**(커밋 cb7b6e2, CEO 드래그 콘솔값)였고, 정렬 참조 자체가 오염: ①위치역산 가짜 홈자세(jbi.json T=64.86, 진짜 -5.53)의 **미러 V1 플랜지** → 클로킹 +68.5°(≈ΔT 70.4°), ②굽힘각 부족 CAD(장착면중심→와이어끝 9.33° vs TOOL0 요구 14.69°) → radial -46.2mm, ③손정렬 잔차 z+9.1mm. 합성 = **134.2mm** (검산 일치: 수평현 133.9 + z 9.1 → 134.20).
- **재발 방지(SSOT 부록 D·E)**: 끝점 위치 검증 = 서로 다른 자세 2개 이상에서 [그려진 메시 끝점 월드좌표]가 자세에 따라 변하고 FK와 일치. 로컬 불변성=부착 검증일 뿐. 같은 메시에서 유도한 기준으로 그 메시를 재는 동어반복 금지. 부착은 SSOT 부록 E 수식으로만(손 정렬값 박제 금지).
- 상태: **규명 완료 후 CEO 종결 (2026-07-04): 토치 메시 동결 — 정렬 재시도 금지(CEO 확정). 메시=표시 전용(134.2mm 표시 오차=알려진 사양), 티칭·JBI·검증 기준=FK WIRE_TCP. 외형 교정은 실물 CAD 확보 시에만 별도 승인.**

---

*최종 업데이트: 2026-07-04 | 검증됨: 6개 | 미해결: 5개 | 재발금지: 10개 (케이스10·11·12·13·14·15·17·19·19-D·19-E) | 자세기준: 1개 (케이스16) | 기능추가: 1개 (케이스18)*

## ⚠️ 케이스 20: V1 표시팔 T축 거울(미러) 버그 — 3일 잡아먹은 근원 [재발 절대 금지]

**날짜**: 2026-07-04 (태그 ceo-confirmed-20260704에서 종결)

**증상**:
- 화면(V1 팔) 기준으로 토치를 눈 정렬하면, RoboDK/실물에서는 반대로 틀어져 있음 (웹↔RoboDK 정반대 현상)
- 그 정렬값으로 기준을 잡으면 TCP·경로 전체가 134.2mm 밀림 (v3 사태)

**측정으로 확정한 정체**:
- 표시 팔(V1)의 T축(플랜지 롤)이 진실 체인(V4=RoboDK)과 **정확히 반대로 회전** — 방향차 = 2×T (T=0→0°, T=30→60°, T=64.9→129.7°)
- S/L/U/R/B는 정상 (rpy로 흡수됨). **오직 T 하나.**

**수정** (커밋 d973ffa):
- `poseRobot`에서 표시팔 T만 부호 반전: `(a==='T'?-1:1)*j[a]*d`
- ★S/L/U/R/B에 부호 추가 절대 금지 — 전축 SGN 시 팔 903mm 이탈 이력 (VERIFIED_LOCK §9)

**같이 잡은 공범** (미러가 크게 보이게 만든 배후):
- 시작/홈 자세가 가짜홈(jbi.json IK 재적합, T=+64.9)이었음 → 진짜홈(IN2222 C0000, T=-5.53)으로 교체 (8b1bfff). 가짜홈에서 정렬하면 미러 오차가 70° 클로킹으로 박제됨.

**재발 방지 수칙**:
1. 화면 눈 정렬 결과는 **반드시 RoboDK 또는 V4 수치로 교차검증** 후에만 기준으로 승격
2. 정렬 작업은 **진짜 홈(T=-5.53)** 에서만
3. 표시 체인(V1)과 계산 체인(V4)의 방향 일치 여부는 "차이=2×관절각" 패턴으로 즉시 판별 가능 — 새 축/새 로봇 추가 시 이 검사 필수
4. RoboDK에 토치 부착 시 setParent(robot) 금지 — 베이스 고정됨. 반드시 툴 아이템(FLANGE_GEOM) 자식으로

---

## ⚠️ 케이스 21: 클릭한 곳과 다른 판이 선택됨 — BVH 삼각형 번호 재정렬 [재발 절대 금지] [8일 소모]

**날짜**: 2026-07-22 (커밋 f882cb2)

**증상**:
- 판A/판B 모드에서 원하는 판을 클릭하면 **전혀 다른 판**이 빨갛게 칠해짐
- CEO 표현: "클릭은 되지만 판재가 이상한 곳으로 가거나 내가 원하는 판재 선택이 안 되거나 표시가 안 됨"
- 7/12~7/22 8일간 미해결. 기하 로직·표시(z-fight)·모드매니저를 반복 수정했으나 전부 헛다리

**측정으로 확정한 정체**:
- `three-mesh-bvh`(충돌검사 가속기)가 `computeBoundsTree()` 시 **geometry의 index buffer 삼각형 순서를 재정렬**함
- `raycast`가 돌려주는 `faceIndex` = **재정렬된 번호**
- 그러나 `_faceGroupId`(loadKnuckleOBJ에서 생성) = **원본 OBJ 순서**
- 두 번호 체계를 섞어 조회 → 엉뚱한 그룹 선택

**판별 측정값** (localhost:8091 실측):
```
km.geometry.index.array 앞 12개 = 28149,28150,28151,28152,28153,28154,28644,...  (항등 아님)

그룹6을 "원본 position 순서"로 해석: bbox 1.684 x 0.930 x 0.010  ← 두께 1cm = 진짜 평면 판 ✓
그룹6을 "재정렬 index 순서"로 해석: bbox 1.437 x 0.776 x 0.268  ← 두께 27cm = 뭉개진 덩어리 ✗
→ faceGroupId는 원본 순서가 맞다

클릭점↔선택그룹 거리: 수정 전 평균 1.5026m / 수정 후 0.098m
그룹 오선택률:        수정 전 79.1%(483중 382) / 수정 후 0%
```

**수정** (커밋 f882cb2, dozikworks_edgepath_v5.js):
- `_origFaceIndex(mesh, f)` 신설 — `index.array[f*3]/3`로 원본 삼각형 번호 복원
- 단언: `ix[f*3+1]===ix[f*3]+1 && ix[f*3+2]===ix[f*3]+2` (삼각형 3정점 통째 이동 확인). 실패 시 정점좌표 역탐색 폴백
- `handlePointerDown`에서 `origFace`를 계산해 판A/판B 양쪽 `_selectGroupRegion` 조회에 사용
- ★`pickFacePlane`/`_planeAHit.faceIndex`는 **원본 변환 금지** — 이들은 `geo.index`를 통해 정점을 읽으므로 재정렬 번호가 맞다. 바꾸면 평면교차가 깨진다
- BVH는 절대 끄지 말 것 (충돌시스템·자기몸충돌이 사용)

**판별법**:
- `__dzw._knuckleMesh.geometry.index.array` 앞부분이 0,1,2,3... 이 아니면 재정렬된 것
- 그룹 bbox 최소축이 판 두께(1~3cm)가 아니라 제품 두께(29cm)로 나오면 번호 체계가 섞인 것

**재발 방지 수칙**:
1. **raycast의 faceIndex를 원본 순서 배열(faceGroupId 등) 조회에 그대로 쓰지 말 것.** BVH가 붙은 메쉬는 반드시 `_origFaceIndex()` 경유
2. 새 메쉬에 `computeBoundsTree()`를 붙일 때, 그 메쉬의 faceIndex를 쓰는 모든 코드를 점검할 것
3. "클릭한 곳과 다른 데가 선택된다"는 증상이 나오면 **기하 로직보다 번호 체계 정합성을 먼저 의심**할 것

**상태**: **수정됨 (2026-07-22)** — run_all_gate 11/11 통과

---

## 케이스 22: 판 선택 범위 — 그룹은 과선택, 평면은 과소선택 [미해결]

**날짜**: 2026-07-22

**증상**: 판 1장만 원하는데 제품이 통째로 잡힘. CEO: "메쉬값이 딱 내가 원하는 1판에 대해서만 잡혀야지 모든 객체가 다 싸잡아 잡힌다"

**원인**: **g그룹은 판의 단위가 아니다 — 양방향으로 어긋난다**
```
grp11: 6289면, bbox 3.00 x 1.17 x 0.29  ← 두께 29cm. 여러 판이 뭉친 통짜 (과선택의 정체)
grp 1: 3715면, bbox 2.99 x 1.17 x 0.03  ← 두께 3cm. 진짜 평면 판
반대로 Group3 = Mesh3/4/5 → gid 2·3·4  ← 한 판이 여러 그룹으로 갈림
```

**시도했다 되돌린 것** (커밋 3dbc8a5 → 104e418 원복):
- 평면 성장(같은 법선 + 평면거리 10mm) 방식 → 5곳 중 4곳이 **3~7cm 손톱조각**으로 과소선택
- 스캔 메쉬는 진짜 평면이 아니라 울퉁불퉁해서, 평면으로 자르면 조각남
- ★허용오차를 늘리는 것은 두더지잡기(철칙 제4조) — 늘리면 다시 통짜로 돌아감

**현재 동작**: 그룹 단위 선택 유지. 판보다 크게 잡히지만 확실히 잡힘. 발표 시연 가능

**올바른 방향 (미구현)**: 스캔 면의 실제 판 경계(모서리 특징선) 검출 후 그 경계로 영역을 가두는 방식. 평면 근사나 그룹 태그가 아니라 형상 경계 기반

**상태**: **미해결** — 발표(7/23) 후 착수
