2026-09-13 원본에서 평가 계약·구현·코퍼스가 달라진 것을 확인해 당시 기록으로 보관했습니다. 본문과 자동 동기화 표는 보관 당시 내용입니다.
현재 기준과 원본 보기 → · 보관 목록
내용 대치를 '병합'으로 하면 정렬이 깨진다. 원인이 두 층이고 해결 레이어가 두 갈래라
담당자가 갈린다 — 어디서 깨지는지를 먼저 판정하면 물어볼 사람이 자동으로 정해진다.
그리고 이 문제의 정면 해결은 이미 MOR-2018에서 진행 중이다.
이 판(2026-08-30)은 위키의 「내용 대치 후 텍스트 요소 재배치 이슈 정의」를 합쳐 범위(§1) · 개입 지점(§3) · 렌더 왕복 계약(§4) · 용어(부록)을 더했다. 위키 답변 중 코드가 이미 달라진 세 곳은 코드를 적고 어긋난 사실을 표시했다.
내용 대치 이후, 텍스트 가독성이 있는(넘침 · 부적절한 줄바꿈 · 과도한 축소가 없는) 디자인 결과물을 생성한다.
여기서 내용 대치는 컴포넌트의 기존 박스는 그대로 두고 그 안의 글자만 바꾸는 동작이다. 원본 문구(예: 8자)보다 긴 문구(예: 11자)가 들어가면 컨테이너가 수직 방향으로만 늘어나고, 렌더러가 폰트를 축소 하한(기본 원래 크기의 70%)까지 줄이거나, 그래도 안 되면 줄이 늘어나 박스·배경 도형을 넘어간다.
컨테이너는 텍스트 요소가 그려지는 배경 도형을 가리킨다 — 텍스트 요소의 bounding box 를 품고 있어야 한다. 용어가 서로 겹쳐 헷갈리므로 부록 「용어」에 따로 정리했다.
전체 공간이 부족한 게 아니라 그 공간의 분배가 안 된다는 것이다. 그래서 해법도 "더 줄이기"가 아니라 "남는 여백을 누구에게 줄지 정하기"가 된다.
| 구분 | 대상 요소 | 무엇을 건드리나 |
|---|---|---|
| Scope 1 이번 | ElementType.Text |
텍스트의 컨테이너 크기, 그리고 요소·컨테이너의 위치 |
| Scope 2 후속 | ElementType.Image · Chart 등 |
대치 이후 심미적 완결성이 있는 thumbnail 까지 |
| 제외 | 격자(grid) 노드 | 텍스트 용량 수요 계산에서 빠지고 대치·레이아웃 규칙이 달라 Scope 1 에서 뺀다 |
Scope 1 에서 이미지·차트 등 다른 요소의 크기는 변형하지 않는다 — 텍스트 컨테이너만 조정한다.
컨테이너 크기는 그대로 두고 텍스트 크기·줄바꿈·행간을 조절하고 컨테이너 위치만 옮기는 접근. 이석님이 이미 작업한 영역이다 (UNICORN-78825).
그 작업은 렌더 전에 텍스트 배치 예측값으로 배치한다 — 예측이 어려워 결과가 정확하지 않다. 그리고 컨테이너 위치는 옮겼지만 크기는 유지했기 때문에 ㉠ 텍스트의 과도 축소에 대응하기 어렵고 ㉡ 가용 여백을 쓰는 데 제약이 있다. 이번 작업이 크기까지 손대는 이유가 여기다.
렌더러(텍스트 흘리기 · 폰트 오토핏 · 재레이아웃)는 우리 통제 밖이다. 그 안을 고치는 경로는 이 이슈가 다루지 않는다 — 다만 성능 개선을 위해 나중에 연결점을 찾는다(§8-D).
배치 순서(alignment-direction)를 바꾸거나, 요소 사이의
위상(topology)을 바꾸는 것(리플로우)은 범위 밖이다.
추후 고려할 가능성은 열어 둔다.
출처 — 위키 「내용 대치 후 텍스트 요소 재배치 이슈 정의」 · MOR-2018
'병합'은 렌더링 장치가 아니다. 검색용 쿼리 트리를 한 단계 접는 변형이고, 접은 만큼 점수를 깎는다. 그림을 고쳐 주는 코드가 아니라 후보를 더 찾아 주는 코드다.
mergeStep병합은 시그니처 트리를 접는 연산이고, 접힌 단계 수만큼 점수에서 빠진다.
/**
* 한 단계 자식 병합. 현재 최대 leaf depth 의 형제 그룹 / 외동 leaf 를
* 모두 한 번에 병합해 하나의 새 forest 를 만든다.
* ...
* 형제 병합할 그룹이 없는 depth ≤1 트리는 null.
*/
// 병합 감점은 root 에 가까운 단계일수록 크게: 1+2+...+step = step(step+1)/2. const mergeWeight = (args.mergeStep * (args.mergeStep + 1)) / 2;
병합은 검색 단계의 연산이라 정렬을 고칠 수단이 애초에 없다. 정렬은 그 뒤 단계(박스 보정 · 재배치)의 일이고, 그 단계에 지금 무엇이 있는지가 §5다.
대치는 렌더러에 ThumbnailLoad 한 덩어리를 넘긴다.
그 안에 구조를 바꾸는 필드가 없다 — 글자(nodeTextMap)와
박스(nodeBboxMap)뿐이다.
return { type: 'COMPONENT_THUMBNAIL', sheetJson, // 후보 원본 — 구조 그대로 nodeTextMap, // 글자 주입 nodeBboxMap, // 박스 보정 (여기가 전부다) siblingGroups: buildSiblingGroups(textMap, sheetJson), ...(rlsc ? { rlsc } : {}), };
| 토글 | 하는 일 | 기본값 |
|---|---|---|
expandText | 텍스트 노드를 형제·부모와 겹치지 않는 선에서 가로 확장 | false |
relieveTopBias | 위로 쏠림 완화 — 글씨 절반만큼 아래로 이동 | true |
ensureBgMargin | 텍스트 박스를 배경 도형 경계 안쪽으로 clamp | true |
tidyLayout 공간 정돈 | force-directed 시뮬레이션으로 box 일괄 조정 — 켜지면 위 셋을 대체 | false |
근거: build-replacement.ts:54-70 ·
즉 정렬 개선 수단(tidyLayout)은 기본으로 꺼져 있다.
"왜 정렬이 안 맞나"의 첫 번째 답이 여기일 수 있다.
위 둘이 "왜 병합으로는 못 고치나"의 답이라면, 아래 셋은 대치 파이프라인 자체가 왜 여백을 못 나누나의 답이다. 세 층이 각각 다른 곳에 있어서 한 곳만 고쳐서는 안 풀린다.
| 원인 | 어디서 | 설명 |
|---|---|---|
| 박스가 원본 텍스트 기준으로 고정 | design_objects. |
컴포넌트 제작 시 원본 문구 길이에 맞춰 디자이너가 잡아 둔 박스다 |
| 대치 전 보정이 노드 단위 국소 보정 | helpers.ts:819 buildNodeBboxMap |
각 노드를 개별적으로 조금 밀거나 clamp 할 뿐, "이 페이지의 남는 여백을 누구에게 줄지"를 못 정한다 |
| 축소가 마지막 방어선 | types.ts:222 fontScaleRange |
기본 { min: 0.7, max: 1 } — 최대 30%까지 줄여 흡수하고, 그래도 안 되면 넘친다 |
⚠ 문서와 코드가 어긋난 곳 — 위키·핸드오프 자료는 buildNodeBboxMap 을
helpers.ts:685 로 적지만, 현재 코드에서는 :819 다.
fontScaleRange 기본값 {min:0.7, max:1} 은 코드와 일치한다.
전부 다른 티켓이지만, 대치 이후 정렬이 깨질 때 어떤 모습으로 나타나는지의 표본이다. 새 케이스를 분류할 때(§8 B-3 #11) 대조군으로 쓸 수 있다.
| 티켓 | 증상 |
|---|---|
| UNICORN-70945 | 불렛 적용 시 원본 정렬(중앙/좌/우) 유지 규칙 |
| UNICORN-70809 | 특정 표에서 내용 대치 시 내용이 전부 상단에 붙어버림 |
| UNICORN-70728 | 컴포넌트 추가 시 텍스트 top 위치가 틀어짐 |
| UNICORN-78435 · 74379 | 줄바꿈이 생긴 열의 너비 계산 문제 |
| UNICORN-72202 | 내용 대치 시 불렛이 작아지는 현상 |
대치 파이프라인에서 우리가 손댈 수 있는 자리는 세 곳뿐이다.
기존 보정도, 실패한 tidyLayout 도 전부 A 에 있다.
이 이슈가 다루는 것은 C — 1차 렌더 후, 최종 렌더 직전이다.
buildContentReplacement(){ sheetJson, nodeTextMap, nodeBboxMap, siblingGroups }fontScaleRangeCOMPONENT_THUMBNAIL_LOADED.pageJsondocs/post-replace-relayout-handoff.md §2 의 파이프라인 지도를 옮긴 것.| 지점 | 방법 | 장점 | 단점 | |
|---|---|---|---|---|
| A | 렌더 전 nodeBboxMap |
대치 전에 박스를 다시 계산해 넘긴다 | 렌더러가 알아서 흘려 준다. 기존 보정·tidyLayout 이 모두 여기 |
실제 텍스트가 어떻게 흐를지는 예측(predictReplacedTextBbox)에 의존 |
| B | fontScaleRange |
축소 하한·상한 조정 | 한 줄 | 근본 해결이 아니다. 하한을 낮추면 더 작아질 뿐 |
| C 이 이슈 | 렌더 후 sheetJson | 렌더 결과를 보고 재배치한 뒤 다시 렌더 | 실제 흐른 결과(줄 수·넘침·축소)를 관측하고 판단한다 | 왕복 2회. 반복 수렴 설계가 필요 |
A 는 "예측 기반 1-pass", C 는 "관측 기반 iterative". 선행 실험(tidyLayout)이
실패한 이유가 렌더 결과가 아니라 예측치로 배치했기 때문이라는 것이 C를 고른 근거다.
x, ywidth, height
실제 필드는 node.data.props.boundingBox = { left, top, width, height } 다.
node.data.boundingBox 가 아니다 — 자주 틀리는 지점.
선행 실험이 실패한 원인이 "예측치로 배치"였다면, 실패를 실패라고 부를 자가 없었다는 것이 두 번째 원인이다. 그래서 지표 정의가 1순위다.
전부 컴포넌트 매칭 이후, 내용 대치가 완료된 것이어야 한다.
original (대치 전 원본) · before (대치 직후) ·
after (before + 재배치 알고리즘) ·
expected (사람이 보기에 optimal 한 정답)
⚠ expected 는 담당자 주관이 들어간 결과라 제3자 리뷰가 필요하다 —
이것이 §8 B-0(채점 방식 결정) 이 최우선인 이유다.
thumbnail 은 화면 확인용일 뿐이라 없어도 될 수 있다.
현재 기본 on 인 국소 보정(ensureBgMargin · expandText)의 지표를
먼저 재서 개선의 기준으로 삼는다.
렌더링 파이프라인을 두 번 거치는 비효율이 있지만, 일단 결과를 얻기 위해 그 방법으로 구현한다. 성능 개선·다른 방법 도출은 그 뒤다.
아래 넷은 그대로 쓴다.
| 부품 | 무엇 | 어디 |
|---|---|---|
buildVisualTreeRebased | 문서를 절대 AABB 트리로 정규화 — 형제·조상 판정의 단일 출처 | helpers.ts:103 |
textPredictInputsFromNodepredictReplacedTextBbox | 대치 텍스트 박스 예측 | textPredict.ts:71 · :84 |
| 배경 도형 식별 · 가이드 선 그룹핑 | "원래 정렬돼 있던 변들" 탐지 — 구조·균형 유지 요구에 직접 대응 | tidyLayout.ts |
TidyLayoutDebugModal | 재배치가 무엇을 하는지 눈으로 보는 디버그 도구 — 새 알고리즘도 프레임만 흘려주면 재사용 | TidyLayoutDebugModal.tsx |
C(렌더 후 후처리)가 성립하려면 1차 렌더 결과를 코드로 읽어올 수 있어야 한다. 결론부터 — 가능하고, 이미 도는 코드가 있다. 다만 무엇을 실측이라 부를 것인가에서 위키 답변과 현재 코드가 갈린다.
parent → iframe COMPONENT_THUMBNAIL_PING iframe → parent COMPONENT_THUMBNAIL_READY parent → iframe COMPONENT_THUMBNAIL { sheetJson, nodeTextMap, nodeBboxMap?, siblingGroups?, fontScaleRange?, rlsc? } iframe → parent COMPONENT_THUMBNAIL_LOADED { pageJson?: Doc } ← 이게 렌더 결과
'COMPONENT_THUMBNAIL*' 의 wire 문자열은 iframe 쪽 코드와 공유한다 — 바꾸면 안 된다.
노드 id 는 원본과 같아서 원본 doc 과 노드 단위로 대조할 수 있다.
| 용도 | 코드 | 메모 |
|---|---|---|
| 헤드리스(Playwright) 추출 | comp-match-eval-render.ts:360 | renderViaPreview() — payload 를 던지고 pageJson 을 받는다 |
| 앱(브라우저) 안에서 | Thumbnail.tsx:314 | setRenderedDoc(e.data.pageJson) — hover 강조 박스 좌표로 쓴다 |
| 재배치 뷰어의 라운드트립 | viewer/DocIframe.tsx:218 | 추출한 sheetJson 을 nodeTextMap 을 비운 채 다시 넣어 렌더(텍스트 재대치 방지) |
| 렌더 재검증 프로브 현행 | viewer/useRenderProbe.tsx | 화면 밖 iframe 하나에 문서를 여러 번 밀어 넣고 Promise 로 답을 받는다 |
| 내장 렌더 호스트 현행 | doc-render/DocRenderHost.tsx:171 | onViewLoaded — pageJson 과 함께 tightBoxes 를 실어 보낸다 |
자세한 것은 엔진 내장 렌더 호스트 문서에 있다.
pageJson 만으로는 재검증이 안 된다
위키 답변은 pageJson 이 "대치·폰트 오토핏·재레이아웃까지 전부 끝난 sheetJson" 이고
data.props.boundingBox 에 재레이아웃 결과가 반영된다고 적는다.
현재 코드는 그렇지 않다고 말한다.
내장 렌더 경로에서 텍스트 노드의 boundingBox 는 문서에 적힌 값 그대로 돌아온다 —
폭을 60%로 줄여 넣어도 높이가 한 자리도 안 변하는 것이 실측됐고,
layout: autoHeight 인 텍스트도 마찬가지였다.
엔진이 재는 것은 뷰 안쪽이고, 잰 값을 노드 props 로 되쓰는 단계가 이 경로에 없다.
그래서 "폭을 열었더니 세 줄이 두 줄이 됐나"를 pageJson 으로 물으면
언제나 「예측대로」라고 답한다 — 조용히 틀리는 종류의 검증이다.
대신 글자 영역(tight bounding box)을 직접 잰다 —
COMPONENT_THUMBNAIL_LOADED.tightBoxes. 엔진 계산기
(src/text-tight-box/compute.ts)로 잰 값이고, CLI 포트
(batch/tightBoxProbe.ts)·구워 둔 값(eval-out/cases/<id>/tight/)과
같은 자·같은 좌표 규약(프레임 원점 기준)이라 뷰어와 CLI 가 같은 답을 낸다.
근거: DocRenderHost.tsx:150-171
(onViewLoaded 주석) · useRenderProbe.tsx:21-38.
이 서술이 §8 B-3 #5「렌더러가 실제로 무엇을 바꾸나」 의 답이기도 하다.
렌더러가 컴포넌트를 페이지의 80%에 fit 시키고 절대좌표로 내보낸다.
실측 케이스에서 uniform scale ×1.910,
그룹이 (378.8, 416.1, 631.6×427.9) → (356.7, 108, 1206.5×864) 로 갔다.
원본 doc 과 직접 좌표 비교는 안 되고, 정규화하거나 렌더된 doc 끼리만 비교해야 한다.
다행히 fit 은 멱등이라 라운드트립은 안정적이다 — 재입력해도 scale 1.0.
rotatedBoundingBox 가 stale 하고 angle/angleType 이 빠진다
Thumbnail.tsx:579 가
preferRotated: renderedDoc == null 로 우회한다.
회전 노드는 rotated 값을 믿으면 안 된다.
위키의 주의 3·4(origin allowlist · 매일 20:00 자동 sleep)는
design-2 의 preview 배포 unicorn-73380 을 띄우던 시절 이야기다.
그 배포는 삭제됐고, 뷰어는 엔진 렌더러를 이 저장소에 내장해
(doc-render/) same-origin iframe 으로 바꿨다.
남의 서버가 켜져 있는지 신경 쓸 일이 없어졌다.
단, CLI 경로는 아직 옮겨지지 않았다 —
comp-match-eval-render.ts:39-49 는
여전히 unicorn-73380.preview.miricanvas.com 을 가리키고
COMP_MATCH_EMBEDDER_ORIGIN(기본 https://iui.miricanvas.com)으로
부모 페이지를 위장해 띄운다. 이 경로는 지금 실패한다.
내장 렌더 호스트에는 글자를 줄이는 동작 자체가 없다. 그래서 재는 값은 언제나
「문서에 박힌 글자 크기로 몇 줄」이고, 재검증이 묻는 것도 그 값이다.
폰트 오토핏이 실제로 얼마나 줄이는지는 preview 렌더러 경로에서만 관측된다 —
위키에 적힌 25.732 → 18.012 같은 값이 그쪽 산출물이다.
두 경로를 섞어서 비교하면 안 된다.
박스가 넘치거나 변이 어긋나는 기하 문제인지, 대상이 담을 수 없는 계층이 필요한 구조 문제인지에 따라 손댈 곳이 다르다.
기존 지도에는 ④a·④b 둘뿐이었지만, 실제로는 이 문제를 정면으로 푸는 세 번째 작업이
이미 돌고 있다 — miricanvas-iui/src/post-replace-relayout/ 의 재배치 후보 다섯 개다.
그리고 그 안에서 tidyLayout 은 후보가 아니라 대조군으로 쓰인다.
자세한 것은 §8-B.
같은 단어가 서로 다른 것을 가리켜서, 어느 쪽을 말하는지에 따라 담당이 갈린다.
아래 줄 번호는 고정 커밋 기준이라 링크가 그 시점을 가리킨다. 코드가 움직이면 어긋날 수 있으니, 엉뚱한 곳이 뜨면 커밋을 갱신할 것.
| 무엇 | 파일 · 줄 | 메모 |
|---|---|---|
| 대치 페이로드 조립 | build-replacement.ts:109 | buildContentReplacement() — 여기가 대치의 출구 |
| 박스 보정 토글 | build-replacement.ts:61-70 | tidyLayout 기본 false |
| 공간 정돈 본체 | comp-match/tidyLayout.ts | 815줄 · buildTidyLayoutBboxMap |
| 병합 트리 생성 | comp-match/matching.ts:459 | 한 단계 자식 병합 |
| 병합 감점 | comp-match/scoring.ts:456 | step(step+1)/2 |
| 재배치 후보 · 지표 | src/post-replace-relayout/ | MOR-2018의 구현 대상 · 14,836줄 |
| 컨테이너 판정 | relayout-core/metrics.ts | findTextContainer — 넘침·겹침 전부의 기반 |
| 렌더 메시지 계약 | comp-match/types.ts:210-249 | PING / READY / THUMBNAIL / LOADED — wire 문자열 고정 |
| 내장 렌더 호스트 | doc-render/DocRenderHost.tsx:171 | onViewLoaded — pageJson + tightBoxes |
| 렌더 재검증 프로브 | viewer/useRenderProbe.tsx | 화면 밖 iframe · Promise 로 실측 회수 |
| 글자 영역 실측기 | src/text-tight-box/compute.ts | computeTightBoxesForDoc — 뷰어·CLI 공용 |
| 무엇 | 파일 | 메모 |
|---|---|---|
| 대치 4단계 | buildMappedComponentSheetJson.ts:108 | 내용→폰트→스타일→이미지 |
| SmartBlock 자동 레이아웃 | AbstractSmartBlockLayoutWithChildrenStrategy.ts | Flex · Grid · V1 세 구현 |
| 캔버스 반영 | ComponentItemReplaceCommand.ts | 대치의 종착점 |
⚠ 렌더러(텍스트 흘리기 · 폰트 오토핏 · 재레이아웃)는 우리 통제 밖이다.
끄는 레버는 shrinkConfigs 하나뿐이고, 줄바꿈·재레이아웃은 그래도 일어난다.
이 문제에 직접 닿는 코드는 없다 — RLSC(structure_json)를 생산하는 쪽이라
대치·정렬 런타임에는 관여하지 않는다. 다만 대치의 판단 근거인 Component RLSC가 여기서 나오므로,
"구조가 애초에 잘못 잡혔나"를 의심할 때 거슬러 올라가는 지점이다.
담당을 가르는 질문(A)과, 그와 별개로 이미 돌고 있는 근본 해결 작업(B)을 나눠 적는다.
B는 MOR-2018 의 조사·구현 문서에서 가져왔고, 아래 ↻ 표시가 붙은 표는
원본이 바뀌면 sync-sources.py 로 다시 채워진다.
병합 · 대치 · 정돈 갈래(④a)는 홍지혜가 맡는다.
이석님 Pod 이 이미 만들어 둔 접근(tidyLayout · comp-sign 스마트블록 대치)은
「기존 접근 방법」 절로 뺐고 참고만 한다 —
전제로 삼지 않는다. 실제 해결 경로는 아래 B 다.
확인 결과 생성엔진 영역이면 AI연구팀으로 넘긴다. ailabs SmartBlock 생성기 소관일 때만.
우선순위·PRD는 내용시각화 스쿼드가 갖는다 — 김수현(스쿼드 리더) + 정원석(PM-E), 창석님은 요청자.
문제가 나는 지점(병합 · 대치 · 정돈)은 홍지혜가 맡는다. 이석님 Pod 의 기존 구현은 참고 자료이고, 구조 재배치가 필요할 때만 주영님.
재배치 알고리즘 파이프라인 v0.7 —
method 1~8의 역할과 대표 조합 3→1→2→5→4→7→8→6,
실제 글자 영역을 이용한 이동, 축소 전 검증과 최종 외곽선 보정을 정리했다.
아래의 기존 측정 표는 각 표에 표시된 시점을 기준으로 읽는다.
"내용 대치가 끝난 컴포넌트를 ① 넘침 ② 과도 축소 ③ 여백 낭비가 없도록 재정렬한다." 이 문제를 다루는 현재 구현은 연산자 8개와 이를 조합한 실행 경로로 구성돼 있다.
후보 다섯을 다 돌리고도 남는 FAIL 을 글자를 줄여 풀 수 있나, 줄인다면 어디서 멈춰야 하나. 커밋된 119건 실측과 하한 기준의 결정 (「원안 대비 배율」 vs 「페이지 대비 크기」)을 그 페이지에 따로 뒀다.
정답(expected_result.json)이 스크립트 산출물이라
알고리즘이 같은 절차를 쓰면 채점이 순환한다. 커밋 a0cb02aa 에서
손으로 만든 정답이 스크립트 산출물로 덮였다.
현재 코드는 (b)에 가깝게 이동했다 — README가 채점 기준을
original.json(대치 이전 원안)으로 바꿨다. 문서(w3 §5)와
코드의 결론이 갈리는 지점이라 확인이 필요하다.
method1 — 세로 공간 재분배. 폭 고정이라 예측 0method2 — 가로 확장. 줄 수를 줄인다 · 줄바꿈 예측이 들어간다method3 — 가로 넘침 해소. 도형을 좌우로 키우고, 막히면 텍스트를 안쪽 폭으로 좁힌다method4 — 대치가 만든 겹침 해소 (유닛 사이) · 크기 고정 · 예측 0method5 — 유닛 안의 겹침 해소. 한 배경 도형 안에서 글자끼리 포개진 것
읽는 법 — 넘침을 줄이는 일은 합본 3+1+2 에서 끝난다
(4,107,470 → 534,454px², 87%). 게이트(신규 겹침)는 +4·+5+4 가 따로 푼다.
tidy 열이 대조군이다 — 게이트 34/119로 before(35)보다도 낮다.
| 열 | 넘침 노드(H/V) | 넘침 면적 | 게이트 PASS | 폰트 배율(평균) | 정렬선(평균) |
|---|---|---|---|---|---|
original(기준) | 11 (8/9) | 129,625px² | 기준 | 1.000 | 1.000 |
before | 97 (63/93) | 3,079,999px² | 22/85 | 1.000 | 0.924 |
method1 | 48 (43/44) | 1,839,292px² | 26/85 | 1.000 | 0.897 |
method2 | 68 (55/65) | 2,387,056px² | 30/85 | 1.000 | 0.879 |
method3 | 89 (54/88) | 2,410,296px² | 23/85 | 1.000 | 0.909 |
합본 1+2 | 40 (37/38) | 1,599,446px² | 51/85 | 1.000 | 0.846 |
합본 3+1+2 | 32 (28/31) | 911,461px² | 51/85 | 1.000 | 0.833 |
합본 3+1+2+4 | 32 (28/31) | 911,461px² | 59/85 | 1.000 | 0.820 |
합본 3+1+2+5+4 | 32 (28/31) | 911,461px² | 63/85 | 1.000 | 0.817 |
합본 3+1+2+5+4+6 | 30 (26/30) | 744,726px² | 69/85 | 1.000 | 0.813 |
합본 …+5+4+7+6 | 30 (26/30) | 744,726px² | 76/85 | 1.000 | 0.809 |
합본 …+5+4+8+6 | 30 (26/30) | 744,726px² | 70/85 | 1.000 | 0.810 |
합본 …+5+4+7+8+6 | 30 (26/30) | 744,726px² | 77/85 | 1.000 | 0.806 |
tidy | 33 (18/29) | 1,213,757px² | 56/85 | 1.000 | 0.887 |
expected(목표선) | 3 (3/3) | 52,676px² | 37/41 | 0.985 | 0.843 |
↻ post-replace-relayout 후보별 실측 — 원본에서 자동 동기화
⚠ 이 표는 갱신이 멈춰 있다 — 원본(src/post-replace-relayout/README.md)의
채점표가 후보별 119건 비교에서 100건 SUCCESS·ACCEPTABLE·FAIL 요약으로 바뀌면서
sync-sources.py 가 절을 못 찾는다(2026-08-30 확인).
최신 성적은 재배치 알고리즘 파이프라인 v0.7 에 있다 —
채점 대상 88건 중 SUCCESS 70(79.5%) · ACCEPTABLE 16(18.2%) · FAIL 2(2.3%).
위 표는 연산자별 기여도를 보는 용도로만 읽는다.
예비 실측에서 커밋된 7건 전부가 세로 여유만으로 부족분을 덮었다 —
가로 넘침 0건. 그래서 method2 는 method1 이 실패하는 케이스를
찾은 뒤에 착수한다는 것이 원래 계획이었다.
(실제로는 method3 이 필요해지면서 계획이 한 번 갈렸다 — B-1 참고)
| # | 항목 | 어디에 | 규모 |
|---|---|---|---|
| 0 | 채점 방식 결정 (§5) | — | 먼저. 이게 없으면 비교 자체가 무의미 |
| 1 | tight box를 판정 기준으로 붙이기 | 공용 | 2일 |
| 2 | 위상 동결 — 관계 그래프 추출 | 공용 | 3일 |
| 3 | Abacus PlaceRow + 스택 추출 | 공용 | 4일 |
| 4 | 여백 대수(1D, 닫힌 형식) | method1 | 2일 |
| 5 | 최대 빈 사각형 열거 | method1 (+ 합본의 분기 기준) | 3일 |
| 6 | method1 완성 → 5건 채점 | — | |
| 7 | DocIframe이 pageJson 반환하도록 수정 | 공용 | 1일 |
| 8 | 최소구성 + 자기 캘리브레이션 | method2 — 조건부 | 4일 |
| 9 | 제약 그래프 여유 + 실현가능성 | method2 — 조건부 | 1주 |
| 10 | method2 완성 → 채점 → §4.3 판정 | — |
↻ 착수 순서 — 원본에서 자동 동기화
이 중 #1 채점 방식은 B-0이고, #6 폰트 복원·#7 행간 변경은 제품 결정이라 엔지니어링으로 못 넘긴다 — B-4.
| # | 항목 | 무엇을 막나 | 상태 |
|---|---|---|---|
| 1 | 채점 방식 결정 (§5의 a/b/c) | 정답이 스크립트 산출물이라 지금은 채점이 성립하지 않는다. 다른 모든 작업의 전제 | 최우선, 방법론 결정 |
| 2 | wrapBlock 오차 2건(78/80)의 정체 — CJK 금칙? 긴 라틴 토큰? | 4위 전체의 전제 | 미측정 |
| 3 | tight box 굽기의 프로덕션 비용 (headless Chrome, 문서당) | 5위 [전] 적용 가능 여부 | 미측정 |
| 4 | 여백 균등 분배 vs 비율 분배 | 2위의 유일한 설계 미결 | 케이스로 확인 가능 |
| 5 | 렌더러가 실제로 무엇을 바꾸나 — 특히 컨테이너를 키우면 폰트를 되돌리나 | 5위 [후]의 "뜻밖의 이득" 가설 | pageJson을 버리고 있어 아무도 안 봄 |
| 6 | 폰트 복원 권한 (original.json 사용 허용) | §6.1 — 문제유형 ②를 다룰 수 있느냐 | 제품 결정 |
| 7 | 행간(lineSpacing) 변경 허용 | §6.2 — 가장 싼 세로 회수 수단 | 제품 결정 |
| 8 | 장식 요소 필터링의 정확도 | 3위의 거짓 양수 사이클 | DECORATION_TYPES 재사용 검증 |
| 9 | 기요틴 분해율 (샘플 ≥500) | §6.4 계측기 착수 | w2에서 이월, 미측정 |
| 10 | 케이스를 7건보다 늘려 method1이 실패하는 사례 찾기 | §4.3.1 실측이 7건 전부 "세로로 해결"이라 method2를 만들 근거가 아직 없다. 반례가 없으면 만들지 않는다 | 미착수 |
| 11 | case.json의 problems 라벨 채우기 (7건 중 6건이 비어 있음) | 케이스가 어떤 증상인지 사람이 분류한 기록이 없어 교차 검증이 불가능 | 사람이 이미지를 봐야 함 |
↻ 남은 확인 필요 항목 — 원본에서 자동 동기화
⚠ 테스트가 0개다 — wrapBlock·boxHeightOf·rendererLineCount 는
method 4·5위의 기반이고 findContainer·overflowOf 는 전부의 기반이라,
여기가 조용히 깨지면 전부 깨진다. (문서 기준. 현재 코드에는
relayout-core/ 아래 테스트 파일이 다수 있어 재확인 필요)
original.json(대치 전 원안)의 글자 크기로 되돌려도 되나.
허용하지 않으면 문제유형 ②(과도 축소)를 아예 다룰 수 없다.
lineSpacing) 변경 허용 — 현행 규칙(before-after-rules.md §2)이 금지한다.
하지만 행간 −20%는 세로 20%를 회수하면서 글자 크기 손실이 0이라
가장 싼 세로 회수 수단이다 (폰트 0.894와 동일한 면적 효과).
이 둘은 A-2(김수현·정원석) 이 답할 항목이다 — 엔지니어링 선택이 아니라 품질 정책이다.
tidyLayout(force-directed)을 후보가 아니라 대조군으로 만든 이유다.tidyLayout 실측에서 5건 중 2건이 신규 겹침 게이트 FAIL,
1건은 중앙 오차 26.1 → 87.4. 소프트로 다루면 안 된다는 우리 데이터.| 날짜 | 구분 | 항목 | 내용 | 반영 위치 |
|---|---|---|---|---|
| 2026-08-20 | 확정 | Method 1 착수 승인 | w2 §3 "크기·위치 동시" 원칙과 모순 없음. 폭 동결 시 높이는 자유변수가 아니라 실측값 — 최소 섭동 목적 하에서 h_i* = h_실측,i가 조인트 최적임이 증명됨(h를 더 키우면 자기 페널티↑ + y 실현가능 영역↓). 잔여 (y, θ) 조인트 QP는 PlaceRow/여백대수가 정확해를 냄. Method 1 = 예측 0 부분공간으로 제한한 조인트 솔브이지 grow-then-move 휴리스틱이 아님. w2의 경고는 폭이 자유변수여서 피드백 루프가 있을 때의 얘기(→ Method 2 영역) | w3 §4 |
| 2026-08-20 | 확정 | 갭 분배는 델타 균등(닫힌 형식) | method1 노트 §5 장난감 예제의 완전 균등(55×4)과 w3 2위 닫힌 형식 θ_j = θ^orig + Δ/m(→ 25/45/25/125)이 불일치. 후자가 디자이너 여백 위계를 보존하므로 구현 기준. 완전 균등화는 idle-margin용 별도 플래그로 분리 채점 | w3 2위 |
| 2026-08-20 | 권고 | 착수 순서 #7을 #6 앞으로 | Method 1의 "예측 0"에 숨은 피드백 채널 = 렌더러 오토핏(박스 확대 → 폰트 복원 → 텍스트 높이 변화). h_실측은 현재 fontSizeScale에서 잰 값이라 최종 렌더와 어긋날 수 있음(w3 §8-5 "아무도 안 봄"). pageJson 회수(1일)를 m1 채점 전에 넣어 5건 채점을 render-verified로. "뜻밖의 이득"(증상 ② 간접 해결) 가설도 같은 실험에서 확인됨 — m1 vs m2 판정에 영향 | w3 §4.4, §8-5 |
| 2026-08-20 | 권고 | m1 1차 채점은 보수 모드 | 확대 + 법제화 + 델타 분배만. 미학적 채움·전면 균등화 제외 — §4.3 축 판정의 귀속(attribution) 보전 | w3 §4.3 |
| 2026-08-20 | 권고 | 배경 도형은 이동 결합이 아니라 크기 결합 | 텍스트 박스가 자라면 배경 도형도 padding 보존으로 커져야 Rule.md §0 overlap 요구 유지. 재줄바꿈 없으므로 예측 0 | w3 1위 필요조건 |
↻ Method 1 착수 전 재검토 결정 — 원본에서 자동 동기화
이 아카이브 표시가 붙은 둘은 이 사이트로 옮겨 왔다 —
사이드바에서 이 문서의 하위로 들어가 있고, 슬라이더·시뮬레이터도 원본 그대로 돈다.
나머지는 MOR-2018/ 안의 원본이다.
| 문서 | 무엇 |
|---|---|
Rule.md | Before/After 정의 규칙 · 불변 조건 · 문제 유형 ①②③ |
research/relayout-research_w2.md | 문헌 조사 정본 · 결정 로그(§10) |
research/relayout-research_w3.md | 순수 알고리즘 Top 5 · 조립 계획(§4) · 남은 항목(§8) |
post-replace-relayout_method1-method2-notes.md | Method 1/2 직관 설명 · 장난감 예제 |
logics/find-text-container.md | findTextContainer 판정 3순위 해부 |
logics/overlap-area-map.md | overlapAreaMap · 신규 겹침 게이트 |
| overlapAreaMap 겹침 게이트 이 아카이브 | 지표 ⑥ 의 2단계 알고리즘 · 제외 규칙 다섯 · 판정 시뮬레이터. 원본 overlap-gate-notes.html |
TC/ · 예시/ | 테스트 케이스 sheetJson · Before/After 예시 이미지 |
logics/ 의 로직 문서는 사내에 배포되어 있다 —
post-replace-relayout-key-func.miridih.app (VPN).
재배치 알고리즘을 적용한 뒤 엔진팀 구현과 이어 붙일 최적화 여지가 있는지 탐색이 필요하다.
아이디에이션 수준의 후보: miricanvas-iui/src/style-mapper/render-page.ts 의
requestComposite 만 호출하는 방향.
⚠ 용어 주의 — 여기서 합성(Composite) 은 헤드리스 Chromium 을 블랙박스로 써서 디자인을 이미지·PDF·PPTX·영상으로 만들어 내는 서버 사이드 잡 파이프라인 전체를 말한다. 브라우저 엔진이 말하는 "Composite" 과 다른 말이다 (결국 그것을 포함하기는 한다).
재배치 후 렌더링 파이프라인 진행 과정의 성능 최적화 수단으로 검토한다. (관련: Canvas 위키 2697266052)
§1 의 Scope 2(Image·Chart)로 넓히는 것,
그리고 스마트블록에 응용하는 것.
1차 범위에서는 제외돼 있다. 언제 포함할지가 아직 안 정해졌다.
아래는 이석님 Pod 이 이미 구현해 둔 접근이다. 담당이 홍지혜로 넘어왔으므로 이번 작업의 전제로 삼지 않는다 — 무엇이 이미 있고 어디까지 커버되는지 알아 두는 용도로만 읽는다.
실제 해결 경로는 §8-B(post-replace-relayout) 다.
그 안에서 tidyLayout 은 후보가 아니라 대조군으로 쓰인다.
각 노드의 bbox 네 변을 1D Verlet 입자로 두고, 아래 힘을 합산해 240회 적분한 뒤 최종 박스만 낸다. 결정론적이지 않고, 비겹침을 하드 제약으로 다루지 않는다 — 이 성질이 §8-B에서 문제가 된다.
| 힘 | 내용 |
|---|---|
| 텍스트 확장 단방향 | 예측 높이가 현재 박스보다 크면 verticalAlign 방향으로 확장. 작으면 그대로 |
| 텍스트 가로 확장 단방향 | 세로로 겹치는 형제·부모 경계 안에서 horizontalAlign 방향으로 펼침 (WIDE_PADDING=24) |
| 배경 확장 단방향 | 자식이 커져 여백이 좁아지면 목표 여백(BG_MAX_MARGIN=24)까지 배경 변을 바깥으로 |
| 가이드 선 유지 | 시작 시 같은 직선(GUIDE_TOL=4)에 있던 변들을 묶어 평균으로 당김. 부모-자식은 서로 영향 없음 |
| 겹침 방지 | 형제끼리 겹치면 최소 침투축으로 밀어냄. 시작 시 이미 겹쳐 있던 쌍은 제외 |
| 여백 균형 | 배경 컨테이너의 자식 텍스트에 한해 상하 여백이 같아지도록 세로 평행이동 (크기 보존) |
| 페이지 제한 hard | 변을 페이지 안으로 clamp. 최초부터 밖이던 입자는 예외 |
ITERATIONS=240 · DAMPING=0.6 ·
comp-match/tidyLayout.ts:13-46
모든 판정은 회전 반영 AABB(vn.absBox) 좌표로 한다.
진입점은 buildTidyLayoutBboxMap (:161) 이다.
새 알고리즘에서 부품으로 떼어 쓸 만한 곳이라 줄 번호를 남긴다.
같은 visual 부모를 가진 입자쌍만 후보로 만들고, 시작 시 이미 겹쳐 있던 쌍은 영구 제외한다 — 원래 겹침은 의도된 디자인일 수 있다.
가로·세로 침투 깊이 ix·iy 중 작은 쪽이 collision normal 이고,
그 축으로 K_OVERLAP × 침투 × 0.5 만큼 민다 (K_OVERLAP = 0.3).
부호는 두 박스의 중심합으로 정한다.
성질 — 마주보는 두 변을 같은 d 로 옮기니 크기 보존(고정인 텍스트 좌우도 함께 옮긴다) ·
형제만 판정 · 배경도 참여 · 한 노드가 여러 쌍에 걸리면 변위가 누적된다.
배경이 밀리면 그 안 자손도 같은 변위로 따라간다 — 쟁반-컵,
shiftSubtree (:417) 의 childrenOf 재귀.
세로로 안 겹치는 형제는 확장을 막지 않는다. 막는 형제 중 중심이 내 중심보다
오른쪽이냐 왼쪽이냐로 extR·extL 을 정한다
(WIDE_PADDING = 24 안쪽까지).
가로로 겹치는 형제로 위·아래 한계를 잡아 상하 여백이 같아지도록 평행이동한다
(K_BALANCE = 0.2). 배경의 직속 자식 텍스트만 대상이다.
| 상수 | 값 | 뜻 |
|---|---|---|
ITERATIONS | 240 | 시뮬레이션 iteration 수(고정) |
DAMPING | 0.6 | Verlet 속도 감쇠 — 클수록 관성이 크다 |
K_MARGIN | 0.25 | 배경 확장(초기 여백 회복) 강성 |
K_GUIDE | 0.2 | 가이드 선 유지 강성 |
K_OVERLAP | 0.3 | 겹침 방지 강성 |
K_BALANCE | 0.2 | 여백 균형 강성 |
BG_MAX_MARGIN · WIDE_PADDING | 24px | 배경 여백 회복 상한 · 가로 확장 시 남길 여백 |
GUIDE_TOL | 4px | 같은 직선(가이드) 판정 허용 오차 |
EXPAND_EPS | 0.05px | 단방향 확장 판정의 부동소수점 허용 |
이 절의 줄 번호·상수는 현재 브랜치 HEAD(7b4354f6) 실측이다.
강성 합이 1을 넘지 않게 보수적으로 잡혀 있다 —
tidyLayout.ts:45-67
④a(기하적 정렬)가 tidyLayout 커버 범위인지, comp-sign '스마트블록 대치'로
④b(구조적 재배치)까지 가능한지 확인.
tidyLayout 을 켜고 있나? 기본값이 false다(§2).| 용어 | 정의 | 어디서 나오나 |
|---|---|---|
| Bounding box | 텍스트를 감싸는 가장 작은 사각형 영역 | sheetJson 에 적혀 있는 값 · data.props.boundingBox |
| Tight bounding box = ink rect · actual bounding box |
엔진이 렌더해서 실측한 결과로 나온, 텍스트를 감싸는 가장 작은 영역 | tightBoxes — §4 가 말하는 "실측"이 이것이다 |
| Visual bounding box | 코드상의 텍스트 데이터 영역(Text Metrics)과 대비해, 사용자 눈에 보이는 모든 시각 요소를 포함한 영역 | 불렛·글자 연관 데코레이션까지 텍스트와 함께 묶어 재배치·겹침 계산에 쓰기 위한 개념 |
예: "불렛을 포함한 컴포넌트의 실제 점유 공간은 Visual Bounding Box 를 기준으로 계산한다."
| 표준 용어 | 어디 | 정의 |
|---|---|---|
| ink overflow | CSS Overflow Level 3 · MDN 용어집 | 글자가 실제로 칠해진 영역 — ascender·descender 가 em box 밖으로 튀어나오는 것을 포함 |
| actual bounding box actualBoundingBoxAscent 등 |
Canvas 2D TextMetrics |
텍스트를 감싸는 가장 작은 사각형. 글자의 실제 픽셀 외곽선 또는 폰트 메트릭 기준.
measureText() 로 width·actualBoundingBoxLeft·
actualBoundingBoxAscent 를 얻는다 |
| ClientRect / DOMRect | DOM | HTML 요소에서 텍스트가 화면에 실제로 그려져 차지하는 사각형.
getBoundingClientRect() 로 얻고, 뷰포트 기준
x, y, width, height 를 돌려준다 |
| line box | CSS Inline Layout Level 3 | 줄 하나가 차지하는 상자 |