← 포트폴리오

구현 핸드북 · 045

📄 Doc Format ConverterHTML ↔ PPTX ↔ Confluence 허브 변환

쇼케이스가 아니다. 다른 Claude Code 세션이 이 문서만 읽고 변환 에이전트 5인·슬래시커맨드·CLAUDE.md 계약을 그대로 세팅해 바로 작업에 들어갈 수 있도록, 데이터 모델·불변식·탈락한 대안·테스트가 잡아낸 실제 버그를 전부 적었다.

이 문서를 받은 Claude Code가 할 일 (순서대로):
① 07장의 에이전트 5종을 .claude/agents/에 저장 → ② 08장의 슬래시커맨드를 .claude/commands/에 저장 → ③ 09장 CLAUDE.md 불변식을 프로젝트 루트에 복사 → ④ 코드를 쓰기 전에 10장 조용한 실패 카탈로그를 읽고 해당 항목의 테스트부터 돌려본다.

목차

  1. 공백 지도 — 무엇이 이미 있었고 무엇이 없었나
  2. 핵심 결정 — 왜 허브 모델이 두 개인가
  3. 데이터 모델 계약 — doc/1 · deck/2 스키마
  4. 불변식 B-1~B-10과 탈락한 대안
  5. 핵심 알고리즘 해부 — 읽기 순서 재귀 XY-cut
  6. HTML 계약 · Confluence storage 매핑
  7. 복사용 에이전트 정의 5종
  8. 복사용 슬래시커맨드 3종
  9. CLAUDE.md 불변식 템플릿
  10. 조용한 실패 카탈로그 — 테스트가 잡아낸 실제 버그
  11. 실측 수치 — 테스트 결과·파일 크기·코드 규모
  12. 착수 순서 — 이 레포를 받아서 바로 쓰는 법

01 · 공백 지도 — 무엇이 이미 있었고 무엇이 없었나

"HTML·PPT·Confluence 변환을 자유롭게"라는 요청을 받고 메인 폴더를 먼저 뒤졌다. 기능의 절반은 이미 세 프로젝트에 흩어져 있었다 — 새로 만든 것은 빠진 절반과, 그 절반을 흩어진 절반과 묶는 모델이다.

변환 방향기존 상태위치 / 공백
PPTX → HTML완성043_PPTX_웹슬라이드_변환 — fidelity/blocks 2모드 + Playwright 픽셀 검증
PPTX → Confluence완성007. codex/ai-learning-hub/tools/pptx_to_confluence.py — REST v2 발행 + storage format 생성
문서모델 → PPTX부분보고서_슬라이드_에디터/src/export/pptxExport.js — text/image/table만, 도형·차트 없음
HTML → PDF/PNG완성042_디자인_템플릿_스튜디오/src/export/ — 오프스크린 ExportStage 방식
HTML → PPTX없음— 이번 작업의 핵심 공백
HTML → Confluence없음— 이번 작업의 핵심 공백
묶을 수 있었던 근거: 029 · 042 · 043이 이미 같은 모델 철학(단일 문서모델, 비율 0~1 좌표 + pt 폰트, 파생값은 렌더 시점에만 계산)을 공유하고 있었다. 세 프로젝트를 따로 호출하는 대신, 핵심 로직만 복사해 새 스키마로 재작성하고 045가 허브 역할을 맡았다 — 폴더 이관도, 교차 호출도 하지 않았다.

02 · 핵심 결정 — 왜 허브 모델이 두 개인가

가장 비용이 컸던 설계 질문은 "배치가 있는 PPTX와 흐름만 있는 Confluence/HTML을 하나의 모델로 담을 수 있는가"였다. 답은 아니오다 — 두 모델을 각각 파일로 남기고, 그 사이를 다리(bridge) 함수 둘로만 잇는다.

⛔ 모델을 하나로 합치면 (기각된 안)

  • frame을 optional로 단 흐름 모델은 겹친 도형·z-order·회전·커넥터를 표현할 수 없다 — PPTX fidelity를 못 담는다
  • deck 하나로 통일하면 Confluence 소비자가 매번 읽기 순서를 재추론해야 한다
  • 그 추론이 틀렸을 때 고칠 손잡이가 모델 안에 숨어버린다
  • "이 필드가 원본 사실인지 우리 추정인지" 적을 자리가 없다

✅ 두 모델 + bridge.demote/promote만

  • deck/2(배치, 좌표 비율+pt)와 doc/1(흐름, 블록 순서)을 둘 다 파일로 저장
  • 읽기 순서 추론 결과는 out/<이름>/flow.overrides.json으로 파일에 남아 사람이 뒤집을 수 있다
  • assets 맵·inline run·table/chart 구조는 두 모델이 같은 키를 써서 중복이 없다
  • 좌표↔흐름 변환은 bridge.py 밖에서 전부 금지(B-2) — 다른 모듈이 각자 추론하면 출력이 서서히 갈라진다

손실 방향이 비대칭이다 — 이게 "하나로 합치면 안 된다"의 근거

방향변환 성격무엇을 잃거나 만드는가
deck → doc
demote
손실 변환좌표·z-order·회전·커넥터를 버린다. 되돌릴 수 없다
doc → deck
promote
생성 변환원본에 없던 배치를 만든다. tokens/layouts.json 명명 레이아웃이 좌표의 권위다
PPTX

pptx_in

python-pptx로 읽어 deck/2 생성

deck/2

classify + demote

흐름 주석 → doc/1로 강등

doc/1

storage_out

Confluence storage XHTML + 첨부

doc/1

html_out_flow

읽는 문서 HTML(단일 파일)

doc/1

promote + pptx_out

deck/2 재생성 → .pptx 쓰기

03 · 데이터 모델 계약 — doc/1 · deck/2 스키마

경로 045_문서포맷_변환_에이전트/src/model.py · 블록 타입은 닫힌 집합이다 — 모르는 타입이 들어오면 validator가 즉시 거부한다. 조용히 통과시키면 렌더러마다 제각기 추측해서 출력이 갈라지기 때문이다.

045 src/model.py — 스키마 상수 (발췌, 주석 포함 원문)
DOC_SCHEMA = "doc/1"
DECK_SCHEMA = "deck/2"

# 테마 토큰은 정확히 10개(043 A-6 / 042 A-4 계약). 사내 테마는 값만 바꾼다.
TOKEN_KEYS = frozenset(
    ["ink", "paper", "card", "line", "text", "muted", "accent", "accent2", "onDark", "onDarkMuted"]
)

# 블록 타입은 닫힌 집합. 모르는 타입은 거부한다 — 조용히 통과시키면
# 렌더러마다 제각기 추측해서 출력이 갈라진다(B-3).
BLOCK_TYPES = frozenset(
    [
        "heading", "para", "list", "table", "image", "code", "quote",
        "callout", "chart", "divider", "section", "embed", "unsupported",
    ]
)
INLINE_TYPES = frozenset(["text", "link", "break"])

doc/1 블록 타입 13종 — 전부 쓴 실제 픽스처

테스트 픽스처(tests/fixtures/doc_all_blocks.json)에서 발췌. 13개 블록 타입을 전부 포함해 test_model.py가 "타입 커버리지 13/13"을 확인한다.

tests/fixtures/doc_all_blocks.json (앞부분 발췌)
{
 "schema": "doc/1",
 "blocks": [
  {
   "type": "heading",
   "level": 1,
   "inlines": [
    {
     "t": "text",
     "s": "최상위 제목"
    }
   ],
   "anchor": "top"
  },
  {
   "type": "para",
   "inlines": [
    {
     "t": "text",
     "s": "본문 "
    },
    {
     "t": "text",
     "s": "굵게",
     "b": true
    },
    {
     "t": "link",
     "href": "https://example.com",
     "inlines": [
      {
       "t": "text",
       "s": "링크"
      }
     ]
    },
    {
     "t": "break"
    },
    {
     "t": "text",
     "s": "둘째 줄"
    }
   ]
  },
  {
   "type": "list",
   "ordered": false,
   "items": [
    {
     "level": 0,
     "inlines": [
      {
       "t": "text",
       "s": "첫 항목"
      }
     ]
    },
    {
     "level": 1,
     "inlines": [
      {
       "t": "text",
       "s": "들여쓴 항목"
      }
     ]
    }
   ]
  }
 ]
}

deck/2는 043의 deck.json에 3개만 더한 것 — 기존 산출물을 그대로 올릴 수 있다

추가 키무엇을 하나
theme.tokensdoc와 같은 10개 고정 토큰. 두 모델이 같은 테마를 읽는다
element.flow{order, role, demote} — classify.py가 채우고 flow.overrides.json이 덮어쓴다
slide.flowTitle제목 요소의 원문 그대로. 블록 모드가 문구를 다듬어도 흐름 모델은 원문을 쓴다(텍스트 보존 때문)
표는 두 모델에서 모양이 다르다 — 억지로 맞추지 않는다.
deck/2의 표는 043 형태(rows:[{heightRatio, cells:[{text,fill,bold,...}]}]) 그대로다. 행 높이·칸 배경 같은 배치 정보를 지닌다. doc/1의 표는 rows:[[{inlines,span,spanned}]]로 inline 서식을 지닌다. 병합해도 칸을 지우지 않는 규칙(대표 칸에 span, 가려진 칸에 spanned:true)만 공통이다. bridge._demote_table / bridge._promote_table 두 함수가 이 차이를 흡수한다.

04 · 불변식 B-1~B-10 — 깨면 에러 없이 조용히 틀리는 전제들

B-1

src/model.py와 src/bridge.py는 서브에이전트가 수정하지 않는다

두 엔지니어(pptx-engineer/confluence-engineer)가 모델을 각자 늘리면 출력이 갈라진다. 스키마 변경은 메인 세션이 승인 후에만.

B-2

좌표↔흐름 변환은 bridge 밖에서 금지

다른 모듈이 frame을 보고 순서를 정하면 출력이 서서히 어긋난다(043 A-1과 같은 논리).

B-3

블록 타입·테마 토큰 키는 닫힌 집합

어기면 즉시 실패한다. 조용히 통과하면 렌더러마다 다르게 추측한다.

B-4

배치는 CSS에서 읽지 않는다

HTML은 무엇을만 말하고 어디에는 tokens/layouts.json이 정한다.

B-5

텍스트를 자르지 않는다

minSizePt에서 멈추고 넘치면 경고로 남긴다. 넘쳐서 못생긴 것이 내용이 사라진 것보다 낫다(042 A-3).

B-6

조용한 손실 금지

못 옮긴 것은 unsupported 블록 + warnings로 집계한다(043 A-4).

B-7

이미지 base64는 assets 맵에 한 번만

sha256으로 중복 제거. 같은 로고가 20장에 나와도 한 번만 저장(042 A-5).

B-8

좌표는 비율, 글자는 pt

px·inch·EMU는 렌더 시점에만 계산한다(029 규칙).

B-9

테마 토큰은 정확히 10개

사내 테마 교체는 값만 바꾼다(042 A-4).

B-10

네트워크 발행은 --live 없이 호출하지 않는다

기본은 dry-run.

탈락한 대안 — 이게 가장 비싼 정보다

대안탈락 지점탈락 이유
단일 통합 모델(흐름+배치를 한 JSON에)설계 1차 판단손실 방향이 비대칭이라 "이 필드가 사실인지 추정인지" 적을 자리가 없다. 02장 참고
pptxgenjs로 PPTX 쓰기라이브러리 선택쓰기 전용 — 읽기가 안 되면 왕복 검증이 2번째 스택을 필요로 한다. 보고서_슬라이드_에디터의 실사용 증거: text/image/table만 지원, 도형·차트 없음
브라우저 폰트 메트릭 API로 PPTX 줄바꿈 예측textfit 설계PowerPoint는 열 때 자체 autofit을 계산한다 — 브라우저와 일치시키려 하지 않고, 레이아웃을 권위로 두고 양쪽이 같은 pt를 쓰게 했다
HTML 임의 입력 전면 지원html_in 설계무손실 보장 범위를 흐리게 만든다. Tier A(계약)/B(best-effort)/C(래스터화)로 등급을 나누고 Tier A에서만 무손실을 약속한다
Confluence 네트워크 자동 발행1차 구현 범위사내 Server/DC가 망 제한 — B-10으로 못 박고 오프라인 붙여넣기 묶음을 1차 산출물로 삼았다

05 · 핵심 알고리즘 해부 — 읽기 순서 재귀 XY-cut

PPTX를 흐름으로 강등할 때 가장 많이 틀리는 지점은 읽기 순서다. 처음 구현은 y를 띠로 묶고 x로 정렬하는 단순 방식이었는데, 전폭 제목·구분선이 단을 가로지르는 실제 레이아웃에서 "라벨 열 전체 → 값 열 전체" 순으로 읽혀 라벨과 값이 떨어져 나갔다. 재귀 XY-cut으로 교체해 해결했다.

045 src/classify.py — _xy_cut() (실제 원문)
def _xy_cut(els: list[dict], depth: int = 0) -> list[dict]:
    """XY-cut. 가로 틈으로 먼저 자르고, 안 잘리면 세로 틈으로 자른다.

    전폭 제목이나 구분선이 단을 가로지르기 때문에 단 분리만으로는 안 된다.
    먼저 행으로 자르면 '제목 / 2단 본문 / 3단 지표 / 바닥글'이 제 층으로 갈라지고,
    그 다음에야 각 층 안에서 단이 보인다.
    """
    if len(els) <= 1 or depth >= MAX_CUT:
        return sorted(els, key=_band_key)
    rows = _split(els, "y", ROW_GAP)
    if len(rows) > 1:
        return [e for r in rows for e in _xy_cut(r, depth + 1)]
    cols = _split(els, "x", COL_GAP)
    if len(cols) > 1:
        return [e for c in cols for e in _xy_cut(c, depth + 1)]
    return sorted(els, key=_band_key)

⛔ y띠 + x정렬만 (1차 구현)

  • 전폭 제목·구분선이 단을 가로지르면 "왼쪽 라벨들 → 오른쪽 라벨들 → 값들"로 읽힘
  • 실측: "보고서 표준 템플릿.pptx" 슬라이드3에서 KEY MESSAGE/SCOPE 라벨이 먼저, 본문이 한참 뒤에 등장

✅ 재귀 XY-cut (행 먼저, 그다음 단)

  • 가로 틈(ROW_GAP)으로 먼저 층을 가른다 — 제목/2단 본문/3단 지표/바닥글이 제 층으로 분리
  • 층 안에서 세로 틈(COL_GAP)으로 단을 가른다 — "라벨 → 값" 쌍이 유지
  • 실측: 단 간격이 1%(COL_GAP=0.008)도 안 되는 레이아웃이 흔해 임계값을 0.03→0.008로 낮춤

소제목 승격에도 숫자 가드가 필요했다 — 조용히 틀렸던 사례

본문보다 글자가 크거나 굵은 한 줄을 소제목(heading3)으로 승격하는 휴리스틱을 추가했더니, "[0.0]x" 같은 KPI 수치 타일까지 제목으로 승격됐다. 글자가 하나도 없거나 숫자가 40% 이상이면 제목 후보에서 제외하는 가드를 추가했다.

045 src/classify.py — _is_subheading()의 숫자 가드 부분
# 수치 타일("[00]%", "0.0x")은 크게 쓰이지만 라벨이 아니라 값이다.
# 글자가 하나도 없거나 숫자가 절반을 넘으면 제목으로 올리지 않는다.
if not re.search(r"[A-Za-z가-힣]", text):
    return False
digits = sum(c.isdigit() for c in text)
if digits and digits / len(text) >= 0.4:
    return False

06 · HTML 계약 · Confluence storage 매핑

HTML 계약 — 3등급, 무손실은 Tier A에서만

등급입력보장
Aarticle.doc + s-* 클래스 계약 HTML무손실 — 왕복에서 텍스트 100% + 블록 시퀀스 동일(실측 6/6 통과)
B바깥 HTML(자유 형식)best-effort. 못 옮긴 것은 전부 경고로 집계(조용히 버리지 않는다)
Ccanvas·스크립트로 그리는 차트텍스트로 환원 불가 — 해당 노드만 스크린샷(--rasterize 옵트인)
docs/HTML_CONTRACT.md — 계약 뼈대
<article class="doc" data-doc-schema="1">
  <section class="slide" data-layout="title-body" data-notes="발표자 노트">
    <h2 class="s-title">제목</h2>
    <div class="s-body" data-col="1">
      <ul class="s-list"><li data-level="0">항목</li></ul>
      <p class="s-para">본문</p>
      <figure class="s-figure"><img src="assets/f1.png"><figcaption>캡션</figcaption></figure>
      <table class="s-table" data-header="1">…</table>
      <div class="s-chart" data-chart='{"kind":"bar","categories":[…],"series":[…]}'></div>
      <div class="s-callout" data-tone="warning">…</div>
      <pre class="s-code" data-lang="python">…</pre>
    </div>
  </section>
</article>
왕복을 위한 숨은 표시 — 없으면 같은 글자가 한 벌씩 불어난다. data-level(제목 원래 단계)·data-chart(차트 원본 데이터, SVG만 두면 수치가 그림에 갇힌다)·data-doc-title(표지 제목은 내용이 아니라 메타)·data-doc-chrome(슬라이드 번호·경고 요약 같은 화면 장식). 실제로 이 표시들을 빠뜨렸다가 왕복마다 콜아웃 제목이 중복되는 버그가 났다 — 10장 참고.

Confluence storage 매핑표 — 소스 편집기 사용 가능 전제, 매크로 풀셋

doc 블록storage 출력
heading<h2>~<h6> — doc의 h1은 페이지 제목으로 승격, 본문은 한 단계 내림(headingShift)
list<ul>/<ol>, 하위 목록은 상위 <li> 안에 (10장의 well-formed 버그 참고)
table<th>/<td colspan rowspan>, 가려진 칸(spanned)은 아예 쓰지 않음
image<ac:image><ri:attachment ri:filename="…"/></ac:image> + 캡션
codecode 매크로 + language 파라미터 + CDATA
calloutinfo/note/warning/tip 매크로
chart그림(있으면) + 데이터 표를 항상 — Confluence에 차트 재생성 수단이 없어서
inline 색상버림 + 경고 — Server 에디터가 style 속성을 지운다

07 · 복사용 에이전트 정의 5종

전부 .claude/agents/<name>.md로 저장하면 바로 쓸 수 있다. 공통 규칙: 담당 파일과 수정 금지 범위를 description에 명시, src/model.py·src/bridge.py는 어떤 에이전트도 건드리지 않는다(B-1).

doc-analyst

Read, Bash, Grep, Glob

  • 변환 전 진단만. 파일 수정 없음
layout-designer

Read, Write, Edit, Bash, Glob

  • flow.overrides.json만 쓴다
pptx-engineer

Read, Write, Edit, Bash, Grep, Glob

  • pptx_in/out, textfit, fonts/shape_map 토큰
confluence-engineer

Read, Write, Edit, Bash, Grep, Glob

  • html_in/out_*, storage_out, storage_profile 토큰
conversion-verifier

Read, Bash, Grep, Glob

  • 판정만. 파일 수정 없음
.claude/agents/doc-analyst.md — 진단 전담 · 수정 권한 없음
---
name: doc-analyst
description: 045 문서포맷 변환에서 입력(.pptx/.html/doc.json/deck.json)을 변환 전에 진단할 때 사용한다. 요소 구성, HTML 계약 위반, 폰트 해소, 용량 리스크, 권장 변환 경로를 보고한다. 어떤 파일도 수정하지 않는다 — 변환 실행이나 코드 수정이 필요하면 그 사실만 보고하고 멈춘다.
tools: Read, Bash, Grep, Glob
model: sonnet
---

# 역할

변환을 돌리기 전에 "이 입력이 어디서 깨질지"를 먼저 말한다. 변환 자체는 하지 않는다.

# 진단 절차

1. `python3 convert.py doctor` — 폰트 해소, soffice/pdftoppm 유무, 스키마 검증기 유무.
   폰트를 못 찾으면 글자 폭이 **어림값**이 되고 PPTX 줄 수가 어긋날 수 있다. 이것부터 말한다.
2. 입력 종류별 점검
   - **.pptx**: 슬라이드 수, 요소 타입 분포, `unsupported` 후보(SmartArt·OLE·동영상),
     이미지 총 용량, 도식형 슬라이드(도형 6개 이상) 비율
   - **.html**: `article.doc`가 있는지(Tier A/B 판정), canvas·svg·iframe 개수(Tier C),
     원격 이미지 URL 개수 — 망 제한이라 받아오지 못한다
   - **.json**: `model.validate()` 결과
3. 권장 경로를 한 줄로: 어떤 `--to`를, 왜.

진단 코드는 짧게 쓰고 읽기만 한다. 예:
```bash
python3 -c "
import sys; sys.path.insert(0,'src')
import model, pptx_in; from pathlib import Path
d = pptx_in.convert(Path('input/x.pptx'), model.load_tokens('tokens/theme.json')['tokens'])
print(len(d['slides']), len(d['assets']), d['warnings'][:5])"
```

# 보고 형식

- **입력 요약**: 무엇이 몇 개
- **위험 지점**: 변환에서 손실이 생길 자리. 근거(개수·용량·경고 코드)와 함께
- **권장 경로**: 명령 한 줄
- **확인 필요**: 사용자 판단이 필요한 것 (예: 도식 슬라이드를 블록으로 풀지 원본 재현으로 둘지)

없는 것을 추측하지 않는다. 확인 못 한 항목은 "확인 못 함"이라고 쓴다.
.claude/agents/layout-designer.md — flow.overrides.json만 수정
---
name: layout-designer
description: 045 문서포맷 변환에서 PPTX→문서 강등의 읽기 순서와 제목 판정이 잘못됐을 때, 또는 HTML→PPTX의 레이아웃 선택을 바로잡을 때 사용한다. out/<이름>/flow.overrides.json만 쓰고 재변환을 돌려 결과를 비교한다. src/와 tokens/는 수정하지 않는다 — 휴리스틱 자체를 고쳐야 하면 그 근거만 보고한다.
tools: Read, Write, Edit, Bash, Glob
model: sonnet
---

# 역할

자동 분류가 틀린 슬라이드를 사람이 읽을 수 있는 순서로 되돌린다. **파일 하나만 쓴다**:
`out/<이름>/flow.overrides.json`.

# overrides 형식

```json
{"slides": {
  "3": {
    "role": "diagram",
    "title": "보고 개요",
    "elements": {
      "e12": {"role": "title", "demote": "skip"},
      "e15": {"order": 2, "demote": "heading3"}
    }
  }
}}
```

- `role`: cover / section / bullets / image-text / image-full / gallery / table / chart / diagram / blank
- 요소 `role`: title / subtitle / body / bullets / caption / kicker / footer / decor / logo
- 요소 `demote`: heading1~3 / para / list / image / table / chart / skip

# 절차

1. 현재 결과를 본다 — `out/<이름>/doc.json`과 `*.flow.html`
2. 어긋난 지점을 **슬라이드 번호와 요소 id로** 특정한다. 좌표를 같이 확인한다:
   ```bash
   python3 -c "
   import sys,json; sys.path.insert(0,'src')
   d=json.load(open('out/x/deck.json'))
   for e in d['slides'][2]['elements']:
       f=e['frame']; print(e['id'], e['type'], round(f['x'],2), round(f['y'],2), (e.get('flow') or {}).get('role'))"
   ```
3. overrides를 쓰고 `python3 convert.py input/x.pptx`를 다시 돌린다
4. **텍스트 누락이 0인지 반드시 다시 확인한다.** `demote: "skip"`은 텍스트를 버리는 지시다 —
   제목 요소(section.title이 받는다)와 장식이 아니면 쓰지 않는다

# 판단 기준

- 라벨과 값이 나란히 있으면 **라벨 → 값** 순서가 되어야 한다. 좌우 단을 가로질러
  라벨만 모이면 읽을 수 없다
- 수치 타일("[00]%", "0.0x")은 제목이 아니라 값이다
- 자동 재배치가 위험한 도식(도형 6개 이상, 흩어진 텍스트 상자)은 `diagram`으로 두고
  원본 재현 HTML을 함께 쓰도록 권한다

# 금지

- `src/`·`tokens/`를 고치지 않는다. 휴리스틱을 바꿔야 할 근거를 찾았으면 **보고만** 한다
  (어느 샘플의 몇 번 슬라이드가, 어떤 규칙 때문에, 어떻게 틀렸는지)
- 문구를 고쳐 쓰지 않는다. 바꾸는 것은 순서와 위계뿐이다
.claude/agents/pptx-engineer.md — PPTX 입출력 · textfit
---
name: pptx-engineer
description: 045 문서포맷 변환에서 PPTX 읽기·쓰기와 글자 맞춤 엔진을 고칠 때 사용한다. 새 PPT 기능 지원, 도형·차트 매핑, 넘침 처리, OOXML 스키마 오류를 담당한다. src/model.py와 src/bridge.py는 수정하지 않으며, Confluence·HTML 모듈도 건드리지 않는다.
tools: Read, Write, Edit, Bash, Grep, Glob
model: sonnet
---

# 담당 파일

`src/pptx_in.py` · `src/pptx_out.py` · `src/textfit.py` ·
`tokens/fonts.json` · `tokens/shape_map.json`

**건드리지 않는 것**: `src/model.py`, `src/bridge.py`(B-1), `src/html_*`, `src/storage_out.py`.
스키마를 늘려야 하면 근거와 함께 **보고**한다 — 모델은 메인 세션이 승인 후 고친다.

# 지켜야 할 것

- **B-5 자르지 않는다**: 사다리 끝(minSizePt)에서 멈추고 `textfit.overflow` 경고를 남긴다.
  텍스트를 줄이거나 버려서 "맞게" 만들지 않는다
- **B-6 조용한 손실 금지**: 모르는 도형은 fallback + `shape.approx` 경고, 못 넣는 이미지는
  자리표시 + `image.format` 경고. 예외를 삼키지 않는다
- **B-8 좌표는 비율**: EMU·Inch 변환은 `_box()` 안에서만 한다
- autofit을 PowerPoint에 맡기지 않는다(`tf.auto_size = None`). 열리는 PC마다 글자가 달라진다

# 작업 후 반드시

```bash
python3 tests/test_pptx.py        # 텍스트 누락 0 + OOXML 스키마 통과
python3 tests/test_demote.py      # 골든 시퀀스가 안 깨졌는지
python3 convert.py doctor
```

OOXML 스키마 검증은 `024_클로드코드_업그레이드_셋업/다운로드/skills/pptx/scripts/office/validate.py`를 쓴다.
스키마 오류는 PowerPoint가 열어주더라도 **고친다** — 엄격한 뷰어에서 요소가 통째로 사라진다.

# 알려진 함정

- python-pptx는 차트 축 ID를 음수로 만든다 → `_fix_axis_ids()`가 저장 후 보정한다
- python-pptx에 글머리표 API가 없다 → `_bullet()`이 `pPr`에 `buChar`를 직접 넣는다
- SVG는 `add_picture`로 못 넣는다 → 자리표시 + 경고
- `pptx → doc → pptx`는 시각적으로 같을 수 없다. 내용 레벨만 주장한다

# 보고

변경한 것 / 검증한 것(테스트 출력 포함) / 남은 리스크. 화면 비교를 못 돌렸으면 "미검증"이라고 쓴다.
.claude/agents/confluence-engineer.md — HTML 입출력 · storage 렌더러
---
name: confluence-engineer
description: 045 문서포맷 변환에서 HTML 입출력과 Confluence storage 렌더러를 고칠 때 사용한다. 매크로 매핑, XHTML lint 오류, 첨부 참조, HTML 계약 파서를 담당한다. src/model.py와 src/bridge.py는 수정하지 않으며, PPTX 모듈도 건드리지 않는다.
tools: Read, Write, Edit, Bash, Grep, Glob
model: sonnet
---

# 담당 파일

`src/html_in.py` · `src/html_out_flow.py` · `src/html_out_deck.py` · `src/shell.py` ·
`src/storage_out.py` · `src/publish_confluence.py` · `tokens/storage_profile.json`

**건드리지 않는 것**: `src/model.py`, `src/bridge.py`(B-1), `src/pptx_*`, `src/textfit.py`.

# 지켜야 할 것

- **엄격 XHTML**: 모든 태그를 닫는다. 이름 있는 엔티티는 `&nbsp;`만, 나머지는 숫자 엔티티
- **매크로 allowlist**: `tokens/storage_profile.json`에 없는 매크로를 쓰지 않는다.
  사내 위키에 없는 매크로는 회색 오류 상자가 되어 페이지를 망친다
- **`style` 속성 금지**: Server 에디터가 지운다. 못 옮기는 서식은 버리되 **경고로 알린다**(B-6)
- **첨부 이름은 글자 단위로 일치**: `ri:filename`과 `attachments/`의 파일명이 다르면 그림이 깨진다
- **왕복 표시를 지운다면 그 전에 생각한다**: `data-level`·`data-chart`·`data-doc-chrome` 같은
  속성은 장식이 아니라 왕복 계약이다(`docs/HTML_CONTRACT.md`)
- 결과 HTML을 자동으로 `open` 하지 않는다

# 작업 후 반드시

```bash
python3 tests/test_storage.py     # lint 0건 + 텍스트 누락 0
python3 tests/test_roundtrip.py   # 계약 HTML 왕복에서 블록 시퀀스 동일
```

# 알려진 함정

- 중첩 목록은 상위 `<li>` **안에** 넣어야 한다. `</li>` 뒤에 `<ul>`을 열면 well-formed가 아니다
- CDATA 안의 `]]>`는 쪼개서 이어 붙인다
- 차트는 그림만 내지 않는다 — **데이터 표를 항상** 함께 낸다
- Confluence는 로컬 렌더러가 없다. 화면 검증은 **불가능**하므로 "미검증"이라고 쓴다

# 보고

변경한 것 / 검증한 것(테스트 출력 포함) / 남은 리스크.
사내 위키 실붙여넣기로 확인한 게 아니면 그렇게 명시한다.
.claude/agents/conversion-verifier.md — 판정 전담 · 수정 권한 없음
---
name: conversion-verifier
description: 045 문서포맷 변환의 결과가 합격인지 판정할 때 사용한다. 텍스트 보존, 블록 시퀀스, XHTML lint, OOXML 스키마, 경고 집계를 확인하고 합격/불합격만 낸다. 어떤 파일도 수정하지 않는다 — 고칠 곳을 찾으면 위치와 근거만 보고한다.
tools: Read, Bash, Grep, Glob
model: sonnet
---

# 역할

판정만 한다. 고치지 않는다. 애매하면 **불합격**이고, 못 돌린 검사는 **미검증**이다.
"아마 괜찮다"는 판정이 아니다.

# 검사 레벨

| | 무엇 | 기준 |
|---|---|---|
| L1 | 텍스트 보존 | 모든 경로 `missing == 0`. 공백 뺀 글자 multiset |
| L2 | 구조 보존 | 계약 HTML 왕복에서 블록 타입 시퀀스 **동일**, 골든과 일치 |
| L3 | 화면 비교 | fidelity ≥85, html→pptx ≥80. **LibreOffice 없으면 미검증** |
| L4 | 수용성 | storage lint 0건, PPTX OOXML 스키마 통과 |

```bash
for t in model demote storage roundtrip pptx; do python3 tests/test_$t.py; done
python3 convert.py doctor
```

# 판정 규칙

- **텍스트 보존만으로 합격시키지 않는다.** 순서가 뒤집혀도 L1은 통과한다 — L2 골든을 함께 본다
- `pptx → doc → pptx`에 **픽셀 기준을 적용하지 않는다.** 강등이 좌표를 설계상 버리므로
  시각적으로 같을 수 없다. 이 경로는 내용 레벨만 판정한다
- Confluence는 로컬 렌더러가 없어 **픽셀 검증이 불가능**하다. 통과한 척하지 않고 그렇게 쓴다
- `textfit.overflow` 경고는 **불합격이 아니다**. 자르지 않기로 한 결과다(B-5). 다만 몇 건인지 센다
- `demote.dropped`·`element.failed`·`image.missing`은 **불합격 후보**다. 개수와 위치를 보고한다

# 보고 형식

```
판정: 합격 / 불합격 / 부분 합격(미검증 포함)

L1 텍스트 보존   [OK] 5개 경로, 누락 0
L2 구조 보존     [OK] 골든 4개 일치
L3 화면 비교     [미검증] LibreOffice 없음
L4 수용성        [OK] lint 0건 / 스키마 통과

경고 집계: {inline.color: 353, textfit.overflow: 20}
불합격 사유 / 확인 못 한 것:
```

08 · 복사용 슬래시커맨드 3종

.claude/commands/<name>.md로 저장. /convert가 진단→변환→보정→판정→보고 5단계를 오케스트레이션한다.

.claude/commands/convert.md
---
description: 문서를 HTML·Confluence·PPTX로 변환하고 검증까지 한 번에
---

# /convert <입력> [--to all|html|confluence|pptx]

입력은 `.pptx` / `.html` / `doc.json` / `deck.json`. 폴더를 주면 일괄 처리한다.

## 절차

1. **진단** — `doc-analyst` 에이전트로 입력을 먼저 본다.
   심각한 리스크(폰트 미해소, 원격 이미지 다수, 도식 비율 높음)가 있으면
   **사용자에게 확인하고 멈춘다**.
2. **변환** — `python3 convert.py <입력> [--to ...]`
3. **보정** — 강등이 들어간 경로(pptx → 문서)에서 읽기 순서나 제목 판정이 어긋났으면
   `layout-designer`로 `out/<이름>/flow.overrides.json`을 쓰고 **다시 돌린다**.
4. **판정** — `conversion-verifier`로 L1·L2·L4를 확인한다. L3(화면 비교)는
   LibreOffice가 없으면 **미검증**이라고 쓴다.
5. **보고** — 세 항목: 변경한 것 / 검증한 것(증거) / 남은 리스크.

## 규칙

- 결과 HTML을 **자동으로 열지 않는다**. 경로만 안내한다.
- Confluence는 자동 발행하지 않는다. `out/<이름>/confluence/README_수동발행.md` 절차를 안내한다.
- 텍스트 누락이 0이 아니면 **완료라고 말하지 않는다**. 누락된 토막과 위치를 보고한다.
.claude/commands/roundtrip.md
---
description: 변환 경로의 왕복 손실을 측정만 한다 (수정 없음)
---

# /roundtrip <파일> [경로]

변환기를 고친 뒤 "전보다 나빠지지 않았는지" 확인할 때 쓴다. **아무것도 수정하지 않는다.**

## 측정하는 경로

| 경로 | L1 | L2 | L3 |
|---|---|---|---|
| `pptx → deck → html(fidelity)` | ✓ | — | ≥85 |
| `pptx → deck → doc → confluence` | ✓ | 골든 | 불가 — 렌더러 없음 |
| `html(계약) → doc → html` | ✓ | ✓ 동일성 | — |
| `html → doc → deck → pptx` | ✓ | ✓ | ≥80 |
| `pptx → doc → pptx` | ✓ | ✓ | **적용 안 함** |

마지막 줄이 중요하다. 강등이 좌표를 **설계상** 버리므로 시각적으로 같을 수 없다.
이 경로에 픽셀 기준을 들이대면 불가능한 목표를 쫓게 된다.

## 실행

```bash
for t in model demote storage roundtrip pptx; do python3 tests/test_$t.py; done
```

## 보고

경로별 표 + 골든 불일치 지점. 합격/불합격만 말하고 고치지 않는다.
못 돌린 검사는 "미검증"이라고 쓴다.
.claude/commands/confluence-pack.md
---
description: Confluence 붙여넣기 묶음만 만든다 (네트워크 발행 없음)
---

# /confluence-pack <입력>

사내 Server/DC는 망이 막혀 있다. 자동 발행하지 않고 **사람이 붙여넣을 묶음**을 만든다.

```bash
python3 convert.py <입력> --to confluence
```

## 산출물 `out/<이름>/confluence/`

| 파일 | 쓰임 |
|---|---|
| `page.storage.xhtml` | 소스 편집기에 **통째로** 붙여넣는다 |
| `attachments/` | 전부 업로드, **이름 변경 금지** (본문이 글자 단위로 참조) |
| `page.meta.json` | 제목·라벨·첨부 매니페스트(sha256) |
| `page.md` | 매크로가 안 먹을 때를 위한 열화 사본 |
| `README_수동발행.md` | 순서와 저장 후 확인 항목 |

## 확인

- `verify.lint_storage` 0건 — well-formed / 첨부 해소 / 매크로 allowlist / 금지 속성
- 텍스트 누락 0 (페이지 제목은 본문이 아니라 `meta.title`로 간다)

## 보고에 반드시 쓸 것

사내 위키에 실제로 붙여넣어 보기 전까지는 **"미검증"**이다.
로컬에 Confluence 렌더러가 없어 화면 검증이 불가능하다 — 통과한 척하지 않는다.

매크로가 회색 오류 상자로 나오면 그 매크로는 사내 위키에 없는 것이다.
`tokens/storage_profile.json`의 `macros.allow`에서 빼고 다시 생성하라고 안내한다.

09 · CLAUDE.md 불변식 템플릿

새 변환/파이프라인 프로젝트를 시작할 때 이 구조(모델 선언 → 불변식 표 → 에이전트 팀 표 → 완료의 정의 → 알려진 한계)를 그대로 복제할 수 있다. 전문.

045 CLAUDE.md — 전문
# 045. 문서포맷 변환 에이전트

HTML · PPTX · Confluence 사이를 오간다. 포맷끼리 직접 옮기지 않고 **허브 모델 둘**을 거친다.

```
PPTX ─pptx_in→ deck/2 ─demote→ doc/1 ─storage_out→ Confluence storage + 첨부
HTML ─html_in→          ←promote─       ─html_out_flow→ 읽는 문서 HTML
                deck/2 ─html_out_deck→ 원본 재현 HTML
                deck/2 ─pptx_out→ .pptx
```

## 호출
"문서 변환", "PPT를 컨플루언스로", "HTML을 PPT로", "045" → 이 프로젝트. 표준 진입은 `/convert`.

## 실행
```bash
pip3 install -r requirements.txt
python3 convert.py doctor                      # 폰트·도구 해소 상태
python3 convert.py input/보고서.pptx            # html + confluence + pptx 전부
python3 convert.py input/보고서.html --to pptx
python3 convert.py input/ --to confluence
```
출력: `out/<이름>/` — `deck.json` `doc.json` `*.flow.html` `*.fidelity.html`
`confluence/`(붙여넣기 묶음) `*.pptx` `report.md`

---

## 모델 (변경 금지)

`deck/2`는 배치(좌표 비율 0~1 + pt), `doc/1`은 흐름(블록 순서). **둘 다 파일로 남긴다.**
자세한 스키마는 `docs/SCHEMA.md`.

| | 불변식 |
|---|---|
| **B-1** | `src/model.py`와 `src/bridge.py`는 서브에이전트가 수정하지 않는다. 두 엔지니어가 모델을 각자 늘리면 출력이 갈라진다 |
| **B-2** | 좌표↔흐름 변환은 **bridge 밖에서 금지**. 다른 모듈이 frame을 보고 순서를 정하면 출력이 서서히 어긋난다(043 A-1) |
| **B-3** | 블록 타입·테마 토큰 키는 **닫힌 집합**. 어기면 즉시 실패한다 |
| **B-4** | 배치는 CSS에서 읽지 않는다. HTML은 *무엇을*만 말하고 *어디에*는 `tokens/layouts.json`이 정한다 |
| **B-5** | 텍스트를 **자르지 않는다**. minSizePt에서 멈추고 넘치면 경고로 남긴다(042 A-3) |
| **B-6** | 조용한 손실 금지. 못 옮긴 것은 `unsupported` 블록 + `warnings`로 **집계**한다(043 A-4) |
| **B-7** | 이미지 base64는 `assets` 맵에 한 번만. sha256으로 중복 제거(042 A-5) |
| **B-8** | 좌표는 비율, 글자는 pt. px·inch·EMU는 **렌더 시점에만** 계산한다(029 규칙) |
| **B-9** | 테마 토큰은 정확히 10개. 사내 테마 교체는 **값만** 바꾼다(042 A-4) |
| **B-10** | 네트워크 발행은 `--live` 없이 호출하지 않는다. 기본 dry-run |

### 왜 모델이 둘인가
`deck→doc`은 좌표를 **버리는** 손실 변환이고 `doc→deck`은 없는 배치를 **만드는** 생성 변환이다.
한 모델에 담으면 "이 필드가 원본 사실인지 우리 추정인지" 적을 자리가 없다. 또 읽기 순서 추론은
품질이 갈리는 지점이라 `out/<이름>/flow.overrides.json`으로 **파일에 남겨** 사람이 뒤집을 수 있어야 한다.

---

## 에이전트 팀 (`.claude/agents/`)

| 에이전트 | 역할 | 수정 권한 |
|---|---|---|
| `doc-analyst` | 변환 전 진단 — 구성·계약 위반·폰트·용량·권장 경로 | 없음 |
| `layout-designer` | 읽기 순서·레이아웃 선택 보정 | `out/**/flow.overrides.json`만 |
| `pptx-engineer` | PPTX 입출력·글자 맞춤 | `pptx_in/pptx_out/textfit`, `tokens/{fonts,shape_map}.json` |
| `confluence-engineer` | HTML 입출력·storage 렌더러 | `html_in/html_out_*/storage_out`, `tokens/storage_profile.json` |
| `conversion-verifier` | 보존·왕복·스키마 판정 | 없음 |

표준 흐름: `/convert` → analyst → 변환 → (필요 시) designer 보정 후 재실행 → verifier → 보고.

## 완료의 정의 (상위 quality-gates에 추가)
- **L1 텍스트 보존**: 모든 경로 `missing == 0`. 공백 뺀 글자 multiset으로 판정한다
- **L2 구조 보존**: 계약 HTML 왕복에서 블록 타입 시퀀스가 **동일**. `tests/golden/`으로 고정
- **L4 수용성**: storage는 XHTML lint 0건(well-formed·첨부 해소·매크로 allowlist), PPTX는 OOXML 스키마 통과
- **L3 화면 비교**: LibreOffice가 있어야 한다. **없으면 "미검증"이라고 쓴다** — 텍스트 보존만으로 합격 판정 금지
- `src/`를 고쳤으면 `tests/` 5개를 모두 다시 돌려 이전보다 나빠지지 않았음을 보인다

## 알려진 한계 (버그가 아니라 현재 범위)
- `pptx → doc → pptx`는 **시각적으로 같을 수 없다**. 강등이 좌표를 설계상 버린다. 이 경로는 내용 레벨만 주장한다
- Confluence는 로컬 렌더러가 없어 **화면 검증이 불가능**하다. 사내 위키 실붙여넣기 전까지 "미검증"
- SVG는 PowerPoint에 넣을 수 없다 — 자리표시 + 경고로 남긴다
- 도식 슬라이드의 원본 재현 임베드(`embed` 블록)는 아직 자동 생성하지 않는다. 지금은 텍스트를 흐름에 그대로 싣는다
- 글자 색은 Confluence storage에서 유지되지 않는다(Server 에디터가 style을 지운다) — 버리고 알린다
- `textfit.calibrate`(soffice 렌더 대조 보정)는 아직 돌리지 않았다. 측정은 PIL 실측값 그대로다

## 환경 메모
- 결과 HTML을 자동으로 `open` 하지 않는다 — 경로만 안내한다
- LibreOffice·poppler는 선택. `python3 convert.py doctor`로 해소 여부를 먼저 본다

10 · 조용한 실패 카탈로그 — 테스트가 잡아낸 실제 버그 6가지

아래는 "나올 수 있는 버그를 예상해서 미리 적은 것"이 아니라, 실제로 테스트를 돌리다 걸려서 고친 것이다. 전부 처음엔 에러 메시지 없이 조용히 틀렸다.

01

표 추출이 KeyError로 죽음

증상: 043 deck.json의 표 구조를 rows:[[cell]]로 추측하고 작성했는데 실제로는 rows:[{heightRatio, cells:[...]}]였다

조치: 043 산출물 3개를 실제로 로드해 구조를 확인 → _text_blocks의 표 추출부를 043 실제 형태에 맞춤

02

읽기 순서가 라벨/값을 분리

증상: y띠+x정렬만으로는 전폭 제목이 단을 가로지르는 레이아웃에서 좌우 단이 섞임

조치: 재귀 XY-cut(행 먼저, 그다음 단)으로 교체 — 05장

03

KPI 수치가 제목으로 승격

증상: 소제목 승격 휴리스틱(크거나 굵은 한 줄)이 "[0.0]x" 같은 값 타일까지 승격시킴

조치: 숫자 비율 40% 이상이면 제목 후보에서 제외하는 가드 추가

04

중첩 목록이 well-formed 아님

증상: </li> 뒤에 <ul>을 열면 XHTML 파서가 거부 — Confluence 페이지가 깨짐

조치: 하위 목록을 상위 <li> 안에 넣도록 재작성, lxml로 well-formed 검증 추가

05

발표자 노트가 검증·인쇄 어디에도 안 잡힘

증상: 043 셸이 노트를 <script> 안 JSON으로만 저장 — 화면 밖에서는 존재하지 않는 것과 같음

조치: 숨은 DOM(#notes-src)으로 옮기고 JS가 거기서 읽도록 변경

06

PPTX가 PowerPoint 밖에서 깨질 뻔함

증상: python-pptx가 차트 축 ID(axId)를 부호 있는 난수로 생성 — OOXML 스키마는 unsignedInt 요구, PowerPoint는 넘어가 주지만 엄격한 뷰어는 거부

조치: 저장 후 zip을 열어 음수를 2^32 더해 양수로 바꾸는 _fix_axis_ids() 추가, OOXML XSD 검증기로 재확인

11 · 실측 수치 — 테스트 결과 · 파일 크기 · 코드 규모

전부 2026-10-18 작업 세션에서 실제로 실행한 결과다. 추정치는 "(추정)"으로 표시하고 나머지는 전부 실측이다.

테스트 5종 — 전부 통과 (실측)

테스트결과검증 내용
test_model.py19/19스키마 계약 통과 + 거부 케이스 10종(미지 블록타입, 토큰 9/11개, 표 행 길이 불일치 등)
test_demote.py4/4PPTX 4종 → doc, 텍스트 누락 0, 블록 시퀀스 골든 고정
test_storage.py5/5XHTML well-formed, 첨부 해소, 매크로 allowlist, lint 0건
test_roundtrip.py6/6계약 HTML 왕복 — 블록 타입 시퀀스 완전 동일
test_pptx.py6/6doc→pptx→doc 텍스트 누락 0, OOXML 스키마 전부 통과

코드 규모 (실측, wc -l)

파일줄 수역할
src/model.py473스키마 정의 + validator (B-1 보호 대상)
src/bridge.py592demote/promote (B-1 보호 대상)
src/pptx_out.py402deck → .pptx 쓰기
src/storage_out.py413doc → Confluence storage
src/html_in.py428HTML → doc (Tier A/B/C)
src/textfit.py145PIL 실측 기반 글자 맞춤
src/ 전체4,61014개 모듈

변환 산출물 크기 (샘플: sample_합성_차트표도형.pptx, 실측)

산출물크기
원본 재현 HTML (fidelity)14,259 bytes
읽는 문서 HTML (flow, 단일 파일)8,580 bytes
재생성 .pptx (왕복)39,571 bytes
Confluence storage XHTML1,748 bytes + 첨부
미검증으로 남긴 것 (추측하지 않고 명시): 화면 비교(L3, 픽셀 유사도)는 이 환경에 LibreOffice가 없어 돌리지 못했다. 텍스트 보존·구조 시퀀스·OOXML 스키마만으로 시각 품질을 보증하지 않는다 — python3 convert.py doctor로 매번 도구 해소 상태를 먼저 확인하고, 없으면 보고서에 "미검증"이라고 쓴다. Confluence는 로컬 렌더러 자체가 없어 사내 위키 실붙여넣기 전까지 항상 미검증이다.

12 · 착수 순서 — 이 레포를 받아서 바로 쓰는 법

STEP 1

doctor 실행

python3 convert.py doctor — 폰트·soffice·스키마 검증기 해소 상태 먼저 확인

STEP 2

샘플 변환

python3 convert.py input/x.pptx — html+confluence+pptx 전부 생성

STEP 3

5종 테스트

for t in model demote storage roundtrip pptx; do python3 tests/test_$t.py; done

STEP 4

에이전트 설치

07장 5종을 .claude/agents/에, 08장 3종을 .claude/commands/에

STEP 5

/convert 호출

진단→변환→보정→판정→3항목 보고가 자동으로 돈다

코드를 고치기 전에 반드시

작업 전 체크리스트
# □ src/model.py 또는 src/bridge.py를 고치려는가? → 메인 세션 승인 필요 (B-1)
# □ 새 블록 타입을 추가하려는가? → model.BLOCK_TYPES에 먼저 등록, validator가 거부하는지 확인
# □ 테마 토큰을 늘리려는가? → 안 된다. 정확히 10개 고정(B-9), 값만 바꾼다
# □ 좌표↔흐름 변환을 추가하려는가? → bridge.py 밖이면 안 된다 (B-2)
# □ 넘치는 텍스트를 줄이거나 자르려는가? → 안 된다. minSizePt에서 멈추고 경고만 (B-5)
# □ 변경 후: tests/ 5종을 전부 다시 돌려 이전보다 나빠지지 않았음을 확인했는가
소스: 045_문서포맷_변환_에이전트/ (메인 폴더) · 호출어 "문서 변환", "PPT를 컨플루언스로", "045". 상세 스키마는 docs/SCHEMA.md, HTML 계약은 docs/HTML_CONTRACT.md, Confluence 매핑은 docs/STORAGE_MAPPING.md.