[verified] feat: add Webmaker visual editor v0.2.0

This commit is contained in:
oz
2026-10-28 15:07:35 +00:00
parent 4b3714e37c
commit 0a56d4612c
40 changed files with 4770 additions and 191 deletions
+74 -1
View File
@@ -1,5 +1,78 @@
# VERSION_HISTORY
## 0.2.0 (2026-10-28)
- **작성자**: 개발 지시 — Neo(`Ariweb-Suitle_Dev-Order_v0.2.0.md`) · 구현 — Claude Fable 5.1(격리 작업 트리 `feature/visual-editor-v0.2.0`, 기준 main `4b3714e37c543fcfa8862dbc68c76f62a9b0bf97`). 커밋·push·병합·배포·운영 서버 접근은 하지 않았다 — Neo 가 검토 뒤 수행한다.
- **이전 버전**: 0.1.1(2026-10-26, 첫 구현 릴리스 — 아래 절) · 0.1.4 개발 지시(2026-10-27, HAL AI 대시보드·다중 AI 공동 작업창 지시 문서만 추가, `4b3714e`). 0.2.0 은 **가운데 버전 번호를 올리는 기능 릴리스**다: 도입 모듈 Webmaker 에 사용자 기능(직접 편집)이 더해졌고 Webmaker 자체도 1.7.3 → **1.8.0** 으로 올랐다. 0.1.1 의 모든 기준·0.1.4 의 지시는 그대로 유효하다(이번 범위 밖의 0.1.4 항목은 구현하지 않았다).
- **구성**: HAL · 고객 포털 · Node Agent 는 0.1.1 그대로. 도입 모듈 **Webmaker 1.8.0**(직접 편집 포함) · AriMail 1.0.11(변경 없음). 외부 npm 의존성 없음, Node 22.13+.
### 요구(사용자 요구 — 약화 없이 반영)
Webmaker 가 초안을 만들어 보이는 페이지가 생긴 뒤 **직접 시각 편집 모드**를 더한다(Wix·아임웹 방식 끌어다 놓기). 변경은 Suite 의 `modules/webmaker` 와 기존 Webmaker 저장소 양쪽에 반영한다. 필수 동작·보안 규칙은 `Ariweb-Suitle_Dev-Order_v0.2.0.md` 에 계약으로 적었다.
### 재사용한 기존 기능(그대로 도입)
- Webmaker 1.8.0 의 **직접 편집(시각 편집기)** 전부 — 미리보기 도구 줄 "직접 편집" 전환(사이트·HTML 페이지가 골라져 있을 때만), 캔버스 글 바로 고치기(두 번 클릭/Enter), 요소 선택·도구 막대(↑↓·복제·삭제·끌기 손잡이), 기존 블록 끌어 옮기기, 구성 요소 팔레트 7종(섹션/컨테이너·제목·문단·버튼(링크)·이미지·구분선·여백) 끌어다 놓기 + "추가" 버튼, 속성 창(상위 선택·이동·복제·삭제·글·링크 주소·이미지 주소/대체 글·정렬·글자색·배경색·안쪽/바깥 여백), 저장 상태(변경 없음/저장되지 않음/저장 중/저장됨/저장 실패 — aria-live), 저장(Ctrl/Cmd+S), 실행 취소/다시 실행, 버리고 다시 불러오기, 편집 끝내기, 페이지·사이트·화면 전환·새로고침·소스 보기 전환·창 닫기 전 저장하지 않은 변경 보호. 기존 소스 편집기·모바일 미리보기·버전/복원·배포·가져오기·다국어·모듈·SEO·Site Brief 는 그대로.
- 재사용한 함수(Webmaker 기존 코드를 그대로 호출): `resolveTextFile`(경로 검증·링크 거부), `listFiles`, `gitRecord`/`lib/gitstore.recordChange`(버전 기록 — 새 종류 `visual-edit`), `publicView`, `broadcast`, `jobOf`(AI 작업·배포 중 판정), `readBody`, `sessionUser`·`fromTenantPage`·`auth.csrfOk`(출처·CSRF), `siteUrl`, `isSecureReq`, `MAX_TEXT_BYTES`(5MB 상한), `store.save`(Suite `rev` 증가). 화면: `discardOk`·`renderPreview`·`renderGit`·`loadFrame`·`setEditorOpen`·`logLine`·`pushMsg`·`api`(CSRF 헤더)·`T`.
### 수정한 기존 기능
- **Webmaker(`modules/webmaker`, 파일 머리·줄 주석 `v1.8.0`)**: `server.js` — `require('./lib/visual-editor')`, `veRuntime()`·`requestOrigin()`, `GET /files` 응답에 `hash`, 새 경로 `GET/PUT /api/sites/<id>/visual/<페이지>`(아래 "직접 편집·끌어다 놓기·저장·버전·보안"). `public/app.js` — `discardOk`(직접 편집 변경도 묻기), `VER_KIND` 에 `visual-edit`(기존 `manual-edit` 는 "소스 편집" 으로 표기), `renderPreview`(직접 편집 중이면 미리보기 덮어쓰지 않고 `VisualEditor.onSiteUpdated`), `btnToggleSource`(한 번에 하나), `beforeunload`, `resetToNew`, `btnReload`·`fileSelect.onchange`, `openSite` 끝(사이트 바뀌면 편집 종료). `public/index.html` — `#btnVisual`, `#veBar`, `.ve-stage`(`#vePalette`·`#frameWrap`·`#veInspector`), 스크립트 2개. `public/style.css` — `.ve-*`(900px 이하 세로 배치·접기). `public/admin.js` — 버전 종류 표기. `package.json` 1.8.0.
- **Suite 최소 연결 변경(`[Ariweb Suite 변경]`, 0.1.1 의 8개 파일 변경은 그대로 유지)**: `modules/webmaker/server.js` 의 직접 편집 `PUT` 에도 저장본 `rev` 충돌 검사(`revConflict`, §9.3)를 소스 편집기·기본 사항 저장과 같이 적용 — 화면이 보낸 `rev` 가 다르면 409 와 양쪽 요약(`conflict.currentRev` 등). 화면(`visual-editor-ui.js`)은 `GET /visual` 의 `rev` 를 기억해 저장 때 보내고 응답의 `rev` 로 갱신한다(upstream Webmaker 는 `rev` 를 돌려주되 무시한다). 그 밖의 Suite 전용 변경(SSO·내부 API·배너·`lib/ariweb.js`·`store.rev`)은 바뀌지 않았다 — `diff` 로 upstream 과의 차이가 0.1.1 의 Suite 블록 + 1.8.0 공통 변경만임을 확인했다.
- **Suite 도구**: `scripts/import-sources.mjs` 에 `--allow-dirty`(참조 저장소를 나란히 개발 중일 때 고정 커밋 위의 작업 트리 변경·추적되지 않은 새 파일을 가져오고 `upstream{version·branch·headCommit·workingTreeDirty·note}` 를 기록) 와 `--only` 때 다른 소스 기록 유지. `hal/modules/index.mjs`·`agent/jobs/module.mjs` 의 Webmaker 버전 주석, `portal/webmaker/REUSE.md` 갱신. Agent·포털 코드 동작은 바뀌지 않았다(릴리스 artifact 는 `package.json` 0.2.0 으로 `webmaker-0.2.0.tar.gz` 등으로 만들어진다). **`scripts/release.mjs` 와 `hal/core.mjs` 는 바뀌었다** — 도입 소스 출처 fail-closed(아래 ⑪)와 릴리스 승인 세 검사 필수(아래 종결 검토 ③).
- 오류 수정: Webmaker 직접 편집 저장에서 JSON 파싱 실패가 500 으로 나오던 것을 400 한국어 안내로(TDD 중 발견, 직접 편집 경로에만 적용).
- **검토 수정(Neo 독립 검토)** — ① 출시 차단: 직접 편집 PUT 이 본문을 읽기(await) 전에 파일 hash 를 잡아 두어 두 저장이 나란히 들어오면 둘째가 첫째를 조용히 덮어쓸 수 있었다 → 본문을 다 읽은 뒤 사이트 기록(`store.load`)·파일을 다시 읽어 hash(Suite 는 `rev` 도)를 비교하고 검증·쓰기까지 await 없이 이어 간다(둘째는 409 stale, 첫 저장 보존). 결정적 회귀 테스트(chunked PUT 두 개의 본문을 순서대로 완료). ② 안전 강화: 직접 편집 저장을 `lib/visual-editor.writeTextAtomic`(같은 폴더 임시 파일 0644 → rename, 실패 때 정리·오류 전달, 파일 시스템 주입 가능)로 바꿔 쓰기 실패·중단 때 마지막 정상 페이지가 잘리지 않는다(500 + "기존 페이지는 그대로 있습니다", 버전 미생성). 소스 편집기의 기존 쓰기는 바꾸지 않았다.
### 고객 화면
- 포털 사이트 상세 › 제작 탭 › "제작 화면 열기"(SSO) → Webmaker 빌더 오른쪽 미리보기 도구 줄의 **직접 편집**. 포털 자체 화면은 바뀌지 않았다. 직접 편집은 AI Wallet(캐시)을 쓰지 않으므로 포털 AI Wallet 카드에 영향이 없고, 직접 편집 저장본(버전 `visual-edit`)은 포털 배포 화면의 저장본 선택·Source 변경 이력에 그대로 보인다(내부 API `projects/<id>` 의 `versionList`).
- 포털 상단 배너(그룹·운영 사이트)는 직접 편집 중에도 그대로 보인다.
### 운영자 화면
- HAL 운영자 콘솔 변경 없음. Webmaker 관리자 콘솔의 버전·변경 종류 표기에 "직접 편집"(`visual-edit`)이 추가되고 기존 `manual-edit` 는 "소스 편집" 으로 보인다. 운영자 조치 항목 없음(설정값·보관소 변경 없음).
### 직접 편집 · 끌어다 놓기 · 저장 · 버전 · 보안 동작
- **캔버스**: `GET /api/sites/<id>/visual/<페이지>` 가 저장된 HTML 을 편집 캔버스 문서로 바꿔 준다(`lib/visual-editor.prepareCanvas`). 페이지 `<script>`(외부 src 포함)는 `type="text/x-wm-ve-disabled"`, 인라인 이벤트 속성은 `data-wm-ve-on-*`, `javascript:` 링크·iframe/frame/embed src·object data·meta http-equiv·base href 는 `data-wm-ve-*` 보존 속성으로 바뀌어 **페이지 스크립트가 실행되지 않는다**(지우지 않고 보존). `<meta http-equiv="Content-Security-Policy">` 가 nonce 있는 편집 런타임(인라인 `visual-editor-core.js`+`wm-ve-canvas.js`)만 허용하고, `<base href="<사이트 주소>/<폴더>/">` 로 상대 경로 자원이 저장 페이지와 같게 풀린다. 화면은 이를 기존 미리보기 iframe(sandbox, allow-same-origin 없음 → 출처 null, 쿠키·저장소·빌더 API 접근 불가)의 `srcdoc` 에 넣는다. 응답에 32자리 무작위 세션·파일 sha256(`hash`)·`rev`.
- **규약**: 봉투 `{wmve:1, session, type, payload}`. 부모는 iframe `contentWindow` + 출처 `'null'` + 세션 + 허용 종류(ready·selected·changed·error·body·hotkey)만, 캔버스는 `window.parent` + 부모 출처 + 세션 + 허용 종류(init·select·move·duplicate·delete·insert·setProps·applyHtml·setMode·requestBody)만 처리한다(`WMVisualCore.validateMessage`). HTML 속성·메시지 내용은 권한으로 쓰지 않는다. 캔버스는 서버와 통신하지 않는다.
- **저장**: `PUT /api/sites/<id>/visual/<페이지>` `{body, baseHash, rev}` — 원본 doctype·`<head>`·`<body …>`·꼬리는 글자 그대로 두고 **본문만** 바꿔 끼운다. 보존 속성 복원, 편집기 표식 제거. 거절(한국어): 로그인 401 · 다른 사용자 404 · 생성 사이트 출처/CSRF 없음 401 · HTML 아님 400 · 경로 탈출·링크 400 · AI 작업·배포 중 409 · `rev` 불일치 409(+ 양쪽 요약, Suite) · `baseHash` 불일치 409(+ currentHash) · baseHash 없음/형식·본문 비문자열·JSON 파싱 실패 400 · 5MB 초과 413 · 원본에 없던 스크립트(추가·변경)·인라인 이벤트 속성·javascript:/vbscript:/data:text 주소·iframe/object/embed/meta/link/base·스타일 표현식·본문 안 html/head/body 400. 저장 뒤 `manualEdits{kind:'visual'}`, 버전 `visual-edit`("직접 편집: <경로>"), Suite `rev` +1, 미리보기·소스 편집기용 `hash`·`files`·`git`·`versions` 갱신. **캐시 미차감·AI 명령·수정 횟수 미계수.**
- **끌어다 놓기**: 팔레트 → 캔버스는 **포인터 끌기 다리가 주 경로**(부모가 팔레트 항목의 포인터를 추적해 iframe 안쪽일 때만 상대 좌표를 검증된 메시지 `paletteDragAt`/`paletteDropAt`/`paletteDragCancel` 로 보내고 캔버스가 id·좌표·모드를 다시 검사해 스스로 놓기 표시·삽입 — Chromium 이 HTML5 끌기를 출처 'null' sandbox iframe 으로 넘기지 않기 때문), HTML5 끌어다 놓기는 포인터 이벤트가 없는 환경의 **대체 경로**(`text/wm-ve-component` + `text/plain` 이름 공간 값만 받고 구성 요소 HTML 은 `isSafeComponentHtml` 검사). 기존 블록은 포인터 기반 끌어 옮기기(같은 문서 안, 놓을 자리 표시). 버튼(↑↓·추가)·키보드(Alt+↑/↓·Delete·Ctrl+D·Enter·Esc·Ctrl+S·Ctrl+Z/Ctrl+Shift+Z) 대안.
- **반응형·접근성**: 팔레트·속성 창은 캔버스 옆, 900px 이하에서 아래로 내려가며 접힌다(캔버스를 가리지 않음). 전환 버튼 `aria-pressed`, 도구 줄 `role=toolbar`, 상태 `aria-live=polite`, 팔레트·속성 창 `role=region`+`aria-label`, 접기 `aria-expanded`, 캔버스 도구 버튼 `aria-label`·포커스 표시.
### 도입 소스(출처)
- 원본 저장소 `https://source.ibit.kr/OZ-Group/WebHosting_AI_agent`, **고정 커밋 `4b8c4d0de0f69025001a9e0a28a58a94e1a1f088`**(Webmaker 1.8.0 — `feature/visual-editor-v1.8.0`·main 원격 ref 검증됨; 0.1.1 의 불변 기준 커밋 `3b10859884ef2fd0f2af11934ab6c16370fd64b8`(1.7.3) 위의 변경). 개발 중에는 커밋 전 작업 트리(참조 저장소 worktree `/home/master/work/WebHoting_AI_Agent-visual-editor-v180`)에서 `node scripts/import-sources.mjs --only webmaker --allow-dirty` 로 가져왔고, 커밋 뒤 `scripts/import-sources.mjs` 의 `pin` 을 `4b8c4d0` 으로 올렸다. 깨끗한 도입 기록(`IMPORTED_SOURCES.json/.md`, 그 pin 으로 `--allow-dirty` 없이 다시 만든 것)의 webmaker 항목: `commit` = `upstream.headCommit` = `4b8c4d0`, `upstream{version:'1.8.0', branch:'feature/visual-editor-v1.8.0', workingTreeDirty:false}`, 파일 184개(0.1.1 의 176 + 신규 8: `lib/visual-editor.js`, `public/visual-editor-core.js`, `public/wm-ve-canvas.js`, `public/visual-editor-ui.js`, `test/visual-editor.test.js`, `test/visual-editor-server.test.js`, `test/visual-editor-app.test.js`, `docs/verification/visual-editor-1.8.0-tdd.md` — 모두 원본과 동일), 수정 8개(0.1.1 과 같은 `lib/auth.js lib/config.js lib/store.js lib/vault.js public/app.js public/index.html public/style.css server.js` — Suite 블록 때문). 이 작업 트리에 있는 `IMPORTED_SOURCES.json/.md` 는 아직 개발 중 기록(기준 `3b10859`, `workingTreeDirty:true`)이라 Neo 가 올린 pin 으로 다시 만든다(이 작업 트리의 참조 체크아웃은 그 커밋에 있지 않아 여기서는 재생성하지 않았다).
- 원본 경로 → 대상 경로는 `IMPORTED_SOURCES.md` 표(`modules/webmaker/<같은 경로>`). 라이선스 검토: OZ-Group 내부 전용(private, 별도 LICENSE 없음) — 같은 조직의 Suite 재사용 허용, 외부 배포 불가(0.1.1 과 같음). AriMail 은 변경 없음(a07f69f).
- **실제 브라우저 시각 검증 수정(Neo 의 브라우저 게이트)** — 직접 편집을 켜도 옛 작업 칸(`.left`)이 남아 1280×633 에서 캔버스 iframe 이 115px 로 줄고 390×844 에서 캔버스가 y≈2228 로 밀리는 출시 차단 결함. `body.ve-on` 의 CSS 배치 규칙이 없던 것이 원인(JS 전환은 정상, 가짜 DOM 실행 테스트로 확인). `modules/webmaker/public/style.css` 에 `body.ve-on .left { display:none }`·`body.ve-on .app { grid-template-columns: var(--rail-w) minmax(0,1fr) }`(1280px 캔버스 568px)·1180px 이하 패널 190px·900px 이하 레일+캔버스 두 행 규칙을 추가(독립 Webmaker 와 동일). 끝내면 원래 세 칸 배치 복원. 회귀: CSS 계약 테스트 2건 + JS 전환 동작 테스트 1건(`test/visual-editor.test.js`).
- **실제 브라우저 시각 검증 수정 2(배치 수정 뒤 게이트)** — 직접 편집을 켜고 요소를 고르기 전인데 어두운 선택 도구 줄(`.wm-ve-tools`)이 이미 페이지 위에 보이는 출시 차단 결함(DOM 은 `hidden`, 속성 창은 "선택한 요소 없음"). 캔버스 런타임의 작성자 규칙 `.wm-ve-tools{display:flex}` 가 UA 의 `[hidden]{display:none}` 을 덮어쓴 것이 원인. `modules/webmaker/public/wm-ve-canvas.js` 런타임 CSS 에 `.wm-ve-tools[hidden],.wm-ve-drop[hidden]{display:none !important}` 한 줄 추가(독립 Webmaker 와 byte-identical). `renderOverlay()` 가 `hidden=false` 로 바꿀 때만 보이고 선택 해제·미리보기·`applyHtml` 뒤 다시 숨겨진다. 회귀: 런타임 CSS 를 파싱해 `hidden` 요소의 최종 display 를 cascade 모형으로 계산하는 계약 테스트 1건(`test/visual-editor.test.js`, RED actual 'flex' → GREEN).
- **실제 브라우저 게이트 3(Playwright 자동 게이트)** — 실제 Chrome 에서 frameLocator 로 캔버스 iframe 을 직접 조작해 선택·글 편집·실행 취소/다시 실행·Alt+↑/↓ 이동은 통과했지만, 팔레트 → 캔버스 실제 HTML5 끌어다 놓기가 들어가지 않는 출시 차단 결함. 부모는 전용 MIME `text/wm-ve-component` 만 넣는데 캔버스는 `sandbox="allow-scripts"`(allow-same-origin 없음) 출처 'null' 문서라 Chromium 이 다른 출처의 전용 MIME 끌기 자료를 넘겨주지 않는 것이 원인. 전용 MIME 은 유지하고 표준 `text/plain` 에 `wm-ve-component:<id>` 를 함께 넣으며, 캔버스는 `core.dragComponentId` 한곳에서 전용 MIME 우선 → 정확히 일치하는 text/plain + 알려진 id 만 받는다(임의 글·모르는 id 는 삽입·표식 없음). `modules/webmaker/public/visual-editor-core.js`·`visual-editor-ui.js`·`wm-ve-canvas.js`(독립 Webmaker 와 byte-identical). sandbox·CSP·메시지 검증·저장 검증·"추가" 버튼 대안 불변. 회귀: 순수 규칙·팔레트 dragstart·캔버스 동작(가짜 DOM 위 실제 런타임 부트) 3건(`test/visual-editor.test.js`, RED → GREEN).
- **실제 브라우저 게이트 4(살균 DnD 진단) — 포인터 끌기 다리** — MIME 보완 뒤에도 끌어다 놓기가 실패해 부모·캔버스 양쪽에 살균 리스너(종류·플래그만)를 붙여 확인: 부모 dragstart 2건(두 종류 모두 유효)인데 캔버스 dragenter·dragover·drop 0건. Chromium 은 HTML5 끌기 자체를 부모 → 출처 'null' sandbox iframe 으로 넘기지 않으므로(제품 결함) sandbox 를 유지한 채 포인터 다리를 주 경로로 넣었다: 부모 팔레트 항목이 포인터(마우스·펜)를 추적해("추가" 버튼·터치·보조 버튼 제외, 포인터 캡처, 4px 문턱) iframe 안쪽일 때만 상대 좌표를 검증된 메시지 `paletteDragAt`/`paletteDropAt`(밖·Escape·pointercancel·끝내기는 `paletteDragCancel`)로 보내고, 캔버스가 메시지 규약 뒤 id(`core.isComponentId`)·좌표(`core.canvasPoint`)·편집 모드를 다시 검사해 스스로 놓기 표시·삽입을 한다(잘못된 입력은 삽입·표식 없음). HTML5 DnD 는 대체 경로로 남김. `modules/webmaker/public/visual-editor-core.js`·`visual-editor-ui.js`·`wm-ve-canvas.js`(독립 Webmaker 와 byte-identical) + `style.css` 2줄(`.ve-comp { user-select:none; touch-action:pan-y }`·끌기 중 class). 터치 스크롤·"추가" 버튼·키보드·CSP·저장 검증 불변. 회귀: 규약·좌표 순수 규칙 / 부모 포인터 순서 동작 / 캔버스 수신 동작 3건(`test/visual-editor.test.js`, RED → GREEN).
- **독립 검토(읽기 전용) 차단 항목 12건 수정** — 보안 7·규격/회귀 5. 공유 파일은 참조 저장소와 byte-identical 로 반영하고 Suite 블록 파일(`server.js`·`app.js`·`lib/store.js`)은 패치했다(상세·RED 증거: `docs/verification/webmaker-visual-editor-0.2.0.md`, upstream `modules/webmaker/docs/verification/visual-editor-1.8.0-tdd.md`). ① 캔버스 연결 규약 — 캔버스가 `MessageChannel` 포트를 첫 'ready' 창 메시지에 동봉하고 그 뒤 모든 명령·이벤트는 포트로만(`core.validatePortMessage`); 부모는 첫 ready 한 번만 연결·연결 뒤 창 메시지 전면 무시·끝내기 때 포트 닫음; 캔버스는 창 메시지를 듣지 않음(iframe 안 문서가 바뀌어도 세션·명령을 이어받을 수 없다). 심층 방어: a/area/제출 클릭·submit 차단(두 모드), `<base target="_blank">`, CSP form-action none, meta refresh 비활성화. ② 주소 스킴 정규화 — 서버·화면·캔버스가 한 구현(`core.urlScheme`)으로 이름·숫자 엔티티(두 겹)·제어·폭 없는 문자를 걷어내고, 스킴 자리의 모르는 엔티티는 거절. ③ `prepareCanvas` 가 구조 스캐너로 진짜 head/html/body 를 찾아 CSP·런타임을 넣고 없으면 진짜 `<head>` 생성. ④ 저장 검증을 완성 문서 전체 ↔ 원본 전체 비교로(머리 요소를 본문에 복사 불가). ⑤ 사이트별 변경 잠금 `withSiteLock` — 소스/직접 편집 PUT·버전 복원은 본문을 읽은 뒤 잠금 안에서 실행 중·배포 중·기록(rev)·hash 재검사 → 쓰기; AI/기획/가져오기/복사/다국어/배포 시작은 `claimJob`; 소스 편집기 PUT 에 선택적 `baseHash`(화면이 보냄, 409 때 다시 불러오기 질문). ⑥ 직접 편집 저장 트랜잭션 — 기록/Git 실패 때 페이지·사이트 기록(rev 포함, `store.save(prev,{restore:true})` 는 rev 를 올리지 않음)을 되돌리고 커밋을 되감아 500(`gitRecord` strict; 종결 검토 ② 에서 원격 푸시 순서·검증된 되돌리기로 재설계). ⑦ **Suite 전용**: 직접 편집 저장은 유한한 숫자 `rev` 필수(없음·문자열·null → 400 `revRequired`, 오래됨·다른 파일 저장으로 오른 rev → 409). ⑧ 전환 보호 — `openSite` 는 전환 전에 묻고 취소하면 현재 사이트 유지, 홈 탭도 묻고, `onSiteUpdated`/`renderPreview` 는 HTML 페이지가 없어져도 변경이 있으면 묻고 캔버스를 남겨 둔다. ⑨ 복제가 자손 id 도 제거. ⑩ 블록 끌기 — 손잡이 포인터 캡처, pointercancel·창 blur·Escape 가 상태를 모두 정리. ⑪ **Suite 전용**: 새 `lib/provenance.mjs` — `scripts/release.mjs build` 는 IMPORTED_SOURCES 의 출처가 깨끗하지 않으면(workingTreeDirty=true·불완전·commit≠headCommit) artifact 를 만들지 않고 종료 1, manifest.json 에 `checks.provenanceClean/provenanceProblems`·`imported[].upstream`; `register --approve` 는 `provenanceClean !== true` 면 승인하지 않고, `hal/core.approveRelease` 는 `provenanceClean === false` 를 `release_checks_failed` 로 거절(종결 검토 ③ 에서 세 검사 모두 명시적 통과 필수로 강화). 개발 중 기록(`workingTreeDirty: true`)으로는 build 가 멈췄다 — 참조 저장소 1.8.0 이 `4b8c4d0` 으로 커밋·검증되어 pin 을 올렸고, 그 pin 으로 다시 만든 깨끗한 기록(`workingTreeDirty:false`, 184 파일·수정 8)으로 build·approve 가 된다. ⑫ 문서 수치를 최종값으로 맞추고 브라우저 게이트 상태를 기록. 도입 기록(`IMPORTED_SOURCES.json`, 생성 manifest 기준): webmaker **184개 파일**(수정 8: lib/auth.js·lib/config.js·lib/store.js·lib/vault.js·public/app.js·public/index.html·public/style.css·server.js), 고정 커밋 `3b10859`, upstream 1.8.0 `feature/visual-editor-v1.8.0` workingTreeDirty=true. upstream 대비 Suite 블록 차이: `server.js` 115줄(107 → 115), `app.js` 4줄, `style.css` 5줄, `index.html` 2줄, `lib/store.js` 4줄.
- **브라우저 게이트 상태** — Neo 의 Playwright 게이트(네이티브 포인터 순서)는 이 검토 **전** 에 37/37 통과. 수정 뒤 Neo 의 첫 재실행은 `edit-text` 에서 멈췄는데 원인은 게이트 멱등성 결함(같은 저장 폴더·같은 고정 제목 → 변경 없음 = clean)이었고 제품 결함이 아니다(로컬 결정적 재현). 같은 조사에서 가운데 클릭(`auxclick`) 틈을 찾아 캔버스 런타임에 차단을 더했다. 고친 게이트(실행마다 다른 제목, 연결 규약·이동 활성화 6종·위조 창 메시지 단언 추가)는 로컬 Chrome 156 에서 61/61(연속 2회), **Neo 호스트의 실제 Chrome 에서도 61/61 통과(2026-10-28)** — 수정 뒤 브라우저 재실행은 완료되었고 남은 재실행은 없다(종결 검토 3건은 브라우저 동작을 바꾸지 않는다).
- **독립 검토 2차(종결 검토) 차단 항목 3건 수정 + 문서 수정** — 참조 저장소와 같은 코드(공유 파일 byte-identical, `server.js` 는 Suite 블록 보존 패치; 상세·RED/GREEN: `docs/verification/webmaker-visual-editor-0.2.0.md`, upstream `modules/webmaker/docs/verification/visual-editor-1.8.0-tdd.md`, 변경 이력 §9). ① **SVG SMIL 저장형 XSS 우회(보안)** — `validateVisualSave` 가 `<animate attributeName="href" values="javascript:…">`·`<set …>` 를 받았다(정적 주소만 검사, 위험 요소 목록에 SMIL 없음; Chromium 에서 실행 확인). `lib/visual-editor.js` 가 SMIL 요소 전부(`animate·set·animateMotion·animateTransform·animateColor·discard·mpath`, 대소문자·`svg:` 접두어 포함)를 위험 요소 묶음으로 세어 원본 수까지만 허용(삭제 가능, 추가·변경 거절), 캔버스에서는 `wm-ve-x-<이름>` 비활성 이름으로 바꿨다가 복원 때 표준 이름으로 되돌린다; `core.isSafeComponentHtml` 도 거절. ② **Git 저장 트랜잭션 재설계** — `gitstore.recordChangeStrict`: 스냅샷 → 로컬 커밋·변경 브랜치 → 사이트 기록 저장(rev +1) → 원격이 있으면 main + 변경 브랜치를 `git push --atomic` 한 번으로(마지막 단계, 그 뒤 상태 변경 없음); 어느 단계가 실패해도 `reset --mixed`(커밋 되감기 + 인덱스 비움, 첫 커밋 전이면 브랜치 삭제 + 빈 인덱스)·변경 브랜치 삭제/복원·페이지 원본·`store.save(prev,{restore:true})`(rev 보존) 뒤 **검증**(HEAD·인덱스·브랜치·작업 트리·페이지 바이트·기록) — `rolledBack:true` 는 검증 통과 때만, 아니면 `rolledBack:false`+`rollbackProblems`. 강제 푸시 되돌리기 없음, 원격 없는 저장소는 기존처럼 로컬 커밋. 푸시 **전** 에 저장되는 기록은 원격 반영을 주장하지 않는다(`pushed:false, remote:'pending'`, 응답도 같음) — 별도 조정 `reconcilePushes`(저장 뒤·기동 때·다음 기록 때, 잠금 안)가 `ls-remote` 로 원격 ref 를 확인한 뒤에만 `pushed:true, remote:'verified'` 로 바꾸고, 원격에 없으면(기록 저장과 푸시 사이에 프로세스가 죽은 경우) 로컬 변경 브랜치 + main 을 원자적으로 올린 뒤 확인한다. 조정의 기록 저장은 `{restore:true}` 라 rev 를 올리지 않는다(Suite 동시 편집 충돌 규칙과 무관한 메타데이터). ③ **Suite 전용 — 릴리스 승인 fail-open**: `hal/core.releaseApprovalProblem` 한 규칙으로 `testsPassed === true`·`dirty === false`·`provenanceClean === true` 를 모두 요구(없음·null·문자열 거절) — `approveRelease`·`registerRelease(approve:true)`·운영자 API `POST /ops/api/releases/:v/approve`·`scripts/release.mjs register --approve`(DB 열기 전) 모두 적용, `--skip-tests` 빌드는 등록만 가능. 기존 테스트 픽스처(hal-cli·hal·e2e 가짜 릴리스)는 깨끗한 checks 를 넣도록 고쳤다(옛 관용 정책 없음). ④ 문서 — "재실행 필요/미실행"·"자동화하지 않음"·옛 수치(103/110·182 파일·HTML5 주 경로·release.mjs 변경 없음)를 현재 증거로 고쳤다. 도입 기록 재생성: webmaker 184 파일·수정 8·workingTreeDirty=true(의도 유지 — 참조 저장소 커밋 뒤 재도입).
- **독립 검토 3차(최종 검토) 차단 항목 4건 수정** — ① **Suite 전용 · 승인된 릴리스 불변**: `hal/core.registerRelease` 가 승인된 버전을 다른 내용(artifact·checks·커밋·빌드 시각·메모)으로 재등록하면 `release_immutable` 로 거절하고 아무것도 바꾸지 않는다(같은 내용 재등록은 멱등, approve 요청이어도 그대로); 승인되지 않은 버전의 내용 변경 재등록은 registered 로 두고 승인을 물려받지 않으며 approve 는 세 검사 규칙(`releaseApprovalProblem`)을 지나야 한다. `hal/index.mjs register-release` 는 거부를 종료 1 로 알린다(DB 불변). ② **사이트 변경 잠금 전면 감사**(공유 코드, Suite 블록 보존): `/api/sites/<id>/…` 변경 요청은 본문을 잠금 밖에서 메모 읽기 → 잠금 안에서 기록 재적재 → 짧은 경로는 처리기 전체를 잠금 안에서, 긴 작업 경로는 재적재한 기록으로(claimJob 까지 await 없음); 재진입 잠금(AsyncLocalStorage)·`detached()`; `changeLanguages` 는 claimJob 먼저. Suite 의 rev 충돌 검사는 재적재된 최신 기록으로 이루어진다. ③ **트랜잭션 완결성·모호한 푸시 판정**: `recordChangeStrict` 의 준비 단계(initSite·스냅샷·원격 설정)도 트랜잭션 안(실패하면 onRollback·새로 만든 저장소 삭제·스냅샷 전 실패는 ok:false), 푸시 오류는 원격 ref 를 다시 읽어 성공/원격 불변(되돌림 + 원격 재검증)/모호(pending 유지, 200 + `remoteState:'ambiguous'`, rolledBack 주장 없음)로 판정. ④ 문서 합계 통일. ⑤ **배경 작업 저장 합치기(저장소 불변식, Suite 블록 포함)**: `modules/webmaker/lib/store.js` 가 읽은 객체마다 읽었을 때 상태(`__base`, 열거 안 됨)와 저장 표식 `saveId` 를 기억하고, 디스크 기록이 그 사이 다른 객체로 저장되었으면 이 객체가 바꾼 최상위 항목만 디스크 최신 기록 위에 합쳐 저장한다 — 배경 AI 작업의 종료 저장이 작업 중의 기본 사항·모델 등급 변경을 지우지 않는다(RED: 제목이 "VE site" 로 되돌아감 → GREEN). Suite 의 `rev` 는 합칠 때 디스크 값을 기준으로 올리고, 원격 반영 조정은 `keepRev`(합치되 rev 불변)로 저장한다(`restore` 는 스냅샷 그대로). upstream 대비 `lib/store.js` 차이는 5줄(rev·keepRev). ⑥ **같은 잎 동시 변경 fail-closed**: 합치기를 잎 단위 3자 병합으로 바꾸고(같은 최상위 항목의 다른 잎은 합침), 같은 잎을 둘 다 다르게 바꾼 경우만 `site_save_conflict`(409, fields)로 거절해 디스크의 먼저 쓴 값을 보존한다. 배경 작업의 모든 저장(실패·중단 기록 포함)은 `saveJob` 으로 가서 충돌 때 `store.update` 로 최신 기록을 다시 읽어 작업이 가진 항목만 다시 적용하고 객체를 맞춘다(`store.adopt`); 다시 푸시·배경 조정은 원격 ref 결과를 `store.update` 로 적용. RED: 작업 중 다시 푸시가 쓴 `verifiedAt` 이 작업 종료 저장에 지워지고 배경 재확인이 다른 시각을 씀 → GREEN. 상세·RED/GREEN: `docs/verification/webmaker-visual-editor-0.2.0.md`, upstream `modules/webmaker/docs/verification/visual-editor-1.8.0-tdd.md`(변경 이력 §10).
- **독립 검토 4차 차단 항목 4건 수정** — ① **Suite 전용 · artifact 설치 불변·fail-closed**: `hal/core.installArtifacts` — 원본 바이트의 sha256·크기를 manifest 와 대조(불일치 `artifact_hash_mismatch`, 아무것도 쓰지 않음), 승인된 버전은 메타데이터·설치 파일 해시가 같을 때만 멱등(복사 없음)·다르면 `release_immutable` 로 파일·DB 불변 거절, 새·미승인 설치는 임시 파일 → 해시 재확인 → rename. `hal/index.mjs register-release`·`scripts/release.mjs register` 모두 설치 → 등록 순서로 이 함수를 쓰고 거부는 종료 1. ② **읽기 요청 부작용 제거**(공유 코드): GET/HEAD/OPTIONS 는 `x-ui-lang` 으로 저장하지 않는다(saveId·rev 불변) — 언어는 변경 요청의 잠금 안 재적재에서만 저장. ③ **폐기 잠금·표식 규약**(공유 코드, Suite 의 포털 영향 확인 블록 보존): 잠금 안 재검사 + 폐기 표식 원자적 claim → 잠금 밖 배포 서버 정리 → 다시 잠금·같은 표식 확인 뒤 삭제; 표식 중 모든 변경 요청·작업 시작 409; 실패는 표식 해제 + `store.update` 로 `deleteFailed`. **포털 연결 보상(잔여 위험 해소)**: 연결 해제(`ariweb.unlink`)를 정리 **전** 에 하던 것을 공유 `destroySite` 의 `beforeIrreversible` 훅으로 옮겨 배포 서버 정리 성공·사이트 폴더/기록을 임시 이름으로 옮긴 **뒤**(되돌릴 수 없는 기록 삭제 직전)에만 하고 readback 으로 확인한다; 정리가 실패하면 연결·파일·기록이 모두 그대로, 연결 해제가 실패하면 옮긴 것을 되돌리고 중단, 기록 삭제가 실패하면 `compensate`(`ariweb.link` 로 되살리기 + readback, `linkedAt` 보존) 뒤 실패로 알린다(되돌리지 못하면 `recoveryRequired` + 감사 `site.destroy.failed`). 뒷정리(폴더 등) 실패는 성공 응답의 `warnings` 와 감사 `site.destroy.cleanup` 으로 운영자에게 보인다. ④ **파서 차이(저장형 XSS)**(공유 코드): 스캐너가 svg/math 안에서는 title·textarea·style·script 를 원문으로 읽지 않아 `<svg><title><script>` 가 거절·비활성화된다. 상세·RED/GREEN: `docs/verification/webmaker-visual-editor-0.2.0.md`, upstream `modules/webmaker/docs/verification/visual-editor-1.8.0-tdd.md`(변경 이력 §11).
### 테스트
- 독립 검토 4차 4건 수정 뒤(**현재 증거**): 참조 저장소 `npm test` **115/115**(단위 39 + 서비스 23 + 화면 전환 1 + 원본 52), `modules/webmaker` `npm test` **125/125**(Suite 전용 `test/ariweb.test.js` 10 — 폐기 보상 3 추가), Suite `node test/run.mjs` **24개 파일 실패 0**(`test/release-provenance.test.mjs` 11건), 실제 Chrome 게이트(Neo 호스트) 61/61 그대로. RED: Suite artifact 불변 2 실패(승인된 바이트가 B 로 덮임), 참조 단위 38/39(svg title script 통과)·서비스 21/23(읽기가 기록 파일을 바꿈, 폐기 중 저장 미거절), Suite 모듈 폐기 보상 7/10(정리 실패 뒤 연결이 풀림·연결 해제 실패인데 정리를 먼저 하지 않음·되살리기 없음).
- 최종 검토(3차) 4건 + 잎 단위 저장소 합치기(fail-closed) 수정 뒤: 참조 저장소 `npm test` **112/112**(단위 38 + 서비스 21 + 화면 전환 1 + 원본 52), `modules/webmaker` `npm test` **119/119**, Suite `node test/run.mjs` **24개 파일 실패 0**(`test/release-provenance.test.mjs` 9건), 실제 Chrome 게이트(Neo 호스트) 61/61. RED: Suite 승인 불변 core·CLI 2 실패(재등록이 승인자를 바꿈), 참조 서비스 15/18(잠금 경쟁·모호한 푸시·준비 단계 실패).
- 종결 검토 3건 + 원격 반영 보수 기록 수정 뒤: 참조 저장소 `npm test` **107/107**(단위 38 + 서비스 16 + 화면 전환 1 + 원본 52), `modules/webmaker` `npm test` **114/114**, Suite `node test/run.mjs` **24개 파일 실패 0**(`test/release-provenance.test.mjs` 7건 — 승인 규칙 3건 추가). RED: 참조 저장소 단위 37/38(SMIL 1 실패)·서비스 11/14(Git 실패 되돌리기 검증·원격 트랜잭션 실패), 그 뒤 서비스 13/16(푸시 순간의 기록이 `pushed:true` 를 주장 — 보수 기록 수정 전), Suite provenance 4/7(core 승인·운영자 API·release.mjs 3 실패). 실제 브라우저 게이트(Neo 호스트 실제 Chrome) 61/61.
- 독립 검토(1차) 차단 항목 수정 뒤: 참조 저장소 `npm test` **103/103**(단위 37 + 서비스 13 + 화면 전환 1 + 원본 52), `modules/webmaker` `npm test` **110/110**, Suite `node test/run.mjs` **24개 파일 실패 0**(새 `test/release-provenance.test.mjs` 4건 포함, `test/webmaker-visual-editor.test.mjs` 5건은 rev 필수·교차 파일 409·되돌리기 rev 보존 검사를 더함). RED: 참조 저장소 단위 24/37(13 실패)·서비스 9/13(4 실패)·app 0/1, Suite provenance 0/4, Suite rev(rev 줄을 뺀 임시 서버 복사본) 4/5.
- 포인터 끌기 다리 뒤: 참조 저장소 `npm test` **87/87**(단위 26 + 서비스 9 포함), `modules/webmaker` `npm test` **94/94**, Suite `node test/run.mjs` **23개 파일 실패 0**.
- 끌어다 놓기 수정 뒤: 참조 저장소 `npm test` 84/84(단위 23 + 서비스 9 포함), `modules/webmaker` `npm test` 91/91, Suite `node test/run.mjs` 23개 파일 실패 0.
- 도구 줄 수정 뒤: 참조 저장소 `npm test` 81/81(단위 20 + 서비스 9 포함), `modules/webmaker` `npm test` 88/88, Suite `node test/run.mjs` 23개 파일 실패 0.
- 배치 수정 뒤: 참조 저장소 `npm test` 80/80(단위 19 + 서비스 9 포함), `modules/webmaker` `npm test` 87/87, Suite `node test/run.mjs` 23개 파일 실패 0.
- 검토 수정 뒤: 참조 저장소 `npm test` 77/77(단위 16 + 서비스 9 포함), `modules/webmaker` `npm test` 84/84, Suite `node test/run.mjs` 23개 파일 실패 0. 아래는 첫 인도 때 수치.
- Webmaker 참조 저장소(1.8.0): `test/visual-editor.test.js` 15건(문서 나누기·캔버스 변환·복원·저장 검증 허용/거절·hash·공용 핵심), `test/visual-editor-server.test.js` 7건(권한·출처·CSRF / 허용 페이지 / 캔버스 계약 / 저장·버전·캐시 미차감 / 오래된 저장 409·잘못된 본문 400 / AI 작업 중 409 — 가짜 claude 실행 파일로 작업을 멈춘 두 번째 서버 / 화면 접근성 조작·런타임 서빙). RED(모듈 없음·경로 404·버튼 없음) → GREEN 기록: `docs/verification/visual-editor-1.8.0-tdd.md`. 전체 `npm test` **74/74**(원본 52 + 22).
- Suite: `test/webmaker-visual-editor.test.mjs` 5건(도입 파일·버전·IMPORTED_SOURCES upstream / 순수 모듈 / 공용 핵심 / 화면 접근성 + Suite 배너 보존 / **실제 서버로 rev 충돌 409 → 맞는 rev 저장 200·버전 visual-edit·rev 증가**). RED(0/5) → GREEN(5/5): `docs/verification/webmaker-visual-editor-0.2.0.md`. `modules/webmaker` 전체 `npm test` **81/81**(59 + 22). Suite 전체 `node test/run.mjs` **23개 파일 실패 0**(로컬 E2E 포함, `docs/verification/local-e2e-2026-10-28.md/.json` 생성). 모든 서버 테스트는 `CLAUDE_BIN` 을 즉시 실패하는 가짜 실행 파일로 두고 실행했다(개발 PC 의 진짜 claude 가 사이트 생성 작업으로 실행되지 않게 — 첫 RED 실행에서 한 번 실행되어 즉시 종료시켰고, 테스트를 고쳤다).
### 알려진 제한
- 실제 브라우저 검증은 Neo 의 게이트(시각 게이트 1280×633·390×844 → Playwright 자동 게이트, 실제 Chrome)로 다섯 차례 수행되어 배치·도구 줄·끌어다 놓기 결함을 찾아 고쳤고, 독립 검토 수정 뒤 재실행(게이트 멱등성 수정 뒤)은 **Neo 호스트에서 61/61 통과** — 캔버스 안 실제 포인터 조작(contenteditable 입력·실행 취소/다시 실행·이동·팔레트 포인터 끌어다 놓기·저장·연결 규약·이동 활성화·위조 메시지)은 이 게이트로 자동 검증했다. Node 에서는 문법 검사·`eval` 부재·정적 구조·CSS 배치 계약·가짜 DOM 위 실제 런타임/app.js 실행을 확인. 자동화하지 않은 것: 포털 SSO 로 들어간 실제 화면·운영 서버 배포·Chrome 외 브라우저 — 사람 확인 목록은 Webmaker `Dev_Order_v1.8.0.md` 끝.
- 본문은 브라우저 직렬화(innerHTML)로 정규화된다(속성 큰따옴표·`&`→`&amp;`·태그 소문자·void 요소 `/>` 제거) — 의미는 같고 머리·doctype·꼬리는 글자 그대로. `</body>` 뒤 스크립트는 브라우저가 본문으로 옮기므로 저장 때 꼬리의 같은 스크립트를 제거해 중복을 막는다.
- 캔버스는 페이지 스크립트를 실행하지 않으므로 스크립트가 만드는 동적 내용(슬라이더·모듈 위젯 목록)은 빈 틀로 보인다. 구성 요소는 7종의 첫 릴리스, 블록 이동은 같은 문서 안. 다국어 사이트는 페이지마다 따로 편집(자동 재번역 없음 — 기존 다국어 카드 흐름 사용).
- 참조 저장소 1.8.0 은 `4b8c4d0de0f69025001a9e0a28a58a94e1a1f088` 으로 커밋·검증되어 `scripts/import-sources.mjs` 의 webmaker `pin` 을 그 커밋으로 올렸다. 이 작업 트리의 `IMPORTED_SOURCES.json/.md` 는 아직 개발 중에 만든 기록(기준 `3b10859`, `workingTreeDirty:true`)이며, 그 pin 으로 `node scripts/import-sources.mjs --only webmaker`(`--allow-dirty` 없이)를 돌려 다시 만들면 `commit = upstream.headCommit = 4b8c4d0`, `workingTreeDirty:false`, 184 파일·수정 8 의 깨끗한 기록이 되어 build·approve 가 가능하다(여기서는 참조 체크아웃이 그 커밋에 있지 않아 재생성하지 않았다). 0.1.4 지시(HAL AI 대시보드 등)는 이번 범위 밖으로 구현되지 않았다. 운영 서버 배포·검증·점검은 하지 않았다(출시 선언 없음).
### 미확정 정책값
- 0.1.1 의 미확정 정책값(`alias_counting`·`large_attachment`·`ai_wallet_pricing`, 후속 상품정책 값)은 그대로다. 직접 편집은 AI Wallet 을 쓰지 않으므로 과금 정책에 새 항목이 없다. 운영자가 뒤에 정할 수 있는 값: 직접 편집 본문 상한(현재 소스 편집기와 같은 5MB 고정), 실행 취소 기록 수(화면 200개 고정), 구성 요소 목록 확장.
## 0.1.1 (2026-10-26)
- **작성자**: 개발 지시 — Hera(`Ariweb-Suitle_Dev-Order_v0.1.1.md`, 통합 검토: Hera v0.1.0·Neo v0.1.0·1차 개발계획서) · 구현 — Claude Fable 5.1(Ariweb Suite 통합 개발, 커밋 Co-Authored-By 표기)
@@ -14,7 +87,7 @@
- AriMail 쪽에도 같은 구간을 적용해 `/api/usage` 가 `warnLevel`·`limitedReasons`·`monthReset` 을 돌려준다.
### 재사용한 기존 기능(그대로 도입)
기능별 상세 표는 `portal/webmaker/REUSE.md`(Webmaker, 원본 revision `3b10859`)와 `portal/mail/REUSE.md`(AriMail, 원본 revision `a07f69f`)에 있다. 두 모듈의 원본 회귀 테스트는 도입 뒤에도 모두 통과한다(Webmaker 59/59 — 원본 52 + 추가 7, AriMail 9개 묶음 모두 통과).
기능별 상세 표는 `portal/webmaker/REUSE.md`(Webmaker, 원본 revision `4b8c4d0` — 1.8.0; 0.1.1 기준 `3b10859`)와 `portal/mail/REUSE.md`(AriMail, 원본 revision `a07f69f`)에 있다. 두 모듈의 원본 회귀 테스트는 도입 뒤에도 모두 통과한다(Webmaker 59/59 — 원본 52 + 추가 7, AriMail 9개 묶음 모두 통과).
- **Webmaker(§0.1)**: `AI에게 맡기기`·`내가 고르기` 제작, Site Brief, Template 과 기존 Module, Page 생성·편집·미리보기·저장, AI 생성·수정과 Wallet/Cache 차감, 저장본 선택, Domain·DNS·TLS 관리(등록 업체·DNS 공급자·와일드카드 인증서), Gitea Source 연결과 변경 이력, 배포 결과 확인과 이전 Version 복구, AriMail Server 연결과 Mail 사용량 표시, 사용자·관리자 화면의 설정·상태·통계. 포털 사이트 상세 › 제작 탭의 "제작 화면 열기"(SSO) 로 진입한다.
- **AriMail(§0.3)**: Multi-domain Mail Service, Webmail(수신함·발신함·작성·회신·전달·검색), SMTP 발신·수신, IMAP·POP3, Resend·Haraka·External SMTP 발송 방식, Mail Domain·Account 관리, Mailbox 용량, 자동 응답·전달, 일반 첨부·링크 방식 대용량 첨부, POP3·IMAP 설정, DNS 설정·도메인 확인, 사용량 API, Webmaker 연동, 초기 설정·관리자·진단. 포털 메일 화면의 "웹메일 열기"(SSO 티켓) 와 외부 프로그램 설정 안내로 진입한다.
- **공통 라이브러리에 옮겨 적은 기존 방식**: Webmaker 의 원자적 JSON 저장·세션 세대·고정 관리자 비밀번호 폐지·scrypt 해시(→ `lib/db`·`lib/session`·`lib/crypto`), AriMail 의 의존성 없는 SMTP 클라이언트와 `node:sqlite` 운영(→ `portal/mailer.mjs`, `lib/db`), Webmaker `deploy/remote-update.sh` 의 갱신 순서(데이터 백업 → 교체 → 재시작 → 유닛 점검)와 AriMail `deploy/deploy.sh` 의 배포 순서(DB 백업 → 릴리스 복사 → 마이그레이션 → 재시작 → health → Haraka)를 HAL 모듈 설명자(`hal/modules/index.mjs`)와 Agent 모듈 설치기(`agent/jobs/module.mjs`)에 그대로 반영.