Dynamic Layout System
Contents API의 동적 배치 시스템 가이드입니다. 템플릿 요소들을 페이지에 자동으로 배치하며 컬럼, 페이지, 면(left/right)을 자동으로 관리합니다.
개요
동적 배치 시스템은 PhotobookAPI의 핵심 기능으로, page_layout_state 테이블 기반의 상태 추적을 통해 컬럼, 페이지, 면(left/right)을 자동으로 관리하며, 텍스트의 동적 높이 계산 및 분할 기능을 지원합니다.
| 특징 | 설명 |
|---|---|
| 자동 배치 | 컬럼, 면, 페이지를 자동으로 선택하여 요소 배치 |
| 컬럼 시스템 | 1~3컬럼 레이아웃 지원, 슬롯 기반 배치 |
| 텍스트 분할 | splittable 속성으로 긴 텍스트 자동 분할 |
| 동적 높이 | isDynamic 속성으로 텍스트 높이 실시간 계산 |
| breakBefore 제어 | 배치 위치를 세밀하게 제어 |
| Lanes | X 범위 기반 독립 Y 흐름 (레인별 독립 배치) |
| shiftUpOnHide | 조건부 요소 숨김 시 레이아웃 자동 재배치 |
| DynamicDelta | isDynamic 요소 높이 변화에 따른 앵커 Y 보정 |
페이지 구조
양면 인쇄 구조
포토북은 양면 인쇄 구조로 되어 있으며, 각 페이지는 왼쪽(left)과 오른쪽(right) 두 면으로 구성됩니다.
페이지 번호 규칙 — PUR 제본 방식 (기본)
| pageNum | pageSide | 설명 | 비고 |
|---|---|---|---|
| 0 | right | 표지 (Cover) | 앞표지와 뒷표지 |
| 1 | right | 첫 내지 (First) | 오른쪽부터 시작 |
| 2 | left | 두 번째 내지 왼쪽 | |
| 2 | right | 두 번째 내지 오른쪽 |
페이지 번호 규칙 — 기타 제본 방식 (슬림앨범, 레이플랫 등)
| pageNum | pageSide | 설명 | 비고 |
|---|---|---|---|
| 0 | right | 표지 (Cover) | 앞표지와 뒷표지 |
| 1 | left | 첫 내지 | 왼쪽부터 시작 |
| 1 | right | 첫 내지 오른쪽 | |
| 2 | left | 두 번째 내지 왼쪽 |
페이지 JSON 파일명 규칙
| pageNum | 파일명 | 설명 |
|---|---|---|
| 0 | cover.json | 표지 |
| 1 | 000.json | 첫 내지 |
| 2 | 001.json | 두 번째 내지 |
| N | {N-1:D3}.json | N번째 페이지 (3자리 숫자로 포맷) |
string fileName = pageNum == 0 ? "cover" : (pageNum - 1).ToString("D3");
// 저장 경로: d:\efiles\{bookid}\page\{fileName}.json컬럼 시스템
컬럼은 페이지 한 면을 세로로 나눈 영역입니다. 템플릿의 layoutRules.flow.columns 값으로 정의됩니다. 슬롯은 0부터 시작하는 인덱스를 가집니다.
X 좌표 계산
double slotWidth = pageWidth / totalColumns;
double xOffset = currentSlotIndex * slotWidth;
if (pageSide == "right") { xOffset += pageWidth; }
// 예시 (2컬럼, pageWidth=978px):
// 왼쪽 면, 슬롯 #0: X = 0
// 왼쪽 면, 슬롯 #1: X = 489
// 오른쪽 면, 슬롯 #0: X = 978
// 오른쪽 면, 슬롯 #1: X = 1467페이지 마진과 컬럼 간격
pageMargin 구조
{
"layoutRules": {
"margin": {
"pageMargin": {
"spine": 40, // 책등 쪽 마진
"fore": 30, // 바깥쪽 마진
"head": 20, // 위쪽 마진
"tail": 30 // 아래쪽 마진
}
}
}
}컬럼 폭 계산 (pageMargin + columnGap 적용)
// 1. 콘텐츠 전체 폭 계산
double contentWidth = pageWidth - (spine + fore);
// 2. 컬럼 폭 계산
double slotWidth = (contentWidth - columnGap * (columns - 1)) / columns;
// 예: 2컬럼, pageWidth=978, spine=40, fore=30, columnGap=20
// contentWidth = 978 - (40 + 30) = 908
// slotWidth = (908 - 20 * 1) / 2 = 444px기존 vs 신규 레이아웃 비교
| 항목 | 기존 레이아웃 | 신규 레이아웃 (pageMargin + columnGap) |
|---|---|---|
| 컬럼 폭 | pageWidth / columns | (contentWidth - columnGap*(columns-1)) / columns |
| 왼쪽 면 X | slotIndex * slotWidth | fore + slotIndex * (slotWidth + columnGap) + templateX |
| 오른쪽 면 X | pageWidth + slotIndex * slotWidth | pageWidth + spine + slotIndex * (slotWidth + columnGap) + templateX |
| 세로 높이 | fullPageHeight | fullPageHeight - (head + tail) |
| 첫 요소 Y | originalY | head + originalY |
pageMargin이 정의되어 있으면 값이 모두 0이어도 신규 레이아웃이 적용됩니다. 1컬럼 템플릿에서는 columnGap이 무시됩니다.breakBefore 파라미터
| 값 | 동작 | 사용 시나리오 |
|---|---|---|
| none (기본값) | 이전 콘텐츠 바로 다음에 배치 | 연속된 콘텐츠, 텍스트 플로우 |
| column | 다음 컬럼(슬롯)으로 이동 후 배치 | 컬럼 구분, 섹션 시작 |
| page | 다음 면 또는 페이지로 이동 후 배치 | 챕터 시작, 중요 콘텐츠 |
# none: 이어서 배치 (기본값)
POST /books/{bookUid}/contents?breakBefore=none
# column: 다음 컬럼으로 이동
POST /books/{bookUid}/contents?breakBefore=column
# page: 다음 페이지로 이동
POST /books/{bookUid}/contents?breakBefore=page텍스트 요소 속성
isDynamic
isDynamic: true일 때 실제 텍스트 내용에 따라 TextUtil.GetHeight()로 높이 계산합니다. 가변 길이 텍스트(일기, 리뷰, 댓글 등)에 사용합니다.
splittable
splittable: true일 때 텍스트가 페이지/컬럼 경계를 넘으면 자동 분할합니다. 이진 탐색으로 분할 지점을 찾고, 단어 경계로 조정합니다.
| isDynamic | splittable | 동작 |
|---|---|---|
| false | false | 템플릿 height 사용, 한 덩어리 배치 (기본) |
| true | false | 동적 height 계산, 한 덩어리 배치 |
| false | true | 템플릿 height 사용, 분할 배치 (비권장) |
| true | true | 동적 height 계산, 분할 배치 (권장) |
{
"type": "text",
"text": "$$userContent$$",
"isDynamic": true,
"splittable": true,
"fontSize": 14,
"width": 400,
"height": 100
}Lanes (X-lane 기반 독립 Y 흐름)
일부 템플릿은 페이지 내에서 X 범위가 다른 영역이 독립적인 Y 흐름을 가져야 합니다.layoutRules.lanes로 정의하며, 각 레인은 독립적인 maxY를 유지합니다.
{
"layoutRules": {
"lanes": [
{ "id": "left", "xMin": 0, "xMax": 155 },
{ "id": "right", "xMin": 155, "xMax": 1000 }
]
}
}shiftUpOnHide (조건부 요소 숨김)
layoutRules.shiftUpOnHide: true와 요소의 visible: "$$param$$" 속성을 조합하면, 파라미터 값이 false일 때 해당 요소가 숨겨지고 아래 요소들이 위로 올라옵니다. lanes가 정의된 경우 숨겨진 요소와 같은 X-lane에 속하는 요소만 상향 이동합니다.
DynamicDelta (isDynamic 앵커 보정)
같은 템플릿 내 요소들은 TemplateBaseY 앵커로 배치됩니다.isDynamic: true 요소의 높이가 변할 때, 그 변화량을 DynamicDelta로 누적하여 후속 요소의 Y를 보정합니다.
최종 Y = max(anchoredY, sequentialY)
anchoredY = TemplateBaseY + OriginalY + DynamicDelta
DynamicDelta = Σ (실제높이 - 템플릿높이) (각 isDynamic 요소별)새 템플릿(isFirstOfTemplate)이 시작될 때마다 DynamicDelta는 0으로 리셋됩니다.
itemSpacing (템플릿 간 간격)
{
"layoutRules": {
"flow": {
"itemSpacing": { "size": 15 }
}
}
}새 템플릿의 첫 요소 배치 시, 이전 콘텐츠의 bottom Y에 itemSpacing.size만큼 간격을 추가합니다. 설정되지 않은 경우 요소의 OriginalY가 간격으로 사용됩니다.