페이지 한 장을 통째로 담은 JSON. 미리캔버스 에디터·컨버터·컴포넌트 파이프라인이 전부 이 한 덩어리를 주고받는다. 어떤 모양인지, 누가 만들고 누가 읽는지, 그 코드가 어느 파일 몇 번째 줄에 있는지를 세 저장소에 걸쳐 한 장에 모았다.
sheet.json, DB 컬럼으로는 design_object_sheet_json, API로는 componentSheetJsonFileUrl.SHEETSIZE·TEMPLATE는 원본 XML의 필드 이름이지 sheetJson의 필드가 아니다.
컨버터가 SHEETSIZE.cx/cy → width/height + data.props.boundingBox로 평탄화한다.Map<id, 노드>를 배열로 편 것). 배열 순서에는 의미가 없다.rootNodeIds → childNodeIds로만 복원한다.
이걸 무시하고 nodes 순서를 쓰면 배경이 글자를 덮는다 — 실제로 2만 2천 건이 그렇게 깨졌다.makePageCreateCommandChunk), ③ 손조립. §6제일 먼저 헷갈리는 지점이다. 같은 JSON 덩어리를 저장소마다 다른 타입 이름으로 부른다. 상속 관계도 아니고 import 관계도 아니다 — 각 저장소가 자기가 필요한 필드만 다시 선언했고, 구조적으로 호환(structurally assignable)되기 때문에 그냥 서로 넘겨 쓴다.
| 이름 | 어디서 쓰나 | 정의 위치 |
|---|---|---|
PageData 정본 |
엔진 framework. 서버 DESIGN/CREATE command payload의 원형 — 모든 필드가 여기서 나온다. |
commandCreateTypes.ts:20 |
PageJson |
엔진 converter. PageReadResponseData['pages'][0]의 별칭 — 즉 위의 PageData & { designId }. |
PageNodeName.ts:24 |
Doc |
miricanvas-iui. 색상 대치·매칭·재배치 도구가 쓰는 축약형. 필요한 props만 좁게 모델링. | color-mapper/types.ts:10 |
Sheet |
miricanvas-iui recolor.ts. 다른 코드베이스로 파일째 이식하려고 의존성 0으로 다시 쓴 최소 타입. |
recolor.ts:220 |
PageJson MORDOR |
aippt-prisonbreak. 파이프라인용 느슨한 재선언 — [key: string]: unknown 탈출구를 열어 뒀다. |
pipeline/common/type/pageJson.ts:314 |
SheetJson |
engine-util. 텍스트 박스 보정만 하는 변환 함수용 — { nodes }만 요구한다. |
adjustTextBoundingBox.ts:22 |
문서의 루트 XML 노드 이름이 SHEET였다. 미리캔버스 1.0의 저장 포맷이
sheet.xml이고, 그걸 JSON으로 바꾼 것이라 sheetJson.
타입 이름은 PageJson인데 변수 이름은 sheetJson인 코드가 많은 이유다.
정본(PageData)을 그대로 옮기면 이렇다. 필드가 많아 보이지만
실제로 의미를 지고 있는 건 다섯 개다.
type PageData = { id: string; // pageId (uuid) — 컨버팅 때 generateUUID()로 새로 발급 designId: string; // 소속 디자인. 컴포넌트 단독 sheetJson에도 실려 있다 width: number; // ← SHEETSIZE.cx (§3) height: number; // ← SHEETSIZE.cy (§3) position?: number; // 디자인 안에서 이 페이지가 몇 번째인가 rootNodeIds: string[]; // ★ 그리는 순서 = z-order (§5) nodes: NodeData[]; // ★ 평평한 조회 테이블 (§4) data: { props: PageProps }; // 페이지 자체의 속성 backgroundNodeId?: string; // 배경 노드는 rootNodeIds 밖에 따로 산다 template?: PageTemplateReference; }; // data.props 안에 실제로 들어 있는 것 data.props.boundingBox // { left:0, top:0, width:1920, height:1080 } — 캔버스 크기 data.props.shape.fill // 배경색/배경 이미지 data.props.guidelines // 안내선 (XML GUIDELINES에서 변환) data.props.playback // 페이지 재생 시간·인트로/아웃트로 애니메이션 data.props.templateReference
type NodeData = { id: string; pageId: string; rootNodeId?: string; // 이 노드가 속한 최상위 노드(그룹 조상)의 id parentNodeId: string; // ★ 부모 노드 id. 페이지 직속이면 문자열 'NONE' (null 아님!) childNodeIds: string[]; // ★ 자식 순서 = 그룹 내부의 z-order position?: number; backgroundNodeId?: string; data: { nodeType: string; // 'TextItem' | 'GroupItem' | 'ShapeItem' | 'ComponentItem' | ... props: NodeProps; // nodeType마다 다른 속성 덩어리 }; };
parentNodeId가 문자열 'NONE'인 것은 실수하기 딱 좋은 지점이다.
null도 undefined도 아니다. "이 컴포넌트의 루트 노드 찾기"는
웹2 코드 어디서나 이 문자열 비교로 이뤄진다:
const rootNodeData = nodes?.find( (n) => (n.data.nodeType === NODE_TYPE.COMPONENT_ITEM || n.data.nodeType === NODE_TYPE.COMPONENT_ITEM_V2) && n.parentNodeId === 'NONE' );
엔진 쪽 정본 목록은 자동 생성 파일
(packages/engine/editor/src/node/item/NodeTypeConstants.ts, 생성기는
generateNodeTypeConstants.mjs)에 있고,
파이프라인이 실제로 마주치는 축약 목록은
MORDOR의 NodeType union(21종)에 있다.
자주 보게 되는 것만 추리면:
TextItem — 유일하게 props.text.textTree(문단/런 트리)를 갖는다. 내용 대치가 손대는 대상.GroupItem — 자식을 묶기만 한다. 박스는 자식들의 합집합.ShapeItem / ExtendableShapeItem / VectorGraphicItem — 도형·일러스트. 색은 props.colorMap.PhotoItem / BitmapItem — 사진. 리소스 키는 props.shape.fill.resource.key.TableItem / ChartItem — 격자형. 셀 스타일은 props.cellProps[][].ComponentItem / ComponentItemV2 — 내용 시각화 컴포넌트의 루트. 이게 붙어 있으면 그 sheetJson은 "페이지"가 아니라 "컴포넌트 한 덩어리"다.
SHEETSIZE와 TEMPLATE를 sheetJson에서 찾으면 안 나온다.
둘 다 원본 XML의 노드 이름이고, 컨버터가 페이지를 만들면서
width/height와 data.props.boundingBox로 접어 넣기 때문이다.
SHEETSIZE.properties.cx/cy 하나가 width·height·data.props.boundingBox
세 자리에 동시에 쓰인다.
const pageBoundingBox = { left: 0, top: 0, width: validatedPageXmlSchema.SHEETSIZE.properties.cx, height: validatedPageXmlSchema.SHEETSIZE.properties.cy, }; // … const pageJson: PageJson = { id: pageId, // generateUUID() — XML의 PagePersistentKey와 맵으로 연결만 해 둔다 designId, rootNodeIds, // 이 시점엔 아직 [] — 요소 변환이 끝나야 채워진다 (§5) backgroundNodeId: backgroundNode?.id, data: { props: { boundingBox: pageBoundingBox, playback, guidelines: convertXmlGuidelinesToJson(pageGuideline, pageBoundingBox), shape: nodeShape, } }, width: pageBoundingBox.width, height: pageBoundingBox.height, nodes: [], // 이 시점엔 아직 [] — assignNodesToPages()가 나중에 채운다 };
컨버터는 SHEETSIZE·TEMPLATE·BACKGROUND가
전부 있으면 페이지, SHEETSIZE만 있고 나머지 노드가 하나면
텍스트/차트/표/프레임 프리셋으로 분기한다.
(isPageSheetXmlSchema PageConverter.ts:40)
nodes는 평평한 배열이다. 그룹 안의 텍스트도, 페이지 직속 도형도,
전부 같은 층위에 나란히 들어 있다. 부모–자식 관계는 오직 parentNodeId·childNodeIds
참조로만 표현된다.
즉 nodes는 Map<id, 노드>를 배열로 직렬화한 것이고,
배열의 순서에는 아무 의미가 없다. 트리를 복원하려면 rootNodeIds에서 출발해
childNodeIds를 따라 내려가야 한다.
nodes(A)는 무엇이 있는지만 말하고, rootNodeIds(B)가 어떤 순서로 그릴지를 말한다.
엔진의 NodeBuilder.build()가
이 둘로 실제 트리를 만든다.
두 번째 함정. props.boundingBox의 left/top은
캔버스 원점이 아니라 부모 노드 기준이다. 절대 좌표를 알려면
루트에서부터 내려오면서 부모 위치를 누적해야 한다.
buildDocAbsBoxMap() docLayout.ts:22가
정확히 이 누적을 한다. 겹침 판정·넘침 판정처럼 절대 좌표가 필요한 계산은 전부 이걸 거친다.
rootNodeIds는 "페이지 직속 노드의 목록"이면서 동시에 z-order 그 자체다.
배열 뒤쪽에 있을수록 위에 그려진다. 이 값이 세팅되는 자리는 세 군데뿐이다.
sortItemJsonByLayerPriority()XML의 Priority 속성으로 nodes와 rootNodeIds를 같은 기준으로 함께 정렬한다.
makePageCreateCommandChunk()살아 있는 Page 객체의 getChildren() 순서를 그대로 rootNodeIds로 굳힌다.
MORDOR 파이프라인이 rootNodeIds를 한 번도 참조하지 않고
nodes 배열 순서로 컴포넌트를 조립하던 시절이 있었다. XML에서 변환된 문서는
②가 두 배열을 같은 기준으로 정렬해 줘서 우연히 맞아떨어졌지만,
CDN이 .json으로 바로 주는 레이아웃은 그 정렬을 안 거친다.
docs/ submodule).
extractComponentNodes.ts:241가
이제 flattenRootIdsThroughGroups(pageJson.rootNodeIds, …) 순서로 순회하고,
rootNodeIds가 없는 레거시 문서만 nodes 순서로 폴백한다.
교훈 한 줄: sheetJson을 다루는 코드에서 doc.nodes.map(...)으로 순서를 만들면 거의 항상 버그다.
sheetJson은 결국 세 가지 방법으로만 생긴다. 그리고 소비는 한 가지 방법뿐이다 —
PageBuilder/NodeBuilder가 살아 있는 Page 객체 트리로 되살린다.
이 왕복이 전체 그림의 전부다.
Page 객체가
makePageCreateCommandChunk를 통해 다시 sheetJson이 된다.
진입점은 convertSheetXmlsToPageReadResponseData().
fast-xml-parser로 XML을 파싱하고, 페이지 속성과 요소를 나눠 각각 변환한 뒤,
마지막에 assignNodesToPages()가
노드를 pageId별로 묶어 page.nodes에 꽂는다.
MORDOR는 이 패키지를 직접 쓰지 않고 processor 서버에 XML을 base64로 보내 compositeId를 받고, 완료되면 결과 JSON URL을 fetch 한다 (converter/route.ts:87). 서버 안에서 도는 코드는 결국 같은 컨버터다.
이게 가장 많이 놓치는 부분이다. 에디터 안에서 텍스트를 대치하거나 박스를 옮기면,
바뀌는 건 sheetJson이 아니라 살아 있는 Page/Node 객체다.
그 상태를 다시 문서로 굳히는 유일한 함수가 이 둘이다.
export const makePageCreateCommandChunk = (page, currentPageIds?) => ({ id: page.getId(), data: { props: page.props }, rootNodeIds: page.getChildren().map((c) => c.getId()), // ← 화면 순서 그대로 nodes: nodes.map((c) => makeNodeCreateCommandChunks(c, page.getId())).flat(), width: page.props.boundingBox.width, // ← boundingBox가 정본 height: page.props.boundingBox.height, template: page.props.templateReference, backgroundNodeId: backgroundNode?.getId(), }); // 자식은 재귀로 평탄화 — 여기서 트리가 flat 배열이 된다 // makeNodeCreateCommandChunks.ts:34 parentNodeId: parentNode && parentNode !== page ? parentNode.getId() : 'NONE',
MORDOR가 컴포넌트(Design Object) 하나를 독립된 페이지로 만들 때 쓴다.
1920×1080 흰 배경 껍데기에 노드 배열을 꽂고 rootNodeIds = [첫 노드 id]로 끝낸다.
"sheetJson 최소 형태"가 궁금하면 이 파일 하나만 보면 된다.
export const buildPageJson = (nodes: PageNode[]) => ({ id: nodes[0].pageId || uuidv4(), data: PAGE_JSON_DEFAULT_DATA, // boundingBox 1920×1080 + 흰 배경 + 안내선 2개 rootNodeIds: [nodes[0].id], // 첫 노드는 GroupItem 이라는 약속 nodes, width: 1920, height: 1080, position: 0, designId: uuidv4(), });
"markdown 요약 + 컴포넌트 → 글자가 바뀐 sheetJson"을 만드는 파이프라인이 두 개 있다. 앞부분은 완전히 같고 뒷부분만 다르다 — 웹2는 엔진을 같은 프로세스에서 돌리고, iui는 브라우저 iframe에 던져서 돌린다.
공통 앞부분은 iui의 packages/comp-sign에 있고, 웹2가 그걸
@miri-unicorn/miricanvas-iui-comp-sign 패키지로 가져다 쓴다.
즉 ThumbnailLoad 타입 하나가 두 저장소의 계약이다.
unicorn-73380.preview.miricanvas.com의 iframe에서 돌린다
(CLI에서는 Playwright가 그 iframe을 헤드리스로 띄운다).
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 } ← 대치·재배치가 끝난 sheetJson iframe → parent COMPONENT_THUMBNAIL_ERROR // { message, stack, componentStack }
LOADED가 돌려주는 pageJson은 노드 id는 원본과 같지만
위치·크기는 대치 결과 기준으로 갱신돼 있다. hover 강조 박스처럼 렌더 화면과
정렬돼야 하는 좌표는 원본 doc이 아니라 이걸 써야 한다.
다만 §8-①의 함정이 여기 붙어 있다.
| 단계 | 하는 일 | 코드 |
|---|---|---|
| 스타일 매핑 | 현재 디자인의 색·폰트를 컴포넌트 노드 props에 통째로 덮어쓴다 | buildMappedComponentSheetJson.ts:91 |
| 역할 폰트 주입 | Title/Subtitle/Description 역할별 폰트 키를 텍스트 노드에 적용 | ApplyRoleFontsToTextNodes.ts |
| 목록 스타일 정렬 | listStyle의 색·크기를 첫 런 값으로 단방향 전파(임시 처리) |
…SheetJson.ts:26 |
| 이미지 대치 | 사진검색으로 고른 resourceKey를 shape.fill.resource에 꽂는다 |
replaceComponentImages.ts |
| 박스 확장·병합 | 글이 길어져 넘치면 텍스트 박스를 넓히고 연속 Description을 합친다 | adjustTextBoundingBox.ts |
| 루트 박스 재계산 | ComponentItem의 boundingBox를 자식 합집합으로 다시 맞춘다 |
adjustComponentBoundingBox.ts |
| ID 재발급 | sheetJson의 노드 id는 고정값이라 그대로 페이지에 붙이면 충돌한다 | reassignNodeIds.ts |
rotatedBoundingBox는 낡았다
대치·재배치가 끝나면 엔진은 boundingBox만 갱신하고
rotatedBoundingBox는 대치 전 값을 그대로 남긴다.
절대 좌표 계산에서 기본값(preferRotated: true)을 쓰면
루트 GroupItem에 붙은 낡은 값 때문에 문서 전체가 통째로 어긋난다
(실측: 텍스트 y가 792 대신 1057로 계산됨).
→ 렌더된 문서에서는 반드시
absBoxMap(doc)
(= buildDocAbsBoxMap(doc, { preferRotated: false }))를 쓴다.
nodes 배열 순서로 무언가를 만들지 말 것
§5의 사고가 정확히 이것이었다. XML 경로에서는 우연히 맞아떨어져서
버그가 몇 년 숨어 있었다. 순서가 필요하면 rootNodeIds → childNodeIds.
검증 규칙도 이 축으로 걸려 있다 —
validatePageJson은
rootNodeIds·childNodeIds가 가리키는 id가 nodes에 실제로 있는지를 검사한다.
props.boundingBox.left를 캔버스 좌표로 착각하면 그룹 안 요소가 전부 어긋난다.
§4의 누적 그림 참고. 겹침·넘침·정렬 판정은 전부 절대 박스로 해야 한다.
같은 컴포넌트를 한 페이지에 두 번 넣으면 id가 충돌한다.
페이지에 붙이기 직전 reassignNodeIds()로 새 id를 발급하고,
contentData 안의 매핑 키도 함께 갈아 끼워야 한다
(remapContentDataMappingKeys).
rootNodeIds 밖에 있다
페이지 배경은 backgroundNodeId로 따로 참조되고 rootNodeIds에는 안 들어간다.
NodeBuilder도 배경 id를 루트 목록에서 걸러낸다.
"모든 노드"를 순회한다고 생각하고 rootNodeIds만 돌면 배경이 빠진다.
파일 이름을 누르면 GitHub 해당 줄로, 편집기를 누르면 로컬 VS Code로 열린다.
main 배지가 붙은 파일은 현재 로컬 체크아웃
(feature/mordor)에 없어서 GitHub 링크만 있다.
| 파일 | 무엇 | 편집기 |
|---|---|---|
| engine/framework/…/commandCreateTypes.ts | NodeData:8 · PageData:20 · CommandCreateDesign:35 — 정본 |
편집기 |
| engine/framework/…/responseTypes.ts | PageReadResponseData:6 — page.nodes가 정본이고 최상위 nodes는 deprecated |
편집기 |
| engine/converter/…/PageNodeName.ts | SHEET·SHEETSIZE·TEMPLATE·BACKGROUND·GUIDELINES 노드 이름 상수, PageJson:24 |
편집기 |
| iui/packages/color-mapper/src/types.ts | Doc:10 · Node:21 · Data:31 — 차트 2.0·표까지 가장 넓게 모델링한 SSOT |
편집기 |
| iui/packages/color-mapper/src/recolor.ts | SheetColor:40 · SheetBox:52 · SheetTextTree:59 · Sheet:220 — 의존성 0 이식용 |
편집기 |
| iui/packages/comp-sign/src/node-labeler/types.ts | node-labeler용 축약 Doc:1 — RotatedBoundingBox가 angle까지 갖는 판 |
편집기 |
| prisonbreak/…/type/pageJson.ts | NodeType union(21종):277 · PageNode:305 · PageJson:314 |
편집기 |
| api-kit/…/ComponentDetail.ts main | componentSheetJsonFileUrl:17 · structureJsonFileUrl — 컴포넌트 저장 스키마(zod) |
— |
| engine-util/…/adjustTextBoundingBox.ts main | AdjustTextBoundingBox.SheetJson:22 — 변환 함수용 최소 구조 |
— |
| 파일 · 함수 | 무엇 | 편집기 |
|---|---|---|
| XmlToJsonConverter.ts:159 | convertSheetXmlsToPageReadResponseData() — 컨버팅 진입점. window에도 붙여 puppeteer에서 부를 수 있게 해 뒀다 |
편집기 |
| XmlToJsonConverter.ts:514 | sortItemJsonByLayerPriority() — XML Priority로 nodes·rootNodeIds를 함께 정렬 |
편집기 |
| XmlToJsonConverter.ts:663 | assignNodesToPages() — page.nodes를 채우는 자리 |
편집기 |
| PageConverter.ts:610 | convertPage() — SHEETSIZE → width/height/boundingBox (:652, :695–711) |
편집기 |
| PageConverter.ts:370 | addItemsToPage() — rootNodeIds가 처음 채워지는 곳 |
편집기 |
| PageJsonToXmlConverter.ts:10 | 역방향 — boundingBox에서 SHEETSIZE·TEMPLATE를 다시 만든다 |
편집기 |
| 파일 · 함수 | 무엇 | 편집기 |
|---|---|---|
| PageBuilder.ts:15 | build() — sheetJson → Page. 배경 노드 분리, 유효하지 않은 노드 필터 |
편집기 |
| NodeBuilder.ts:12 | build(nodesData, rootNodeIds) — flat 배열 → 트리. rootNodeIds가 없으면 nodes 순서로 폴백(:14) |
편집기 |
| makePageCreateCommandChunk.ts:7 | Page → sheetJson. width/height는 props.boundingBox에서, rootNodeIds는 getChildren()에서 |
편집기 |
| makeNodeCreateCommandChunks.ts:8 | 노드 트리 → flat 배열. parentNodeId: 'NONE'이 찍히는 자리(:34) |
편집기 |
| 파일 · 함수 전부 main | 무엇 |
|---|---|
| text-visualization/ComponentBuilder.ts | buildPageFromContentData():107 · buildPageFromSignature():127 · makeComponentItem():297 · buildPageFromSheetJson():367 |
| text-visualization/buildMappedComponentSheetJson.ts | 내용 대치 → 폰트 주입 → 스타일 매핑 → 이미지 대치 순서를 엮는 오케스트레이터:91 |
| node/item/component/buildSignatureMappedSheetJson.ts | parseSummaryTree(summary) → applySignatureContentReplacement → 루트 박스 보정:16 |
| node/item/component/applySignatureContentReplacement.ts | PageBuilder → 노드 변형 → makePageCreateCommandChunk 왕복이 벌어지는 곳:60 |
| …/ComponentItemV1AddCommandParamBuilder.ts | buildContentMappedComponentSheetJson():27 — ContentData 경로의 대치 |
| style_replacement/TemplateStyleMapper.ts | getMappedStyles():46 — 현재 디자인 스타일을 컴포넌트에 이식 |
| …/_components/TextVisualizationThumbnail.tsx | 패널 썸네일 — componentSheetJson → ComponentBuilder → EditorThumbnailRenderer:221 |
| text-visualization/EditorThumbnail.tsx | Page 객체를 NodeViewFactory로 DOM 렌더:25 |
| 파일 · 함수 | 무엇 | 편집기 |
|---|---|---|
| comp-sign/src/build-replacement.ts:109 | buildContentReplacement() — ThumbnailLoad 페이로드 생성. web-2도 이 함수를 import 한다 |
편집기 |
| comp-sign/src/comp-match/types.ts:210 | 메시지 프로토콜 타입 — ThumbnailLoad:217 · ThumbnailLoaded:237 |
편집기 |
| comp-sign/src/comp-match/docLayout.ts:22 | buildDocAbsBoxMap() — 상대 → 절대 좌표. preferRotated 함정 주석 포함 |
편집기 |
| src/comp-match/Thumbnail.tsx:389 | preview iframe에 postMessage. iframe URL은 :45 |
편집기 |
| scripts/comp-match-eval-render.ts | renderViaPreview() — Playwright로 preview iframe을 헤드리스 구동, 렌더된 pageJson을 회수 |
편집기 |
| scripts/comp-replace-render.ts | 컴포넌트 1개 + markdown → 대치 → 렌더 → composite 이미지. --dump-rendered로 sheetJson 저장 |
편집기 |
| src/style-mapper/render-page.ts | requestComposite():8 · getCompositeResult():49 — sheetJson → 이미지 |
편집기 |
| post-replace-relayout/relayout-core/metrics.ts | 재배치 지표 + 좌표계 함정 문서화:15 · absBoxMap():309 |
편집기 |
| post-replace-relayout/relayout-core/docFrame.ts | 원안 문서를 렌더본 좌표계로 옮긴다 — 배율의 정본은 text.fontSizeScale:55 |
편집기 |
| 파일 | 무엇 |
|---|---|
| …/component-thumbnail/ComponentThumbnailIframeHost.tsx | postMessage 핸들러. LOADED { pageJson: makePageCreateCommandChunk(page) }:203 |
| …/component-thumbnail/buildComponentThumbnailPage.ts:183 | sheetJson + nodeTextMap → 텍스트 대치 → 폰트/자간/행간 축소 → Page |
| …/component-thumbnail/ComponentThumbnailView.tsx | NodeViewContextProvider renderingEnv='PREVIEW'로 실제 DOM 렌더 |
| apps/…/pages/v2/design2/component-thumbnail.tsx | iframe이 실제로 로드하는 라우트 |
| 파일 · 함수 | 무엇 | 편집기 |
|---|---|---|
| src/app/api/converter/route.ts | XML → processor API 변환 배치. .json URL은 변환 없이 그대로 fetch(:139) — §5 사고의 뿌리 |
편집기 |
| src/lib/component/utils/extractComponentNodes.ts | 페이지 sheetJson에서 컴포넌트 노드만 잘라낸다. flatten(rootNodeIds) 순서로 순회:241 |
편집기 |
| src/lib/component/utils/buildPageJson.ts | 노드 배열 → 최소 sheetJson 껍데기:51 | 편집기 |
| …/S3UploadValidator/utils/validatePageJson.ts | BE 전달 전 골격·참조 무결성·nodeType 검증 — 불변 규칙의 실행 가능한 정의 | 편집기 |
| …/body-component/lib/sheet-json-splitter.ts | 페이지 sheetJson을 body / master 두 조각으로 분할:14 | 편집기 |
| mordor-wiki · do-sheetjson-order-investigation.md | §5 사고의 전문(원인·영향 22,666건·복구 계획). prisonbreak에서는 docs/ submodule |
편집기 |
타입 정의를 읽는 것보다 실제 문서 하나를 손에 쥐는 게 빠르다. iui 저장소에서:
# 1. 컴포넌트 sheetJson 원본을 파일로 (렌더 없음, 가장 빠름) pnpm exec tsx scripts/dump-sheet-json.ts --id 6357697 --outdir out # 2. markdown을 대치해 렌더 → 이미지 + 대치 후 sheetJson pnpm exec tsx scripts/comp-replace-render.ts \ --id 6357697 --text-file page.md \ --dump-rendered out/ --dump-payload out/ # 3. 이미 있는 sheetJson 파일을 그림으로만 굽기 (브라우저 불필요) pnpm exec tsx scripts/sheetjson-thumb.ts out/6357697_base.json --out thumb.webp
"가장 작은 sheetJson"이 보고 싶으면
aippt-prisonbreak/emptyPageJson.json —
노드 0개짜리 빈 1920×1080 페이지다. 여기에 노드를 하나씩 넣어 보면 구조가 몸에 붙는다.
MORDOR가 BE로 문서를 넘기기 전에 실제로 검사하는 항목들이다. sheetJson을 만드는 코드를 새로 쓴다면 이 목록이 곧 체크리스트다.
id는 비어 있지 않은 문자열, width·height는 양수rootNodeIds·nodes는 각각 최소 1개id·parentNodeId·childNodeIds·data.nodeType·data.props.boundingBox를 갖는다rootNodeIds의 모든 id가 nodes[].id에 실재한다childNodeIds의 id가 nodes[].id에 실재한다nodeType은 알려진 21종 안에 있다TextItem은 props.text를, TableItem은 props.style·props.cellProps를 반드시 갖는다