# 오프라인 빌드 시스템 구조 감사 — 2026-09-14

대상: `E:\도진팩토리\3D스캔및티칭시스템\_현장오프라인_500PRO_20260911\_build\`
성격: **읽기 전용 감사**. 이 감사 중 코드는 한 줄도 고치지 않았다 (다른 에이전트가 같은 폴더에서 작업 중).
검증 근거: `node verify.js --web` 실제 실행 → **전체 판정 PASS** (2026-09-14 22:04 시점 shim.js 기준, 미커밋 변경 포함).

---

## 0. 한눈에 — 결론 먼저

| 항목 | 판정 | 한 줄 |
|---|---|---|
| shim.js 덩어리화 | ⚠️ **경고** | 1,927줄 중 1,792줄이 **단일 IIFE 하나**. 기능 12개가 한 통 안에 있다. |
| 기능별 분리 가능성 | ✅ 가능 | 줄 범위가 이미 기능별로 뭉쳐 있다. 다만 **전역 공유 상태 3곳**을 먼저 풀어야 한다. |
| 회귀 방지 | 🟡 **의외로 좋다** | `sandbox_test.html` 이 브라우저에서 기능 10종을 실측한다. 다만 **한 방에 도는 자동 실행이 아니고**, 복원(import)은 아무도 안 본다. |
| git 백업 | 🔴 **구멍 있음** | 빌드가 먹는 입력 `fab_*.js` 100여 개가 **git 미추적** → 앞으로의 변경이 git 에 안 보인다. |
| 커밋 관례 | ✅ 지킴 | "백업:" → 작업 → "성공:" 흐름이 로그에 일관되게 남아 있다. |

**CEO 걱정("shim 이 단일 덩어리가 되고 있다")은 사실이다.** 다만 아직 늦지 않았다 — 지금은 "한 파일이지만 내부 구획은 깨끗한" 상태이고, 구획선이 그대로 파일 경계가 된다.

---

## 1. shim.js 현재 상태

### 1-1. 크기와 구조

- **97,801 바이트 / 1,927줄**
- 최상위 IIFE는 **3개**뿐이고, 그중 세 번째가 **1,792줄(전체의 93%)** 을 차지한다.

| IIFE | 줄 | 역할 |
|---|---|---|
| ① | 20–81 | 저장소 안전망 (localStorage 가 아이패드 file:// 에서 throw → 메모리 저장소로 교체) |
| ② | 86–134 | 부팅 오류 화면 배너 (`window.__FABOFF_ERR`) |
| ③ | **136–1927** | **나머지 전부** |

### 1-2. 세 번째 IIFE 안의 기능 12개 (줄 범위 = 잠재적 파일 경계)

| # | 기능 | 줄 범위 | 대략 줄수 | 분리 난이도 |
|---|---|---|---|---|
| A | 임베드 해제 (base64/gzip) | 138–171 | 34 | **코어** (전원이 쓴다) |
| B | 경로 정규화 + 키찾기 (`rawPath/cands/lookupKey`) | 303–352 | 50 | **코어** |
| C | fetch 갈아끼우기 + 저장(POST) 라우팅 | 353–451, 1323–1347 | 125 | **코어** |
| D | 사진 보관소 (IndexedDB) + `<img>` 가로채기 | 173–241, 370–419 | 120 | 중 (C와 얽힘) |
| E | 회의 녹음 (보관소 + 패널 UI) | 242–664 | 420 | **낮음 — 가장 깨끗** |
| F | 타임라인 패널 | 666–812 | 147 | **낮음** |
| G | 치수 재기 (측정) | 813–846, 939–1260 | 355 | 중 |
| H | CAD 스냅 (꼭짓점/모서리/원중심) | 847–938 | 92 | **낮음 — G의 하위 모듈** |
| I | 오프라인 불가 버튼 안내 + 죽은 버튼 숨김 | 1261–1322 | 62 | **낮음** |
| J | 내보내기/가져오기(백업·복원) | 1348–1454 | 107 | **낮음** |
| K | 토스트 / 메모리모드 경고 / 툴바 UI | 1455–1596 | 142 | **코어(UI)** |
| L | 현장 모드 + 안내 3장 + 액션 위임 | 1597–1784 | 188 | 중 |
| M | 조립재생 선택잠금 (2026-09-14 신규) | 1786–1908 | 123 | **낮음** |

→ **코어(A+B+C+K) 약 350줄**, **기능 모듈 약 1,440줄**. 비율이 나쁘지 않다. 쪼개면 한 파일당 100~420줄로 fab_*.js 와 비슷한 크기가 된다.

---

## 2. 위험 결합점 — "여기 건드리면 저기 깨진다"

### 🔴 톱5

**① `MEAS` 객체가 잡동사니 서랍이 됐다 (813줄)**
치수 기능 상태로 만들었는데, 무관한 두 기능이 여기에 자기 물건을 넣는다.
- `ui()` 1596줄 → `MEAS.btn = TB.btns.meas` (툴바가 치수 상태에 쓴다)
- `lateSetup()` 1792줄 → `MEAS.__introShown` (첫 안내 3장이 치수 상태에 쓴다)
→ **치수만 떼어내면 툴바와 안내가 같이 죽는다.** 분리 시 가장 먼저 풀어야 할 매듭.

**② `ui()` 가 9개 버튼을 전부 하드코딩 (1565–1597줄)**
`measToggle / openTimeline / openRecPanel / doExport / doImport / setField / actProduct / actExplode / actAssembly` — 12개 기능 전부의 입구가 이 한 함수 안에 나열돼 있다.
→ **여기가 리팩토링 성패를 가른다.** 등록(registry) 방식으로 안 바꾸면, 파일만 쪼개고 "모두가 고치는 파일" 하나가 그대로 남는다 = 덩어리를 옮긴 것일 뿐.

**③ `lateSetup()` 의 단일 타이머가 6개 기능을 동시에 챙긴다 (1786–1797줄)**
```
ui() · measWrapLoaders() · measHideDead() · markOffline() · applyField() · showIntro()
→ 종료조건: (okUI && okWrap && hid) || tries > 40   // 300ms × 40 = 약 12초
```
→ 한 기능이 준비 안 되면 **12초를 전원이 함께 낭비**하고, 그 뒤엔 아무도 재시도하지 않는다. 새 기능을 여기 끼워 넣을수록 부팅이 취약해진다.

**④ `window.fetch` 단일 관문에 읽기·저장·사진이 전부 매달려 있다 (1325줄)**
임베드 읽기(B), 현장 저장분 덮어쓰기(overlay), POST 라우팅(`SAVE`/`trySave`), 사진 저장(`savePhoto`)이 한 함수에서 갈린다. 게다가 사진은 `patchImg()`(398줄)로 **전역 `<img>` src 를 가로채기**까지 한다.
→ **사진은 독립 기능이 아니다.** 절반은 저장 계층, 절반은 fetch 라우터. 분리 설계 시 "사진 파일"을 따로 만들려 하면 오히려 더 엉킨다 → 코어에 남겨라.

**⑤ 빌드·검증 도구가 shim.js 를 "글자 위치"로 자른다 — 파일을 쪼개는 순간 둘 다 깨진다**
- `build.py` 417–425줄 `shim_src()` : `--nofix` 대조군을 만들 때 `s.index('/* ── 0. 저장소 안전망')` ~ `s.index('/* ── 0-b.')` 로 **주석 문자열을 찾아 잘라낸다**. 주석 한 글자만 바뀌어도 터진다.
- `verify.js` : 산출 HTML 에서 embed 태그 뒤 **두 번째 `<script>`** 를 찾아 슬라이스하고 `/var EMB = window\.__FAB_EMBED__/` 와 `/저장소 안전망/` 이 있는지 검사한다.
→ 리팩토링 설계에 **반드시 명시**: build.py 는 여러 파일을 **하나의 `<script>` 태그로, 안전망을 맨 앞에 두는 고정 순서로** 이어붙여야 하고, `--nofix` 는 문자열 절단이 아니라 **파일 하나를 빼는 방식**으로 바꿔야 한다.

### 🟡 그 밖의 결합점

- **`toast()` (1471줄)** — 9개 기능이 전부 호출. 코어 UI로 올려야 한다.
- **원본 앱 전역 4종 의존** — `window.FAB_HAND`(9회), `FAB_ASM`(7회), `FAB_EXP`(4회), `FAB_LIB`(3회). 전부 `if (window.X && window.X.메서드)` 가드가 붙어 있어 **안전하게 짜여 있다**(원본 fab_*.js 를 고치지 않는다는 원칙 준수). 유지할 것.
- **`window.FAB.loadOBJ/loadBlock` 몽키패치 (1309줄)** — 치수 기능이 원본 로더를 감싼다. `__measWrapped` 플래그로 이중 적용은 막혀 있으나, 다른 기능이 같은 짓을 하면 감싸기 순서 문제가 생긴다. 감싸기는 **코어에서 한 번만** 하도록 규칙화 필요.
- **`localStorage` 키 접두사 `faboff:`** — overlay 저장분과 `faboff:fieldmode`/`faboff:introdone` 같은 설정이 **같은 네임스페이스에 섞여 있다**. `dump()`(1350줄)가 저장소를 통째로 퍼가므로 백업 파일에 설정까지 함께 실린다(현재는 무해하나, 키 충돌 시 조용히 덮인다).

---

## 3. 회귀 방지 장치 실태

### 3-1. 두 겹으로 되어 있다 (이 부분은 잘 만들어져 있다)

**① `verify.js` — 데이터·경로 검증 (node, 자동)**
산출 HTML 을 실제로 파싱해 **shim 의 진짜 키찾기 로직을 가짜 브라우저에서 실행**한다. (파이썬으로 따로 구현해 "검사는 통과, 실물은 실패"가 나는 것을 피한 설계 — 좋다.)

| 검사 | 커버 |
|---|---|
| `<script src=` 잔존 0, `<script>`/`</script>` 짝, BOM 없음 | ✅ |
| 저장소 안전망 (`--opaque` 로 불투명 오리진 재현) | ✅ |
| manifest OBJ / 블럭 부품 OBJ 키찾기 전수 | ✅ |
| 곁딸림 JSON 7종 × 인코딩 2가지 불일치 | ✅ |
| **저장→읽기 왕복** (notes/view_state/grouping × 3제품 = 9/9 통과) | ✅ |
| `file://` 절대 URL(`localhost:8091`) 저장 경로 | ✅ |
| 캐나다 블럭 159조각 / 면 118,274 정합 | ✅ |
| 용접정반 실측 치수 2000×4000×850.5, zup 판정 | ✅ |

**② `sandbox_test.html` — 브라우저 실동작 검증 (수동, 아이패드 조건 재현)**
`sandbox="allow-scripts"`(allow-same-origin 없음) iframe 으로 **불투명 오리진을 진짜로 재현**하고, 결과를 `rxsrv.py`(127.0.0.1:8800)로 POST 해 파일로 남긴다. 확인 항목 약 **130개**:

| 기능 | 확인 예 |
|---|---|
| 부팅/저장소 | `저장소모드`, `localStorage메모리냐`, `오류배너`, `저장불가경고` |
| 블럭/캐나다 | `캐나다부위수`, `캐나다삼각형`, `캐나다색수`, `캐나다회전X도` |
| 조립순서 | `조립스텝수`, `보임_처음/restart/next`, `조립자막` |
| 타임라인 | `작업기록`, `불량부위`, `공정서단계`, `하이라이트부위` |
| **치수** | `raycast적중`, `측정값mm`, `측정X차mm`, `라벨문구`, `지운뒤치수객체` |
| **스냅** | `스냅_꼭짓점`, `스냅_모서리중앙`, `스냅_면안쪽`, `스냅_원중심좌표` |
| **현장모드** | `현장_상단바`, `버튼최소높이`, `고급_치수버튼보임`, `복귀_상단바` |
| **녹음** | `녹음가능`, `녹음형식`, `녹음패널생성`, `삭제후녹음수` |
| **사진** | `사진저장`, `사진읽기폭`, `사진일치`, `백업사진수` |
| **조립잠금** | `잠금_카메라됨`, `잠금_선택차단됨`, `잠금_기즈모enabled`, `대조군_선택복구됨` |
| 용접정반 | `정반월드크기mm`, `정반높이축OK`, `정반상판OK` |

→ **CEO 가 걱정한 "기능별 회귀 테스트가 없다"는 사실이 아니다.** 이미 기능 10종을 실측한다.

### 3-2. 그럼에도 남는 구멍 5개

| # | 구멍 | 왜 위험한가 |
|---|---|---|
| **1** | **복원(`doImport`)을 아무도 테스트하지 않는다** | `sandbox_test.html` 에 `import`/`복원` 이라는 글자 자체가 없다. 백업 파일을 만드는 것(`dumpAll`)은 보는데 **되돌려 넣는 것은 안 본다.** CEO 의 데이터 손실 방어선인데 검증이 없다 → **최우선 구멍.** |
| 2 | `sandbox_test.html` 은 **수동**이다 | 브라우저를 열고, 파일을 고르고, `rxsrv.py` 를 띄워야 한다. `verify.js` 처럼 한 줄로 안 돈다 → 바쁠 때 건너뛰게 된다. |
| 3 | 세 판 중 **기본판만** 본다 | 기본값 `FILE = 제관웹_오프라인_단일파일_500PRO.html`. **축소판(16.5MB)·웹게시용(4.8MB, gzip)** 은 브라우저 검증을 안 거친다. 특히 웹게시용은 `DecompressionStream` 경로(shim `unpack()`)가 다르다. |
| 4 | `verify.js` 는 **shim 의 압축해제를 안 쓴다** | verify 는 자기가 `zlib.gunzipSync` 로 푼다. 즉 shim 의 `unpack()` 이 깨져도 verify 는 PASS 를 낸다. |
| 5 | **`--nofix` 대조군이 주석 문자열에 묶여 있다** | build.py 417–425. shim.js 주석 한 글자 수정으로 조용히 터진다. |

### 3-3. git 저장소 · 백업 체계

**저장소:** `E:\도진팩토리\3D스캔및티칭시스템\.git` — 존재. 커밋 관례는 **잘 지켜지고 있다**:
```
7494066 백업: 용접정반 임베드 + 조립재생 선택잠금 작업 전 빌드시스템 현재 상태
2ea80b6 성공: 3D 용접정반 2x4m 라이브러리 등록 — 세 판 모두 opaque origin 검증 PASS
```
→ 제1조(성공 즉시 커밋) · 제8조(작업 전 백업 커밋) 준수 확인.

**🔴 구멍 1 — 빌드 입력이 git 밖에 있다.**
`build.py` 의 `SRC` 는 `_현장오프라인_500PRO_20260911` 폴더 자신이다. 그런데 이 폴더의 `fab.html` + `fab_*.js` + `dzw_*.js` **100여 개가 전부 미추적(`??`)** 이다. git 이 추적하는 건 `_build/` 안의 5개(build.py, shim.js, verify.js, timeline.py, sandbox_test.html)뿐.
→ 위험은 "**앞으로 이 사본을 고쳐도 git 이 아무것도 못 본다**"는 것이다. 현재 갈라진 파일은 1개뿐이다(아래).

**ℹ️ `fab_fea.js` 차이는 문제가 아니다 — 의도된 동결로 보인다. 동기화하지 말 것.**
오프라인 사본 **67,087 바이트(8/17 동결)** vs 상위 원본 **106,106 바이트(git 최종 변경 9/13, `하중·고정점 응력집중 분리`)**.
- 이 폴더는 **9/11 날짜 스냅샷**이고, 구조해석은 그 뒤로 상위에서 계속 개발됐다.
- 게다가 shim 의 `OFFLIMIT` 이 **구조해석을 오프라인에서 아예 막는다**("PC 에서만 가능 — 포트 8097 필요").
→ 즉 **오프라인에서 안 쓰는 기능의 옛 판이 얼어 있는 것**이다. 최신판을 끌어오면 **쓰지도 않을 코드 +39KB** 가 16MB 웹 한도·아이패드 메모리를 더 압박한다. **작은 쪽이 유리하다.**
*(다만 이 판단은 CEO 확인 대상 — 아래 4-3 참고)*

**비교 범위 주석:** 위 대조는 **최상위 `*.js` + `fab.html` 기준**이다. `threejs/` 등 하위 폴더 스크립트는 이 감사에서 대조하지 않았다.

**🟡 구멍 2 — 미커밋 변경 방치.** `shim.js` 에 **115줄 추가 / 2줄 삭제**가 커밋되지 않은 채 있다(다른 에이전트의 조립재생 잠금 작업). `build_report.json` 도 미추적.
→ *이 감사에서는 커밋하지 않았다.* 해당 작업이 검증되는 대로 `성공:` 커밋이 필요하다(제1조).

**🟡 구멍 2-b — `rxsrv.py` 가 미추적이다.**
`sandbox_test.html`(브라우저 검증 본체)은 git 에 있는데, **그 결과를 받아 파일로 남기는 `rxsrv.py` 는 없다.** 이게 사라지면 브라우저 검증 결과를 수집할 수 없다 = 가장 강한 회귀 방지 자산이 반쪽이 된다. 즉시 추적 대상.

**🟡 구멍 3 — `.gitignore` 가 `*.obj` `*.png` `fab_models/` 를 제외**한다(IP 규칙상 타당). 즉 **모델 데이터는 git 백업 대상이 아니다.** 이건 정책상 맞지만, 그렇다면 `fab_models/` 의 별도 백업 경로가 문서화되어 있어야 한다(현재 이 감사 범위에서는 확인 안 됨).

---

## 4. 조치안

### 4-1. 리팩토링 설계 — shim 을 기능별 파일로

**핵심은 "파일을 쪼개는 것"이 아니라 "등록 방식으로 바꾸는 것"이다.**
`ui()` 와 `lateSetup()` 을 그대로 두고 파일만 쪼개면, 새 기능마다 **모두가 그 두 함수를 고치게 되어** 덩어리가 자리만 옮긴다.

#### 목표 구조

```
_build/shim/
  00_storage.js    저장소 안전망 (현 20–81)               ~62줄  ※ 반드시 맨 앞
  01_errbanner.js  부팅 오류 배너 (현 86–134)             ~49줄
  10_core.js       임베드 해제 + 경로 키찾기 + fetch 라우팅 + 저장 POST
                   + toast + 사진 IDB/patchImg  (A·B·C·D)  ~330줄
  11_registry.js   ★ 기능 등록소 + ui() + lateSetup()      ~150줄
  20_measure.js    치수 (G)                                ~355줄
  21_snap.js       CAD 스냅 (H)                            ~92줄
  30_record.js     회의 녹음 (E)                           ~420줄
  40_timeline.js   타임라인 (F)                            ~147줄
  50_backup.js     내보내기/가져오기 (J)                   ~107줄
  60_fieldmode.js  현장 모드 + 안내 (L)                    ~188줄
  70_offlimit.js   오프라인 불가 안내 (I)                  ~62줄
  80_asmlock.js    조립재생 선택잠금 (M)                   ~123줄
```
합계 ≈ 2,085줄 (등록소 오버헤드 포함). 파일당 **62~420줄** — 기존 fab_*.js 와 같은 체급.

#### 등록소(registry) 계약 — 이게 설계의 심장

각 기능 파일은 **오직 이 한 가지만 한다**:
```js
FABOFF.register({
  key:   'meas',
  icon:  'ruler',
  label: '치수',
  title: '치수 재기 — 표면 두 점을 탭하면 mm 로 표시',
  order: 20,
  onClick: measToggle,
  ready:   function () { return measWrapLoaders(); }  // 선택. true 면 준비 완료
});
```
- `11_registry.js` 의 `ui()` 는 등록된 항목을 `order` 순으로 **훑어서** 버튼을 만든다 → **기능 추가 시 코어를 안 고친다.**
- `lateSetup()` 은 각 기능의 `ready()` 를 **개별로** 추적한다. 준비된 기능은 더 안 부르고, 안 되는 기능만 계속 재시도 → 결합점 ③ 해소.
- 기능 간 호출은 전역 변수가 아니라 `FABOFF.get('meas').api` 로만 → 결합점 ①(MEAS 잡동사니) 해소. `MEAS.btn` 은 registry 가 보관하고, `MEAS.__introShown` 은 `60_fieldmode.js` 로 옮긴다.

#### build.py 변경 (결합점 ⑤ 해소)

```python
SHIM_DIR = os.path.join(SRC, '_build', 'shim')
SHIM_ORDER = ['00_storage.js', '01_errbanner.js', '10_core.js', '11_registry.js',
              '20_measure.js', '21_snap.js', '30_record.js', '40_timeline.js',
              '50_backup.js', '60_fieldmode.js', '70_offlimit.js', '80_asmlock.js']

def shim_src():
    files = list(SHIM_ORDER)
    if '--nofix' in sys.argv:
        files.remove('00_storage.js')   # ← 문자열 절단이 아니라 '파일 제외'
        say('[대조군] 저장소 안전망 파일 제외')
    return '\n'.join('/* ===== shim/%s ===== */\n%s' % (f, rd(os.path.join(SHIM_DIR, f)))
                     for f in files)
```
**불변식 3개 (깨지면 아이패드에서 흰 화면):**
1. `00_storage.js` 는 **무조건 맨 앞** — 다른 코드가 localStorage 를 만지기 전에 안전망이 걸려야 한다.
2. 전부 **하나의 `<script>` 태그**로 합친다 — 여러 태그로 쪼개면 verify.js 추출이 깨지고, 실행 순서 보장이 약해진다.
3. 파일 목록은 **명시적 리스트**로 — `os.listdir()` 정렬에 의존하면 파일 하나 추가할 때 순서가 조용히 바뀐다.

#### 작업량 추정

| 단계 | 내용 | 추정 |
|---|---|---|
| 1 | 작업 전 `백업:` 커밋 (`rxsrv.py` 포함) | 0.5시간 |
| 2 | `11_registry.js` 신규 작성 + `MEAS` 잡동사니 3건 분리 | **3~4시간** ← 가장 어려운 구간 |
| 3 | 기능 10개 파일 이동 (순수 잘라내기·붙이기, 로직 변경 없음) | 2~3시간 |
| 4 | `build.py` 이어붙이기 + `--nofix` 변경 | 1시간 |
| 5 | `verify.js` 추출 로직 수정 + 신규 테스트 5종 추가 | 2~3시간 |
| 6 | `sandbox_test.html` 로 브라우저 전수 재검 + 세 판 비교 | 2시간 |
| | **합계** | **11~14시간 (실작업 2~3일)** |

*가정:* 기능 로직은 **일절 바꾸지 않는다**(순수 이동). 제6조에 따라 **한 턴에 한 파일씩** 옮기고, 각 파일 이동 후 `node verify.js` 를 돌려 PASS 를 확인한 뒤 커밋한다. 그러면 어느 단계에서 깨져도 직전 커밋으로 즉시 롤백된다(제2조).

**⚠️ 착수 시점:** 다른 에이전트의 미커밋 115줄(조립재생 잠금)이 **먼저 검증·커밋된 뒤**에 시작해야 한다. 지금 시작하면 충돌한다.

### 4-2. 기능별 자동 회귀 테스트 목록 (각 1개 이상 라운드트립)

**A. `verify.js` 에 추가 — node 에서 도는 것 (우선순위 순)**

| # | 기능 | 라운드트립 테스트 | 왜 |
|---|---|---|---|
| **1** | **백업/복원** | `dumpAll()` → JSON 문자열 → `doImport` 의 복원 로직 → 저장소 키/값이 원본과 **일치** | **최우선.** 지금 아무도 안 본다. 데이터 손실 방어선. ※ 가짜 window 의 `FileReader` 가 빈 `class {}` 스텁이라 **먼저 구현해야 한다** |
| 2 | 압축해제 | 웹게시용에서 **shim 의 `unpack()`** 으로 OBJ 하나를 풀어 `zlib.gunzipSync` 결과와 바이트 일치 | 구멍 4 — 지금은 shim 의 해제기가 깨져도 PASS |
| 3 | 현장 모드 | `setField(true)` → `applyField()` → `fieldOn()===true` → `setField(false)` → `false` | 거의 공짜. 가짜 DOM 에 `documentElement.setAttribute/getAttribute` 가 이미 있다 |
| 4 | 타임라인 | `tlLoad()` 결과가 build_report 의 **작업기록 23건 / 불량부위 7곳 / 공정서 11단계**와 일치 | build_report.json 에 이미 기대값이 찍혀 있다 = 공짜 기준선 |
| 5 | 등록소 | 등록된 기능 수 == 12, `key` 중복 없음, `order` 충돌 없음 | 리팩토링 후 "파일 하나가 조용히 안 실렸다"를 잡는다 |
| 6 | 오프라인 안내 | `OFFLIMIT` 4항목의 selector 가 산출 HTML 안에 **실제로 존재**하는지 | 원본이 id 를 바꾸면 안내가 조용히 사라진다 |
| 7 | 세 판 전부 | `verify.js` 를 기본/`--lite`/`--web` 3회 + `--opaque` 조합으로 일괄 실행하는 `verify_all` | 구멍 3 |

**B. `sandbox_test.html` 유지 — 브라우저에서만 가능한 것**
치수·스냅(THREE raycast 필요), 사진(IndexedDB), 녹음(MediaRecorder+마이크), 조립잠금(PointerEvent), 툴바 실측(getBoundingClientRect) — **node 로 옮기려 하지 말 것.** 대신:
- `--lite` / `--web` 판도 돌리도록 `?f=` 파라미터로 **3회 실행을 표준 절차에 명시**
- **복원(import) 확인 항목 추가** — 백업 → 저장소 비우기 → 복원 → 사진·메모 복귀

**C. 한 줄로 도는 진입점**
`_build/run_checks.cmd` (또는 `.py`) 하나에 위를 모두 묶어, 빌드 후 **항상 이것만 치면 되게** 한다.

### 4-3. "새 기능 추가 시 지켜야 할 규칙" 체크리스트

> 이 체크리스트를 `_build/shim/README.md` 로 두고, 새 기능 추가 전에 읽는다.

**착수 전**
- [ ] `git status` / `git log --oneline -10` 으로 현재 위치 확인 (제8조)
- [ ] 미커밋 변경이 있으면 **먼저 정리**하고, `백업:` 커밋으로 현재 상태 저장
- [ ] 같은 기능이 이미 fab_*.js 나 shim 에 있는지 검색 (중복 금지)
- [ ] `node verify.js` 를 **먼저 돌려 PASS 기준선**을 확보 (뭐가 원래 깨져 있었는지 구분하려고)

**작성 중**
- [ ] **새 파일 하나**로 만든다 (`_build/shim/NN_이름.js`). 기존 파일에 덧붙이지 않는다
- [ ] 자기 상태는 **자기 파일 안의 지역 변수**로 둔다. 남의 객체(`MEAS` 같은)에 얹지 않는다
- [ ] 툴바 버튼이 필요하면 **`FABOFF.register()` 만** 쓴다. `ui()` 를 직접 고치지 않는다
- [ ] 부팅 대기가 필요하면 **`ready()` 를 등록**한다. `setInterval` 을 새로 만들지 않는다
- [ ] 원본 `fab_*.js` 는 **읽기만** 한다. 고쳐야 할 것 같으면 멈추고 CEO 승인
- [ ] 원본 전역 접근은 반드시 가드: `if (window.FAB_XXX && window.FAB_XXX.method)`
- [ ] 원본 함수를 감쌀 일이 있으면 **코어에 요청**한다. 자기 파일에서 몽키패치하지 않는다
- [ ] localStorage 키는 **`faboff:기능명:` 접두사**를 쓴다 (overlay 와 섞이지 않게)
- [ ] 되돌리기 스위치를 상단에 둔다 (`var XXX_ON = true;` — `ASM_LOCK_PICK` 방식이 좋은 예)
- [ ] 왜 이 방식인지 **한국어 주석**으로 남긴다 (비개발자가 읽는다)

**완료 전 — 이거 없이 "됐습니다" 금지 (제5조)**
- [ ] `build.py` 의 `SHIM_ORDER` 에 파일 추가 (안 하면 **조용히 안 실린다**)
- [ ] `verify.js` 에 **이 기능의 라운드트립 테스트 1개 이상** 추가
- [ ] 빌드 3판 전부: `python build.py` / `--lite` / `--web`
- [ ] `node verify.js` × 3판 + `--opaque` → **전부 PASS 실제 출력 확인**
- [ ] `sandbox_test.html` 을 브라우저에서 열어 해당 기능 항목 실측
- [ ] **아이패드 실기 확인** — CEO 화면이 유일한 기준
- [ ] 확인 후 `성공: (무엇이 됐는지 한 줄)` 즉시 커밋 (제1조)

**하면 안 되는 것**
- ❌ shim 파일 하나가 **500줄을 넘기면** 멈추고 쪼갤 방법부터 논의
- ❌ `10_core.js` / `11_registry.js` 를 기능 추가 때문에 고치는 것 (고쳐야 하면 설계가 틀린 것)
- ❌ 원인 규명 없이 숫자(임계값·대기시간·tries) 바꾸기 (제4조)
- ❌ 한 턴에 여러 파일 동시 수정 (제6조)

---

## 5. 권고 요약

| 순위 | 조치 | 소요 | 왜 지금 |
|---|---|---|---|
| **1** | 다른 에이전트의 미커밋 115줄 검증 후 `성공:` 커밋 | — | 제1조. 이게 안 되면 아무것도 시작 못 한다 |
| **2** | `rxsrv.py` git 추적 + **입력 갈라짐 감시 장치** (아래 둘 중 CEO 선택) | 0.5~1h | 빌드 입력이 git 밖에 있다 |
| **3** | **복원(import) 테스트** 추가 | 2h | 유일하게 검증 없는 데이터 손실 경로 |
| **4** | shim 기능별 분리 + 등록소 도입 | 11~14h | CEO 우려의 본체. **지금이 적기** — 아직 구획이 깨끗하다 |
| **5** | `run_checks` 한 줄 진입점 + 3판 일괄 검증 | 2h | 검증을 건너뛰지 않게 만드는 장치 |

### 🔸 권고 #2 — CEO 판단이 필요한 갈림길

**질문: 이 오프라인 폴더는 "동결된 배포본"인가, "살아있는 작업 사본"인가?**

| | (a) 스냅샷을 git 에 추적 | (b) 해시 대조 관문만 둔다 ← 추천 |
|---|---|---|
| 내용 | `fab_*.js` 100여 개를 git 에 등록 | `run_checks` 에 **오프라인 사본 vs 상위 원본 해시 대조** 단계 추가. 다르면 경고. `fab_fea.js` 처럼 **의도된 동결은 예외목록**에 명시 |
| 장점 | 배포본이 완전히 재현된다 | 갈라짐을 **바로** 잡는다. 사본 100개를 git 에 안 늘린다 |
| 단점 | **CLAUDE.md 제2절(동일기능 사본 금지)** 과 정면 충돌. 중복 100개를 공식화 | 배포본 자체의 과거 재현은 안 된다 |
| 맞는 경우 | 이 폴더를 **정식 배포 산출물**로 선언할 때 | 이 폴더가 **작업 사본**일 때 |

현재 갈라진 파일이 100개 중 **1개뿐**이고 그마저 의도된 동결로 보이므로, **(b)가 비용 대비 효과가 훨씬 낫다.** 다만 CEO 가 "이건 배포본이다"라고 정하면 (a)가 맞다. **가정하지 않고 여쭌다.**

**한 줄 결론:** shim 은 아직 "덩어리"가 아니라 "구획이 뚜렷한 큰 파일"이다. 그 구획선이 그대로 파일 경계가 되므로 분리 비용이 낮다. 다만 **`ui()` 와 `lateSetup()` 을 등록 방식으로 바꾸지 않으면 분리는 무의미**하다 — 새 기능마다 모두가 고치는 파일이 남아, CEO 가 겪었던 "연쇄 에러"가 그대로 재현된다.

---
*작성: 구조 감사 에이전트 / 2026-09-14. 코드 수정 없음. 근거: shim.js 1,927줄 전수 구조 분석, build.py·verify.js·sandbox_test.html·timeline.py·rxsrv.py 통독, `node verify.js --web` 실행(PASS), git log·status·파일 해시 대조.*
