Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfe9225967 | ||
|
|
5210aea00f | ||
|
|
761a43da5e | ||
|
|
8a201337f7 | ||
|
|
aca6a992b9 | ||
|
|
565e6b092e | ||
|
|
b6b9173d2b | ||
|
|
4320f5e838 | ||
|
|
425a3054c1 | ||
|
|
02a6d44944 | ||
|
|
3f843d73f7 | ||
|
|
4144dc4c21 | ||
|
|
2ec8fc5a77 | ||
|
|
e7848b602d | ||
|
|
79f0c55745 | ||
|
|
620da8de13 | ||
|
|
b60a089e6c | ||
|
|
a385ba08c0 | ||
|
|
8e577b3163 | ||
|
|
7a2615fb4b |
@@ -12,10 +12,89 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 아키텍처 (Phase Q 파이프라인)
|
## 위계 + 용어 (Phase Z 정리, 2026-04-28)
|
||||||
|
|
||||||
|
> 매칭 시스템 (V1~V4) 통합 설계 시 정리. 상세는 [`IMPROVEMENT-REDESIGN.md`](IMPROVEMENT-REDESIGN.md) 4 장 참조.
|
||||||
|
>
|
||||||
|
> Phase Z 사전 작업 — Frame Zone 적용 분류 [`docs/architecture/FRAME-INTEGRATION-MAP.md`](docs/architecture/FRAME-INTEGRATION-MAP.md), Frame / Style / Token 인벤토리 [`docs/architecture/PHASE-Z-FRAME-STYLE-INVENTORY.md`](docs/architecture/PHASE-Z-FRAME-STYLE-INVENTORY.md).
|
||||||
|
|
||||||
|
### 슬라이드 위계
|
||||||
|
|
||||||
|
```
|
||||||
|
[ slide ] 1280×720 (전체 슬라이드)
|
||||||
|
│
|
||||||
|
├─ slide-title ← MDX 대목차 제목 (자동 매핑)
|
||||||
|
├─ slide-divider (고정)
|
||||||
|
│
|
||||||
|
├─ slide-body ≈ 1200×590 (콘텐츠 영역 — `templates/blocks/slide-base.html`)
|
||||||
|
│ │
|
||||||
|
│ └─ 레이아웃 (8-preset layout vocabulary; legacy hint = Type A/B/B'/B'')
|
||||||
|
│ │
|
||||||
|
│ └─ Zone (top / bottom_l / bottom_r 등)
|
||||||
|
│ │
|
||||||
|
│ └─ 프레임 (Figma 디자인 단위)
|
||||||
|
│
|
||||||
|
└─ slide-footer ← MDX 대목차 결론 (자동 매핑)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 용어
|
||||||
|
|
||||||
|
| 용어 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| **슬라이드** | 1280×720 한 장 |
|
||||||
|
| **slide-base** | 모든 슬라이드 공통 그릇 (배경 + 제목 + 구분선 + 결론 pill) — `templates/blocks/slide-base.html` |
|
||||||
|
| **slide-body** | 본문 가용 영역 (≈ 1200×590) |
|
||||||
|
| **레이아웃 (Layout)** | 8-preset layout vocabulary — slide-body 안 zone 분배 형태 (legacy hint = Type A/B/B'/B'') |
|
||||||
|
| **Zone (영역)** | 레이아웃이 결정한 콘텐츠 구역 |
|
||||||
|
| **Internal Region** | Zone 안의 placement / planning unit (Layer A) — content_object 를 배치하는 내부 영역 (SPEC v1 §2) |
|
||||||
|
| **컨테이너 (Container)** | zone 의 px 명세 (코드 레벨) |
|
||||||
|
| **프레임 (Frame)** | Figma 디자인 단위 (= 기존 "블록") — zone 안 |
|
||||||
|
| **Frame Slot** | Frame 안의 declared placement slot (Layer B) — frame contract 가 선언하는 콘텐츠 placement target (SPEC v1 §3) |
|
||||||
|
| **Layer A** | composition planning 의 zone-내 layer — content_object 를 Internal Region 으로 배치하는 단위 (SPEC v1 §1~§2) |
|
||||||
|
| **Layer B** | frame contract 기반 frame-내 layer — 콘텐츠 매핑 대상인 Frame Slot 구조를 선언하는 단위 (SPEC v1 §3) |
|
||||||
|
|
||||||
|
### MDX → 슬라이드 매핑
|
||||||
|
|
||||||
|
| MDX 위치 | 슬라이드 위치 |
|
||||||
|
|---|---|
|
||||||
|
| `# 대목차 제목` | `slide-title` |
|
||||||
|
| 본문 (`##` / `###`) | `slide-body` 안 (레이아웃 + zone + 프레임) |
|
||||||
|
| `# 대목차 결론` | `slide-footer` |
|
||||||
|
| `<details>` 팝업 | 슬라이드 위 별도 레이어 |
|
||||||
|
|
||||||
|
Layer A planning telemetry is active in trace-only mode; render path activation remains the next axis.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처 — Phase Z 흐름 (5 단계)
|
||||||
|
|
||||||
|
> 상세는 [`IMPROVEMENT-REDESIGN.md`](IMPROVEMENT-REDESIGN.md) 5 장 참조.
|
||||||
|
|
||||||
|
```
|
||||||
|
STAGE 1) MDX 분석 + 레이아웃 매칭 (8-preset layout vocabulary; legacy = Type A/B/B'/B'')
|
||||||
|
STAGE 2) Zone 별 텍스트 1차 배치
|
||||||
|
STAGE 3) Zone 별 프레임 매칭 (V1~V4; B4 frame_selection evidence integration pending)
|
||||||
|
├ 매칭 완벽 → 텍스트 업데이트
|
||||||
|
├ 매칭 어정쩡 → 디자인 참고 재구성
|
||||||
|
└ 매칭 안 됨 → 디자인 컨셉 바탕 재구성
|
||||||
|
STAGE 4) 프레임 검토 + 컨테이너 조정 (5 차 Fallback)
|
||||||
|
STAGE 5) HTML 조립 + 검증 + 출력
|
||||||
|
```
|
||||||
|
|
||||||
|
핵심 원칙 :
|
||||||
|
- **MDX 1 파일 = 슬라이드 1 장** (절대 분할 X)
|
||||||
|
- **텍스트 원문 무손실 보존** (본문 미리보기 + 팝업 원문)
|
||||||
|
- **자유 디자인 금지** (항상 Figma 프레임 DB 참고)
|
||||||
|
- **불일치 시 레이아웃 회귀** (콘텐츠 줄이지 않고 그릇 변경)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 아키텍처 (Phase Q 파이프라인 — 기존)
|
||||||
|
|
||||||
> Phase P(다후보 렌더링 비교) 실행 결과 20/100점. 업계 조사(Beautiful.ai, Napkin.ai, VASCAR 등) 기반으로 Phase Q에서 재설계.
|
> Phase P(다후보 렌더링 비교) 실행 결과 20/100점. 업계 조사(Beautiful.ai, Napkin.ai, VASCAR 등) 기반으로 Phase Q에서 재설계.
|
||||||
> 핵심 전환: "계산 먼저, AI 판단 나중에, 렌더링은 검증만"
|
> 핵심 전환: "계산 먼저, AI 판단 나중에, 렌더링은 검증만"
|
||||||
|
>
|
||||||
|
> ⚠️ Phase Z (매칭 시스템 통합) 진행 후 이 섹션의 일부는 새 흐름으로 대체될 예정.
|
||||||
|
|
||||||
```
|
```
|
||||||
[1단계] Kei 실장 (Opus) — AI 사고
|
[1단계] Kei 실장 (Opus) — AI 사고
|
||||||
@@ -65,30 +144,48 @@ reference 꼭지 있음 → sidebar-right
|
|||||||
나머지 → single-column
|
나머지 → single-column
|
||||||
```
|
```
|
||||||
|
|
||||||
### 역할 분리 (Phase R')
|
### 역할 분리 (Phase Z)
|
||||||
|
|
||||||
|
> ⚠️ Phase R' 의 역할 분리 (AI 가 HTML 구조 직접 생성) 는 **Phase Z 에서 폐기**. 아래는 Phase Z 기준.
|
||||||
|
|
||||||
| 역할 | 담당 | 방식 | 하는 일 | 하지 않는 일 |
|
| 역할 | 담당 | 방식 | 하는 일 | 하지 않는 일 |
|
||||||
|------|------|------|---------|------------|
|
|------|------|------|---------|------------|
|
||||||
| Kei 실장 | Opus (Kei API) | AI | 꼭지 추출, 비중 판단, relation_type + expression_hint 부여, 최종 검수 | HTML 생성, 레이아웃 계산 |
|
| Kei 실장 | Opus (Kei API) | AI | MDX 본문 분석 보조, 최종 검수 | HTML 생성, 레이아웃 계산 |
|
||||||
| 컨테이너 계산 | 코드 | 결정론적 | Kei 비중 → 역할별 컨테이너 px 확정 | AI 판단 불필요 |
|
| 레이아웃 결정 | 코드 | 룰 매칭 | MDX 콘텐츠 → 8-preset layout vocabulary 기반 slide/zone topology 선택 (legacy hint = Type A/B/B'/B'') | — |
|
||||||
| 프리셋 선택 | 코드 | 규칙 | 실장의 role에 따라 프리셋 자동 선택 | AI 판단 불필요 |
|
| 컨테이너 계산 | 코드 | 결정론적 | zone 별 px 확정 (`space_allocator`) | — |
|
||||||
| **HTML 생성** | **AI (Kei API)** | **AI** | **콘텐츠 전달 의도에 맞는 HTML 구조를 직접 생성. 블록 CSS를 참고하되 구조는 AI가 결정.** | 블록 "선택" 안 함. 슬롯 "채우기" 안 함. |
|
| 프레임 매칭 | 코드 (V1~V4; B4 frame_selection evidence integration pending) | 결정론적 | zone ↔ Figma 프레임 매칭 | — |
|
||||||
| 검증 | 코드 + AI | Selenium + 비전 모델 | overflow 측정, 시각 품질 평가 | |
|
| **콘텐츠 매핑** | **코드 + AI** | **하이브리드** | **zone 안 콘텐츠 / 텍스트를 프레임 슬롯에 배치 / 다듬기 / 변형** | **HTML 구조 생성 X** |
|
||||||
|
| HTML 조립 | 코드 (Jinja2) | 결정론적 | `slide-base` + 프레임 + 슬롯 콘텐츠 합쳐서 final.html | AI 호출 X |
|
||||||
|
| 검증 | 코드 + AI | Selenium + Vision | overflow 측정, 시각 품질 평가 | — |
|
||||||
|
|
||||||
### HTML 생성 원칙 (Phase R')
|
### HTML 생성 원칙 (Phase Z)
|
||||||
|
|
||||||
```
|
```
|
||||||
블록이 구조를 결정 (P=Q=R, 실패) → 콘텐츠가 구조를 결정 (R', 접근 C)
|
블록이 구조를 결정 (P=Q=R, 실패)
|
||||||
|
콘텐츠가 구조를 결정 (R', 폐기 — AI 가 HTML 직접 생성하면 회귀 가능성 큼)
|
||||||
|
→ slide-base + 프레임 DB 가 구조를 결정, AI 는 zone 안 콘텐츠만 (Phase Z)
|
||||||
```
|
```
|
||||||
|
|
||||||
- AI가 콘텐츠 전달 의도(expression_hint)에 맞는 HTML 구조를 직접 생성
|
**Phase Z 의 핵심 원칙**:
|
||||||
- 기존 38개 블록의 CSS(색상, 폰트, 배경, radius)를 **스타일 참고**로 활용
|
- **HTML 구조** = `slide-base.html` + 코드 (Jinja2) 가 결정 (절대 AI 가 생성 X)
|
||||||
- 블록을 **"선택"하지 않음**. topic 합침/분리, 포함 관계, 핵심 메시지 분리 가능
|
- **AI 의 역할** = zone 안 콘텐츠 매핑 / 텍스트 다듬기 / 디자인 변형 (콘텐츠 단위)
|
||||||
- 디자인 토큰(CSS 변수)으로 품질 제약 + Selenium 측정 + 비전 모델 검증
|
- **프레임 DB 참고 필수** — 자유 디자인 금지
|
||||||
|
- **MDX 원문 무손실 보존** (본문 preview, 팝업에 원문)
|
||||||
|
|
||||||
|
⚠️ **Phase R' 회귀 방지**: AI 한테 "HTML 구조 만들어줘" 같은 호출 절대 X. AI 호출은 zone 안 콘텐츠 단위로만.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 핵심 프로세스
|
## 핵심 프로세스 (구 Phase Q 기준 — Phase Z 에서 대체됨)
|
||||||
|
|
||||||
|
> ⚠️ **아래 흐름은 구 Phase Q 기준의 5 단계 (Kei 실장 → 디자인 팀장 → 텍스트 편집자 → 디자인 실무자 → 재검토). Phase Z 에서 대체됨.**
|
||||||
|
>
|
||||||
|
> **신규 흐름은 위 "아키텍처 — Phase Z 흐름 (5 단계)" 섹션 또는 [IMPROVEMENT-REDESIGN.md](IMPROVEMENT-REDESIGN.md) 5 장 따름.**
|
||||||
|
>
|
||||||
|
> ⚠️ **특히 Phase Z 와 충돌하므로 무시할 것**:
|
||||||
|
> - "페이지 분리" → Phase Z: MDX 1 파일 = 슬라이드 1 장 (절대 분할 X)
|
||||||
|
> - "텍스트 압축 / 요약" → Phase Z: MDX 원문 무손실 보존 (본문 preview, 팝업 원문)
|
||||||
|
> - "AI 5 단계" → Phase Z: AI 호출은 zone 안 콘텐츠 단위로만 (HTML 구조 / 레이아웃 / 프리셋 결정 X)
|
||||||
|
|
||||||
```
|
```
|
||||||
사용자 콘텐츠 입력 (텍스트/MDX 붙여넣기 또는 파일 업로드)
|
사용자 콘텐츠 입력 (텍스트/MDX 붙여넣기 또는 파일 업로드)
|
||||||
|
|||||||
@@ -1,134 +1,38 @@
|
|||||||
# C.E.L. Slide Pipeline
|
# C.E.L. Slide Pipeline
|
||||||
|
|
||||||
MDX 기반 콘텐츠를 분석하여 1280×720 슬라이드 HTML로 자동 변환하는 파이프라인입니다.
|
## 이 프로젝트는 무엇인가
|
||||||
|
|
||||||
## 한 줄 요약
|
MDX 기반 콘텐츠를 입력하면, 1280×720 슬라이드 HTML로 자동 변환하는 파이프라인입니다.
|
||||||
|
|
||||||
**텍스트 콘텐츠를 넣으면, 코드가 구조를 분석하고 디자인을 입혀서 슬라이드로 만들어줍니다.**
|
텍스트 콘텐츠를 넣으면, 구조를 분석하고 BEPs(Figma) 디자인을 매칭하여 슬라이드를 만들어줍니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 현재 프로세스 (AS-IS)
|
## 어떻게 구성/구현되어 있는가
|
||||||
|
|
||||||
|
### 전체 흐름
|
||||||
|
|
||||||
```
|
```
|
||||||
1. MDX 입력
|
MDX 입력 → 정규화 → 꼭지 추출(AI) → zone 구분 → BEPs 매칭 → 조립 → 검증 → 출력
|
||||||
- MDX 파일 읽기
|
|
||||||
- 정규화 (제목/본문/이미지/표/핵심요약 분리)
|
|
||||||
|
|
||||||
2. 꼭지 추출 (AI)
|
|
||||||
- 중목차별 핵심 주제 파악
|
|
||||||
- 각 꼭지의 성격 분류
|
|
||||||
|
|
||||||
3. zone 구분 (코드)
|
|
||||||
- 중목차(##) 기준으로 영역 분할 (top/bottom)
|
|
||||||
- 콘텐츠 양 기준으로 비중(weight) 산출
|
|
||||||
|
|
||||||
4. BEPs 디자인 매칭 (코드)
|
|
||||||
- catalog에서 tag/schema 기준으로 블록 검색
|
|
||||||
- 매칭되면 → 해당 블록에 콘텐츠 삽입
|
|
||||||
- 미매칭이면 → 기존 스타일 따라 코드가 고정 렌더 (1회)
|
|
||||||
|
|
||||||
5. 조립 (코드)
|
|
||||||
- slide-base(1280×720)에 zone별 배치
|
|
||||||
- 상세 내용은 첨부 파일로 분리
|
|
||||||
|
|
||||||
6. 검증
|
|
||||||
- overflow 측정 (Selenium)
|
|
||||||
- 구조 검증 (본문 visible, 링크 수)
|
|
||||||
|
|
||||||
7. 출력
|
|
||||||
- final.html + 첨부 HTML
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 현재 수준
|
### 구조
|
||||||
|
|
||||||
| 대상 | 상태 |
|
- **slide-base:** 1280×720 슬라이드 프레임. 대목차 + 구분선 + 본문 영역 + 핵심 인사이트(footer)
|
||||||
|------|------|
|
- **zone:** 본문 영역 안에서 중목차(##) 기준으로 나뉘는 영역 (top/bottom 등)
|
||||||
| MDX 03 (3개 목표 + 비교표) | ✅ 정상 동작 |
|
- **블록:** zone 안에 들어가는 디자인 단위. Figma에서 추출한 BEPs 디자인을 HTML/CSS로 변환한 것
|
||||||
| MDX 02 (목표 + 프로세스 + 상세표) | △ 구조 동작, 시각 품질 보정 중 |
|
- **catalog:** 블록의 메타 정보 (구조, 슬롯, 매칭 조건)
|
||||||
|
|
||||||
---
|
### 주요 파일
|
||||||
|
|
||||||
## 개선 프로세스 (TO-BE)
|
|
||||||
|
|
||||||
```
|
|
||||||
1. MDX 입력
|
|
||||||
- MDX 파일 읽기
|
|
||||||
- 정규화 (제목/본문/이미지/표/핵심요약 분리)
|
|
||||||
|
|
||||||
2. zone 구분 (코드)
|
|
||||||
- 중목차(##) 기준으로 영역 분할
|
|
||||||
|
|
||||||
3. BEPs 디자인 매칭 검토
|
|
||||||
3-1. 매칭 시
|
|
||||||
- 해당 디자인 선택
|
|
||||||
- zone 크기에 맞게 재구성
|
|
||||||
3-2. 미매칭 시
|
|
||||||
- 중목차에 대한 꼭지 정리 (AI)
|
|
||||||
- 꼭지별 유사 디자인 선택
|
|
||||||
- 해당 중목차/소목차에 맞게 redesign
|
|
||||||
- 입력 → 조정 반복 (빈 공간/overflow 자동 조절)
|
|
||||||
|
|
||||||
4. 검증
|
|
||||||
- overflow / 빈 공간 측정
|
|
||||||
- 시각 품질 검증 (정렬, 위계, 가독성)
|
|
||||||
|
|
||||||
5. 출력
|
|
||||||
- final.html + 첨부 HTML
|
|
||||||
```
|
|
||||||
|
|
||||||
### AS-IS → TO-BE 핵심 차이
|
|
||||||
|
|
||||||
| 항목 | AS-IS | TO-BE |
|
|
||||||
|------|-------|-------|
|
|
||||||
| 꼭지 추출 시점 | zone 구분 전에 항상 실행 | 미매칭 시에만 (3-2) |
|
|
||||||
| 디자인 미매칭 | 코드 고정 렌더 1회 | redesign + 반복 조정 |
|
|
||||||
| 빈 공간 처리 | 그냥 둠 | 자동 재분배 |
|
|
||||||
| 검증 | overflow만 | 시각 품질까지 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 핵심 원칙
|
|
||||||
|
|
||||||
| 원칙 | 설명 |
|
|
||||||
|------|------|
|
|
||||||
| **콘텐츠가 기준** | 디자인이 콘텐츠에 맞춤 (콘텐츠를 디자인에 맞추지 않음) |
|
|
||||||
| **코드가 판단** | AI는 꼭지 추출만, 레이아웃/디자인 선택은 규칙 기반 |
|
|
||||||
| **기존 디자인 재활용** | Figma에서 추출한 BEPs 블록을 우선 활용, 없으면 같은 스타일로 생성 |
|
|
||||||
| **넘치면 분리** | 슬라이드에 안 들어가는 상세 내용은 첨부로 분리 (자세히보기) |
|
|
||||||
| **통일이 아닌 다양성** | 같은 구조라도 콘텐츠에 따라 다른 표현 가능 (recipe = 표현 범주) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 프로젝트 구조
|
|
||||||
|
|
||||||
### 파이프라인 핵심
|
|
||||||
|
|
||||||
| 파일 | 역할 |
|
| 파일 | 역할 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `src/pipeline.py` | 전체 파이프라인 오케스트레이션 |
|
| `src/pipeline.py` | 파이프라인 오케스트레이션 |
|
||||||
| `src/section_parser.py` | 중목차 추출, schema 분류, subsection typing |
|
| `src/section_parser.py` | 중목차 추출, 구조 분류 |
|
||||||
| `src/block_reference.py` | BEPs 디자인 매칭 (catalog 검색) |
|
| `src/block_reference.py` | BEPs 디자인 매칭 |
|
||||||
| `src/block_assembler.py` | 슬라이드 HTML 조립, recipe executor, direct render |
|
| `src/block_assembler.py` | 슬라이드 HTML 조립 |
|
||||||
| `src/space_allocator.py` | zone 크기/비율 계산 |
|
| `templates/blocks/slide-base.html` | 슬라이드 프레임 |
|
||||||
| `src/pipeline_context.py` | 파이프라인 데이터 모델 (PopupItem 등) |
|
| `templates/catalog.yaml` | 블록 메타 정보 |
|
||||||
|
|
||||||
### 템플릿 / 카탈로그
|
|
||||||
|
|
||||||
| 파일 | 역할 |
|
|
||||||
|------|------|
|
|
||||||
| `templates/catalog.yaml` | BEPs 블록 목록 (when/slots/template) |
|
|
||||||
| `templates/blocks/slide-base.html` | 슬라이드 기본 틀 (1280×720) |
|
|
||||||
| `templates/blocks/new/` | Figma 추출 블록 (prerequisites-3col 등) |
|
|
||||||
| `templates/blocks/redesign/` | 재디자인 블록 (process-product-2col 등) |
|
|
||||||
|
|
||||||
### 검증
|
|
||||||
|
|
||||||
| 파일 | 역할 |
|
|
||||||
|------|------|
|
|
||||||
| `src/slide_measurer.py` | Selenium overflow 측정 |
|
|
||||||
| `src/step_visualizer.py` | 단계별 디버그 보드 생성 |
|
|
||||||
| `src/validators.py` | 구조 검증 |
|
|
||||||
|
|
||||||
### 산출물
|
### 산출물
|
||||||
|
|
||||||
@@ -137,15 +41,84 @@ MDX 기반 콘텐츠를 분석하여 1280×720 슬라이드 HTML로 자동 변
|
|||||||
| 파일 | 내용 |
|
| 파일 | 내용 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `final.html` | 최종 슬라이드 |
|
| `final.html` | 최종 슬라이드 |
|
||||||
| `final_context.json` | 전체 파이프라인 결과 데이터 |
|
| `final_context.json` | 파이프라인 결과 데이터 |
|
||||||
| `steps/*.html` | 단계별 디버그 보드 |
|
| `steps/*.html` | 단계별 디버그 보드 |
|
||||||
| `첨부*_상세*.html` | popup 상세 내용 |
|
| `첨부*_상세*.html` | popup 상세 내용 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 향후 개선 방향
|
## 무슨 문제가 있는가
|
||||||
|
|
||||||
1. **3-2 입력→조정 반복 구현** — redesign 후 빈 공간/overflow 자동 재분배
|
| 문제 | 설명 |
|
||||||
2. **검증 강화** — overflow뿐 아니라 정렬, 위계, 가독성까지 시각 품질 검증
|
|------|------|
|
||||||
3. **BEPs 매칭 범위 확대** — 더 많은 Figma 블록 추출 및 catalog 등록
|
| **블록마다 스타일이 제각각** | 각 블록 HTML 안에 font-size, color, padding이 직접 박혀있어서, 같은 슬라이드 안에서 블록이 섞이면 위계가 안 맞음 |
|
||||||
4. **다양한 MDX 확장** — MDX 02, 03 이후 추가 문서 유형 검증
|
| **slide-base에 구조+스타일 혼재** | 프레임 HTML 안에 전체 CSS가 인라인으로 들어있어서 유지보수가 어려움 |
|
||||||
|
| **블록이 완성 HTML** | 블록이 구조+스타일+값을 모두 포함하고 있어서, 재사용/조합이 안 됨 |
|
||||||
|
| **매칭 안 되면 고정 렌더** | BEPs에 맞는 블록이 없을 때 코드가 1회 고정 렌더하고 끝. 반복 조정 없음 |
|
||||||
|
| **빈 공간/overflow 방치** | 렌더 후 빈 공간이 있어도 조정 안 하고, overflow만 감지 |
|
||||||
|
| **검증이 약함** | overflow 측정만 하고, 정렬/위계/가독성 같은 시각 품질은 미검증 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 어떻게 개선하려 하는가
|
||||||
|
|
||||||
|
핵심은 3가지입니다.
|
||||||
|
|
||||||
|
1. **블록을 구조 부품화** — 완성 HTML이 아니라, 구조만 담고 스타일은 토큰으로 분리
|
||||||
|
2. **스타일을 토큰으로 통일** — 블록마다 제각각인 폰트/색/여백을 공통 기준으로
|
||||||
|
3. **2경로 파이프라인** — 매칭되면 바로 쓰고(direct-fit), 안 되면 재구성(recipe)
|
||||||
|
|
||||||
|
### AS-IS → TO-BE
|
||||||
|
|
||||||
|
```
|
||||||
|
AS-IS:
|
||||||
|
AI가 먼저 꼭지를 추출하고
|
||||||
|
→ 매칭 블록이 있으면 삽입, 없으면 코드가 1회 고정 렌더
|
||||||
|
→ 빈 공간이 있어도 그냥 둠
|
||||||
|
→ 블록마다 font-size, color가 직접 박혀있어서 섞이면 위계 안 맞음
|
||||||
|
|
||||||
|
TO-BE:
|
||||||
|
중목차 기준으로 zone을 먼저 나누고
|
||||||
|
→ TF-IDF로 BEPs 매칭 시도
|
||||||
|
→ 매칭되면 블록 삽입 + 크기 조절 (direct-fit)
|
||||||
|
→ 안 되면 AI가 꼭지 정리 + 유사 디자인으로 redesign + 반복 조정 (recipe)
|
||||||
|
→ 빈 공간/overflow를 자동 재분배
|
||||||
|
→ 모든 블록이 토큰 기반이라 스타일 통일
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
| 대상 | 슬라이드 변환 | 파이프라인 자동화 |
|
||||||
|
|------|-------------|----------------|
|
||||||
|
| MDX 03 (3개 목표 + 비교표) | ✅ 완료 | ✅ 파이프라인 연결 |
|
||||||
|
| MDX 02 (목표 + 프로세스 + 상세표) | ✅ 완료 | △ 파이프라인 연결, 시각 품질 개선 중 |
|
||||||
|
| MDX 01 (Type A, sidebar 구조) | ✅ 완료 (개별) | 미연결 |
|
||||||
|
| 토큰 기반 CSS 체계 | - | ✅ 정의 완료, slide-base 적용 |
|
||||||
|
| Figma 블록 추출 | - | 진행 중 (`figma_to_html_agent/blocks/`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 다음 단계 방향
|
||||||
|
|
||||||
|
| 순서 | 단계 | 내용 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1 | 폴더 구조 정리 | structures/recipes/legacy 분리 |
|
||||||
|
| 2 | 기존 블록 점진 전환 | 분류(direct-fit/recipe/rewrite) → 토큰 기반 전환 |
|
||||||
|
| 3 | catalog 고도화 | 파일명 중심 → 속성 테이블 기반 매칭 |
|
||||||
|
| 4 | 파이프라인 연결 | TF-IDF 매칭 + recipe/composition 경로 |
|
||||||
|
| 5 | fit 루프 확장 | 빈 공간 재분배, preview 축약, 자동 조정 반복 |
|
||||||
|
| 6 | 시각 품질 검증 | 정렬, 위계, 가독성 검증 강화 |
|
||||||
|
|
||||||
|
상세: [IMPROVEMENT-PLAN.md](docs/architecture/IMPROVEMENT-PLAN.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 참고 문서
|
||||||
|
|
||||||
|
| 문서 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| [IMPROVEMENT-PLAN.md](docs/architecture/IMPROVEMENT-PLAN.md) | 개선 설계 (목표/방향/6단계 계획) |
|
||||||
|
| [TOKENS-v1.md](docs/architecture/TOKENS-v1.md) | 토큰 위계 기준표 초안 |
|
||||||
|
| [BLOCK-RULES.md](docs/architecture/BLOCK-RULES.md) | 블록 작성 규칙 (에이전트 간 계약서) |
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# 블록 작성 규칙
|
||||||
|
|
||||||
|
Figma 1:1 HTML을 재사용 가능한 블록으로 전환할 때, 이 규칙을 따른다.
|
||||||
|
figma_to_html_agent와 design_agent 간의 **계약서**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. HTML 규칙
|
||||||
|
|
||||||
|
### 구조 스타일은 OK
|
||||||
|
블록의 배치/정렬을 위한 CSS는 블록 안에 있어야 한다.
|
||||||
|
```css
|
||||||
|
/* OK */
|
||||||
|
display: flex;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 직접값은 금지
|
||||||
|
하드코딩된 폰트/색/여백은 블록 안에 넣지 않는다.
|
||||||
|
```css
|
||||||
|
/* NG */
|
||||||
|
font-size: 11px;
|
||||||
|
color: #475569;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
/* OK — 토큰 참조 */
|
||||||
|
font-size: var(--font-body);
|
||||||
|
color: var(--color-body);
|
||||||
|
padding: var(--card-padding);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 클래스명 규칙
|
||||||
|
|
||||||
|
### 공통 클래스 (모든 블록에서 사용)
|
||||||
|
| 클래스 | 용도 | 토큰 참조 |
|
||||||
|
|--------|------|-----------|
|
||||||
|
| `.zone-title` | 중목차 제목 | `--font-zone-title`, `--color-zone-title` |
|
||||||
|
| `.sub-title` | 소목차 제목 | `--font-sub-title` |
|
||||||
|
| `.bul` | 본문 블릿 (hanging indent) | `--font-body`, `--bullet-indent` |
|
||||||
|
| `.body-text` | 본문 텍스트 | `--font-body`, `--color-body` |
|
||||||
|
| `.caption` | 캡션/보조 | `--font-caption`, `--color-caption` |
|
||||||
|
|
||||||
|
### 블록 고유 클래스
|
||||||
|
블록별 고유 요소는 블록 prefix를 붙인다.
|
||||||
|
```
|
||||||
|
.p3c-bar (prerequisites-3col의 gradient 바)
|
||||||
|
.pp2-col (process-product-2col의 컬럼)
|
||||||
|
.cid-card (card-icon-desc의 카드)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 슬롯 규칙
|
||||||
|
|
||||||
|
블록이 받아들이는 콘텐츠 슬롯. catalog에 명시.
|
||||||
|
|
||||||
|
| 슬롯 | 용도 | 예시 |
|
||||||
|
|------|------|------|
|
||||||
|
| `zone_title` | 중목차 제목 | "DX의 궁극적 목표" |
|
||||||
|
| `sub_title` | 소목차 제목 | "안전과 품질" |
|
||||||
|
| `body` | 본문 텍스트 | 설명 문장 |
|
||||||
|
| `bullets` | 블릿 리스트 | D2: 항목들 |
|
||||||
|
| `image` | 이미지 | 시각 앵커 |
|
||||||
|
| `preview` | 상세 preview | 표 헤더+행 |
|
||||||
|
| `detail_link` | 자세히보기 링크 | 첨부 파일 연결 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 토큰 참조 방법
|
||||||
|
|
||||||
|
### 토큰 파일 위치
|
||||||
|
```
|
||||||
|
templates/styles/tokens/
|
||||||
|
├── typography.css ← 글자 위계
|
||||||
|
├── spacing.css ← 여백/간격
|
||||||
|
└── colors.css ← 색상
|
||||||
|
```
|
||||||
|
|
||||||
|
### 사용법
|
||||||
|
```css
|
||||||
|
/* 블록 CSS에서 */
|
||||||
|
.my-block-title {
|
||||||
|
font-size: var(--font-sub-title);
|
||||||
|
font-weight: var(--weight-sub-title);
|
||||||
|
line-height: var(--lh-sub-title);
|
||||||
|
color: var(--color-zone-title);
|
||||||
|
margin-bottom: var(--heading-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.my-block-body {
|
||||||
|
font-size: var(--font-body);
|
||||||
|
color: var(--color-body);
|
||||||
|
padding-left: var(--bullet-indent);
|
||||||
|
text-indent: calc(var(--bullet-indent) * -1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 블록 의미색이 필요할 때
|
||||||
|
공통 테마색으로 안 되는 역할색은 colors.css의 2층 의미색을 참조.
|
||||||
|
```css
|
||||||
|
/* 3열 비교의 열별 색상 */
|
||||||
|
.col-1 .bar { background: linear-gradient(180deg, var(--color-col-1-from), var(--color-col-1-to)); }
|
||||||
|
.col-2 .bar { background: linear-gradient(180deg, var(--color-col-2-from), var(--color-col-2-to)); }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 블록 HTML 예시
|
||||||
|
|
||||||
|
### 좋은 예
|
||||||
|
```html
|
||||||
|
<section class="block block-3col">
|
||||||
|
<div class="col">
|
||||||
|
<div class="sub-title">{{ col.title }}</div>
|
||||||
|
<div class="bul">• {{ col.desc }}</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.block-3col {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: var(--card-gap);
|
||||||
|
}
|
||||||
|
.block-3col .col {
|
||||||
|
padding: var(--card-padding);
|
||||||
|
background: var(--color-bg-subtle);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 나쁜 예
|
||||||
|
```html
|
||||||
|
<!-- NG: 직접값 하드코딩 -->
|
||||||
|
<div style="font-size:12px; color:#1e293b; padding:16px; background:#f8fafc; border-radius:6px;">
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 전환 체크리스트
|
||||||
|
|
||||||
|
Figma 1:1 HTML → 재사용 블록으로 전환할 때:
|
||||||
|
|
||||||
|
- [ ] font-size 직접값 → `var(--font-*)` 교체
|
||||||
|
- [ ] color 직접값 → `var(--color-*)` 교체
|
||||||
|
- [ ] padding/margin 직접값 → semantic spacing 또는 `var(--space-*)` 교체
|
||||||
|
- 카드 내부: `var(--card-padding)`
|
||||||
|
- 블릿 들여쓰기: `var(--bullet-indent)`
|
||||||
|
- 소제목 아래: `var(--heading-gap)`
|
||||||
|
- 일반 여백: `var(--space-sm)`, `var(--space-md)` 등
|
||||||
|
- [ ] gap → `var(--card-gap)`, `var(--flex-gap)` 등
|
||||||
|
- [ ] border-radius → `var(--card-radius)` 교체
|
||||||
|
- [ ] 공통 클래스 사용 (`.bul`, `.sub-title` 등)
|
||||||
|
- [ ] 블록 고유 요소에 prefix 클래스
|
||||||
|
- [ ] catalog에 슬롯/구조/호환 정보 등록
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# Frame Integration Map — 32 Figma Frame ↔ Phase Z Zone 통합 매핑
|
||||||
|
|
||||||
|
> **핵심 방향 (2026-04-28 정정)**
|
||||||
|
>
|
||||||
|
> Figma frame 은 슬라이드에 **원본 그대로 꽂는 게 아니라**, **디자인 레퍼런스 / 구조 패턴 / 슬롯 힌트**로 본다.
|
||||||
|
> **최종 결과는 항상 현재 slide-body 의 Zone 안에 맞게 재구성한다.**
|
||||||
|
>
|
||||||
|
> - 원본이 full-slide 디자인이어도 → Zone 안 맞게 축약 / 재배치 / 슬롯화해서 사용
|
||||||
|
> - 복합 슬라이드여도 → Zone 안에서는 일부 패턴만 참고해 재구성
|
||||||
|
>
|
||||||
|
> → 분류는 **"원본 크기"** 가 아니라 **"Zone 에 어떻게 적용할지"** 기준으로 한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 라벨 정의
|
||||||
|
|
||||||
|
### Zone 적용 방식 (zone_application)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| `zone_direct` | Zone 안에 거의 그대로 적용 가능 (구조 / 사이즈 변환 최소) |
|
||||||
|
| `zone_adapt` | 구조는 맞지만 Zone 크기에 맞게 재구성 필요 |
|
||||||
|
| `zone_extract` | 전체 frame 중 일부 패턴만 추출해서 Zone 에 사용 |
|
||||||
|
| `reference_only` | 직접 구조로 쓰기보다 디자인 톤 / 아이디어만 참고 |
|
||||||
|
| `reject` | Phase Z 에서 사용하지 않음 |
|
||||||
|
|
||||||
|
### Zone 적합성 (zone_fit)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| `high` | Zone 에 바로 맞추기 쉬움 (단순 리스트 / 표 등) |
|
||||||
|
| `medium` | 조정하면 가능 (3 단 카드 / 다이어그램 등) |
|
||||||
|
| `low` | 많이 재구성해야 함 (복합 구조 / 분할 패널) |
|
||||||
|
| `reference` | 참고용 |
|
||||||
|
|
||||||
|
### 검토 상태 (review_status)
|
||||||
|
|
||||||
|
| 값 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| `auto_estimated` | 코드가 layout 패턴으로 추정. 사용자 검토 필요 |
|
||||||
|
| `user_confirmed` | 사용자 검토 후 확정 |
|
||||||
|
| `needs_review` | 자동 추정에 의문 — 사람이 Figma 캔버스 직접 확인 필요 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1차 — 32 Frame Zone 적용 분류
|
||||||
|
|
||||||
|
> ⚠️ **`legacy 스타일 출처(참고)` 컬럼 의미**
|
||||||
|
>
|
||||||
|
> 기존 `templates/blocks/` 는 **Phase Z 의 실제 조립 재료가 아님** (삭제 / 폐기 방향).
|
||||||
|
> 이 컬럼은 **frame ↔ 블록 매핑이 아니라**, frame 의 디자인 / 시각 언어를 만들 때 참고할 수 있는 **스타일 출처** (색감, 여백, 폰트 위계, 표 스타일, 카드 스타일, pill / badge, SVG / CSS 구현 힌트) 만 가리킨다.
|
||||||
|
>
|
||||||
|
> Phase Z 의 실제 실행 기준은 **새 frame / zone catalog**.
|
||||||
|
> 정밀화는 [`PHASE-Z-STYLE-SOURCES.md`](PHASE-Z-STYLE-SOURCES.md) (예정) 에서 별도 진행.
|
||||||
|
|
||||||
|
| Frame | Figma ID | 패턴 | Zone 적합성 | Zone 적용 방식 | 검토 상태 | legacy 스타일 출처(참고) | 비고 |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **01** | `1171281172` | `circular-nodes-6` | `medium` | `zone_adapt` | `auto_estimated` | `templates/blocks/visuals/` (다이어그램) | S/W 개발 방향 순환도. 6 노드 — Zone 사이즈에 맞춰 시각 재구성 |
|
||||||
|
| **02** | `1171281173` | `bullet-cards-4-plus-center` | `low` | `zone_extract` | `auto_estimated` | (복합 — 일부 패턴 추출) | 4 카드 + 중앙 강조. Zone 에서는 4 카드 부분만 추출하거나 중앙만 강조로 사용 |
|
||||||
|
| **03** | `1171281174` | `list-numbered-4` | `high` | `zone_direct` | `auto_estimated` | `templates/blocks/cards/` 또는 list 류 | 4 항목 번호 리스트 — 단순 구조, Zone 직접 적용 |
|
||||||
|
| **04** | `1171281175` | `quadrilateral-relations` | `medium` | `zone_extract` | `user_confirmed` | (관계도 — 기존 venn / cycle 로 부족 가능) | 4 actors + 관계도 + hierarchy 단계 결합된 복합형. 큰 Zone 에서는 zone_adapt 가능, 기본은 actor / hierarchy 패턴 추출 |
|
||||||
|
| **05** | `1171281176` | `side-card-with-list` | `medium` | `zone_extract` | `auto_estimated` | (좌우 분할 — 일부 추출) | 좌측 카드 + 우측 리스트. Zone 안에서 좌·우 패턴 추출 |
|
||||||
|
| **06** | `1171281177` | `full-page-map-banner` | `reference` | `reference_only` | `user_confirmed` | (콘텐츠 슬롯형 X — 참고용) | 지도 / 마커 / 현황 시각자료 중심. 일반 텍스트 Zone 구조로 직접 사용 X. 지도형 콘텐츠가 있을 때 디자인 참고 |
|
||||||
|
| **07** | `1171281178` | `2col-paired-list` | `high` | `zone_adapt` | `auto_estimated` | `templates/blocks/tables/` 또는 페어드 | 2 컬럼 페어드 리스트 — Zone 사이즈 맞춰 표 재구성 |
|
||||||
|
| **08** | `1171281179` | `3-section-framework` | `medium` | `zone_extract` | `auto_estimated` | (3 섹션 — 일부 추출) | 효율적 정보 관리 (What/How/When). 3 섹션 패턴 추출 |
|
||||||
|
| **09** | `1171281180` | `list-stacked-vertical` | `high` | `zone_direct` | `auto_estimated` | `templates/blocks/cards/` 또는 list 류 | 5 항목 세로 리스트 — Zone 직접 적용 |
|
||||||
|
| **10** | `1171281181` | `radial-diagram-5` | `medium` | `zone_adapt` | `auto_estimated` | `templates/blocks/visuals/` | 5 way 방사형 다이어그램 — Zone 시각 재구성 |
|
||||||
|
| **11** | `1171281182` | `cards-3-category` | `medium` | `zone_extract` | `auto_estimated` | (3 카드 — 패턴 추출) | 시공단계 BIM 활용 — 3 카드 패턴 추출 |
|
||||||
|
| **12** | `1171281189` | `cycle-3way-intersection` | `medium` | `zone_adapt` | `auto_estimated` | `templates/blocks/visuals/venn-diagram.html` | 3 way 벤다이어그램. Zone 사이즈 맞춰 SVG 재구성 |
|
||||||
|
| **13** | `1171281190` | `3-column` | `medium` | `zone_extract` | `auto_estimated` | (3 단 — 패턴 추출) | 필수조건 (pillar 3). Zone 안에 3 단 패턴 추출. 02-2.2 매칭 실패 정답 frame (3차 대상) |
|
||||||
|
| **14** | `1171281191` | `persona-3col` | `medium` | `zone_extract` | `auto_estimated` | `templates/blocks/cards/card-text-grid.html` (참고) | 주체별 기대효과. Zone 안에서 3 카드 패턴 추출 |
|
||||||
|
| **15** | `1171281192` | `policy-4card-plus-list` | `low` | `zone_extract` | `auto_estimated` | (복합 — 일부 추출) | 4 카드 + 부속 리스트. Zone 안에서 4 카드만 추출하거나 리스트만 사용 |
|
||||||
|
| **16** | `1171281193` | `quadrant-4` | `medium` | `zone_extract` | `auto_estimated` | (4 사분면 — 패턴 추출) | BIM 수행 이슈 4사분면. 4 사분면 패턴 추출 |
|
||||||
|
| **17** | `1171281194` | `paired-rows-2x2` | `high` | `zone_adapt` | `auto_estimated` | `templates/blocks/tables/` | 2x2 페어드 행 — Zone 표 재구성 |
|
||||||
|
| **18** | `1171281195` | `compare-rows` | `high` | `zone_adapt` | `auto_estimated` | `templates/blocks/tables/compare-3col-badge.html` (참고) | BIM·DX 비교. 매칭 시스템 검증된 대표 frame. Zone 안 표 재구성 |
|
||||||
|
| **19** | `1171281197` | `cards-3-compare` | `medium` | `zone_extract` | `auto_estimated` | (3 단 비교 — 패턴 추출) | 설계방식 왜곡 — 3 비교 패턴 추출 |
|
||||||
|
| **20** | `1171281198` | `cards-3-header` | `medium` | `zone_extract` | `auto_estimated` | (3 헤더형 — 패턴 추출) | DX 는 S/W 가 필수 — 3 헤더 패턴 추출 |
|
||||||
|
| **21** | `1171281201` | `split-panel-diagram` | `low` | `zone_extract` | `auto_estimated` | (분할 패널 — 패턴 추출) | Solution Engn. S/W. 분할 패널 패턴 일부 추출 |
|
||||||
|
| **22** | `1171281202` | `split-panel-numbered` | `low` | `zone_extract` | `auto_estimated` | (분할 패널 + 번호 — 패턴 추출) | Model 특화 Engn. S/W. 분할 + 번호 패턴 추출 |
|
||||||
|
| **23** | `1171281203` | `table-2col` | `high` | `zone_adapt` | `auto_estimated` | `templates/blocks/tables/` | Application S/W 의 구분. **table family 중복 — variant 통합 검토** |
|
||||||
|
| **24** | `1171281204` | `table-3col` | `high` | `zone_adapt` | `auto_estimated` | `templates/blocks/tables/` | Engn. S/W 구성과 특징. **table family 중복 — variant 통합 검토** |
|
||||||
|
| **25** | `1171281205` | `left-categories-right-logos` | `medium` | `zone_extract` | `auto_estimated` | (좌우 분할 — 패턴 추출) | 상용 Engn. S/W. 좌우 분할 패턴 추출 |
|
||||||
|
| **26** | `1171281206` | `cards-4-grid` | `medium` | `zone_extract` | `auto_estimated` | (4 카드 — 패턴 추출) | 상용 S/W 의존 4 대 문제. 4 카드 그리드 추출 |
|
||||||
|
| **27** | `1171281208` | `central-split-synthesis` | `low` | `zone_extract` | `auto_estimated` | (중앙 합성 — 일부 추출) | 건설산업 고부가가치화. 중앙 + 양쪽 패턴 일부 추출 |
|
||||||
|
| **28** | `1171281209` | `title-plus-3-emphasis` | `medium` | `zone_extract` | `auto_estimated` | (제목 + 3 강조 — 패턴 추출) | 현존 상용 S/W 의 현실. 제목 + 3 카드 패턴 추출 |
|
||||||
|
| **29** | `1171281210` | `banner-top-2col-bottom` | `low` | `zone_extract` | `auto_estimated` | (banner + 2col 복합 — 패턴 추출) | Process/Product 혁신 (AS-IS/TO-BE). Zone 안에서 banner + 2col 패턴만 추출 |
|
||||||
|
| **30** | `1171281211` | `table-3col` | `high` | `zone_adapt` | `auto_estimated` | `templates/blocks/tables/` | 산업별 현황. **table family 중복 — variant 통합 검토** |
|
||||||
|
| **31** | `1171281212` | `table-3col` | `high` | `zone_adapt` | `auto_estimated` | `templates/blocks/tables/` | 산업별 특성과 발전방향. **table family 중복 — variant 통합 검토** |
|
||||||
|
| **32** | `1171281213` | `central-5-goals` | `low` | `zone_extract` | `auto_estimated` | (5 목표 복합 — 일부 추출) | 정책 달성. 중앙 5 목표 패턴 일부 추출 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 자동 추정 룰
|
||||||
|
|
||||||
|
### layout 패턴 → Zone 적용 방식
|
||||||
|
|
||||||
|
| 패턴 / family | Zone 적용 방식 | Zone 적합성 | 근거 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `list-*` (단순 리스트) | `zone_direct` | `high` | 구조 단순 — Zone 안 그대로 |
|
||||||
|
| `compare-rows` / `table-*col` / `paired-rows-*` (표 / 페어드) | `zone_adapt` | `high` | 표 구조 — Zone 사이즈 맞춰 재구성 |
|
||||||
|
| `cycle-*` / `circular-*` / `radial-*` / `quadrilateral-*` (시각 다이어그램) | `zone_adapt` | `medium` | 시각 디자인 — Zone 사이즈 맞춰 SVG 재구성 |
|
||||||
|
| `cards-3-*` / `persona-3col` / `3-column` / `cards-4-grid` / `quadrant-4` / `3-section-*` (3·4 단 카드 / 칼럼) | `zone_extract` | `medium` | 패턴 일부 추출 (카드 / 칼럼) |
|
||||||
|
| `side-*` / `left-*-right-*` / `title-plus-*` (좌우 / 제목+카드) | `zone_extract` | `medium` | 부분 패턴 추출 |
|
||||||
|
| `banner-top-*-bottom` / `bullet-cards-*-plus-center` / `policy-*-plus-list` (복합) | `zone_extract` | `low` | 복합 구조 — Zone 에서는 일부만 추출 |
|
||||||
|
| `split-panel-*` / `central-*` (분할 / 중앙 합성) | `zone_extract` | `low` | 새 시각 패턴 — 일부 추출 |
|
||||||
|
| `full-page-map-*` (지도 / 배너) | `reference_only` | `reference` | 콘텐츠 슬롯형 X — 디자인 참고만 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 분포 요약
|
||||||
|
|
||||||
|
### Zone 적용 방식
|
||||||
|
|
||||||
|
| 적용 방식 | 개수 | Frame |
|
||||||
|
|---|---|---|
|
||||||
|
| `zone_direct` | **2** | 03, 09 |
|
||||||
|
| `zone_adapt` | **11** | 01, 04, 07, 10, 12, 17, 18, 23, 24, 30, 31 |
|
||||||
|
| `zone_extract` | **18** | 02, 05, 08, 11, 13, 14, 15, 16, 19, 20, 21, 22, 25, 26, 27, 28, 29, 32 |
|
||||||
|
| `reference_only` | **1** | 06 |
|
||||||
|
| `reject` | **0** | — |
|
||||||
|
|
||||||
|
**합계 검증** : 2 + 11 + 18 + 1 + 0 = **32** ✓
|
||||||
|
|
||||||
|
### Zone 적합성
|
||||||
|
|
||||||
|
| 적합성 | 개수 | Frame |
|
||||||
|
|---|---|---|
|
||||||
|
| `high` | **9** | 03, 07, 09, 17, 18, 23, 24, 30, 31 |
|
||||||
|
| `medium` | **15** | 01, 04, 05, 08, 10, 11, 12, 13, 14, 16, 19, 20, 25, 26, 28 |
|
||||||
|
| `low` | **7** | 02, 15, 21, 22, 27, 29, 32 |
|
||||||
|
| `reference` | **1** | 06 |
|
||||||
|
|
||||||
|
**합계 검증** : 9 + 15 + 7 + 1 = **32** ✓
|
||||||
|
|
||||||
|
### 검토 상태
|
||||||
|
|
||||||
|
| review_status | 개수 | Frame |
|
||||||
|
|---|---|---|
|
||||||
|
| `auto_estimated` | **30** | (04, 06 제외 모두) |
|
||||||
|
| `needs_review` | **0** | (04, 06 사용자 확정 완료) |
|
||||||
|
| `user_confirmed` | **2** | **04** (zone_extract 확정), **06** (reference_only 확정) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 핵심 메시지
|
||||||
|
|
||||||
|
> **모든 frame 은 최종적으로 Zone 에 맞게 들어간다.**
|
||||||
|
> **원본이 full-slide 인지 아닌지는 참고 정보일 뿐, 최종 사용 단위는 Zone 이다.**
|
||||||
|
>
|
||||||
|
> - 사용자가 의미한 "needs_split" = 실제 의미는 `zone_extract` 또는 `zone_adapt`
|
||||||
|
> - frame 은 디자인 레퍼런스 / 구조 패턴 / 슬롯 힌트
|
||||||
|
> - Phase Z output 은 항상 Zone 에 맞는 재구성 결과
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 다음 단계
|
||||||
|
|
||||||
|
### ✅ 완료 (1 차)
|
||||||
|
- 32 frame 자동 추정 표 작성
|
||||||
|
- Frame 04, 06 사용자 확정 (`user_confirmed`)
|
||||||
|
- 컬럼명 정정 : `legacy 대응 블록(참고, 미검증)` → `legacy 스타일 출처(참고)`
|
||||||
|
|
||||||
|
### ✅ 완료 (2 차, 2026-04-28) — Phase Z Frame Style Inventory 작성
|
||||||
|
|
||||||
|
산출물 : [`PHASE-Z-FRAME-STYLE-INVENTORY.md`](PHASE-Z-FRAME-STYLE-INVENTORY.md)
|
||||||
|
|
||||||
|
- Source Policy : 메인 (`figma_to_html_agent/blocks/` 32 frame) / 토큰 (`templates/styles/tokens/`) / legacy (`templates/blocks/structures/`)
|
||||||
|
- Frame Inventory 32 행 (변환 14 + 미변환 18 보일러플레이트)
|
||||||
|
- Token Inventory 18 행 (`covered` 7 / `gap_candidate` 5 / `hierarchy_mapping_only` 3 / `hold_recheck_after_conversion` 3)
|
||||||
|
- Legacy Reference 6 행 (모두 `delete_after_extract` 후보)
|
||||||
|
|
||||||
|
> ⚠️ inventory = **추출 / 검증 단계**. 실제 token 파일 생성 / 변경 / catalog 설계 / templates/blocks 삭제 등 **실행은 별도 승인 단계**.
|
||||||
|
|
||||||
|
### 후속 작업
|
||||||
|
- **3차** — Frame 14 anchor_sets 재라벨링 (02-2.2 매칭 실패 교정 — 매칭 성능 교정 작업, 별도 진행)
|
||||||
|
- **추가** — table family (23/24/30/31) variant 통합 설계 (Phase Z catalog 작성 시)
|
||||||
|
- **확정 처리** — 2 차 조사 결과 후 high confidence frame 부터 `user_confirmed` 일괄 처리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 부록 — 제외 / 특수 항목
|
||||||
|
|
||||||
|
`1171281171` 은 `texts.md` 만 존재하고 `index.html` / `analysis.md` 가 없어 Phase Z frame / style inventory 메인 대상에서 제외한다. 정체는 미확인.
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# C.E.L. Slide Pipeline — 개선 설계
|
||||||
|
|
||||||
|
## 목표
|
||||||
|
|
||||||
|
**MDX 콘텐츠를 넣으면, 중목차/소목차 기준으로 BEPs 디자인과 매칭하여 슬라이드를 자동 생성한다.**
|
||||||
|
|
||||||
|
현재는 블록마다 스타일이 제각각이고, 매칭/조립/크기 조정이 불완전하여 수작업 보정이 필요한 상태. 이를 토큰 기반 통일 + 블록 구조 부품화 + 파이프라인 2경로화로 개선한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개선 방향
|
||||||
|
|
||||||
|
| 현재 문제 | 개선 방향 |
|
||||||
|
|-----------|-----------|
|
||||||
|
| 블록마다 폰트/색/여백이 직접 박혀있어 통일 안 됨 | 토큰으로 공통 기준 확정, 블록은 변수 참조 |
|
||||||
|
| slide-base에 구조+스타일이 섞여있음 | 프레임(HTML)과 스타일(CSS) 분리 |
|
||||||
|
| 블록이 완성 HTML이라 재사용/조합이 어려움 | 블록 = 구조 부품, 스타일은 테마로 분리 |
|
||||||
|
| 블록 매칭이 파일명/tag 중심 | 속성 테이블(catalog) 기반 매칭 |
|
||||||
|
| 크기 조정이 1회 렌더로 끝남 | 입력→조정 반복 (빈 공간/overflow 자동 재분배) |
|
||||||
|
| 검증이 overflow만 | 시각 품질까지 검증 (정렬, 위계, 가독성) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 핵심 축 3개
|
||||||
|
|
||||||
|
### 1. 표현 기준 통일
|
||||||
|
|
||||||
|
typography, spacing, colors, layout을 토큰으로 확정하여 모든 블록이 이 공통 기준을 따르게 한다.
|
||||||
|
|
||||||
|
### 2. 블록의 역할 재정의
|
||||||
|
|
||||||
|
블록 = 완성 HTML이 아니라 **구조 부품**. 구조 스타일(flex, grid)은 블록에, 직접값(font-size, color)은 토큰으로 분리한다.
|
||||||
|
|
||||||
|
### 3. 파이프라인 2경로화
|
||||||
|
|
||||||
|
- **direct-fit:** BEPs 매칭 → 블록 삽입 + 크기 조절
|
||||||
|
- **recipe/composition:** 미매칭 → 꼭지 정리 → redesign + 반복 조정
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 실행 원칙
|
||||||
|
|
||||||
|
**direct-fit 기준:**
|
||||||
|
direct-fit은 구조, 슬롯, 시각 위계가 기존 블록과 거의 1:1로 대응될 때만 허용한다.
|
||||||
|
|
||||||
|
**recipe-path 목적:**
|
||||||
|
recipe/composition 경로는 미매칭 콘텐츠를 새로운 구조로 재정리하되, 기존 visual language를 최대한 상속한다.
|
||||||
|
|
||||||
|
**fit 루프 우선순위:**
|
||||||
|
fit 조정은 줄바꿈 → 간격 조정 → preview 축약 → 폰트 1단계 축소 순으로 수행한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 진행 계획
|
||||||
|
|
||||||
|
### 1단계: 토큰 기준 확정
|
||||||
|
|
||||||
|
**목표:** 모든 블록이 따라야 할 공통 기준표 확정
|
||||||
|
|
||||||
|
| 토큰 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| typography | 대목차/중목차/소목차/본문/캡션/footer 크기·굵기·줄간격 |
|
||||||
|
| spacing | padding/gap/indent/margin |
|
||||||
|
| colors 1층 | 공통 테마색 (title, body, border, card-bg, accent) |
|
||||||
|
| colors 2층 | 블록 의미색 (compare-left/right, role-a/b, pill-left/right) |
|
||||||
|
| layout | slide-base 위치값 (body-top, body-height, footer-bottom) |
|
||||||
|
|
||||||
|
### 2단계: slide-base CSS 분리
|
||||||
|
|
||||||
|
**목표:** slide-base를 프레임 전용으로 얇게
|
||||||
|
|
||||||
|
- HTML에서 `<style>` 추출 → 외부 CSS
|
||||||
|
- 직접값 → 토큰 변수로 교체
|
||||||
|
- slide-base = 대목차 + divider + body container + footer만 책임
|
||||||
|
|
||||||
|
### 3단계: 블록 작성 규칙 문서
|
||||||
|
|
||||||
|
**목표:** 블록화 시 참조할 기준 제공 (두 에이전트 간 계약서)
|
||||||
|
|
||||||
|
- 구조 스타일 OK (display:flex, grid, align-items, overflow)
|
||||||
|
- 직접값 금지 (hardcoded font-size/color/padding)
|
||||||
|
- 값은 토큰 참조 (`var(--font-body)`)
|
||||||
|
- 클래스명 규칙 (`.zone-title`, `.bul` 등)
|
||||||
|
- 슬롯 규칙 (zone_title, sub_title, body, bullets, image 등)
|
||||||
|
|
||||||
|
### 4단계: 폴더 구조 정리
|
||||||
|
|
||||||
|
**목표:** 프레임/구조/스타일/메타/자산 분리
|
||||||
|
|
||||||
|
```
|
||||||
|
templates/
|
||||||
|
├── base/ ← 프레임 (slide-base)
|
||||||
|
├── blocks/
|
||||||
|
│ ├── structures/ ← 구조 부품 (토큰 기반)
|
||||||
|
│ ├── recipes/ ← 조합형 구조
|
||||||
|
│ └── legacy/ ← 기존 블록 (점진 전환)
|
||||||
|
├── styles/
|
||||||
|
│ ├── tokens/ ← 공통 기준값
|
||||||
|
│ ├── themes/ ← 톤 변형 (light/dark/formal)
|
||||||
|
│ ├── base/ ← slide-base CSS
|
||||||
|
│ └── blocks/ ← 블록별 CSS
|
||||||
|
├── assets/ ← 이미지/SVG/텍스처
|
||||||
|
└── catalog/ ← 속성 테이블 (blocks/recipes/rules)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5단계: 기존 블록 점진 전환
|
||||||
|
|
||||||
|
**목표:** Figma 1:1 HTML → 토큰 기반 재사용 블록으로 전환
|
||||||
|
|
||||||
|
**5-1. 블록 분류 먼저**
|
||||||
|
|
||||||
|
| 분류 | 의미 | 전환 순서 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| direct-fit candidate | 바로 사용 가능 | 먼저 (토큰 교체만) |
|
||||||
|
| recipe component candidate | 조합용 부품 | 다음 |
|
||||||
|
| rewrite required | 구조부터 다시 | 마지막 |
|
||||||
|
|
||||||
|
**5-2. 분류 결과에 따라 전환**
|
||||||
|
- `figma_to_html_agent/blocks/` 완료물을
|
||||||
|
- 토큰 기준에 맞춰 `templates/blocks/structures/`로
|
||||||
|
- 한 번에 다 뜯지 않고 점진적으로
|
||||||
|
|
||||||
|
### 6단계: 파이프라인 연결
|
||||||
|
|
||||||
|
**목표:** TO-BE 프로세스 완성
|
||||||
|
|
||||||
|
- TF-IDF 기반 **direct-fit block 매칭**
|
||||||
|
- 미매칭 시 **recipe/composition 경로 전환**
|
||||||
|
- 토큰 기반 resize
|
||||||
|
- 입력→조정 반복 (fit 루프 확장)
|
||||||
|
- 시각 품질 검증 강화
|
||||||
|
|
||||||
|
**Source of Truth:**
|
||||||
|
|
||||||
|
| 기준 | 위치 |
|
||||||
|
|------|------|
|
||||||
|
| 구조 판정 | `normalized.sections` |
|
||||||
|
| 블록 매칭 | `catalog/blocks.yaml` |
|
||||||
|
| 스타일 기준 | `styles/tokens/` + `styles/themes/` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 역할 분리 (동시 진행)
|
||||||
|
|
||||||
|
```
|
||||||
|
figma_to_html_agent (원재료) design_agent (조립 시스템)
|
||||||
|
───────────────────── ──────────────────────
|
||||||
|
"이 블록은 무엇인가" "이 블록을 언제 쓸까"
|
||||||
|
구조 유형 발견 토큰 기준 확정
|
||||||
|
슬롯 정의 catalog 필드 설계
|
||||||
|
자산 메타 direct-fit vs recipe 규칙
|
||||||
|
seam/crop/anchor theme/token 적용
|
||||||
|
fit/validation
|
||||||
|
|
||||||
|
↓ 접점: 블록 작성 규칙 문서 (3단계) ↓
|
||||||
|
블록화 시 이 규칙에 맞춰 전환
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,285 @@
|
|||||||
|
# Phase Z — composition planning schema plan (Layer A + Layer B)
|
||||||
|
|
||||||
|
**Status** : v1 plan (2026-04-30 refactor — SPEC v1 의 Layer A + Layer B 구조 반영). schema 작업 *범위 / 순서 / 완료 기준* 정의. 구현 X.
|
||||||
|
**Anchor** : [`PHASE-Z-PIPELINE-STATUS-BOARD.md`](PHASE-Z-PIPELINE-STATUS-BOARD.md) 의 핵심 missing — Step 3 / 4 / 11 + Step 10 (부분).
|
||||||
|
|
||||||
|
> **v0 → v1 변경 요약**
|
||||||
|
> - SPEC v1 의 renumbered section 으로 cross-reference 일관 갱신 (§1 / §2 신규 / §3 / §4 / §5 …)
|
||||||
|
> - Step 4 의 schema 가 SPEC v1 §2 (Internal Region — Layer A) 로 *이미 정의됨* → 본 plan 에서는 *신규 작성* 이 아니라 *완성도 점검* 으로 전환
|
||||||
|
> - Step 11 의 schema 가 SPEC v1 §4 (2-stage placement: Stage A → Stage B) 로 재정의됨에 따라 validation 기준 보강
|
||||||
|
> - mechanical rename : `sub_zone` → `Frame Slot` (PLAN 본문). YAML 필드명 `sub_zones` 는 코드 reality 로 유지
|
||||||
|
> - 4 step 범위 (Step 3 / 4 / 10-partial / 11) 그대로. Step 10 의 density envelope 제외 그대로
|
||||||
|
> - implementation / MDX 실행 검증 / AI 호출 / 파일명 변경 / code marker 이름 결정 — *모두 제외 그대로*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 문서 역할 분리
|
||||||
|
|
||||||
|
| 문서 | 역할 |
|
||||||
|
|---|---|
|
||||||
|
| [`PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md`](PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md) (v1) | schema / contract *명세 자체* (authoritative) |
|
||||||
|
| **본 문서 (`PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md`)** (v1) | 그 schema 작업을 *어떤 범위 / 순서 / 산출물 / validation 기준* 으로 진행할지 *계획* |
|
||||||
|
|
||||||
|
본 plan 은 SPEC 을 *대체하지 않음*. SPEC 의 어느 section 이 어느 step 의 schema 인지 *cross-reference + 완성도 점검* 까지가 본 plan 의 영역.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 목적
|
||||||
|
|
||||||
|
일반 MDX 1 파일이 *render 전 단계* 에서 typed content_object → Internal Region → Frame Slot 으로 결정 가능한 상태에 도달하도록, 다음 4 step 의 schema 정의를 *완결* 시킨다.
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 3. Content Object 추출
|
||||||
|
Step 4. Section Internal Composition Planning (Layer A)
|
||||||
|
Step 10. Frame Contract 의 accepted_content_types + Frame Slot 선언 (부분, Layer B)
|
||||||
|
Step 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping (2-stage)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 범위
|
||||||
|
|
||||||
|
### 2.1 포함
|
||||||
|
|
||||||
|
| Step | schema 항목 | SPEC v1 의 위치 |
|
||||||
|
|---|---|---|
|
||||||
|
| Step 3 | content_object base schema + 6 type 별 schema (text_block / table / transform_table / image / diagram / details) | SPEC v1 §1.1, §1.2 |
|
||||||
|
| Step 3 | role 의미 (summary / detail / decorative / reference) | SPEC v1 §1.3 |
|
||||||
|
| Step 4 | Internal Region entity schema (region_id / role / content_type / ratio_estimate / content_unit_ids / frame_match_strategy) | SPEC v1 §2.1 |
|
||||||
|
| Step 4 | Universal Region Model (every zone = 1+ regions) | SPEC v1 §2.2 |
|
||||||
|
| Step 4 | 3-way decision tree (whole / group / split) | SPEC v1 §2.3 |
|
||||||
|
| Step 4 | region 비율 산정 (content type 별 size proxy) | SPEC v1 §2.4 |
|
||||||
|
| Step 4 | region → frame / display 매칭 interface (frame_match / display_only) | SPEC v1 §2.6 |
|
||||||
|
| Step 10 | frame contract 의 `accepted_content_types` 필드 schema | SPEC v1 §3.1 |
|
||||||
|
| Step 10 | Frame Slot 선언 schema (YAML 필드명 `sub_zones` — 의미 = Frame Slot) | SPEC v1 §3.1 |
|
||||||
|
| Step 10 | F13 / F29 / F16 의 Frame Slots 선언 예시 | SPEC v1 §3.2 |
|
||||||
|
| Step 11 | placement algorithm 2-stage I/O schema | SPEC v1 §4.1 |
|
||||||
|
| Step 11 | Stage A — content → Internal Region (Layer A 매핑) | SPEC v1 §4.2 |
|
||||||
|
| Step 11 | Stage B — Internal Region content → Frame Slot (Layer B 매핑) | SPEC v1 §4.3 |
|
||||||
|
| Step 11 | tie-break (Frame Slot 단위) | SPEC v1 §4.4 |
|
||||||
|
| Step 11 | display_only region 처리 (frame 우회) | SPEC v1 §4.5 |
|
||||||
|
| Step 11 | display_strategy schema (inline_full / inline_preview_with_details / details_only / dropped) | SPEC v1 §5.1, §5.5 |
|
||||||
|
| Step 11 | display_strategy 의 region-level + slot-level 양쪽 적용 | SPEC v1 §5.3 |
|
||||||
|
|
||||||
|
### 2.2 Step 10 의 *부분 범위* 명시 (불변)
|
||||||
|
|
||||||
|
> 본 plan 에서 Step 10 은 **두 항목** 까지만 다룸 :
|
||||||
|
> - `accepted_content_types`
|
||||||
|
> - Frame Slot 선언 (YAML 필드명 `sub_zones`)
|
||||||
|
>
|
||||||
|
> 본 plan 에서 *다루지 않는* Step 10 항목 :
|
||||||
|
> - `density envelope`
|
||||||
|
>
|
||||||
|
> `density envelope` 은 `frame_internal_fit_candidate` (Step 17 / Step 19 Gap) 와 연결된 *frame internal fit policy* 영역. 본 plan 에 넣으면 *공통 padding 축소 antipattern* (`feedback_phase_z_spacing_direction`) 과 섞일 risk 있어 *별 plan* 으로 분리.
|
||||||
|
>
|
||||||
|
> 따라서 본 plan 이 완료되어도 Step 10 전체가 ✅ 완료되는 것은 아니고 — `accepted_content_types` + Frame Slot 부분만 ✅ / density envelope 부분은 ❌ 잔존.
|
||||||
|
|
||||||
|
### 2.3 명시적 제외
|
||||||
|
|
||||||
|
- 구현 (extractor / planner / parser 코드 작성) — 본 plan 은 schema 까지
|
||||||
|
- frame partial template 변경 (Frame Slot / region container marker 추가)
|
||||||
|
- mapper / classifier 의 region / Frame Slot-aware 진화
|
||||||
|
- details / popup runtime
|
||||||
|
- backward flow 자동화 (telemetry → composition 재호출)
|
||||||
|
- MDX 01 / 02 / 03 / 04 적용 검증
|
||||||
|
- AI 호출 (plan 단계 자체 + plan 산출 schema 자체 모두)
|
||||||
|
- code / module / HTML marker / attribute *이름 결정* (SPEC v1 와 동일 — implementation step 에서 결정)
|
||||||
|
- 파일명 변경 (PLAN / SPEC 둘 다 — 향후 별 결정)
|
||||||
|
- 다음 단계 우선순위 / A/B/C 선택지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 산출물
|
||||||
|
|
||||||
|
### 3.1 schema-side 산출물
|
||||||
|
|
||||||
|
본 plan 의 schema 작업이 완료되면, SPEC v1 가 다음 상태에 도달 :
|
||||||
|
|
||||||
|
| schema | 현재 SPEC v1 상태 | plan 종료 후 목표 상태 |
|
||||||
|
|---|---|---|
|
||||||
|
| content_object base + 6 type 별 schema | draft (SPEC v1 §1.1, §1.2) | completeness reviewed + gaps listed |
|
||||||
|
| role 의미 4 종 | draft (SPEC v1 §1.3) | completeness reviewed + gaps listed |
|
||||||
|
| Internal Region entity + Universal Region Model | draft (SPEC v1 §2.1, §2.2) | completeness reviewed + gaps listed |
|
||||||
|
| 3-way decision tree | draft (SPEC v1 §2.3) | completeness reviewed + gaps listed |
|
||||||
|
| region 비율 산정 | draft (SPEC v1 §2.4) | completeness reviewed + gaps listed |
|
||||||
|
| region → frame / display 매칭 interface | draft (SPEC v1 §2.6) | completeness reviewed + gaps listed |
|
||||||
|
| accepted_content_types + Frame Slot 선언 | draft (SPEC v1 §3.1, 3 frame 예시 §3.2) | completeness reviewed + gaps listed |
|
||||||
|
| 2-stage placement (Stage A + Stage B I/O) | draft (SPEC v1 §4.1, §4.2, §4.3) | completeness reviewed + gaps listed |
|
||||||
|
| tie-break + display_only region 처리 | draft (SPEC v1 §4.4, §4.5) | completeness reviewed + gaps listed |
|
||||||
|
| display_strategy 4 종 + region/slot 양쪽 적용 | draft (SPEC v1 §5.1, §5.3, §5.5) | completeness reviewed + gaps listed |
|
||||||
|
|
||||||
|
### 3.2 plan-side 산출물
|
||||||
|
|
||||||
|
- 본 PLAN 문서 (v1) — schema 작업 범위 정의
|
||||||
|
- 작업 결과로 SPEC v1 의 *추가 / 수정 부분* (edge case 보강 등 — substantive 신규 schema 가 아닌 점검 결과)
|
||||||
|
- pipeline data flow 위치 표 (§5 참조)
|
||||||
|
- schema validation 기준 표 (§6 참조)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. SPEC ↔ PLAN cross-reference
|
||||||
|
|
||||||
|
본 plan 의 4 step 각각이 SPEC v1 의 어느 section 으로 매핑되는지 :
|
||||||
|
|
||||||
|
| Step | SPEC v1 section | plan 에서의 작업 |
|
||||||
|
|---|---|---|
|
||||||
|
| Step 3 | §1 (content_object schema) | *완성도 점검*. nested_list / sub-decomposition edge case (SPEC v1 §9.2 미해결 부분 — text_block 의 nested 구조를 sub_text_block 으로 sub-decompose vs Frame Slot cardinality aggregate 해석) |
|
||||||
|
| Step 4 | §2 (Internal Region schema, Layer A) | *완성도 점검*. 3-way decision 의 boundary 케이스 / region 비율의 fallback / frame_match_strategy 가 unknown / ambiguous 일 때 거동 |
|
||||||
|
| Step 10 (partial) | §3.1 (schema) + §3.2 (3 frame 예시) | *완성도 점검*. `density envelope` 미포함 명시 |
|
||||||
|
| Step 11 | §4 (placement algorithm 2-stage) + §5 (display strategy) | *완성도 점검*. Stage A → Stage B *interface* 정합 (Stage A 의 frame_match_strategy 가 Stage B 의 입력으로 자연 호환) / backward flow 자동화 X 등 v1 한계 재확인 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Pipeline data flow 위치
|
||||||
|
|
||||||
|
본 4 step 의 schema 가 *어디서 생성 / 누가 소비* 하는지.
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 1 MDX 업로드
|
||||||
|
↓
|
||||||
|
Step 2 MDX 정규화 (section / heading / raw_content 분리)
|
||||||
|
↓
|
||||||
|
├─→ Step 3. Content Object 추출
|
||||||
|
│ INPUT : section.raw_content (markdown 문자열)
|
||||||
|
│ OUTPUT : section.content_objects = [ContentObject ...]
|
||||||
|
│ SCHEMA : SPEC v1 §1
|
||||||
|
│ 소비자 : Step 4, Step 11
|
||||||
|
│
|
||||||
|
└─→ Step 4. Section Internal Composition Planning (Layer A)
|
||||||
|
INPUT : section.content_objects (from Step 3) + section metadata + V4 evidence
|
||||||
|
OUTPUT : zone.internal_regions = [
|
||||||
|
{region_id, role, content_type, ratio_estimate,
|
||||||
|
content_unit_ids, frame_match_strategy}, ...
|
||||||
|
]
|
||||||
|
SCHEMA : SPEC v1 §2 (entity / Universal Region Model / 3-way decision / 비율 / interface)
|
||||||
|
소비자 : Step 6 (composition planning) / Step 8 (region ratio) /
|
||||||
|
Step 9 (region-level frame match) / Step 11 (Stage A 결과)
|
||||||
|
|
||||||
|
Step 5 Matching Evidence 생성 (V4 top-k)
|
||||||
|
Step 6 Composition Planning (Step 4 region 분할 결과 입력)
|
||||||
|
Step 7 Slide-Level Layout Planning
|
||||||
|
Step 8 Zone + Internal Region Ratio Planning (region 비율 = Step 4 산출)
|
||||||
|
Step 9 Region-Level Frame / Display Selection (region 별 frame_match_strategy = Step 4 산출)
|
||||||
|
↓
|
||||||
|
└─→ Step 10 (partial). Frame Contract 확인
|
||||||
|
INPUT : selected frame_id (region 별)
|
||||||
|
OUTPUT : frame_contract.accepted_content_types
|
||||||
|
+ frame_contract.sub_zones (= Frame Slots, Layer B)
|
||||||
|
SCHEMA : SPEC v1 §3
|
||||||
|
소비자 : Step 11 (Stage B)
|
||||||
|
|
||||||
|
↓
|
||||||
|
└─→ Step 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping (2-stage)
|
||||||
|
INPUT : section.content_objects (from Step 3)
|
||||||
|
+ zone.internal_regions (from Step 4)
|
||||||
|
+ frame_contract.sub_zones (= Frame Slots) (from Step 10, frame_match region 만)
|
||||||
|
Stage A : content → Internal Region (Step 4 의 region 분할 결과 소비)
|
||||||
|
Stage B : Internal Region content → Frame Slot (frame_match region 만 진입)
|
||||||
|
display_only region → display strategy 처리 (Stage B 우회)
|
||||||
|
OUTPUT : placement = {
|
||||||
|
internal_regions: [
|
||||||
|
{..., slot_assignments[], overflow_buffer[], rejection[]}, ...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
SCHEMA : SPEC v1 §4 + §5
|
||||||
|
소비자 : Step 12 (slot payload 생성, region + Frame Slot 단위 grouping)
|
||||||
|
|
||||||
|
Step 12 Slot Payload 생성
|
||||||
|
Step 13 Render
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. schema validation 기준
|
||||||
|
|
||||||
|
> validation = *schema 자체의 구조 검증* 까지만. *실제 MDX 적용 검증 X* (sample budget rule).
|
||||||
|
|
||||||
|
### 6.1 Step 3 — content_object schema
|
||||||
|
|
||||||
|
- [ ] base schema 의 모든 필수 필드 (id, type, role, size_estimate, raw_payload, type_specific) 정의됨
|
||||||
|
- [ ] 6 type 모두 type_specific schema 있음 (text_block / table / transform_table / image / diagram / details)
|
||||||
|
- [ ] role 4 종 (summary / detail / decorative / reference) 정의됨
|
||||||
|
- [ ] role 별 fallback 거동 (decorative drop / detail → details escalate / summary·reference rejection) 명시됨
|
||||||
|
- [ ] *원문 raw_payload 보존* 룰이 schema 에 포함됨 (자름 / 변형 X)
|
||||||
|
|
||||||
|
### 6.2 Step 4 — Internal Region schema (Layer A)
|
||||||
|
|
||||||
|
- [ ] Internal Region entity 의 모든 필수 필드 (region_id, role, content_type, ratio_estimate, content_unit_ids, frame_match_strategy) 정의됨
|
||||||
|
- [ ] Universal Region Model 명시 (every zone has 1+ regions / single-region for text-only / multi-region for mixed-content)
|
||||||
|
- [ ] 3-way decision tree 의 3 분기 (whole-section frame match / child-section grouping / content-type split) 가 *결정론적 함수* 로 표현됨 (AI 판단 X)
|
||||||
|
- [ ] 각 분기의 판단 기준 (cardinality / accepted_content_types / heading depth + content 구조) 명시됨
|
||||||
|
- [ ] region 비율 산정의 size proxy (text_block: line_count / table: rows × line_height / transform_table: pair_count × pair_height / image: aspect_ratio / details: summary line_count) 정의됨
|
||||||
|
- [ ] zone 내 region ratio 합 = 1.0 normalize 룰 명시됨
|
||||||
|
- [ ] frame_match_strategy 의 두 kind (frame_match / display_only) 정의됨
|
||||||
|
- [ ] display_only path 가 frame contract 없이 동작 가능함이 명시됨
|
||||||
|
- [ ] decision 이 unknown / ambiguous 일 때의 fallback 명시됨
|
||||||
|
|
||||||
|
### 6.3 Step 10 (partial) — frame contract schema (Layer B 선언)
|
||||||
|
|
||||||
|
- [ ] `accepted_content_types` 필드 schema 정의됨 (list of type 이름)
|
||||||
|
- [ ] `not_accepted` 필드 schema 정의됨 (디버그용)
|
||||||
|
- [ ] Frame Slot 선언 schema 정의됨 (id / role / accepts / cardinality / partial_target_path) — YAML 필드명 `sub_zones`, 의미 = Frame Slot
|
||||||
|
- [ ] cardinality 표현 방식 (`strict` 또는 `min`/`max`) 정의됨
|
||||||
|
- [ ] F13 / F29 / F16 3 frame 의 Frame Slot declaration 예시 *완비*
|
||||||
|
- [ ] *density envelope 미포함* 이 schema 위에 명시 주석으로 박혀 있음
|
||||||
|
|
||||||
|
### 6.4 Step 11 — placement (2-stage) + display_strategy schema
|
||||||
|
|
||||||
|
- [ ] placement algorithm 2-stage I/O schema 정의됨 (input + Stage A 출력 + Stage B 출력)
|
||||||
|
- [ ] Stage A schema 정의됨 (content_object → Internal Region 매핑 결과 + frame_match_strategy 결정)
|
||||||
|
- [ ] Stage B schema 정의됨 (Internal Region content → Frame Slot 매핑 결과 — frame_match region 만)
|
||||||
|
- [ ] sorting / type 매칭 / cardinality 적용 / role 우선순위 / tie-break 의 결정론적 룰 (Stage B) 명시됨
|
||||||
|
- [ ] display_only region path 의 display_strategy 매핑 정의됨 (image area / table preview / details button / diagram inline)
|
||||||
|
- [ ] 6 type × 3 escalation (inline / preview+details / popup-only) 매트릭스 정의됨
|
||||||
|
- [ ] display_strategy 4 종 (inline_full / inline_preview_with_details / details_only / dropped) 정의됨
|
||||||
|
- [ ] display_strategy 가 *region-level + slot-level 둘 다* 적용됨이 명시됨
|
||||||
|
- [ ] *AI 호출 X* + *원문 손실 금지* 룰이 schema 위에 명시됨
|
||||||
|
- [ ] backward flow 자동화 X (v1 한계) 가 schema 위에 명시됨
|
||||||
|
|
||||||
|
### 6.5 통합 validation
|
||||||
|
|
||||||
|
- [ ] Step 3 → Step 4 의 type 호환 — content_object.type 이 Internal Region.content_type 으로 *결정론적* 매핑
|
||||||
|
- [ ] Step 3 → Step 11 (Stage B) 의 type 호환 — Step 3 의 모든 type 이 어딘가 frame.accepted_content_types 에 등장 가능 (또는 명시적 reject) 또는 display_only path 로 처리 가능
|
||||||
|
- [ ] Step 4 → Step 8 의 ratio 호환 — Internal Region.ratio_estimate 가 Step 8 의 region-level ratio 입력으로 호환
|
||||||
|
- [ ] Step 4 → Step 9 의 frame_match_strategy 호환 — Step 9 의 region-level frame 매칭 입력으로 호환
|
||||||
|
- [ ] Step 4 → Step 11 (Stage A) 의 region 분할 호환 — Stage A 가 Step 4 의 internal_regions 를 *재계산하지 않고 그대로 소비*
|
||||||
|
- [ ] Step 10 의 cardinality 가 Step 11 Stage B 의 placement algorithm 이 소비 가능한 형태
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. AI 원칙
|
||||||
|
|
||||||
|
- 본 plan 작성 / schema 정의 단계 — **AI 호출 없음**
|
||||||
|
- runtime AI = Step 12 의 *light_edit / restructure 의 content_object → Internal Region / Frame Slot proposal* 1 곳만
|
||||||
|
- Step 0 (Figma → HTML 변환 등 사전 준비) 의 AI 사용은 *precondition phase* 로, runtime AI 가 아님
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 금지
|
||||||
|
|
||||||
|
- 구현 금지 (extractor / planner / parser 코드 작성)
|
||||||
|
- render 변경 금지
|
||||||
|
- frame partial 변경 금지 (Frame Slot / region container marker 추가 미포함)
|
||||||
|
- mapper / classifier 의 region / Frame Slot-aware 진화 금지
|
||||||
|
- details / popup runtime 작성 금지
|
||||||
|
- MDX 01 / 02 / 03 / 04 실행 금지
|
||||||
|
- AI 호출 금지
|
||||||
|
- code / module / HTML marker / attribute *이름 결정* 금지 (SPEC v1 와 동일 — implementation step 에서 결정)
|
||||||
|
- 파일명 변경 금지
|
||||||
|
- next step 추천 금지
|
||||||
|
- 우선순위 결정 금지
|
||||||
|
- A / B / C 선택지 제시 금지
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 본 plan 의 보존 / 변경 정책
|
||||||
|
|
||||||
|
- 본 plan 은 *schema 작업 범위 + 완료 기준* 의 기준점. schema 작업 진행 중 새로운 edge case 발견 시 본 plan 의 6 절 (validation 기준) 에 *항목 추가* 형태로 갱신
|
||||||
|
- *범위 확장* (예: Step 7 추가, density envelope 포함, code marker 이름 결정 포함) 은 사용자 명시 잠금 후에만
|
||||||
|
- 본 plan 이 완료되면 STATUS-BOARD 의 :
|
||||||
|
- Step 3 → ⚠ partial (schema 정의 완료, 구현 미완)
|
||||||
|
- Step 4 → ⚠ partial (Layer A schema 정의 완료, 구현 미완)
|
||||||
|
- Step 11 → ⚠ partial (2-stage schema 정의 완료, 구현 미완)
|
||||||
|
- Step 10 → ⚠ partial (accepted_content_types + Frame Slot 부분 완료, density envelope 미포함 잔존)
|
||||||
|
- SPEC 의 *추가 갱신* (edge case 보강 등) 은 본 plan 의 §3.1 목표 상태 (completeness reviewed + gaps listed) 의 자연 산출물
|
||||||
@@ -0,0 +1,849 @@
|
|||||||
|
# Phase Z-2 — content composition planning spec (Layer A: Internal Region + Layer B: Frame Slot)
|
||||||
|
|
||||||
|
**Status** : v1 spec (2026-04-30 refactor — Layer A / Internal Region 추가 + Layer B / Frame Slot 명확화). 정의만. 구현은 별도 step (사용자 승인 후).
|
||||||
|
|
||||||
|
> **v0 → v1 변경 요약**
|
||||||
|
> - Zone Internal Region (Layer A) 를 first-class entity 로 추가 (§2 신규)
|
||||||
|
> - 기존 §2 ~ §8 → §3 ~ §9 로 renumber (v0 의 ## 9 다음 step → v1 의 ## 10)
|
||||||
|
> - 기존 `sub_zone` 단어 = *Frame Slot (Layer B)* 의미로 일관 정리. *YAML 필드명 `sub_zones` 는 코드 reality 로 유지 — 의미만 명시*
|
||||||
|
> - placement algorithm (§4) 을 *2-stage* (Stage A: content → Internal Region / Stage B: Internal Region content → Frame Slot) 로 재작성
|
||||||
|
> - content_object schema (§1) / display strategy 어휘 (§5) / telemetry 구조 (§6) 는 *layer-agnostic 공유 개념* 으로 보호 — substantive 미변경
|
||||||
|
> - code / module / HTML marker 이름은 *implementation step* 으로 defer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §0. 목적 / 위치
|
||||||
|
|
||||||
|
본 spec 은 **render *전* composition planning layer** 의 정의. fit_classifier / overflow_router / zone_ratio_retry 같은 *post-render telemetry* 가 아니라, *애초에 content 를 어디에 어떻게 배치할지* 결정하는 *진짜 fit policy 의 중심*.
|
||||||
|
|
||||||
|
```
|
||||||
|
1. PLANNING (composition) ← 본 spec 의 영역
|
||||||
|
- section raw_content → content_object 정규화
|
||||||
|
- Zone Internal Region (Layer A) 분할 — text/table/image/details 에 따라
|
||||||
|
- frame contract → accepted_content_types + Frame Slot (Layer B) 선언
|
||||||
|
- content_object → Internal Region → Frame Slot 배치 (compatibility 기반)
|
||||||
|
- inline preview vs details/popup 표시 전략
|
||||||
|
↓
|
||||||
|
2. RENDER (Jinja2 + frame partial — Frame Slot aware)
|
||||||
|
↓
|
||||||
|
3. POST-RENDER TELEMETRY (A1~A4) ← 별 spec, 이미 구축됨
|
||||||
|
- Selenium → fit_classifier → router → retry → failure_classifier → next_action
|
||||||
|
- 1 단계 (본 spec 영역) 가 정밀하면 거의 trigger 안 됨
|
||||||
|
- exception 케이스의 *진단 + 다음 capability 안내*
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer 구분
|
||||||
|
|
||||||
|
```
|
||||||
|
Slide → Zone → Internal Region → Frame → Frame Slot → Content
|
||||||
|
──────────── ──────────
|
||||||
|
Layer A Layer B
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Layer A — Zone Internal Region** : Zone *내부* 영역, frame *밖*. content type 기반 분할 (text region / table region / image region / details region). region 별 frame 또는 display strategy 선택.
|
||||||
|
- **Layer B — Frame Slot** : frame *내부* 자리 (= F13 의 pillar_1, F29 의 process_column 등). frame 안에서 content unit 이 들어갈 곳.
|
||||||
|
|
||||||
|
Layer A 와 Layer B 는 *별개 entity* 가 아니라 *한 composition pipeline 의 두 sub-phase*. content_object schema / display strategy 어휘 / telemetry interface 는 *공유*.
|
||||||
|
|
||||||
|
### 본 spec 의 핵심 원칙
|
||||||
|
|
||||||
|
- *render 전* 결정. *render 후 retry* 가 아님
|
||||||
|
- content_object 의 *type* 이 핵심 — text / table / image / diagram / details
|
||||||
|
- Layer A 가 *region 분할* 결정 / Layer B 가 *region 안 Frame Slot 매핑* 결정
|
||||||
|
- 매칭 안 되는 content 는 *details/popup 으로 escalate*. *원문 삭제 / 압축 X*
|
||||||
|
- frame 의 *Frame Slot* 이 어떤 type 을 받을 수 있는지 *명시적으로 선언*
|
||||||
|
|
||||||
|
본 spec 은 *정의만*. 구현 우선순위는 별도 step (사용자 승인 후).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1. content_object 정규화 schema
|
||||||
|
|
||||||
|
> **layer-agnostic** — Layer A / Layer B 둘 다 사용. v0 → v1 refactor 시 *substantive 미변경*.
|
||||||
|
|
||||||
|
MDX section 의 `raw_content` (markdown 문자열) 를 *typed content_object list* 로 정규화.
|
||||||
|
|
||||||
|
### 1.1 base schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
section:
|
||||||
|
section_id: str
|
||||||
|
title: str
|
||||||
|
content_objects:
|
||||||
|
- id: str # section 내 unique
|
||||||
|
type: str # text_block / table / image / diagram / details / transform_table
|
||||||
|
role: str # summary / detail / decorative / reference
|
||||||
|
size_estimate:
|
||||||
|
line_count: int # text/details 의 경우
|
||||||
|
rows: int # table 의 경우
|
||||||
|
aspect_ratio: float # image/diagram 의 경우
|
||||||
|
bytes: int # raw payload 크기 (heuristic용)
|
||||||
|
raw_payload: str # 원본 (자름 / 변형 X)
|
||||||
|
type_specific: {...} # 아래 type 별 schema
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 type 별 schema
|
||||||
|
|
||||||
|
#### `text_block` — 자유 텍스트 / 불릿
|
||||||
|
```yaml
|
||||||
|
type: text_block
|
||||||
|
type_specific:
|
||||||
|
format: paragraph | bullet_list | nested_list
|
||||||
|
bullet_count: int # 불릿이면
|
||||||
|
max_indent_level: int
|
||||||
|
has_emphasis: bool # **bold** 등 inline emphasis
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `table` — markdown 표
|
||||||
|
```yaml
|
||||||
|
type: table
|
||||||
|
type_specific:
|
||||||
|
rows: int # header 제외 데이터 row
|
||||||
|
cols: int
|
||||||
|
header_present: bool
|
||||||
|
is_transform: bool # AS-IS / arrow / TO-BE 구조면 true → transform_table 으로 분류
|
||||||
|
raw_md: str # 원본 markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `transform_table` — AS-IS / TO-BE pair (`table` 의 specialization)
|
||||||
|
```yaml
|
||||||
|
type: transform_table
|
||||||
|
type_specific:
|
||||||
|
pair_count: int # 행 수 (각 행 = 1 transform pair)
|
||||||
|
arrow_glyph: str # ➠ 등
|
||||||
|
rows: [{from: str, arrow: str, to: str}]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `image` — markdown / HTML 이미지
|
||||||
|
```yaml
|
||||||
|
type: image
|
||||||
|
type_specific:
|
||||||
|
src: str
|
||||||
|
alt: str
|
||||||
|
aspect_ratio: float | null # 알면 (asset metadata 에서)
|
||||||
|
intrinsic_width_px: int | null
|
||||||
|
intrinsic_height_px: int | null
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `diagram` — SVG / 도식
|
||||||
|
```yaml
|
||||||
|
type: diagram
|
||||||
|
type_specific:
|
||||||
|
source_type: svg_inline | svg_file | mermaid | other
|
||||||
|
src: str | null
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `details` — `<details>/<summary>` 또는 ":::note[...]" 같은 명시 marker
|
||||||
|
```yaml
|
||||||
|
type: details
|
||||||
|
type_specific:
|
||||||
|
summary: str # 펼치기 전 보일 헤더
|
||||||
|
body_raw: str # 펼친 후 content (자름 X)
|
||||||
|
display_hint: button | inline_collapse | popup # MDX 가 hint 줄 수 있음
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 role 의미
|
||||||
|
|
||||||
|
- `summary` — section 의 *핵심 메시지*. inline 으로 반드시 표시
|
||||||
|
- `detail` — 보조 / 부연. 공간 부족 시 details 로 escalate 가능
|
||||||
|
- `decorative` — 시각 보조 (배경 이미지 등). 공간 부족 시 *생략 가능*
|
||||||
|
- `reference` — 출처 / footnote / 보충 자료
|
||||||
|
|
||||||
|
### 1.4 정규화 parser 위치
|
||||||
|
|
||||||
|
신규 module (이름 *implementation step 에서 결정* — defer) :
|
||||||
|
- `extract_content_objects(section: MdxSection) -> list[ContentObject]`
|
||||||
|
- markdown AST parser (예: mistune) 활용 또는 regex 기반 v0
|
||||||
|
- 현재 `align_sections_to_v4_granularity` 다음 단계에 삽입
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2. Zone Internal Region schema (Layer A — 신규)
|
||||||
|
|
||||||
|
본 section 은 **v1 신규**. Zone *내부* 영역 (frame *밖*) 의 entity 정의 + 3-way decision tree + region 비율 + region → frame/display interface.
|
||||||
|
|
||||||
|
### 2.1 Internal Region entity schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
zone:
|
||||||
|
zone_id: str
|
||||||
|
layout_position: str # top / bottom / left / right / ...
|
||||||
|
internal_regions:
|
||||||
|
- region_id: str # zone 내 unique
|
||||||
|
role: str # primary / secondary / supporting / reference
|
||||||
|
content_type: str # text / table / image / diagram / details / mixed
|
||||||
|
ratio_estimate: float # 0.0 ~ 1.0 (zone 내 비율, 합 = 1.0)
|
||||||
|
content_unit_ids: [str] # 이 region 에 배치된 content_object id 들 (Layer A → B 의 입력)
|
||||||
|
frame_match_strategy: # region → frame/display 매칭 결과
|
||||||
|
kind: str # frame_match | display_only
|
||||||
|
frame_id: str | null # frame_match 이면 실제 frame
|
||||||
|
display_strategy: str # inline_full | inline_preview_with_details | details_only | dropped
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Universal Region Model
|
||||||
|
|
||||||
|
```
|
||||||
|
모든 Zone 은 1 개 이상의 Internal Region 을 가짐.
|
||||||
|
|
||||||
|
text-only zone = single-region zone (현 거동의 자연 표현)
|
||||||
|
mixed-content zone = multi-region zone
|
||||||
|
|
||||||
|
각 Internal Region 은 *자기만의* frame match + display strategy 를 가질 수 있음.
|
||||||
|
```
|
||||||
|
|
||||||
|
text-only section 도 *single-region zone* 으로 표현 (= 현 거동 보존). mixed-content (text + table / text + image / 등) 은 *multi-region zone* 으로 확장. region 이 *first-class entity* — special case 가 아님.
|
||||||
|
|
||||||
|
### 2.3 3-way decision tree
|
||||||
|
|
||||||
|
각 section 에 대해 *Internal Region 분할* 여부 결정 :
|
||||||
|
|
||||||
|
```
|
||||||
|
section 전체 → 1 frame 매칭 가능?
|
||||||
|
├ YES → whole-section frame match
|
||||||
|
│ → single-region zone (region 1개, content_type=primary)
|
||||||
|
│
|
||||||
|
└ NO → child-section grouping 가능?
|
||||||
|
├ YES → group merge → 1 frame 매칭
|
||||||
|
│ → single-region zone (region 1개, content_type=primary)
|
||||||
|
│
|
||||||
|
└ NO → content-type split
|
||||||
|
→ text region / table region / image region / details region
|
||||||
|
→ region 비율 산정 (예: text 80% / table 20%)
|
||||||
|
→ multi-region zone (region 2~N개)
|
||||||
|
```
|
||||||
|
|
||||||
|
**판단 기준** :
|
||||||
|
- *whole-section frame match* — section 전체와 frame contract 의 accepted_content_types 가 호환 + cardinality 가 맞는 경우
|
||||||
|
- *child-section grouping* — sibling section 들이 같은 frame contract (예: F16 의 4-quadrant) 와 묶이는 경우. heading depth + content 구조 + frame cardinality 로 판단
|
||||||
|
- *content-type split* — section 안에 *호환 안 되는 content type 조합* 이 있을 때 (text + table 처럼)
|
||||||
|
|
||||||
|
### 2.4 region 비율 산정
|
||||||
|
|
||||||
|
content type 별 *expected size* 기반 :
|
||||||
|
|
||||||
|
| content type | size proxy |
|
||||||
|
|---|---|
|
||||||
|
| `text_block` | line_count (text_block.size_estimate.line_count) |
|
||||||
|
| `table` | rows × line_height_factor (rows × 1.2 ~ 1.5) |
|
||||||
|
| `transform_table` | pair_count × pair_height |
|
||||||
|
| `image` | aspect_ratio 기반 height (width 고정 시) |
|
||||||
|
| `diagram` | aspect_ratio 기반 height |
|
||||||
|
| `details` | summary line_count (펼치기 전) |
|
||||||
|
|
||||||
|
*zone 내 합 = 1.0* 으로 normalize. role 가중치 (primary > supporting) 는 v1 에서 균등 — *별 step refinement*.
|
||||||
|
|
||||||
|
### 2.5 Internal Region Layout / Topology Vocabulary
|
||||||
|
|
||||||
|
region 들의 *공간 배치 패턴* 어휘. multi-region zone 의 *방향 / 배치* 결정. ratio 와 content_type 만으로는 *어떻게 배치되는지* 가 결정 안 되므로 *vocabulary 단계* 가 명시적으로 필요.
|
||||||
|
|
||||||
|
#### 명명 style — slide-level vs region-level 의 *의도된 비대칭*
|
||||||
|
|
||||||
|
- **Slide-level 8 vocabulary** (Step 7) = *count-based* 명명 (`horizontal-2`, `top-1-bottom-2`). zone 의 *layout-driven* 성격 반영
|
||||||
|
- **Region-level vocabulary** (본 §) = *descriptor-based* 명명 (`vertical-stack`, `main-support`). region 의 *content-type / role-driven* 성격 반영
|
||||||
|
- 이 비대칭은 *의도된 것*. region count 가 작고 (1~4) content type / role 이 핵심 결정 기준이라 descriptor 가 더 의미 전달
|
||||||
|
|
||||||
|
#### v1 vocabulary (6 entry)
|
||||||
|
|
||||||
|
| region_layout_type | 의미 | 사용 조건 |
|
||||||
|
|---|---|---|
|
||||||
|
| `region-single` | 1 region 만 (zone 전체 = region 1개) | region count = 1 (single-region zone) |
|
||||||
|
| `region-vertical-stack` | 위·아래 수직 stack | region count ≥ 2, content type 이 *순차적 흐름* (예: 본문 + supporting). default fallback |
|
||||||
|
| `region-horizontal-split` | 좌·우 수평 분할 | region count = 2, content type 이 *대등 비교* 또는 *side-by-side 시각* (text + image, text + diagram 등) |
|
||||||
|
| `region-main-support` | main region + supporting region (asymmetric ratio) | region count = 2, role = [primary, supporting], ratio asymmetric (예: 0.7 / 0.3) |
|
||||||
|
| `region-preview-details` | inline preview region + details/popup region | details_presence = true, 또는 큰 content (table N ≥ 5, long text 등) |
|
||||||
|
| `region-grid-2x2` | 2×2 grid (4 region) | region count = 4, content type 이 *대등 4 항목* |
|
||||||
|
|
||||||
|
#### deterministic decision rule
|
||||||
|
|
||||||
|
`region_layout_type` 은 AI 호출 X. 다음 *결정론적 함수* 로 도출 :
|
||||||
|
|
||||||
|
```
|
||||||
|
입력 :
|
||||||
|
- region_count : int
|
||||||
|
- content_type_mix : list[str]
|
||||||
|
- ratio_estimate : list[float]
|
||||||
|
- role 분포 : list[str] (primary / supporting / ...)
|
||||||
|
- details_presence : bool
|
||||||
|
|
||||||
|
결정 분기 (순차 적용, 첫 매칭 채택) :
|
||||||
|
1. region_count == 1
|
||||||
|
→ region-single
|
||||||
|
|
||||||
|
2. details_presence == true 또는 큰 content (table N ≥ 5 / long text 등)
|
||||||
|
→ region-preview-details
|
||||||
|
|
||||||
|
3. region_count == 4 AND content_type_mix 가 *4 종 대등*
|
||||||
|
→ region-grid-2x2
|
||||||
|
|
||||||
|
4. region_count == 2 AND role == [primary, supporting] AND ratio asymmetric (max / min ≥ 2)
|
||||||
|
→ region-main-support
|
||||||
|
|
||||||
|
5. region_count == 2 AND content_type_mix 내 visual element (image / diagram) 포함
|
||||||
|
→ region-horizontal-split
|
||||||
|
|
||||||
|
6. fallback (위 모든 분기 미매칭)
|
||||||
|
→ region-vertical-stack
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 출력 schema
|
||||||
|
|
||||||
|
각 zone 의 `internal_regions` 컨테이너에 region_layout 필드 추가 :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
zone:
|
||||||
|
zone_id: str
|
||||||
|
internal_regions: [...]
|
||||||
|
region_layout:
|
||||||
|
region_layout_type: str # 위 6 entry 중 하나
|
||||||
|
region_order: [str] # region_id 의 배치 순서 (위→아래 / 좌→우 등)
|
||||||
|
region_placement: str # vertical | horizontal | grid | main-side | stack
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 구현 위치 (예정)
|
||||||
|
|
||||||
|
신규 module (이름 *implementation step 에서 결정* — defer) :
|
||||||
|
- input : `zone.internal_regions` (from §2.1, ratio + content_type 산정 후)
|
||||||
|
- output : `zone.region_layout` (region_layout_type + order + placement)
|
||||||
|
- 위치 : §2.4 region 비율 산정 *직후*, §2.6 region → frame / display interface *직전*
|
||||||
|
- 결정 함수 deterministic — AI 호출 X
|
||||||
|
|
||||||
|
#### v1 vocabulary 의 한계 / 향후 확장 (참고)
|
||||||
|
|
||||||
|
- 현재 6 entry = v1 starting set. 추후 sample / frame DB 확장 시 vocabulary 추가 가능 (예: `region-vertical-3` / `region-horizontal-3` / `region-main-side-bottom` 등)
|
||||||
|
- v1 fallback (`region-vertical-stack`) 이 매칭 안 되는 패턴 발견 시 별 step 으로 entry 추가
|
||||||
|
|
||||||
|
### 2.6 region → frame / display 매칭 interface
|
||||||
|
|
||||||
|
각 region 은 다음 중 하나 :
|
||||||
|
- **frame_match** — region 의 content_type 에 호환되는 frame 선택. 매칭된 frame 의 contract 가 §3 의 입력. Stage B 진입
|
||||||
|
- **display_only** — frame 없이 display strategy 로 처리 (image area 직접 / table preview / details button). frame contract 미사용. Stage B 우회
|
||||||
|
|
||||||
|
현재 *runtime contract-registered / verified* frame set (F13 / F29 / F16) 은 모두 text region 만 수용. image / table / details region 은 현재 *display_only* path.
|
||||||
|
|
||||||
|
### 2.7 구현 위치 (예정)
|
||||||
|
|
||||||
|
신규 module (이름 *implementation step 에서 결정* — defer) :
|
||||||
|
- input : `section.content_objects` (from §1)
|
||||||
|
- output : `zone.internal_regions` (with ratio + frame_match_strategy) + `zone.region_layout` (from §2.5)
|
||||||
|
- 현재 composition planner 의 frame 매칭 *직전* 에 삽입
|
||||||
|
- region 분할 / 비율 산정 / topology vocabulary 선택 / 매칭 기준 — 모두 deterministic rule 기반 (AI 호출 X)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3. frame contract 확장 — accepted_content_types + Frame Slot (Layer B)
|
||||||
|
|
||||||
|
> **v0 의 §2 → v1 의 §3 (renumber)**. *Layer B / Frame Slot* spec.
|
||||||
|
|
||||||
|
`templates/phase_z2/catalog/frame_contracts.yaml` 에 *2 개 신규 필드* 추가.
|
||||||
|
|
||||||
|
### 3.1 schema
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
<template_id>:
|
||||||
|
... (기존 필드 그대로 — source_shape / cardinality / payload / visual_hints / ...)
|
||||||
|
|
||||||
|
# NEW : 이 frame 이 받을 수 있는 content type 들
|
||||||
|
accepted_content_types:
|
||||||
|
- text_block
|
||||||
|
- transform_table
|
||||||
|
- ...
|
||||||
|
not_accepted: # 명시적 비호환 (디버그용)
|
||||||
|
- image
|
||||||
|
- diagram
|
||||||
|
|
||||||
|
# NEW : frame 내부 Frame Slot 선언
|
||||||
|
# YAML field name = 'sub_zones' — 코드 / catalog reality 로 유지. 의미 = Frame Slot (Layer B).
|
||||||
|
sub_zones:
|
||||||
|
- id: str # Frame Slot 식별자
|
||||||
|
role: main_text | supporting_visual | label | details_button | ...
|
||||||
|
accepts: # 이 Frame Slot 이 받는 content_object type
|
||||||
|
- text_block
|
||||||
|
- transform_table
|
||||||
|
cardinality: # Frame Slot 내 capacity
|
||||||
|
strict: int # 정확히 N개
|
||||||
|
# or
|
||||||
|
min: int
|
||||||
|
max: int
|
||||||
|
partial_target_path: # frame partial template 에서 이 Frame Slot 의 위치
|
||||||
|
# 예 : "f29b__cell--left.row-1" — partial 안 marker (attribute name *implementation step 에서 결정*)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **YAML field 이름** : 코드 / catalog reality 로 `sub_zones` 유지. 의미는 *Frame Slot* (= Layer B).
|
||||||
|
|
||||||
|
### 3.2 구체 예시 (현재 3 frame)
|
||||||
|
|
||||||
|
#### F13 — three_parallel_requirements
|
||||||
|
```yaml
|
||||||
|
three_parallel_requirements:
|
||||||
|
...
|
||||||
|
accepted_content_types: [text_block]
|
||||||
|
sub_zones: # = Frame Slots (Layer B)
|
||||||
|
- id: pillar_1
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
- id: pillar_2
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
- id: pillar_3
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### F29 — process_product_two_way
|
||||||
|
```yaml
|
||||||
|
process_product_two_way:
|
||||||
|
...
|
||||||
|
accepted_content_types: [text_block, transform_table]
|
||||||
|
sub_zones: # = Frame Slots (Layer B)
|
||||||
|
- id: process_column
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block, transform_table]
|
||||||
|
cardinality: { strict: 3 } # 3 sections per column
|
||||||
|
- id: product_column
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block] # product 쪽은 transform 안 받음 (현재 frame 의 시각적 구분)
|
||||||
|
cardinality: { strict: 3 }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### F16 — bim_issues_quadrant_four
|
||||||
|
```yaml
|
||||||
|
bim_issues_quadrant_four:
|
||||||
|
...
|
||||||
|
accepted_content_types: [text_block]
|
||||||
|
sub_zones: # = Frame Slots (Layer B)
|
||||||
|
- id: quadrant_1
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
- id: quadrant_2
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
- id: quadrant_3
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
- id: quadrant_4
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 partial template 의 Frame Slot 마커
|
||||||
|
|
||||||
|
frame partial 의 HTML 에 Frame Slot 식별 marker 추가 — render 후 Selenium 이 Frame Slot 단위 측정 가능, A1~A4 의 정밀도 향상. *marker attribute name (예: `data-subzone` / `data-frame-slot` / 기타) 은 implementation step 에서 결정 — defer*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4. placement algorithm — 2-stage (Layer A → Layer B)
|
||||||
|
|
||||||
|
> **v0 의 §3 → v1 의 §4 (renumber + 2-stage 재작성)**. *layer 순차 dependency*.
|
||||||
|
|
||||||
|
### 4.1 input / output
|
||||||
|
|
||||||
|
```
|
||||||
|
input :
|
||||||
|
section: { section_id, content_objects: [...] }
|
||||||
|
zone: { zone_id, layout_position }
|
||||||
|
available_frames: [...] # V4 top-k from Step 5
|
||||||
|
|
||||||
|
output :
|
||||||
|
internal_regions: [
|
||||||
|
{
|
||||||
|
region_id, role, content_type, ratio_estimate,
|
||||||
|
content_unit_ids: [...],
|
||||||
|
frame_match_strategy: { kind, frame_id, display_strategy },
|
||||||
|
# Stage B 결과 (frame_match region 만)
|
||||||
|
slot_assignments: [
|
||||||
|
{ content_object_id, frame_slot_id, display_strategy }
|
||||||
|
],
|
||||||
|
overflow_buffer: [...],
|
||||||
|
rejection: [...]
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 Stage A — content → Internal Region (Layer A)
|
||||||
|
|
||||||
|
> region 분할 결정 + content_object → region 배치.
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 3-way decision (§2.3) 적용
|
||||||
|
- whole-section frame fit 가능 → single-region (Stage B 로 1 region)
|
||||||
|
- child-grouping 가능 → group merge → single-region
|
||||||
|
- content-type split 필요 → multi-region
|
||||||
|
|
||||||
|
2. multi-region 인 경우 :
|
||||||
|
- content_object.type 별로 region 분류 (text region / table region / image region / ...)
|
||||||
|
- 같은 region 안의 content_object 끼리 묶음
|
||||||
|
- region 별 ratio 산정 (§2.4)
|
||||||
|
|
||||||
|
3. region 별 frame_match_strategy 결정 :
|
||||||
|
- region.content_type 이 frame.accepted_content_types 에 매칭 가능 → frame_match
|
||||||
|
- 매칭 frame 없음 → display_only (image / table / details path)
|
||||||
|
|
||||||
|
4. 결과 : zone.internal_regions = [region_1, region_2, ...]
|
||||||
|
각 region 은 content_unit_ids + frame_match_strategy 를 가짐
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Stage B — Internal Region content → Frame Slot (Layer B)
|
||||||
|
|
||||||
|
> region 의 content 를 frame 의 Frame Slot 에 배치. *frame_match_strategy.kind == "frame_match"* 인 region 에만 적용.
|
||||||
|
|
||||||
|
각 frame_match region 에 대해 :
|
||||||
|
|
||||||
|
```
|
||||||
|
1. content_object 정렬
|
||||||
|
- role 기준 우선순위 : summary > reference > detail > decorative
|
||||||
|
- 같은 role 내 raw_payload 등장 순서
|
||||||
|
|
||||||
|
2. content_object.type 이 frame.accepted_content_types 에 없는 것
|
||||||
|
→ rejection 으로 분리. 본 frame 부적합 신호 (Stage A 의 frame_match_strategy 재검토 신호)
|
||||||
|
|
||||||
|
3. 남은 content_object 를 Frame Slot 들에 배치
|
||||||
|
- 각 Frame Slot 을 순회 (frame contract 의 declaration 순서)
|
||||||
|
- Frame Slot.accepts 에 매칭되는 content_object 들에서
|
||||||
|
cardinality.strict 또는 max 수만큼 할당
|
||||||
|
- role 우선순위 높은 것부터
|
||||||
|
|
||||||
|
4. 배치 안 된 content_object
|
||||||
|
- role = decorative → 무조건 drop (생략)
|
||||||
|
- role = detail → overflow_buffer 로 (details/popup 후보)
|
||||||
|
- role = summary / reference → rejection (frame 부적합)
|
||||||
|
|
||||||
|
5. 결과 :
|
||||||
|
- slot_assignments : 정확히 무엇이 어디로
|
||||||
|
- overflow_buffer : details/popup 으로 escalate 할 candidate
|
||||||
|
- rejection : 본 frame 으로는 표현 불가 — frame_reselect 신호
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 매칭 충돌 / tie-break
|
||||||
|
|
||||||
|
Frame Slot 단위로 :
|
||||||
|
|
||||||
|
- 동일 Frame Slot 에 다수 content_object 후보 시 :
|
||||||
|
- role 우선순위 (summary > reference > detail)
|
||||||
|
- 동률 시 size_estimate 작은 것 우선 (fit 가능성 ↑)
|
||||||
|
- 동일 content_object 가 여러 Frame Slot 에 매칭 가능 시 :
|
||||||
|
- role 매칭 우선 (Frame Slot.role == content_object.role)
|
||||||
|
- 그래도 동률이면 contract declaration 순서 (앞쪽 Frame Slot 우선)
|
||||||
|
|
||||||
|
### 4.5 display_only region 의 처리
|
||||||
|
|
||||||
|
`frame_match_strategy.kind == "display_only"` 인 region 은 Stage B 우회. 대신 :
|
||||||
|
- image region → image area 직접 배치 (frame 없이, region 안에 직접 inline)
|
||||||
|
- table region → table preview (rows ≤ N inline) + 자세히보기 (rows > N popup)
|
||||||
|
- details region → details button + popup
|
||||||
|
- diagram region → diagram inline
|
||||||
|
|
||||||
|
display strategy 어휘는 §5 와 동일 — region-level 적용.
|
||||||
|
|
||||||
|
### 4.6 구현 위치 (예정)
|
||||||
|
|
||||||
|
신규 module (이름 *implementation step 에서 결정* — defer) :
|
||||||
|
- `plan_placement(section, zone, available_frames) -> Placement`
|
||||||
|
- composition planner 의 frame 매칭 *직후*, slot_payload 생성 *직전*
|
||||||
|
- Stage A → Stage B 순차 실행
|
||||||
|
- 결과를 slot_payload 생성 단계에 전달
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5. 표시 전략 — inline preview vs details / popup escalation
|
||||||
|
|
||||||
|
> **v0 의 §4 → v1 의 §5 (renumber)**. **layer-agnostic** — region-level (Stage A) + slot-level (Stage B) 둘 다 적용. *어휘 미변경*.
|
||||||
|
|
||||||
|
### 5.1 결정 기준 (per content_object type)
|
||||||
|
|
||||||
|
| type | inline 가능 조건 | preview + details 전환 | popup-only 전환 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `text_block` | line_count ≤ Frame Slot capacity | line_count > capacity AND role=detail | role=detail AND line_count >> capacity (예: 20+) |
|
||||||
|
| `table` (rows N) | N ≤ 4 | 5 ≤ N ≤ 7 (preview 첫 N rows + details) | N ≥ 8 (popup-only) |
|
||||||
|
| `transform_table` | rows ≤ frame 의 transform Frame Slot capacity (보통 3) | rows > capacity, 일부 inline | rows >> capacity |
|
||||||
|
| `image` | aspect_ratio fit 가능 | 일부 frame 에서 inline + details 의 thumbnail | 거의 없음 (image 는 보통 inline 또는 drop) |
|
||||||
|
| `diagram` | Frame Slot 호환 | preview thumbnail + popup full | popup-only |
|
||||||
|
| `details` (already-marked) | inline 만 안 함 (정의상) | summary inline + body popup | summary + body popup |
|
||||||
|
|
||||||
|
### 5.2 *원문 손실 금지* 룰
|
||||||
|
|
||||||
|
- 표시 전략 결정은 *어디 보여줄지*. *content 자르지 / 압축 / 요약 X*
|
||||||
|
- inline preview 도 *raw_payload 의 일부* 만 빌려옴. 나머지는 details 로
|
||||||
|
- AI 호출 X — 모든 결정은 deterministic rule 기반
|
||||||
|
|
||||||
|
### 5.3 적용 layer
|
||||||
|
|
||||||
|
display strategy 어휘 (`inline_full` / `inline_preview_with_details` / `details_only` / `dropped`) 는 *동일* :
|
||||||
|
- **region-level** (Stage A 의 display_only region) — image area / table preview / details button 등
|
||||||
|
- **slot-level** (Stage B 의 frame_match region 안 Frame Slot 별 content) — Frame Slot 안 content 가 fit 안 되면 escalate
|
||||||
|
|
||||||
|
### 5.4 details / popup runtime
|
||||||
|
|
||||||
|
- frame partial 또는 region container 에 `<details>/<summary>` 또는 별 button + popup layer
|
||||||
|
- 단순 v0 : `<details>` 내장 — 클릭으로 펼침
|
||||||
|
- 향후 v1 : 별도 popup overlay (CLAUDE.md 의 자세히보기 원칙)
|
||||||
|
|
||||||
|
### 5.5 구현 위치 (예정)
|
||||||
|
|
||||||
|
placement planner (§4.6) 의 후속 단계 — 각 assignment / region 에 `display_strategy` 부착 :
|
||||||
|
- `inline_full` — content 전체 inline
|
||||||
|
- `inline_preview_with_details` — 일부 inline, 나머지 details
|
||||||
|
- `details_only` — summary 만 inline, content 는 popup
|
||||||
|
- `dropped` — decorative 가 공간 부족으로 생략
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §6. A1~A4 telemetry 와의 interface
|
||||||
|
|
||||||
|
> **v0 의 §5 → v1 의 §6 (renumber)**. *layer-agnostic*. *구조 미변경* — `sub_zone` 단어 mechanical rename + region-level metadata 추가.
|
||||||
|
|
||||||
|
본 composition layer 와 기존 telemetry layer (A1~A4) 가 *어떻게 흐르는지*.
|
||||||
|
|
||||||
|
### 6.1 forward flow (composition → render → telemetry)
|
||||||
|
|
||||||
|
```
|
||||||
|
section
|
||||||
|
↓ extract_content_objects
|
||||||
|
content_objects
|
||||||
|
↓ placement_planner (Stage A → Stage B)
|
||||||
|
placement {
|
||||||
|
internal_regions: [
|
||||||
|
{
|
||||||
|
region_id, content_type, ratio_estimate,
|
||||||
|
slot_assignments: [{content_object_id, frame_slot_id, display_strategy}],
|
||||||
|
overflow_buffer: [...],
|
||||||
|
rejection: [...],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
↓ slot_payload 생성 (region + Frame Slot 단위로 grouping)
|
||||||
|
slot_payload (with region + Frame Slot metadata)
|
||||||
|
↓ render (frame partial — Frame Slot aware + region container aware)
|
||||||
|
HTML
|
||||||
|
↓ Selenium check
|
||||||
|
overflow signals
|
||||||
|
↓ A1 fit_classifier
|
||||||
|
categories
|
||||||
|
↓ A2 router
|
||||||
|
proposed_actions
|
||||||
|
↓ A3 retry / A4 failure_classifier / next_action
|
||||||
|
final_status
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 telemetry 에 전달되는 새 metadata
|
||||||
|
|
||||||
|
각 zone 의 debug entry 에 추가 :
|
||||||
|
```yaml
|
||||||
|
zone:
|
||||||
|
... (기존)
|
||||||
|
internal_regions: # Layer A
|
||||||
|
- region_id
|
||||||
|
content_type
|
||||||
|
ratio_estimate
|
||||||
|
frame_match_strategy
|
||||||
|
placement:
|
||||||
|
slot_assignments: [...] # 이 zone 에 어떤 content_object 가 어디 Frame Slot 으로
|
||||||
|
overflow_buffer: [...] # details 로 간 것
|
||||||
|
rejection: [...] # frame 부적합 후보
|
||||||
|
region_metrics: # Selenium 이 region 별로 측정 (Layer A)
|
||||||
|
- region_id
|
||||||
|
ch / sh / excess_y # region 단위 overflow
|
||||||
|
frame_slot_metrics: # Selenium 이 Frame Slot 별로 측정 (Layer B, frame_match region 만)
|
||||||
|
- frame_slot_id
|
||||||
|
content_object_id
|
||||||
|
ch / sh / excess_y # Frame Slot 단위 overflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 backward flow (telemetry → composition)
|
||||||
|
|
||||||
|
A4 의 `next_proposed_action` 이 `frame_internal_fit_candidate` 또는 `frame_reselect` 일 때 :
|
||||||
|
- composition layer 가 *재호출* 됨 (단, retry budget 별도)
|
||||||
|
- 다른 frame 또는 다른 placement 시도
|
||||||
|
|
||||||
|
본 v1 에서는 *backward flow 자동화 X* (구현 단계). placement 가 정확히 되어 있으면 telemetry 거의 trigger X.
|
||||||
|
|
||||||
|
### 6.4 fit_classifier 의 *content_type aware* 진화
|
||||||
|
|
||||||
|
현재 fit_classifier 는 *className → semantic_content_type* 매핑. 본 spec 적용 후 :
|
||||||
|
- Selenium 이 region marker / Frame Slot marker / content_object_id marker 를 읽음 (marker attribute name *implementation step 에서 결정* — defer)
|
||||||
|
- classifier 는 *content_object 의 type* 을 직접 알 수 있음
|
||||||
|
- 분류 정밀도 향상 (예: F29 의 frame_match region 안 Frame Slot 의 transform-block 이 transform_table content_object 임을 *직접* 알 수 있음 — 현재는 inner_content_signals 로 추론)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §7. current code gap — 재사용 / 신규 분리
|
||||||
|
|
||||||
|
> **v0 의 §6 → v1 의 §7 (renumber)**. 신규 module 이름 *defer*.
|
||||||
|
|
||||||
|
### 7.1 이미 있는 것 (재사용)
|
||||||
|
|
||||||
|
- MDX parser : section 단위 (## / ### drilling)
|
||||||
|
- align_sections_to_v4_granularity
|
||||||
|
- composition planner (parent_merged_inferred 포함)
|
||||||
|
- frame_contracts.yaml + builder/parser registry
|
||||||
|
- mapper (catalog-driven slot_payload 생성)
|
||||||
|
- Jinja2 render
|
||||||
|
- 8-preset layout vocabulary
|
||||||
|
- A1~A4 telemetry chain
|
||||||
|
|
||||||
|
### 7.2 신규 필요
|
||||||
|
|
||||||
|
| 항목 | 위치 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| **content_object 정규화** | 신규 module (이름 *defer*) | markdown AST 또는 regex 기반 v0 |
|
||||||
|
| **Internal Region planner (Layer A)** | 신규 module (이름 *defer*) | 3-way decision + region 비율 + frame_match_strategy 결정 |
|
||||||
|
| **frame_contracts.yaml** 의 `accepted_content_types` + `sub_zones` 필드 (= Frame Slot 선언) | catalog (기존 yaml 확장) | 3 frame (F13/F29/F16) 우선 |
|
||||||
|
| **placement_planner (Layer A → Layer B)** | 신규 module (이름 *defer*) | Stage A: content → Internal Region / Stage B: region content → Frame Slot |
|
||||||
|
| **display_strategy** 결정기 | placement_planner 내부 | inline_full / inline_preview_with_details / details_only / dropped |
|
||||||
|
| **frame partial 에 Frame Slot 마커** | `templates/phase_z2/families/*.html` | marker attribute name *defer* |
|
||||||
|
| **region container 마커** | `templates/phase_z2/slide_base.html` 또는 partial | region 단위 측정 marker, name *defer* |
|
||||||
|
| **details/popup runtime** | partial template 또는 base slide | `<details>` 우선, 추후 popup overlay |
|
||||||
|
| **fit_classifier 의 region / Frame Slot 인식** | `src/phase_z2_classifier.py` 확장 | inner_content_signals → region / Frame Slot 직접 read |
|
||||||
|
| **mapper 의 region / Frame Slot-aware slot_payload** | `src/phase_z2_mapper.py` 확장 | builder 들이 region + Frame Slot 그룹핑 인식 |
|
||||||
|
|
||||||
|
### 7.3 정의 vs 구현 분리
|
||||||
|
|
||||||
|
본 spec 은 *정의만*. 구현 axis 는 별도 step :
|
||||||
|
- B1. content_extractor (MDX → content_object 정규화)
|
||||||
|
- B2. internal_region_planner (Layer A — 3-way decision + 비율 + frame_match_strategy)
|
||||||
|
- B3. frame_contracts 의 accepted_content_types + sub_zones (= Frame Slot) 선언 (3 frame)
|
||||||
|
- B4. placement_planner (Layer A → Layer B 통합)
|
||||||
|
- B5. partial / region container marker 추가 + telemetry 연동 (이름 결정 포함)
|
||||||
|
- B6. details/popup runtime
|
||||||
|
|
||||||
|
각 axis 는 *별도 step*. 한 axis 씩 사용자 승인 후 진행.
|
||||||
|
|
||||||
|
> **module / marker / attribute 이름** : 본 spec 에서 *defer*. implementation step 에서 결정.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §8. 본 spec 의 활용
|
||||||
|
|
||||||
|
> **v0 의 §7 → v1 의 §8 (renumber)**.
|
||||||
|
|
||||||
|
### 8.1 composition layer 의 룰북
|
||||||
|
|
||||||
|
향후 frame 추가 / content_object 변경 / Layer A 재분할 / Frame Slot 매핑 변경 시 본 spec 의 schema 를 따름. *임의 매핑 / hack 차단*.
|
||||||
|
|
||||||
|
### 8.2 telemetry 와의 cross-check
|
||||||
|
|
||||||
|
A1~A4 의 분류 결과 (`structural_minor_overflow` 등) 가 본 spec 의 placement 결과와 *일치하는가* 확인 가능. 불일치 = composition planning 의 *예상치 못한 케이스* — 진단 자료.
|
||||||
|
|
||||||
|
### 8.3 미사용 sample (MDX 01 / 02) 진단
|
||||||
|
|
||||||
|
본 spec 적용 후 MDX 01/02 를 돌리면 :
|
||||||
|
- 각 section 의 content_object 정규화 결과 visible
|
||||||
|
- 각 zone 의 Internal Region 분할 결과 visible (single vs multi)
|
||||||
|
- 어떤 content type 이 frame contract 에 없는지 (frame 추가 필요 신호)
|
||||||
|
- placement 의 rejection 비율 (frame coverage gap)
|
||||||
|
- overflow_buffer 의 details 후보 (popup runtime 필요 신호)
|
||||||
|
- display_only region 비율 (현재 frame DB 의 Layer A 미커버 영역)
|
||||||
|
|
||||||
|
이 정보가 *generalization validation* 의 진짜 신호.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §9. MDX 03 의 case 를 본 spec 으로 검증 (illustrative)
|
||||||
|
|
||||||
|
> **v0 의 §8 → v1 의 §9 (renumber)**. mechanical rename + 2-stage 표현.
|
||||||
|
|
||||||
|
> MDX 03 = sample. *fix 대상 X*. 본 spec 룰의 *예시 적용*.
|
||||||
|
|
||||||
|
### 9.1 03-1 의 content_object 정규화 (예상)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
section_id: "03-1"
|
||||||
|
title: "1. DX 시행을 위한 필수 요건"
|
||||||
|
content_objects:
|
||||||
|
- id: "03-1.text-1"
|
||||||
|
type: text_block
|
||||||
|
role: summary
|
||||||
|
type_specific: { format: nested_list, bullet_count: 3 (top), nested_count: 7 }
|
||||||
|
size_estimate: { line_count: ~12 }
|
||||||
|
```
|
||||||
|
|
||||||
|
→ 1 content_object (text_block, role=summary).
|
||||||
|
|
||||||
|
### 9.2 03-1 의 Stage A → Stage B (F13 contract 적용)
|
||||||
|
|
||||||
|
**Stage A** :
|
||||||
|
- 3-way decision : section 전체가 F13 (3 pillars) 의 child grouping 으로 매칭 → *whole-section frame match*
|
||||||
|
- single-region zone, content_type=text, ratio=1.0
|
||||||
|
- frame_match_strategy = { kind: "frame_match", frame_id: "F13" }
|
||||||
|
|
||||||
|
**Stage B** :
|
||||||
|
- F13 sub_zones (= Frame Slots) : [pillar_1, pillar_2, pillar_3] (각 cardinality strict 1, accepts text_block)
|
||||||
|
- text_block 1 개 → 3 Frame Slot 에 어떻게 배치?
|
||||||
|
- 현재 mapper (`pillar_item` parser) 가 *implicit* 으로 top_bullet 3 개를 3 pillar 에 분배
|
||||||
|
- 본 spec 적용 시 : text_block 의 nested 구조를 *3 sub_text_block* 으로 sub-decompose 하거나, Frame Slot cardinality 를 *aggregate (3)* 으로 해석할지 결정 필요
|
||||||
|
- v1 단순화 : text_block 의 top-bullet 단위가 *implicit 한 sub-content_object* — 향후 explicit 화
|
||||||
|
|
||||||
|
### 9.3 03-2 의 case (transform_table 포함)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
section_id: "03-2"
|
||||||
|
content_objects:
|
||||||
|
- id: "03-2.transform-1"
|
||||||
|
type: transform_table
|
||||||
|
role: summary
|
||||||
|
type_specific: { pair_count: 3 }
|
||||||
|
- id: "03-2.text-1"
|
||||||
|
type: text_block
|
||||||
|
role: detail
|
||||||
|
type_specific: { bullet_count: 1 }
|
||||||
|
- id: "03-2.text-2"
|
||||||
|
type: text_block
|
||||||
|
role: detail
|
||||||
|
type_specific: { bullet_count: 1 }
|
||||||
|
- id: "03-2.text-3"
|
||||||
|
type: text_block
|
||||||
|
role: detail
|
||||||
|
type_specific: { bullet_count: 3 (large) }
|
||||||
|
- ... (product 쪽도 4 개)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stage A** :
|
||||||
|
- 3-way decision : section 전체가 F29 (process/product 2-column structure) 와 매칭 → *whole-section frame match*
|
||||||
|
- single-region zone, content_type=text+transform_table, ratio=1.0
|
||||||
|
- frame_match_strategy = { kind: "frame_match", frame_id: "F29" }
|
||||||
|
|
||||||
|
**Stage B** :
|
||||||
|
- F29 sub_zones (= Frame Slots) : [process_column (accepts: text_block + transform_table, cardinality 3), product_column (accepts: text_block, cardinality 3)]
|
||||||
|
- process_column → transform_table + 2 text_block (3 개)
|
||||||
|
- product_column → 3 text_block
|
||||||
|
- 모두 inline_full 로 표시
|
||||||
|
|
||||||
|
이건 *현재 mapper (column_with_transform / column_plain) 가 implicit 으로 하는 것* — 본 spec 이 *explicit 하게 표현*.
|
||||||
|
|
||||||
|
### 9.4 03-2 의 cell row 1 (transform_table) 의 10 px overflow 재해석
|
||||||
|
|
||||||
|
placement 가 explicit 하게 되어도 transform_table 이 row 1 cell 에 *콘텐츠 height 131 vs 가용 121* 인 건 변하지 않음.
|
||||||
|
|
||||||
|
**그러나** :
|
||||||
|
- placement 가 *transform_table 의 size_estimate* 를 미리 알면
|
||||||
|
- frame contract 의 Frame Slot 이 *expected_height* 를 declare 하면
|
||||||
|
- planning 단계에서 *"transform_table 이 row 1 Frame Slot 의 expected_height 초과한다"* 를 사전 감지 가능
|
||||||
|
- 그 시점에서 display_strategy = `inline_preview_with_details` 로 자동 전환 (3 transforms 중 2 inline + "1 더 보기")
|
||||||
|
- 또는 placement 가 *frame 부적합* 으로 판정 → frame_reselect 신호
|
||||||
|
|
||||||
|
즉 *본 spec 의 §4 placement algorithm 에 size_estimate 기반 fit pre-check* 가 들어가면 — A1~A4 telemetry 가 *trigger 안 되는 정상 path* 가 됨.
|
||||||
|
|
||||||
|
이게 본 spec 이 가리키는 *진짜 fit policy 의 자리*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 다음 step (사용자 결정)
|
||||||
|
|
||||||
|
본 spec v1 정의 후 구현 axis 후보 :
|
||||||
|
- B1. content_extractor (MDX → content_object 정규화)
|
||||||
|
- B2. internal_region_planner (Layer A — 3-way decision + 비율 + frame_match_strategy)
|
||||||
|
- B3. frame_contracts 에 accepted_content_types + sub_zones (= Frame Slot) 선언 (3 frame)
|
||||||
|
- B4. placement_planner (Layer A → Layer B 통합)
|
||||||
|
- B5. partial / region container marker 추가 + telemetry 연동 (이름 결정 포함)
|
||||||
|
- B6. details/popup runtime
|
||||||
|
|
||||||
|
각 axis 는 *별도 step*. 사용자가 우선순위 결정.
|
||||||
|
|
||||||
|
본 spec 자체는 *implementation 0 단계의 정의*. 다음 step 은 사용자가 잠근 후 진행.
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
# Phase Z-2 — fit_classifier / overflow_router spec
|
||||||
|
|
||||||
|
**Status** : v0 spec (2026-04-29). 정의만. 구현은 별도 step (사용자 승인 후).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 목적 / 위치
|
||||||
|
|
||||||
|
자동 파이프라인이 Selenium 으로 *detect 한* overflow / clipping 을 *어떤 pipeline action 으로 routing 할지* 결정하는 layer 의 spec.
|
||||||
|
|
||||||
|
현재 파이프라인은 detection 까지 정상 작동 (Selenium + debug.json 으로 신호 캡처). 그러나 detection 결과를 받은 직후 `sys.exit(1)` 으로 abort — **detection 과 action 사이의 decision layer 가 비어 있음**.
|
||||||
|
|
||||||
|
```
|
||||||
|
parse_mdx → align → composition (v0.2: capacity_fit) → render (Jinja2)
|
||||||
|
↓
|
||||||
|
Selenium visual_runtime_check ← 기존 (detection)
|
||||||
|
↓
|
||||||
|
🆕 fit_classifier ← 신규 (사실 분류)
|
||||||
|
↓
|
||||||
|
🆕 overflow_router ← 신규 (정책 결정)
|
||||||
|
↓
|
||||||
|
action :
|
||||||
|
- zone_ratio_retry ← 신규 미구현
|
||||||
|
- layout_adjust ← 신규 미구현
|
||||||
|
- details_popup_escalation ← 신규 미구현 (CLAUDE.md 의 <details> 원칙 활성)
|
||||||
|
- frame_reselect ← 신규 미구현 (V4 top-k 활용)
|
||||||
|
- adapter_needed ← composition v0.1.1 partial
|
||||||
|
- abort ← 기존 (현재 default)
|
||||||
|
```
|
||||||
|
|
||||||
|
**핵심 원칙** : classifier = *사실 분류* (이 overflow 가 어떤 종류인가), router = *정책 결정* (그 종류면 무엇을 할 것인가). 두 layer 분리 — 같은 분류가 context (retry 횟수 등) 에 따라 다른 action 을 요구할 수 있음.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. fit_classifier 입력 schema
|
||||||
|
|
||||||
|
### 1.1 detection-side (기존 — 이미 캡처됨)
|
||||||
|
|
||||||
|
| 입력 | 출처 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| `clipped_inner: [{class_name, excess_x, excess_y, scrollWidth/Height, clientWidth/Height}]` | `run_overflow_check` Selenium JS | ✅ |
|
||||||
|
| zone 별 `overflowed`, `excess_y/x` | 같음 | ✅ |
|
||||||
|
| slide / slide_body level overflow | 같음 | ✅ |
|
||||||
|
|
||||||
|
### 1.2 composition-side (기존 — composition v0.2)
|
||||||
|
|
||||||
|
| 입력 | 출처 | 비고 |
|
||||||
|
|---|---|---|
|
||||||
|
| `unit.frame_template_id` / `contract_id` | composition + pipeline | ✅ |
|
||||||
|
| `capacity_fit` (item count, fit_status) | `mapper.compute_capacity_fit` | ✅ (v0.2) |
|
||||||
|
| `content_truncated_count` (zone 별) | pipeline 의 mapper 호출 후 | ✅ (v0.1.1) |
|
||||||
|
| zone size (`height_px`, `min_height_px`, `content_weight`) | `compute_zone_layout` | ✅ |
|
||||||
|
|
||||||
|
### 1.3 신규 입력 (이번 spec 에서 정의)
|
||||||
|
|
||||||
|
| 입력 | 비고 |
|
||||||
|
|---|---|
|
||||||
|
| `semantic_content_type` | className → 의미적 분류. §2 registry 참조 |
|
||||||
|
| `line_equivalent` | excess_y / 해당 element 의 line-height (1 줄 단위로 환산) |
|
||||||
|
| `structural_unit_drop_count` | structural_unit 중 *완전히 또는 부분적으로 잘린* 개수 |
|
||||||
|
| `retry_budget_used` (router 의 상태) | 같은 slide 에 대해 router 가 이미 시도한 retry 횟수 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. className → semantic content_type registry
|
||||||
|
|
||||||
|
| className 패턴 | semantic type | 설명 |
|
||||||
|
|---|---|---|
|
||||||
|
| `transform-block`, `transform-block__*`, `transform-row*` | `structural_unit` | paired comparison (AS-IS/TO-BE 한 쌍이 의미 단위). 행 단위 자르면 의미 깨짐 |
|
||||||
|
| `text-line`, `text-line--bullet`, `text-line--indent-*` | `text_flow` | 자유 wrap, 줄 단위 자르기 가능 |
|
||||||
|
| `*table*`, native `<table>` | `tabular` | 행/열 단위 의미 — 행 잘리면 의미 손실 |
|
||||||
|
| `f29b`, `f13b`, `f16b` (frame-family root) | `frame_internal` | frame 자체가 zone 안에 못 들어감 (zone level 문제) |
|
||||||
|
| `*__cell`, `*__pillar`, `*__quadrant` 등 frame 내부 cell | `frame_internal_cell` | frame 내부 cell 단위 (cell 내부 content 가 cell 경계 초과) |
|
||||||
|
| `*__title`, `*__section-title`, `*__banner`, `*__label` 등 | `frame_label` | 제목/라벨 단위 (text 와 비슷하지만 wrap 제약 있음) |
|
||||||
|
| `<img>`, `<svg>`, `*-bg` 등 | `visual_asset` | 시각 자산 (cropping 가능) |
|
||||||
|
| 매칭 안 됨 | `unknown` | classifier 가 보수적으로 처리 (가장 안전한 action 선택) |
|
||||||
|
|
||||||
|
본 registry 는 신규 module (예: `src/phase_z2_classifier.py`) 의 상수 또는 catalog yaml entry 로 구현.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. fit_classifier 출력 taxonomy
|
||||||
|
|
||||||
|
### 3.1 카테고리 정의 (계산 가능한 룰)
|
||||||
|
|
||||||
|
| 카테고리 | 판정 룰 |
|
||||||
|
|---|---|
|
||||||
|
| `frame_capacity_mismatch` | composition 단계의 `capacity_fit.fit_status` ∈ {`strict_mismatch`, `exceeds_max`, `below_min`, `exceeds_truncate`}. → 이미 v0.2 가 잡고 있는 영역. 본 카테고리는 *post-render 검증 / 누락된 케이스 캐치* 용 |
|
||||||
|
| `structural_major_overflow` | content_type = `structural_unit` 또는 `tabular` AND `structural_unit_drop_count` ≥ 1 (1 개 이상 *완전 단위* 잘림) |
|
||||||
|
| `structural_minor_overflow` | content_type = `structural_unit` 또는 `tabular` AND `structural_unit_drop_count` < 1 (마지막 1 단위가 *부분만* 잘림, 즉 boundary spill) |
|
||||||
|
| `tabular_overflow` | content_type = `tabular` (위와 별도 — 표는 행 1개라도 잘리면 popup) |
|
||||||
|
| `layout_zone_mismatch` | content_type = `frame_internal` (frame root 자체 overflow) — zone 이 frame 을 못 담음 |
|
||||||
|
| `moderate_overflow` | content_type ∈ {`text_flow`, `frame_label`} AND `line_equivalent` ∈ (1.5, 4] |
|
||||||
|
| `minor_overflow` | content_type ∈ {`text_flow`, `frame_label`} AND `line_equivalent` ≤ 1.5 |
|
||||||
|
| `hard_visual_fail` | 위 어디에도 매핑 안 됨 OR retry budget 소진 |
|
||||||
|
|
||||||
|
### 3.2 분류 우선순위 (위에서 아래로)
|
||||||
|
|
||||||
|
1. `frame_capacity_mismatch` (composition 결과 우선)
|
||||||
|
2. `tabular_overflow` (표는 즉시 popup 영역)
|
||||||
|
3. `structural_major_overflow` (1+ 완전 단위 잘림)
|
||||||
|
4. `layout_zone_mismatch` (frame root level)
|
||||||
|
5. `structural_minor_overflow` (boundary spill — 양 작음)
|
||||||
|
6. `moderate_overflow`
|
||||||
|
7. `minor_overflow`
|
||||||
|
8. `hard_visual_fail` (fallback)
|
||||||
|
|
||||||
|
### 3.3 핵심 구분
|
||||||
|
|
||||||
|
- **structural_minor vs structural_major** : 부분만 잘렸나 (`< 1` unit) vs 완전 단위가 잘렸나 (`≥ 1` unit). 부분 잘림은 zone 을 조금 더 주면 fit 가능. 완전 단위 잘림은 의미 손실 — popup escalation.
|
||||||
|
- **structural vs moderate vs minor** : content type 이 *구조적 의미 단위* 인지 여부. 같은 px 양이라도 text_flow 는 minor, structural_unit 은 structural_minor 이상.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. overflow_router action mapping
|
||||||
|
|
||||||
|
| 카테고리 | action | retry budget | fallback (안 풀리면) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `minor_overflow` | `zone_ratio_retry` (양보 가능 zone 식별 → compute_zone_layout 재실행) | 1 | escalate → `moderate_overflow` 처리 |
|
||||||
|
| `moderate_overflow` | `layout_adjust` (8-preset 중 다른 preset 검토 + zone ratio 재분배) | 1 | escalate → `structural_major_overflow` 처리 |
|
||||||
|
| `structural_minor_overflow` | `zone_ratio_retry` (구조 자르지 않도록 zone 키움) | 1 | escalate → `structural_major_overflow` 처리 |
|
||||||
|
| `structural_major_overflow` | `details_popup_escalation` (`<details>/<summary>` path) | N/A | popup 미구현 시 → `frame_reselect` → `adapter_needed` |
|
||||||
|
| `tabular_overflow` | `details_popup_escalation` 또는 `frame_reselect` (table-friendly frame 후보) | N/A | 없으면 `adapter_needed` |
|
||||||
|
| `frame_capacity_mismatch` | `frame_reselect` (V4 top-k rank 2+ 평가) | 1 | 없으면 `adapter_needed` |
|
||||||
|
| `layout_zone_mismatch` | `layout_adjust` 또는 `zone_ratio_retry` | 1 | escalate → `frame_reselect` |
|
||||||
|
| `hard_visual_fail` | `abort` (현재 sys.exit(1) 그대로) | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. current code gap
|
||||||
|
|
||||||
|
### 5.1 이미 있어서 *재사용* 할 것
|
||||||
|
|
||||||
|
- detection (Selenium `run_overflow_check`) — clipped_inner / excess_y / className 모두 캡처
|
||||||
|
- composition v0.2 의 `compute_capacity_fit` (item count level)
|
||||||
|
- composition v0.1.1 의 `adapter_needed` catch (mapper FitError)
|
||||||
|
- debug.json 의 `slide_status` / `zones` / `candidates_summary` (입력 자료원)
|
||||||
|
- 8-preset layout vocabulary + `compute_zone_layout`
|
||||||
|
|
||||||
|
### 5.2 새로 만들어야 할 것
|
||||||
|
|
||||||
|
| 신규 항목 | 비고 |
|
||||||
|
|---|---|
|
||||||
|
| **content_type registry** (§2) | className → semantic type. classifier 의 핵심 입력 |
|
||||||
|
| **`fit_classifier` 모듈** | §1 입력 → §3 카테고리 |
|
||||||
|
| **`overflow_router` 모듈** | §4 카테고리 → action |
|
||||||
|
| `zone_ratio_retry` action 구현 | compute_zone_layout 의 retry path |
|
||||||
|
| `layout_adjust` action 구현 | preset 동적 변경 |
|
||||||
|
| `details_popup_escalation` 구현 | `<details>/<summary>` runtime + slot_payload 분리 룰 (큰 작업) |
|
||||||
|
| `frame_reselect` 구현 | V4 top-k 사용 (rank-2+ 평가) |
|
||||||
|
|
||||||
|
### 5.3 정의 vs 구현 분리
|
||||||
|
|
||||||
|
본 spec 은 **정의만**. 위 신규 항목 중 어느 axis 부터 구현할지는 *별도 step* 에서 사용자 승인 후. 한꺼번에 다 만들지 X.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 본 spec 의 활용 — *visual fix 결정의 검증 자료*
|
||||||
|
|
||||||
|
본 spec 은 *룰북*. 향후 overflow 발생 시 :
|
||||||
|
|
||||||
|
1. classifier 가 *어떤 카테고리* 인지 결정
|
||||||
|
2. router 가 *어떤 action* 인지 결정
|
||||||
|
3. action 이 *현재 구현되어 있나* 확인
|
||||||
|
4. 미구현이면 → "본 spec 의 이 path 미구현이라 처리 불가" 로 명확히 보고
|
||||||
|
|
||||||
|
**중요 활용** : 누군가 (Claude 든 사람이든) "padding 줄여서 끼우자" 같은 fix axis 를 제시하면 — 이 fix 가 본 spec 의 어느 action 에도 매핑되지 않음 → **자동 반려**. 본 spec 을 검증 자료로 가지면 *symptom-silencing fix* 가 들어올 자리가 없어짐.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. MDX 03 의 10px clipping 을 본 spec 으로 분류 (검증용 sample, fix 대상 X)
|
||||||
|
|
||||||
|
> MDX 03 = sample instance. spec 룰의 *작동 검증* 용 — fix 대상 아님.
|
||||||
|
|
||||||
|
### 측정값
|
||||||
|
- excess_y = 10px (~0.6 줄, transform-row line-height 15.95px 기준)
|
||||||
|
- clipped element className = `transform-block` 의 마지막 row (cell 내부)
|
||||||
|
- semantic content_type = `structural_unit` (transform-row pair)
|
||||||
|
- structural_unit_drop_count = 0.6 (1 개의 마지막 row 가 부분만 잘림 — 완전 단위 1 개가 아님)
|
||||||
|
- composition `capacity_fit.fit_status` = `ok`
|
||||||
|
|
||||||
|
### 분류 적용 (§3.2 우선순위)
|
||||||
|
|
||||||
|
1. `frame_capacity_mismatch`? — capacity_fit ok → ✗
|
||||||
|
2. `tabular_overflow`? — content_type 이 tabular 아님 → ✗
|
||||||
|
3. `structural_major_overflow`? — drop_count `< 1` → ✗
|
||||||
|
4. `layout_zone_mismatch`? — frame_internal 아님 → ✗
|
||||||
|
5. `structural_minor_overflow`? — content_type = structural_unit AND drop_count `< 1` → **✓**
|
||||||
|
|
||||||
|
**카테고리 = `structural_minor_overflow`**
|
||||||
|
|
||||||
|
### Action mapping 적용 (§4)
|
||||||
|
|
||||||
|
→ `zone_ratio_retry` (구조 자르지 않도록 F29 zone 을 더 키움)
|
||||||
|
|
||||||
|
### 현재 구현 상태
|
||||||
|
|
||||||
|
→ `zone_ratio_retry` 는 **MISSING**. 따라서 본 spec 기준으로 MDX 03 은 *classifier 가 정상 작동하면 structural_minor_overflow 로 분류되고 zone_ratio_retry 로 routing 되어야 하는 case* 인데, *그 path 가 현재 미구현*. 따라서 정직한 상태는 `RENDERED_WITH_VISUAL_REGRESSION` 그대로 유지.
|
||||||
|
|
||||||
|
### 반례 검증 (이전 잘못된 fix)
|
||||||
|
|
||||||
|
이전에 시도했던 "transform-block padding 6→4 + transform-row padding 3→2" :
|
||||||
|
- 본 spec 의 어떤 action 에도 매핑되지 X (`density_reduce` 같은 action 자체가 없음)
|
||||||
|
- 따라서 본 spec 기준으로 **자동 반려되는 fix axis** — 들어올 자리 없음
|
||||||
|
|
||||||
|
이게 본 spec 이 *visual fix 결정의 검증 자료* 로 작동한다는 증거.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 다음 step (구현 우선순위 — 사용자 결정 영역)
|
||||||
|
|
||||||
|
본 spec 정의 후 구현 axis 후보 (사용자가 우선순위 결정):
|
||||||
|
|
||||||
|
- A. content_type registry + fit_classifier (분류 layer)
|
||||||
|
- B. overflow_router (정책 layer)
|
||||||
|
- C. `zone_ratio_retry` action (가장 자주 트리거될 action)
|
||||||
|
- D. `details_popup_escalation` (큰 작업 — 새 path)
|
||||||
|
- E. `frame_reselect` (V4 top-k 사용 layer)
|
||||||
|
|
||||||
|
각 axis 는 *별도 step* 으로 한 단위씩. 한꺼번에 묶지 X.
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
# Phase Z Frame Style Inventory
|
||||||
|
|
||||||
|
> Phase Z 가 계승할 **색감, 여백, 폰트 위계, 표 / 카드 / 다이어그램 스타일, pill / badge, SVG / CSS 구현 힌트** 를 추출하는 인벤토리.
|
||||||
|
>
|
||||||
|
> ⚠️ 이 문서는 **블록 매핑이 아니다.** Figma frame 은 디자인 레퍼런스 / 구조 패턴 / 슬롯 힌트로 본다 ([`FRAME-INTEGRATION-MAP.md`](FRAME-INTEGRATION-MAP.md) 참조).
|
||||||
|
>
|
||||||
|
> ⚠️ `Phase Z Target` 컬럼은 **결정이 아니라 후보**. 사용자 승인 후 프로모션 게이트에서 확정.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Source Policy
|
||||||
|
|
||||||
|
| 구분 | 위치 | 역할 |
|
||||||
|
|---|---|---|
|
||||||
|
| **메인 소스** | `figma_to_html_agent/blocks/{figma_id}/` | 32 frame (1171281171 제외) — Phase Z 스타일 추출의 1 차 출처 |
|
||||||
|
| **토큰 소스** | `templates/styles/tokens/` | `colors.css` / `spacing.css` / `typography.css` — Phase Z 에서 계승 / 조정 |
|
||||||
|
| **legacy 참고** | `templates/blocks/structures/` 등 | Phase Z 의 실제 조립 재료 X. 폐기 / 아카이브 방향. 스타일 / 시각 언어 참고만 |
|
||||||
|
|
||||||
|
추출 우선 순위 :
|
||||||
|
|
||||||
|
1. **변환 완료 frame** (`index.html` + `flat.md` 보유) — 실제 CSS 관찰 기반 스타일 추출
|
||||||
|
2. **미변환 frame** — `Style Elements` / `Extracted Style Hints` / `Phase Z Target` 보류, 변환 완료 후 갱신
|
||||||
|
3. **MCP / Figma 직접 조회 사용 X** — `figma_to_html_agent` 의 본업이므로 inventory 작성 단계에서 침범하지 않음
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Frame Inventory — 컬럼 정의
|
||||||
|
|
||||||
|
| 컬럼 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| **Frame** | `FRAME-INTEGRATION-MAP.md` 의 row 번호 (01~32) |
|
||||||
|
| **Figma ID** | `figma_to_html_agent/blocks/` 디렉토리 ID |
|
||||||
|
| **Layout** | `layouts.yaml` controlled vocabulary |
|
||||||
|
| **Style Elements** | frame 안에서 *관찰되는* 스타일 요소 (gradient bar, pill, table header, radial node, soft shadow 등) |
|
||||||
|
| **Extracted Style Hints** | Phase Z 에 *계승할* 구체 힌트 (예 : "table header dark fill + white text", "card gap 12~16px") |
|
||||||
|
| **Phase Z Target** | **후보** 위치 (예 : `tokens/colors.css`, `styles/frame-patterns/table.css`, `svg-helpers/`). 결정 X |
|
||||||
|
| **Asset Notes** | 자산 의존도 / Phase Z 재사용 가능성 (예 : "타이틀 아이콘 PNG 1 개, conclusion box 는 CSS 변환 가능") |
|
||||||
|
| **Notes** | cardinality / optional slot / 변형 축 / 기타 |
|
||||||
|
|
||||||
|
### 작성 룰
|
||||||
|
|
||||||
|
1. **관찰 가능한 값만 작성** — 변환 frame 의 셀은 `flat.md` + `index.html` 에 *실제 있는* 관찰값만 채운다. flat.md 깊이는 frame 마다 다를 수 있고, 빠진 항목 (예 : 변형 축 명시 없음) 은 채우지 않고 비운다. **다른 frame 깊이에 맞추기 위한 추론 / 추측 채움은 하지 않는다.**
|
||||||
|
2. **Phase Z Target 후보는 가능한 한 family 단위로 수렴** — 같은 layout family (표 / 카드 / 다이어그램 / 리스트 / banner) 의 frame 들은 동일한 target 파일 후보를 가리키게 작성. variant 차이는 별도 target 으로 쪼개기보다 `Notes` 에 메타로 남긴다. 표기는 항상 `(후보)` 접미사 — "만들 파일" 이 아니라 "수렴 위치 후보" 로 읽히게.
|
||||||
|
3. **scale / zoom 은 Notes 에 메타** — Figma 원본 폭이 1280 이 아닐 경우 `flat.md` 의 scale / zoom 값을 그대로 두 (raw px 는 원본 기준). Phase Z slide-body (≈1200×590) 에 적용 시 재계산이 필요하다는 사실을 `Notes` 에 한 줄 기록.
|
||||||
|
4. **redescription 금지** — `analysis.md` 의 cardinality / slot / anchor / layout 설명, `FRAME-INTEGRATION-MAP.md` 의 비고 / 검토 상태를 Inventory 에 다시 베끼지 않는다. 같은 정보가 두 문서에 들어가면 drift 위험.
|
||||||
|
|
||||||
|
### 미변환 frame 의 셀 표기
|
||||||
|
|
||||||
|
- `Style Elements` : *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.*
|
||||||
|
- `Extracted Style Hints` / `Asset Notes` / `Notes` : `—`
|
||||||
|
- `Phase Z Target` : `TBD after conversion` (단, `reference_only` frame 은 `N/A — reference_only`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Frame Inventory (32 행)
|
||||||
|
|
||||||
|
> 14 변환 완료 frame 은 `flat.md` + `index.html` 관찰 기반 스타일 추출.
|
||||||
|
> 18 미변환 frame 은 보일러플레이트 일괄.
|
||||||
|
> 작성 룰 #1~4 (위 섹션 2) 준수.
|
||||||
|
|
||||||
|
| Frame | Figma ID | Layout | Style Elements | Extracted Style Hints | Phase Z Target | Asset Notes | Notes |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| **01** | `1171281172` | `circular-nodes-6` | • 6 원형 노드 absolute 배치 (각 노드 = 배경 원 + 내부 아이콘 + 라벨)<br>• 모든 노드 / 연결선 / 중앙 / 배경 = 이미지 자산 (9 개) | • **2D 다이어그램 패턴** — 노드 좌표 절대 배치<br>• 자산 의존도 매우 큼 — 본 frame 의 시각 구성은 거의 이미지 | svg helpers (helper area 후보, 자산 의존 제한적) | 9 자산 (배경, 중앙, 노드 ×6, 연결선) — 모두 이미지 유지. Phase Z 재현 시 자산 풀 / placeholder 필요 | 원본 1579×981, scale 0.81064. 변형 축 명시 없음 (flat.md sparse) |
|
||||||
|
| **02** | `1171281173` | `bullet-cards-4-plus-center` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **03** | `1171281174` | `list-numbered-4` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **04** | `1171281175` | `quadrilateral-relations` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **05** | `1171281176` | `side-card-with-list` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **06** | `1171281177` | `full-page-map-banner` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `N/A — reference_only` | — | — |
|
||||||
|
| **07** | `1171281178` | `2col-paired-list` | • 좌 H/W 7 항목 + 우 S/W 6 항목 + 중앙 시스템 원 + 하단 ground 이미지<br>• 16 자산 (배경 / 패널 / 중앙 원 / 장식 아이콘 / 헤더 바 SVG) | • **2D 복합 시스템 구성도 패턴**<br>• 자산 의존도 매우 큼 — Phase Z 재구성 곤란 | `styles/frame-patterns/system-diagram.css` (후보, 자산 의존 제한적) | 16 자산 모두 이미지 유지. Phase Z 재현 시 자산 풀 필수 | 원본 2446×1943, scale 0.52331. 변형 축 명시 없음 (flat.md sparse) |
|
||||||
|
| **08** | `1171281179` | `3-section-framework` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **09** | `1171281180` | `list-stacked-vertical` | • 5 pill 행 : `bg: rgba(255,255,255,0.5)`, `border-bottom: 3px solid {color}`, `radius: 30px`, `box-shadow: 2px 4px 5px rgba(0,0,0,0.5)`, `padding: 10px 20px`<br>• pill 색상 5 개 : `#fb5915` / `#e79000` / `#e9a804` / `#919f00` / `#0d6361`<br>• 다이아몬드 stacking : 넓→좁→좁→넓→넓 (좌측 indent 변화)<br>• 타이틀 바 : `#fbd5b9`, `radius: 5px`, shadow<br>• 좌측 아크 장식 SVG (이미지) + 화살표 SVG (`rotate(-90deg)`) | • **pill row + colored bottom border** : 핵심 패턴, 색만 갈아끼우면 N=3~7 동작<br>• **다이아몬드 stacking 패턴** : indent 차이로 시각 리듬<br>• translucent bg + colored border = 부드러운 카테고리 분리 | `styles/frame-patterns/pill-list.css` (후보) + `tokens/colors.css` 5 pill color palette (후보) | 좌측 아크 SVG / 화살표 SVG 2 개 — 이미지 유지. pill 본체는 CSS 변환 완료 | 변형 축 : `items[N=3~7]`, `stacking_pattern` (required), `arc_decoration` / `vertical_label` (optional). 원본 1153×592 (scale 1.11015 — 원본이 1280 보다 작음, zoom up 처리) |
|
||||||
|
| **10** | `1171281181` | `radial-diagram-5` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **11** | `1171281182` | `cards-3-category` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **12** | `1171281189` | `cycle-3way-intersection` | • 메인 3 원 (350×350) : outer + inner SVG (Ellipse585~592) + 중앙 라벨 (50px Bold white, `text-shadow: #cc5200`)<br>• 액센트 6 원 (130.9 px) : 한자 라벨 (45px Bold white, 같은 text-shadow)<br>• 사이드 라벨 6 그룹 : 40px Bold + 30px Medium desc<br>• 영역별 heading color : 상단 `#cc5200` / 좌측 `#604f32` / 우측 `#124133`, desc 공통 `#525151`<br>• 장식 RECT : gradient 회전 + `mix-blend-mode: multiply`<br>• 타이틀 : 70px Bold gradient `#000→#883700` | • **3 원 교차 다이어그램** : main 3 + accent 6 으로 영역 표현<br>• **white text + colored text-shadow** : 깊이 부여 효과<br>• **영역별 hue 분리** (`#cc5200` / `#604f32` / `#124133`) — 시각 zone 구분<br>• bg_texture multiply blending — 부드러운 배경 강조<br>• 사이드 라벨 위계 : Bold heading + Medium desc | svg helpers (helper area 후보, 원 / 교차 영역) + `styles/frame-patterns/cycle.css` (후보) + `tokens/colors.css` 영역별 hue palette (후보) | 19 SVG (Ellipse585~603 outer/inner pairs) — 좌표 기반 SVG 재구성 가능. bg_texture PNG 1 개는 이미지 유지 | 변형 축 명시 없음 (메인 3 원 고정 가능성). 원본 2195×1195, scale 0.58312. 수학 : main 350px = 15.94% width / accent 130.9px = 5.96% width |
|
||||||
|
| **13** | `1171281190` | `3-column` | • 3 컬럼 (각 690 원본) : 좌 BAR (152.5 px) gradient + 우 본문<br>• BAR gradient 3 가지 : 기술 `#0D78D0→#023056` / 사람 `#FF9A23→#CC5200` / 자연 `#39BE49→#23742C`<br>• 한자 (技術 / 人材 / 天地) : 50px Bold white on bar<br>• 헤딩 : 45px Bold gradient (top / bottom 별도 gradient 2 종)<br>• 본문 : 35px Medium `#3E3523`<br>• 세로 라벨 (rotate 90°) — 옵셔널 메타<br>• 테두리 : 실선 + 점선 SVG | • **3-pillar 카드 패턴** : 동등 카테고리 3 개 (예 : 기술/사람/자연)<br>• **gradient bar + 한자 + heading + body** 조합<br>• **컬럼별 hue rotation** (blue / orange / green) — 의미 차별화<br>• heading 도 gradient (단색 X) — 일관된 시각 언어 | `styles/frame-patterns/three-pillar.css` (후보) + `tokens/colors.css` 3 column gradient palette (후보) | 아이콘 PNG 1 개 + 테두리 SVG 4 개 (CSS border 변환 가능). 자산 의존 적음 | 변형 축 명시 없음 (3 pillar 고정 가능성 큼). 원본 2123×724, scale 0.60290. 수학 : 열 너비 416px / 바 92px after scale |
|
||||||
|
| **14** | `1171281191` | `persona-3col` | • 3 컬럼 동일 사이즈 (833×1845 원본) + 각 컬럼 텍스처 BG 이미지<br>• 컬러 오버레이 (opacity 0.80) — 컬럼별 다른 색감 hue<br>• 하단 사진 3 개 : `border-radius: 49~50px`, opacity 0.70<br>• 상단 원형 뱃지 (3 개) : outer + inner 이미지 + 한글 라벨<br>• 라벨 색 hue rotation : 발주자 `#285B4A` / 시공자 `#445A2F` / 설계자 `#743002`<br>• 체크박스 불릿 아이콘 (이미지, 32×32)<br>• 불릿 텍스트 : 40px Medium `#000` | • **3 컬럼 persona / actor 카드 패턴** : 역할별 한 컬럼<br>• **컬럼별 hue rotation** : 같은 톤 안에서 색상만 다르게 (역할 구분)<br>• **타이틀을 원형 뱃지로 표현** — 컬럼 상단 절반 걸침 (overhang)<br>• **사진을 borderless 가 아니라 둥근 corner + opacity 처리** (텍스트 가독성 확보)<br>• **체크박스 불릿** — 토큰화 가능 (Phase Z list-marker 후보) | `styles/frame-patterns/persona-cards.css` (후보) + `tokens/colors.css` 컬럼별 actor hue palette (후보) + svg helpers 원형 뱃지 (helper area 후보) | 다수 이미지 자산 : BG texture (×3), overlay (×3), photo (×3), badge outer/inner (×6), 체크박스 (×20 동일). 사진은 컨텍스트 의존 → Phase Z 에서 placeholder / 사용자 제공 자산 필요. 체크박스는 SVG 단순 대체 가능 | 원본 2601×1927 (대형 frame), scale 0.49213 |
|
||||||
|
| **15** | `1171281192` | `policy-4card-plus-list` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **16** | `1171281193` | `quadrant-4` | • 2×2 사분면 (각 1080×270 헤더/푸터바 + 본문)<br>• 헤더/푸터 bar (4 개) : brown gradient (좌측, `270deg, rgba(165,161,150,0.5)→#39321E`) / green gradient (우측, `270deg, rgba(41,107,85,0.5)→#032118`)<br>• Bar 라벨 : 60px Black white + `text-shadow: 0 0 4px #322c1e`<br>• 사분면 헤드라인 : red `#ff0000` 55px Bold (강조)<br>• 본문 : black 42px Regular, bullet list (`<ul><li>`, 텍스트 마커)<br>• 중앙 원 + 영문 인용 (white 55px Bold)<br>• 배경 텍스처 PNG (`border-radius: 50px`, ×4 동일) | • **2×2 quadrant 패턴** : 4 사분면에 헤드라인 + body 쌍 + 헤더/푸터 bar<br>• **bar 색 양극** (brown / green) — 좌·우 의미 구분<br>• **bar text 강한 시각 강조** : 60px Black + text-shadow<br>• **사분면 헤드라인 red** — 문제 / 이슈 강조 패턴<br>• 중앙 원 + 인용 — 결론 표현 (옵셔널)<br>• bullet — `<ul><li>` 텍스트 마커 (이미지 마커 X) | `styles/frame-patterns/quadrant.css` (후보) + `tokens/colors.css` bar gradient + 강조 red (후보) | 배경 텍스처 PNG (×4 동일) + 중앙 원 PNG + bar SVG (×4, CSS gradient 변환 가능). 배경 / 원만 이미지 유지 | 변형 축 : `quadrants[4]` (required, 고정), `bar_labels[4]` (required), `center_quote` / `center_image` / `bg_texture` (optional). 원본 2226×1766, scale 0.57503 |
|
||||||
|
| **17** | `1171281194` | `paired-rows-2x2` | • 4 행 (각 좌 pill + 본문 + 분할선 + 우 pill + 본문)<br>• 행 배경 : `border: 3px #60A451`, `radius: 30px`, `bg: rgba(250,237,203,0.15)`<br>• 분할선 : `dashed 2px #60A451` (CSS)<br>• pill 이미지 (R16: 두루마리 곡선) — 좌측 `left:-45.3% width:145.3%` / 우측 `left:0 width:151.25%`<br>• pill 라벨 : 40px Bold white<br>• 본문 : 36px Medium `#0c271e`<br>• 행 교대 pill 회전 : 상행 정상 / 하행 `rotate(180deg)`<br>• 타이틀 : 70px Bold gradient `#CC5200→#883700` | • **paired-rows 패턴** : 좌 / 우 라벨 + body 페어, 분할선 중앙<br>• **두루마리 pill (R16)** : 이미지 기반 곡선 형상 — CSS 재구성 곤란<br>• **상/하 pill 회전 교대** = 시각 리듬<br>• translucent bg + colored border = visual containment<br>• dashed 분할선 = soft separation | `styles/frame-patterns/compare-paired.css` (후보, Frame 18 과 같은 family) + `tokens/colors.css` border / bg color (후보) | 타이틀 아이콘 PNG + 두루마리 pill PNG (R16 frame 배치, CSS 재구성 곤란) + 분할선 SVG (CSS 변환 가능) | 변형 축 : `rows[N=3~6]`, `pill_alternation` 상/하 교대 (required), `pill_image` (required), `bg_border_color: #60A451` (required). 원본 1857×1326, scale 0.68927. **compare-paired family — variant : `paired-rows` (pill alternation)** |
|
||||||
|
| **18** | `1171281195` | `compare-rows` | • 타이틀 70px Bold gradient text (`#CC5200 → #883700`) + 아이콘<br>• 서브헤더 pill bar : `linear-gradient(270deg, #285B4A → #4A4026)`, `border-radius: 50px`<br>• 중앙 카테고리 뱃지 12 개 : 같은 gradient (alpha 0.64~0.8), `border-radius: 10px`<br>• 좌·우 텍스트 색 양극 : `#5C3714` (BIM 측, 갈색계) ↔ `#285B4A` (DX 측, 청록계), 40px Bold<br>• 결론 박스 : `#FAEDCB` + `mix-blend-mode: multiply`<br>• 결론 강조 텍스트 : `#AE3607` 55px Bold | • **다행 비교표 패턴** : 좌 (BIM/AS-IS) ↔ 중앙 카테고리 라벨 ↔ 우 (DX/TO-BE) 의 3 컬럼 페어드<br>• **양극 색 표현** : 좌·우를 명도·색상이 다른 두 hue 로 분리 (대비 의도)<br>• **gradient 재사용** : title gradient + bar/badge gradient 가 동일 팔레트 (저채도 그린 + 다크 brown) 변주<br>• **결론 처리** : multiply blending + accent color 로 강조 | `styles/frame-patterns/compare-paired.css` (후보, Frame 17 과 같은 family) + `tokens/colors.css` 의 `--g-title` / `--c-as-is` / `--c-to-be` (후보) + `styles/blocks/conclusion.css` multiply blend (후보) | 타이틀 아이콘 PNG 1 개 (이미지 유지) + 화살표 SVG 1 개 (`rotate(180deg)`, 이미지 유지) + 결론 박스 SVG → CSS 변환 완료 (자산 불필요) | 변형 축 : `rows[N=8~15]` (required), `title` (required), `conclusion` / `arrow_decoration` (optional). 원본 1868×1908, scale 0.68524. **compare-paired family — variant : `vs-center-badge` (좌·우 텍스트 + 중앙 카테고리 라벨 컬럼)** |
|
||||||
|
| **19** | `1171281197` | `cards-3-compare` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **20** | `1171281198` | `cards-3-header` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **21** | `1171281201` | `split-panel-diagram` | • 좌 다이어그램 이미지 + 우 4 번호 항목 + 하단 결론 바<br>• 14 자산 (다이어그램 요소 / 번호 뱃지 / 행 바 / 화살표 / 결론 바) — 모두 이미지 유지 | • **split-panel 패턴** (이미지-기반 다이어그램 + 번호 리스트)<br>• 자산 의존 큼 — Phase Z 재구성 시 다이어그램 이미지 제공 필요 | `styles/frame-patterns/split-panel.css` (후보, Frame 22 와 같은 family) | 14 자산 모두 이미지 유지. 다이어그램 이미지 = 핵심 자산 | 변형 축 : `right_items[N=3~6]`, `conclusion_text` (optional). 원본 1889×824, scale 0.67761. flat.md sparse |
|
||||||
|
| **22** | `1171281202` | `split-panel-numbered` | • 좌 패널 : 배경 IMG + 카테고리 텍스트 (40px Bold white, text-shadow) + detail (35px Medium black)<br>• 우 패널 : 5 행 (번호 뱃지 IMG + 행 바 IMG + 텍스트 45px Medium `#11231d` + 화살표 IMG `rotate(180deg)`)<br>• 중앙 연결 : 세로 괄호 IMG + 커넥터 IMG<br>• 행 바 (×5 동일 이미지)<br>• 번호 뱃지 (5 개 별개 이미지)<br>• 타이틀 : 70px/50px gradient | • **split-panel + numbered list 패턴** : 좌 카테고리 패널 + 우 번호 항목 페어<br>• 카테고리 텍스트 = text-shadow + white (배경 위 가독성)<br>• 번호 뱃지 + 행 바 + 화살표 = 단위 리스트 행 컴포넌트<br>• 중앙 괄호 / 커넥터 = 좌 ↔ 우 연결 시각화 | `styles/frame-patterns/split-panel.css` (후보, Frame 21 과 같은 family) | 15 자산 (좌측 BG, 타이틀 장식, 구분선 ×3, 세로 괄호, 커넥터, 행 바 ×5 동일, 뱃지 ×5 별개, 화살표 ×5 동일, 타이틀 아이콘) — 모두 이미지 유지 | 변형 축 : `left_categories[N=2~5]`, `right_items[N=3~8]` (required), `bg_image` (required), `bracket_image` (optional). 원본 1863×834, scale 0.68707 |
|
||||||
|
| **23** | `1171281203` | `table-2col` | • 열 헤더 bar (3 개) : `#589e8d` (구분/좌) / `#ef7a26` (우), 40px Bold white<br>• 행 배경 교대 : white / `rgba(253,198,158,0.16)`<br>• 강조 키워드 : `#a14101` Bold inline<br>• 본문 : black 40px Medium<br>• 그리드 라인 : 모두 CSS border<br>• 배경 텍스처 PNG (상단 / 하단 분할) | • **compare-table 패턴** (구분 컬럼 + N 열 비교)<br>• **헤더 bar 색상 양극** (`#589e8d` 청록 / `#ef7a26` 오렌지) — 의미 구분<br>• **행 alternating bg** = readability<br>• **강조 키워드 inline color** (`#a14101`) — 표 셀 안 강조 | `styles/frame-patterns/compare-table.css` (후보, Frame 24 와 같은 family) + `tokens/colors.css` 헤더 bar palette (후보) | 배경 PNG 2 개 + 아이콘 PNG + line SVG ×5 (모두 CSS border 변환). 배경 PNG 만 이미지 유지 | 변형 축 : `columns[2]`, `rows[N=3~7]` (required), `header_colors[2]` (required), `bg_images[2]` (optional). 원본 1924×2014, scale 0.66527. **table family — Frame 24 / 30 / 31 과 column 수 / 행 수만 다름, variant 통합 후보** |
|
||||||
|
| **24** | `1171281204` | `table-3col` | • 열 헤더 bar (4 개) : `#589e8d` (구분/상용) / `rgba(62,53,35,0.9)` (3rd) / `#ef7a26` (전용), 40px Bold white<br>• 행 배경 교대 : white / `rgba(253,198,158,0.16)`<br>• 강조 키워드 : `#a14101` Bold inline<br>• 본문 : black 35px Medium<br>• 그리드 라인 : 모두 CSS border<br>• 행 라벨 (좌측 열) : 35px Bold | • **compare-table 패턴** (Frame 23 의 3-column variant — 같은 family)<br>• **헤더 색상 3-way** (`#589e8d` / 다크 brown / `#ef7a26`) — Frame 23 의 2-way 확장<br>• **행 라벨 좌측 열** = 행 그룹 식별자 (예 : 개념 / 개발주체 / 성과품 / 사용) | `styles/frame-patterns/compare-table.css` (후보, Frame 23 과 같은 family — variant: `columns[N=2~4]`) + `tokens/colors.css` 헤더 bar palette 확장 (후보) | 아이콘 PNG + line SVG ×8 (전부 CSS border 변환). 자산 의존 거의 없음 | 변형 축 : `columns[N=2~4]`, `rows[N=3~6]` (required), `header_colors[N]` (required), `highlight_color` (optional, default `#a14101`). 원본 1869×1926, scale 0.68511. **table family — Frame 23 / 30 / 31 과 variant 통합 후보** |
|
||||||
|
| **25** | `1171281205` | `left-categories-right-logos` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **26** | `1171281206` | `cards-4-grid` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **27** | `1171281208` | `central-split-synthesis` | • 좌 (생산성 향상) + 우 (디지털 전환) + 중앙 원 (건설산업의 고부가가치화)<br>• 상단 헤더 bar / 하단 결론 bar (SVG `rotate(180deg)`)<br>• 2D 배치 (중앙 원 좌 / 우 영역 겹침) → absolute + zoom | • **split-center 패턴** : 좌 / 우 / 중앙 3-area 합성<br>• 중앙 원 = 좌·우 영역에 걸침 (overhang) — 결론 표현 방식 | `styles/frame-patterns/split-center.css` (후보) | 변환 완료 (preview.png 존재). 자산 상세 미기록 (flat.md sparse) | 원본 1697×914, scale 0.75427. flat.md sparse — 추가 관찰 필요 |
|
||||||
|
| **28** | `1171281209` | `title-plus-3-emphasis` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **29** | `1171281210` | `banner-top-2col-bottom` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **30** | `1171281211` | `table-3col` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **31** | `1171281212` | `table-3col` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
| **32** | `1171281213` | `central-5-goals` | *미변환 — 스타일 추출 보류. analysis.md 기준 layout / slot 존재만 확인.* | — | `TBD after conversion` | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Token Inventory
|
||||||
|
|
||||||
|
> 14 변환 frame 에서 관찰된 값과 기존 `templates/styles/tokens/` 의 매칭 검증.
|
||||||
|
>
|
||||||
|
> ⚠️ **본 inventory 는 신규 발굴표가 아니라 *기존 token 검증 + gap 발견 + 보류* 정리표**.
|
||||||
|
> ⚠️ 추출 / 검증만. 실제 token 파일 생성 / 변경 / 폐기는 별도 승인 단계.
|
||||||
|
|
||||||
|
### 작성 룰 (최종 6 개)
|
||||||
|
|
||||||
|
1. **2+ frame 에서 반복 관찰된 값만 행 승격**.
|
||||||
|
2. **1 frame 에서만 관찰된 값은 Token Inventory 에 올리지 않음** — 해당 Frame Inventory 의 `Notes` 에 "관찰 보류 (single-frame)" 로만 남김.
|
||||||
|
3. **패턴 / 기법은 token 이 아니라 family CSS / variant 영역** — `mix-blend-mode`, R16 pill 곡선, hue rotation, badge overhang, pill alternation 등.
|
||||||
|
4. **타이포 / 스페이싱은 값 직접 매칭이 아니라 위계 매핑** — figma raw px (frame 1280 폭 기준) ↔ slide-body token (slide-body 스케일) 은 스케일이 다르므로 위계만.
|
||||||
|
5. **gradient 는 from / to pair 단위 한 행**.
|
||||||
|
6. **`covered` 는 hex 정확 일치일 때만** — 부분 일치 / 변형은 `gap_candidate` 또는 Notes 에 별도 표기.
|
||||||
|
|
||||||
|
### 명명 컨벤션 (가벼운 가이드)
|
||||||
|
|
||||||
|
신규 token 후보에만 적용. 기존 token (`--color-*`, `--font-*`, `--space-*`, `--card-*`) 은 그대로 사용.
|
||||||
|
|
||||||
|
- `--c-*` color
|
||||||
|
- `--g-*` gradient
|
||||||
|
- `--fs-*` font-size
|
||||||
|
- `--sp-*` spacing
|
||||||
|
- `--r-*` radius
|
||||||
|
- `--sh-*` shadow
|
||||||
|
|
||||||
|
### 컬럼 정의
|
||||||
|
|
||||||
|
| 컬럼 | 의미 |
|
||||||
|
|---|---|
|
||||||
|
| **Token Category** | color / gradient / typography / spacing / radius / shadow |
|
||||||
|
| **Existing Token** | `templates/styles/tokens/{file}` 의 token 명. 없으면 `—` |
|
||||||
|
| **Covered Frame Observations** | frame 번호 + 짧은 컨텍스트 라벨만 (값 X). status 에 따라 의미 다름 (아래 참조) |
|
||||||
|
| **Gap / Candidate** | 신규 token 후보 (이름 + 값 + `(후보)`). 기존 token 검증인 행은 `—` |
|
||||||
|
| **Status** | `covered` / `gap_candidate` / `hierarchy_mapping_only` / `hold_recheck_after_conversion` |
|
||||||
|
| **Notes** | family CSS cross-reference + 변형 메타 + scale 경계 등 |
|
||||||
|
|
||||||
|
### `Covered Frame Observations` 셀의 Status 별 의미
|
||||||
|
|
||||||
|
| Status | `Covered Frame Observations` 의 의미 |
|
||||||
|
|---|---|
|
||||||
|
| `covered` | 이 token 이 *cover 한* frame 들 (hex 정확 일치) |
|
||||||
|
| `gap_candidate` | 이 후보가 *target 으로 하는* frame 들 (2+ frame 에서 동일 값 관찰) |
|
||||||
|
| `hierarchy_mapping_only` | 이 위계 매핑이 *적용 가능한* frame 들 (값 직접 매칭 X) |
|
||||||
|
| `hold_recheck_after_conversion` | 현재 cover 한 frame 없음 (`—`). 14 converted 기준 미관찰 — 전체 변환 후 재검증 |
|
||||||
|
|
||||||
|
### Token Inventory — 18 행
|
||||||
|
|
||||||
|
| Token Category | Existing Token | Covered Frame Observations | Gap / Candidate | Status | Notes |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| gradient | `--color-block-title-from` / `--color-block-title-to` (`#CC5200` / `#883700`) | F17, F18 (frame inner title) | — | `covered` | compare-paired family 의 title gradient slot. F12, F13 의 title 은 `from` 이 `#000` 으로 변형 — 별도 행 (`gap_candidate`) 처리 |
|
||||||
|
| gradient | `--color-col-1-from` / `--color-col-1-to` (`#0D78D0` / `#023056`) | F13 (기술 bar — blue tone) | — | `covered` | three-pillar.css 의 column 1. 1 frame 매칭이지만 token 자체가 frame 값을 정확 흡수 |
|
||||||
|
| gradient | `--color-col-2-from` / `--color-col-2-to` (`#FF9A23` / `#CC5200`) | F13 (사람 bar — orange tone) | — | `covered` | three-pillar.css 의 column 2. `#CC5200` 는 title gradient `from` 과 같은 hex 이지만 의미 다름 (column-2 끝값) |
|
||||||
|
| gradient | `--color-col-3-from` / `--color-col-3-to` (`#39BE49` / `#23742C`) | F13 (자연 bar — green tone) | — | `covered` | three-pillar.css 의 column 3 |
|
||||||
|
| color | `--color-compare-left` (`#5c3714`) | F18 (BIM / AS-IS 측 텍스트) | — | `covered` | compare-paired.css 의 좌측 / AS-IS 색 |
|
||||||
|
| color | `--color-compare-right` (`#285b4a`) | F14 (발주자 라벨), F18 (DX / TO-BE 측 텍스트) | — | `covered` | compare-paired.css 의 우측 / TO-BE + persona-cards.css 의 actor 색. F14, F18 모두 hex 정확 일치 (대소문자 제외) — 의미 다르지만 token 재사용 가능 |
|
||||||
|
| color | `--color-compare-badge` (`#ae3607`) | F18 (결론 강조 / VS 뱃지) | — | `covered` | compare-paired.css 의 결론 / VS 뱃지 강조색 |
|
||||||
|
| gradient | — | F12, F13 (title — `#000` → `#883700`) | `--g-title-dark: linear-gradient(#000, #883700)` (후보) | `gap_candidate` | title gradient 변종 (`from` 이 `#000`). `--color-block-title-to` 와 끝값 공유 — 신규 token 으로 묶을지 / variant 처리할지 검토. 영향 family : 미정 (cycle.css / three-pillar.css) |
|
||||||
|
| color | — | F23, F24 (table 헤더 좌 / 구분 — 청록 톤) | `--c-table-header-cyan: #589e8d` (후보) | `gap_candidate` | compare-table.css 의 좌측 / 구분 헤더. 기존 `--color-table-header-bg: #64748b` (회색) 과 다른 톤 — 회색 헤더 token 은 `hold` 행 참조 |
|
||||||
|
| color | — | F23, F24 (table 헤더 우 / 전용 — 오렌지 톤) | `--c-table-header-orange: #ef7a26` (후보) | `gap_candidate` | compare-table.css 의 우측 헤더 |
|
||||||
|
| color | — | F23, F24 (table 행 강조 키워드) | `--c-table-highlight: #a14101` (후보) | `gap_candidate` | compare-table.css 의 inline 강조색 |
|
||||||
|
| color | — | F23, F24 (table 행 교대 배경) | `--c-table-row-alt: rgba(253,198,158,0.16)` (후보) | `gap_candidate` | compare-table.css 의 alternating row bg. white / `--c-table-row-alt` 교대 |
|
||||||
|
| typography | `--font-slide-title` (22px) / `--font-zone-title` (13px) / `--font-sub-title` (12px) | F12, F13, F17, F18 (frame inner title — raw 70px Bold) | — | `hierarchy_mapping_only` | frame raw 70px 는 figma 1280 폭 기준 — slide-body 스케일과 다름. **위계 매핑만**. frame inner title 이 slide-body 안에서 어느 위계 (`--font-zone-title` / `--font-sub-title`) 로 매핑될지는 catalog 설계 단계 결정 |
|
||||||
|
| typography | `--font-body` (11px) | F12, F13, F16, F17, F18, F22, F23, F24 (본문 — raw 35~42px Medium) | — | `hierarchy_mapping_only` | frame raw 35~42px 는 figma 1280 폭 기준. slide-body 안에서는 `--font-body` 위계 적용 |
|
||||||
|
| spacing / radius | `--space-md` / `--space-lg` / `--card-radius` 등 | F09 (pill `radius: 30px`), F16 (배경 `radius: 50px`), F17 (행 `radius: 30px`), F18 (badge `radius: 10px`, pill `radius: 50px`), F23/F24 (셀 padding) | — | `hierarchy_mapping_only` | frame raw radius / gap / padding 은 figma 폭 기준. slide-body 안에서는 위계 매핑 + 재산정 필요 |
|
||||||
|
| color | `--color-dark-card-1` (`#1a365d`) / `-2` (`#1e3a2f`) / `-3` (`#3b1f2b`) / `-title` (`#fbbf24`) / `-body` (`#e2e8f0`) | — | — | `hold_recheck_after_conversion` | 다크 카드 시각 시스템 5 token. 14 converted 기준 미관찰. 전체 32 frame 변환 후 재검증 (특히 미변환 zone_extract 18 개) |
|
||||||
|
| color | `--color-pill-bg` (`#1e293b`) / `--color-pill-text` (`#ffffff`) | — | — | `hold_recheck_after_conversion` | 다크 pill 스타일. F09 pill (translucent + colored border) / F18 badge (gradient) 와 다른 톤. 14 converted 기준 미관찰 — 전체 변환 후 재검증 |
|
||||||
|
| color | `--color-table-header-bg` (`#64748b`) / `--color-table-header-text` (`#ffffff`) | — | — | `hold_recheck_after_conversion` | 회색계 표 헤더. F23 / F24 의 colored 헤더 (`#589e8d` / `#ef7a26`) 와 다른 톤. 14 converted 기준 미관찰. 향후 회색 헤더 frame 등장 시 재검증 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Legacy Reference
|
||||||
|
|
||||||
|
- legacy structures 6 개는 runtime 재사용 후보 X
|
||||||
|
- frame 변환본 (`figma_to_html_agent/blocks/`) 이 우선 source
|
||||||
|
- disposition 분류 목적은 archive / delete 판단
|
||||||
|
- Phase Z catalog / runtime 설계 근거로 **직접 사용 X**
|
||||||
|
|
||||||
|
### 발견 — 6 파일 모두 frame 변환본의 *slide-body 스케일 재구현*
|
||||||
|
|
||||||
|
각 legacy file 의 헤더 주석에 `Source: figma_to_html_agent/blocks/{figma_id}` 명시 (확인). figma 변환 (raw 1280 폭, 40~70px 폰트) → slide-body 스케일 (`var(--font-sub-title)` 12px, `var(--space-sm)` 8px 등 token 적용) 재구현 시도. 즉 *figma 변환과 별개의 legacy* 가 아니라, *figma 변환에서 파생된 slide-body 스케일 시도*.
|
||||||
|
|
||||||
|
→ Phase Z runtime 은 frame catalog + family CSS 로 rebuild 예정. legacy structures 는 *변환 검증 증거 / 토큰 매핑 참고* 외 직접 사용 X.
|
||||||
|
|
||||||
|
### `Phase Z Disposition` 값
|
||||||
|
|
||||||
|
- `archive` — 보존 (스타일 / 토큰 매핑 증거 가치)
|
||||||
|
- `delete_after_extract` — Style Note 추출 후 삭제
|
||||||
|
- `hold_until_catalog_ready` — Phase Z catalog 안정화 전까지 유지
|
||||||
|
|
||||||
|
### Legacy Reference — 6 행
|
||||||
|
|
||||||
|
| Legacy File | Current Role | Phase Z Disposition | Style Note | Notes |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `compare-table-2col.html` | F23 (`1171281203`) 의 slide-body 스케일 재구현. 표 헤더 colored / 행 교대 bg / 강조 inline color | `delete_after_extract` (후보) | inline hex (`#589e8d` / `#ef7a26` / `#a14101` / `rgba(253,198,158,0.16)`) 가 Token Inventory 의 `gap_candidate` 4 행과 정확 일치 | `compare-table.css` family 의 first reference 가치 |
|
||||||
|
| `compare-table-3col.html` | F24 (`1171281204`) 의 3-column variant slide-body 재구현 | `delete_after_extract` (후보) | F23 과 같은 hex + 추가 `rgba(62,53,35,0.9)` (column 2 어두운 brown 헤더). 단일 frame 관찰값 — Token Inventory 비승격 (룰 #2) | `compare-table.css` family variant — F23 과 통합 후보 |
|
||||||
|
| `compare-vs-rows.html` | F18 (`1171281195`) 의 slide-body 재구현 | `delete_after_extract` (후보) | `var(--color-compare-left)` / `var(--color-compare-right)` 기존 token 활용 — covered token 검증 증거 | `compare-paired.css` family |
|
||||||
|
| `issues-paired-rows.html` | F17 (`1171281194`) 의 slide-body 재구현 | `delete_after_extract` (후보) | `--color-row-border: #60A451` inline 정의 — F17 단일 frame 관찰값, Token Inventory 비승격 (룰 #2). family CSS variant 처리 검토 | `compare-paired.css` family |
|
||||||
|
| `prerequisites-3col.html` | F13 (`1171281190`) 의 slide-body 재구현 | `delete_after_extract` (후보) | `--color-col-N-{from,to}` 기존 token 활용 가능 — covered token 검증 증거 | `three-pillar.css` family |
|
||||||
|
| `stacked-arrow-list.html` | F09 (`1171281180`) 의 slide-body 재구현 | `delete_after_extract` (후보) | 타이틀 바 `#fbd5b9` / 텍스트 `#144838` inline — F09 단일 frame 관찰값, Token Inventory 비승격 (룰 #2) | `pill-list.css` family |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 진행 단계
|
||||||
|
|
||||||
|
| 단계 | 상태 |
|
||||||
|
|---|---|
|
||||||
|
| 33 frame ↔ Integration Map 32 행 대조 | ✅ 완료 (`FRAME-INTEGRATION-MAP.md` row 21~28 ID 정정 반영) |
|
||||||
|
| Inventory 골격 + 샘플 5 행 (이 문서) | ✅ 본 단계 |
|
||||||
|
| 사용자 검토 | ⬜ |
|
||||||
|
| Frame Inventory 27 행 일괄 확장 (32 frame 완성) | ⬜ |
|
||||||
|
| Token Inventory 본격 작성 | ⬜ |
|
||||||
|
| Legacy Reference 본격 작성 | ⬜ |
|
||||||
|
| Phase Z catalog / runtime template 설계 | ⬜ (별도 단계) |
|
||||||
|
| 사용자 승인 → `templates/blocks/` 신규 구조 교체 (프로모션 게이트) | ⬜ (별도 단계) |
|
||||||
|
|
||||||
|
### 샘플 5 검증 포인트
|
||||||
|
|
||||||
|
| 검증 항목 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| 변환 / 미변환 frame 이 같은 양식에 들어가는가 | ✅ — 미변환은 `Style Elements` 셀 1 개에 통일 보일러플레이트, 나머지 4 셀은 `—` (`Phase Z Target` 만 `TBD after conversion` 또는 `N/A — reference_only`) |
|
||||||
|
| `Style Elements` 와 `Extracted Style Hints` 의 구분이 명확한가 | ✅ (Frame 18, 14) — 관찰 사실 (CSS 값 / 픽셀) vs 계승 의도 (패턴 / 의미) |
|
||||||
|
| `Phase Z Target` 후보 표현이 통일되는가 | ✅ — 변환 frame 은 "(후보)" 명시, 미변환은 `TBD after conversion`, reference_only 는 `N/A — reference_only` |
|
||||||
|
| 미변환 frame 이 `analysis.md` 내용을 redescribe 하지 않는가 | ✅ — 보일러플레이트 1 행 외 어떤 redescription 도 없음 |
|
||||||
|
| 변환 frame 의 `Notes` 가 스타일 계승에 의미 있는 메타만 담는가 | ⚠️ 샘플은 적정. 32 행 확장 시 단순 출처 (Scale 값, "대표 frame" 등) 는 추가로 정리 필요 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 부록 — 제외 / 특수 항목
|
||||||
|
|
||||||
|
`figma_to_html_agent/blocks/` 의 33 개 디렉토리 중 `1171281171` 은 본 인벤토리 메인 32 frame 에서 제외한다 (`texts.md` 만 존재, `index.html` / `analysis.md` 없음, 정체 미확인). 상세는 [`FRAME-INTEGRATION-MAP.md` 부록](FRAME-INTEGRATION-MAP.md#부록--제외--특수-항목) 참조.
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Phase Z 매칭 아키텍처 — 원칙 anchor
|
||||||
|
|
||||||
|
> 22-step pipeline 의 *매칭 layer* (Step 5/6/7 + B-axis Step 9/10/11) 를 다루는 *원칙 anchor* doc.
|
||||||
|
>
|
||||||
|
> 본 문서는 *새 디자인 생성* 이 아니라 *기존 코드 / 과거 docs / 관찰 사례 / session 인사이트* 를 근거로 한 *forward improvement consolidation*.
|
||||||
|
>
|
||||||
|
> 관련 anchors :
|
||||||
|
> - `docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md` (D1, 22-step pipeline)
|
||||||
|
> - `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` (D2, Layer A/B SPEC)
|
||||||
|
> - `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-PLAN.md` (D3, schema completion plan)
|
||||||
|
> - `docs/architecture/PHASE-Z-PIPELINE-STATUS-BOARD.md` (D4, 현재 status)
|
||||||
|
|
||||||
|
|
||||||
|
## §0. 본 문서의 작성 방법론 (P0)
|
||||||
|
|
||||||
|
**evidence-based forward improvement** — 매번 *4 source* 를 거쳐 도출 :
|
||||||
|
|
||||||
|
1. 과거 docs (D1 / D2 / D3 / D4) — *원래 의도*
|
||||||
|
2. 현재 코드 (`src/phase_z2_*.py` / `templates/phase_z2/catalog/frame_contracts.yaml`) — *실제 동작*
|
||||||
|
3. 관찰된 혼선 / 실패 사례 — *경험적 evidence*
|
||||||
|
4. session 인사이트 — *최근 정리된 mental model*
|
||||||
|
|
||||||
|
이걸 *관찰 → 혼선 → 도출 원칙 → 개선 방향* form 으로 표면화. *과거 회귀* 가 아니라 *과거를 근거로 한 개선*. *감으로 결정* / *가장 쉬운 선택* 금지.
|
||||||
|
|
||||||
|
|
||||||
|
## §1. 현재 코드 상태 (관찰)
|
||||||
|
|
||||||
|
### 1.1 매칭 단위 — `CompositionUnit`
|
||||||
|
|
||||||
|
매칭의 *atomic unit* 은 section 이 아니라 `CompositionUnit`. `src/phase_z2_composition.py` 의 `collect_candidates` 가 *3 종 candidate type* 을 동시 생성 :
|
||||||
|
|
||||||
|
- `single` — `src/phase_z2_composition.py:230-243` (1 section 단독)
|
||||||
|
- `parent_merged` — `src/phase_z2_composition.py:260-273` (parent 자체가 V4 매칭, children 묶음)
|
||||||
|
- `parent_merged_inferred` — `src/phase_z2_composition.py:352-368` (children rep_match 기반 inferred merge)
|
||||||
|
|
||||||
|
각 candidate 는 `source_section_ids[]` + `merge_type` (line 232 / 262 / 354) 로 *어떤 section 들을 어떻게 묶었는지* 표현.
|
||||||
|
|
||||||
|
### 1.2 frame 선택 — render path authority = V4 rank-1
|
||||||
|
|
||||||
|
현재 render path 의 `frame_template_id` 는 composition planner 의 *V4 rank-1* 기반 :
|
||||||
|
- `lookup_v4_match()` in `src/phase_z2_pipeline.py` (D1:212 명시) — rank-1 만 반환
|
||||||
|
- composition planner 가 V4 결과로 candidate 생성
|
||||||
|
|
||||||
|
B4 (`src/phase_z2_placement_planner.py:90-107` 의 `_select_frame`) 도 frame selection 을 *수행* 하지만 *trace-only* — render path authority 는 V4 rank-1.
|
||||||
|
|
||||||
|
V4 = `top-k` 선언만 됐고 *rank-1 사용* 만 활성. D4 의 ⚠ partial.
|
||||||
|
|
||||||
|
### 1.3 binding — `phase_z2_mapper.map_with_contract` (Layer 1)
|
||||||
|
|
||||||
|
`src/phase_z2_mapper.py:584-609` 의 `map_with_contract(section, contract)` 는 contract 에서 *오직* 두 field 만 read :
|
||||||
|
- `contract["source_shape"]` (split 규칙)
|
||||||
|
- `contract["payload"]["builder"]` (named PAYLOAD_BUILDERS dispatch)
|
||||||
|
|
||||||
|
`accepted_content_types` / `sub_zones` 등 Layer 2 field 는 *접근 X*. 즉 mapper 는 *Layer 1 binder* 로 격리.
|
||||||
|
|
||||||
|
### 1.4 placement — `phase_z2_placement_planner` (Layer 2 reader, trace-only)
|
||||||
|
|
||||||
|
`src/phase_z2_placement_planner.py` :
|
||||||
|
- `_select_frame` (90-107) — `accepted_content_types ⊇ content_type_set` cover + declaration order first
|
||||||
|
- `_assign_region_to_sub_zone` (113-151) — `sub_zones` 의 narrowest-accepts first
|
||||||
|
- `plan_placement` (157+) — Stage A (B2 internal regions) + Stage B (region ↔ sub_zone) 통합
|
||||||
|
|
||||||
|
B4 는 *Layer 2 reader* 이지만 `src/phase_z2_pipeline.py:1060-1145` 에서 *trace-only* 로만 호출. render path (slot_payload 생성) 미연결. D4:78-83 의 *render path placement_trace 미사용* 과 일치.
|
||||||
|
|
||||||
|
### 1.5 capacity / fit — classifier (post-render telemetry)
|
||||||
|
|
||||||
|
`src/phase_z2_classifier.py` 의 `classify_visual_runtime_check` — *render 후* visual measurement 기반 fit 판정. *Architectural reframe* (D2:16-33 의 PLANNING / RENDER / POST-RENDER 3-layer) : A1~A4 = *post-render telemetry layer*. *덜 중요* 가 아니라 *위치가 다름*. 매칭 결정 자체에는 직접 입력 X (현재).
|
||||||
|
|
||||||
|
|
||||||
|
## §2. 확인된 혼선 (resolution)
|
||||||
|
|
||||||
|
### 2.1 Sub-zone 용어 4-방향 충돌 → 4-tier 분리 (P5'')
|
||||||
|
|
||||||
|
session 안 누적 혼선 :
|
||||||
|
- D1 의 "Zone (top / bottom_l / bottom_r 등)"
|
||||||
|
- composition.py 의 child unit 분리 (single / parent_merged 등)
|
||||||
|
- yaml 의 `sub_zones` field
|
||||||
|
- 사용자 mental model 의 "### split"
|
||||||
|
|
||||||
|
→ *4 tier 별 명칭 lock* :
|
||||||
|
|
||||||
|
| Tier | 명칭 | 위치 | 정체 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **Layout** | slide-level | slide-body 안 zone topology (8-preset vocabulary) |
|
||||||
|
| 2 | **Zone** | layout 안 | 콘텐츠 구역 (top / bottom_l / bottom_r 등). 1 frame 매칭 단위 |
|
||||||
|
| 3 | **Split Zone (Child Unit)** | zone 안 | composition planner 가 만든 child unit (parent_merged 의 분리 단위) |
|
||||||
|
| 4 | **Frame Slot** | frame 안 | yaml 의 `sub_zones` = Layer B placement target. region 매칭 대상 |
|
||||||
|
|
||||||
|
이후 본 문서 / 후속 doc 은 *위 명칭 어휘* 만 사용. yaml field `sub_zones` 는 *기존 이름 유지하되 의미 = Frame Slot* 으로 lock (mechanical rename 은 D3 plan).
|
||||||
|
|
||||||
|
### 2.2 composition tie-break ≠ Frame Slot tie-break
|
||||||
|
|
||||||
|
scope-lock 단계에서 *D2:524-533 의 tie-break* 을 *composition tie-break evidence* 로 잘못 분류. 실제로는 :
|
||||||
|
- D2:524-533 = *Frame Slot 단위* tie-break (Stage B 안, frame 이미 선택된 후)
|
||||||
|
- composition tie-break = `composition.py:433-441` (sort + greedy + coverage)
|
||||||
|
|
||||||
|
두 layer 가 다름. P7 evidence 는 *composition layer* 만, *Frame Slot layer* 는 별개 location.
|
||||||
|
|
||||||
|
### 2.3 "Layer 2 미사용" framing 오류
|
||||||
|
|
||||||
|
scope-lock 초안의 *"mapper 가 Layer 2 를 consume 안 함"* framing 은 암묵적으로 *mapper 가 Layer 2 reader 가 돼야 한다* 의 fix direction 함의. 실제 architectural intent :
|
||||||
|
- mapper = Layer 1 binder (의도된 격리)
|
||||||
|
- B4 = Layer 2 reader (의도된 분리)
|
||||||
|
|
||||||
|
*문제* 는 *Layer 2 unconsumed* 가 아니라 *B4 의 Layer 2 read 가 trace-only 라 render path 까지 안 닿음*. *bridge* 가 답이지 *mapper 확장* 이 아님.
|
||||||
|
|
||||||
|
### 2.4 Step 4/5/9 분리 미완
|
||||||
|
|
||||||
|
현재 구현은 D1 이 의도한 *Step 4 → Step 5 → Step 9* 분리가 완성되지 않음 :
|
||||||
|
- Step 4 (composition planning input — section_layout_signature / content_object 구조) = *partial/dormant* (D4:73 의 ⚠ partial transition)
|
||||||
|
- Step 5 evidence 와 Step 9 final selection = *conflate* (D1:255)
|
||||||
|
|
||||||
|
D1:185 가 명시적으로 *Step 4 가 frame matching 보다 먼저 와야 함* 을 지적.
|
||||||
|
|
||||||
|
|
||||||
|
## §3. 도출된 원칙
|
||||||
|
|
||||||
|
### P1 : 매칭 단위 = `CompositionUnit` (section 아님)
|
||||||
|
|
||||||
|
- *근거* : `composition.py:230-243` (single) / `:260-273` (parent_merged) / `:352-368` (parent_merged_inferred). 각 line `:232` / `:262` / `:354` 가 `merge_type` signal
|
||||||
|
- *함의* : *MDX section* 을 1:1 zone 매칭 단위로 가정하는 코드 / doc 는 모두 *추상화 누락*. *CompositionUnit* 으로 통일.
|
||||||
|
|
||||||
|
### P3 : frame 선택 / content binding *layer 분리*
|
||||||
|
|
||||||
|
- *근거* : 현재 코드의 *de facto* 분리 (mapper §1.3 + placement_planner §1.4)
|
||||||
|
- *원칙* : frame *선택* (어떤 frame 을 쓸지) 과 content *binding* (선택된 frame 에 콘텐츠를 어떻게 채울지) 는 *별개 axis*. 함께 결정하면 search space 가 곱셈으로 폭발 + 실패 격리 불가.
|
||||||
|
- *함의* : 어떤 axis 가 frame 을 *결정* 하는지 + 어떤 axis 가 *binding* 하는지 *명시적 분리* — 한 module 이 둘 다 하면 회귀 위험.
|
||||||
|
|
||||||
|
### P5'' : 4-tier terminology lock
|
||||||
|
|
||||||
|
§2.1 표 그대로. *후속 doc / commit message / 코드 docstring* 모두 4-tier 어휘만 사용.
|
||||||
|
|
||||||
|
### P7 : candidate-based composition (sequential A/B 아님)
|
||||||
|
|
||||||
|
- *근거* :
|
||||||
|
- 원리 : `docs/architecture/PHASE-Z-PIPELINE-OVERVIEW.md:216-218` ("child 따로 / sibling 묶기 / parent 단위" + scoring inputs)
|
||||||
|
- 구현 : `composition.py:230-243` / `:260-273` / `:352-368` (3 candidate type 동시 생성)
|
||||||
|
- 선택 : `composition.py:433-441` (`(score desc, source_section_ids count desc)` + greedy covered skip)
|
||||||
|
- *원칙* : composition 은 "## → fail → ### split → re-merge" sequential 이 아니라 *3 candidate type 을 동시 생성 → score + coverage tie-break* 의 *parallel evaluation*.
|
||||||
|
- D2:524-533 은 *Frame Slot tie-break (Stage B)* 로 본 P7 evidence 와 *별도 layer*.
|
||||||
|
|
||||||
|
### P8 : staged migration — frame 선택 authority + binding 진화 분리
|
||||||
|
|
||||||
|
#### P8-a (near-term)
|
||||||
|
|
||||||
|
- *목표* : frame selection authority 정리 (V4 rank-1 단독 → B4 informant 또는 B4-mediated 의 *어떤 형태*)
|
||||||
|
- *유지* : mapper binding channel (PAYLOAD_BUILDERS) — 현재 working channel 안전
|
||||||
|
- *open* : B4 결과를 render path frame selection 에 *어떻게 반영* 할지의 bridge 형태 → §5 의 Q-V4B4 / Q-LB 에서 별도 scope-lock
|
||||||
|
|
||||||
|
#### P8-b (longer-term, *지금 결정 X*)
|
||||||
|
|
||||||
|
- region / Frame Slot-aware slot_payload 진화
|
||||||
|
- PLACEMENT_PAYLOAD_BUILDERS 등 별도 namespace 가능성
|
||||||
|
- *DC2 Open question (Q-LB) 해결 후* 결정. 본 문서 시점에서는 *deferred*.
|
||||||
|
|
||||||
|
|
||||||
|
## §4. 코드 변경 결정 지점
|
||||||
|
|
||||||
|
### DC1 : Step 4/5/9 분리 미완
|
||||||
|
|
||||||
|
- *현 상태* : D1 의도한 Step 4 → Step 5 → Step 9 분리가 완성되지 않음. Step 4 (composition planning input — section_layout_signature / content_object 구조) 는 *partial/dormant* (D4:73 의 ⚠ partial), Step 5 evidence 와 Step 9 final selection 은 *conflate* (D1:255).
|
||||||
|
- *결정 지점* :
|
||||||
|
- Step 4 가 frame matching *전* 에 composition candidate 평가 input 으로 흘러야 (D1:185 명시적 지적)
|
||||||
|
- Step 5 evidence layer 와 Step 9 final selection layer 의 *명시적 분리* 필요
|
||||||
|
- *영향 module* : `src/phase_z2_pipeline.py` orchestrator + `src/phase_z2_composition.py` (input 확장)
|
||||||
|
|
||||||
|
### DC2-a : `phase_z2_mapper` = Layer 1 working channel
|
||||||
|
|
||||||
|
- *현 상태* : `src/phase_z2_mapper.py:584-609` 가 `source_shape` + `payload.builder` 만 read. *현재 render path 의 유일 working channel*.
|
||||||
|
- *결정 지점* : *Layer 1 격리 유지*. mapper 에 Layer 2 read 를 추가하는 방향은 *반대* — 실제 의도는 별 axis (DC2-b + Open question Q-LB).
|
||||||
|
|
||||||
|
### DC2-b : `phase_z2_placement_planner` = Layer 2 reader, render path 미연결
|
||||||
|
|
||||||
|
- *현 상태* : `src/phase_z2_placement_planner.py:90-107` (accepted_content_types) + `:113-151` (sub_zones) — Layer 2 read 활성. 그러나 `src/phase_z2_pipeline.py:1060-1145` 에서 *trace-only*. final render 미연결.
|
||||||
|
- *결정 지점* : Layer 2 read 결과를 render path 에 *어떻게 합류시킬지* 의 bridge 결정 — §5 의 Q-LB 에서 별도 scope-lock.
|
||||||
|
|
||||||
|
### DC3 : Internal Region runtime — trace-only partial
|
||||||
|
|
||||||
|
- *현 상태* : SPEC v1 (D2) 가 Internal Region (Layer A) / Frame Slot (Layer B) 의 layered placement 정의. Layer A runtime 은 *trace-only partial* — `src/phase_z2_placement_planner.py:157+` 의 `plan_placement` 가 Stage A 호출 (`:192-197` 의 `plan_internal_regions` call) 수행하지만, 결과가 render path 미합류. *부재* 가 아니라 *render path authority 미연결*. D4:73 의 ⚠ partial 분류와 일치.
|
||||||
|
- *결정 지점* : Layer A planning telemetry 활성 단계 → render path 합류 단계의 *separate axis*. P8-b 와 연관 (slot_payload 진화 이전 단계).
|
||||||
|
|
||||||
|
|
||||||
|
## §5. open scope-lock questions
|
||||||
|
|
||||||
|
본 axis 범위 *밖* 의 deferred 결정. 후속 axis 에서 각각 별도로 lock.
|
||||||
|
|
||||||
|
### Q-FW : 4 filter score weight / threshold
|
||||||
|
- F1 frame internal fit / F2 child frame divergence / F3 content type-structure match / F4 content loss risk 의 *score weight* 와 *auto vs review threshold* 결정 필요.
|
||||||
|
|
||||||
|
### Q-CO : candidate priority tie-break
|
||||||
|
- composition.py:433 의 `(score, source_section_ids count)` 외에 *추가 axis* (cardinality_fit / hierarchy_coherence / density) 도입 시점.
|
||||||
|
|
||||||
|
### Q-RT : review / adapter_needed threshold
|
||||||
|
- W1/W2/W3 신호 + auto_selectable=False candidate 의 review surface 정책.
|
||||||
|
|
||||||
|
### Q-V4B4 : V4 → B4 frame selection authority transition timing
|
||||||
|
- P8-a 의 *언제* — V4 단독 / B4 informant / B4-mediated 의 단계.
|
||||||
|
|
||||||
|
### Q-LB : Layer 1 ↔ Layer 2 bridge architecture
|
||||||
|
- DC2-b 의 *어떻게* — mapper direct consume / B4-mediated integration / 별도 translator. Q-V4B4 와 연관되나 *별개 layer* (frame 선택 timing vs Layer 2 → render path bridge 구조).
|
||||||
|
|
||||||
|
### Q-CE : catalog extension surface
|
||||||
|
- frame_contracts.yaml 에 신규 frame 추가 시 *어떤 layer 부터 채워야 self-consistent* 인지의 declarative form.
|
||||||
|
|
||||||
|
### Q-DT : decision trace 표준 form
|
||||||
|
- composition / placement / fit_classification 의 trace 통합 schema. 현재 각각 다른 dict 구조.
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
# Phase Z — master pipeline overview
|
||||||
|
|
||||||
|
**Status** : 마스터 reference (2026-04-30 잠금). 본 문서 = *워크플로우 전체 도면*. 향후 모든 작업은 *이 22-step 도면의 어느 위치에 속하는지* 먼저 self-locate 해야 함.
|
||||||
|
|
||||||
|
**용도** :
|
||||||
|
- 새 작업 시작 시 — "지금 하는 게 22-step 중 어느 step 인가" 식별
|
||||||
|
- 새 spec / memory rule 추가 시 — "어느 step 의 어느 의사결정에 적용되는가" 매핑
|
||||||
|
- 새 sample / 새 frame 추가 시 — "어디서 막힐 가능성이 높은가" 사전 예측
|
||||||
|
|
||||||
|
**본 문서가 *하지 않는* 것** :
|
||||||
|
- 새 구현 제안 X
|
||||||
|
- next step 추천 X
|
||||||
|
- 우선순위 결정 X
|
||||||
|
- A/B/C 선택지 X
|
||||||
|
- specific MDX sample 분석 X
|
||||||
|
|
||||||
|
본 문서는 *기준점*. 의사결정은 별도 step 에서.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3-block 구조
|
||||||
|
|
||||||
|
전체 22 step 은 다음 3 block 으로 grouping :
|
||||||
|
|
||||||
|
| Block | Step 범위 | 역할 |
|
||||||
|
|---|---|---|
|
||||||
|
| **A. PRE-RENDER PLANNING** | 0 — 12 | render *전* 모든 결정 — *slide-level zone 분배* + *zone-internal region 분배* + frame / slot 매핑. *진짜 fit policy 의 중심* |
|
||||||
|
| **B. RENDER** | 13 | Jinja2 + frame partial → final.html |
|
||||||
|
| **C. POST-RENDER TELEMETRY / EXCEPTION HANDLING** | 14 — 22 | render 결과 검증 + 분류 + routing + status. *exception 처리 layer* |
|
||||||
|
|
||||||
|
**중요** : 진짜 fit policy 의 자리는 A block (composition planning). C block (telemetry) 은 *exception 처리 + 진단 안내* layer. 둘 다 필요하지만 *위치가 다름*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 위계 + 용어 (entity hierarchy)
|
||||||
|
|
||||||
|
본 파이프라인은 다음 entity 위계 위에서 동작 :
|
||||||
|
|
||||||
|
> **Lock phrase (canonical)** : `Slide → Zone → Internal Region → Frame → Frame Slot → Content`
|
||||||
|
|
||||||
|
```
|
||||||
|
Slide
|
||||||
|
└─ Zone (slide-level layout 이 만든 큰 영역)
|
||||||
|
└─ Internal Region (zone *내부* 영역, frame *밖*)
|
||||||
|
└─ Frame (Figma design 단위)
|
||||||
|
└─ Frame Slot (frame *내부* 자리)
|
||||||
|
└─ Content unit (text / table / image / details / ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Universal Region Model
|
||||||
|
|
||||||
|
> **Lock phrase (canonical)** :
|
||||||
|
> `Every Zone has 1+ Internal Regions.`
|
||||||
|
> `text-only zone = single-region.`
|
||||||
|
> `mixed-content zone = multi-region.`
|
||||||
|
|
||||||
|
```
|
||||||
|
모든 Zone 은 1 개 이상의 Internal Region 을 가짐.
|
||||||
|
|
||||||
|
text-only zone = single-region zone (현 거동의 자연 표현)
|
||||||
|
mixed-content zone = multi-region zone
|
||||||
|
|
||||||
|
각 Internal Region 은 *자기만의* :
|
||||||
|
- frame match
|
||||||
|
- display strategy (inline / preview+details / popup-only / dropped)
|
||||||
|
을 가질 수 있음.
|
||||||
|
```
|
||||||
|
|
||||||
|
text-only section 도 *single-region zone* 으로 표현 (= 현 거동 보존). mixed-content (text + table / text + image / 등) 은 *multi-region zone* 으로 확장. region 이 *first-class entity* — special case 가 아님.
|
||||||
|
|
||||||
|
### 용어 표
|
||||||
|
|
||||||
|
| 용어 | 의미 | 위치 |
|
||||||
|
|---|---|---|
|
||||||
|
| **Slide** | 1280×720 한 장 | 최상위 |
|
||||||
|
| **Zone** | slide-level layout 이 만든 큰 영역 (top / bottom / left / right 등) | Slide 안 |
|
||||||
|
| **Internal Region** | Zone *내부* 영역, frame *밖*. content type 기반 분할 (text region / table region / image region / details region) | Zone 안 |
|
||||||
|
| **Frame** | Figma design 단위 (= F13 / F29 / F16 등) | Internal Region 안 |
|
||||||
|
| **Frame Slot** | frame *내부* 자리 (= pillar_1 / quadrant_1 / process_column 등) | Frame 안 |
|
||||||
|
| **Content unit** | MDX section 안의 typed 콘텐츠 조각 (text_block / table / image / details / ...) | Frame Slot 에 배치 |
|
||||||
|
|
||||||
|
> **주의** : `PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` 의 `sub_zones` (YAML 필드명) 은 본 표의 *Frame Slot (Layer B)* 의미로 정의되어 있음. 본 표의 *Internal Region (Layer A)* 는 SPEC v1 §2 에 정의됨.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operating Principles / Hard Locks
|
||||||
|
|
||||||
|
본 섹션 = *anchor / index*. 각 원칙의 상세 정의 / 적용 룰 / 예외 처리는 *referenced source* 에 있음. drift 방지를 위해 OVERVIEW 는 *짧은 anchor* 로만 둠.
|
||||||
|
|
||||||
|
### 1. MDX mapping convention
|
||||||
|
|
||||||
|
| MDX | 슬라이드 |
|
||||||
|
|---|---|
|
||||||
|
| `# 대목차 제목` | `slide-title` |
|
||||||
|
| `# 대목차 결론` / note | `slide-footer` |
|
||||||
|
| `##` / `###` 본문 | `slide-body` 안 (layout + zone + region + frame + slot) |
|
||||||
|
| `<details>` | 별도 details layer |
|
||||||
|
|
||||||
|
> 참조 : `CLAUDE.md` 의 *MDX → 슬라이드 매핑* 표
|
||||||
|
|
||||||
|
### 2. 자유 디자인 금지
|
||||||
|
|
||||||
|
Figma frame DB / catalog / frame contract 기반으로만 디자인 결정. *임의 HTML / CSS 디자인 생성 X*. AI 가 frame 자체 / layout 자체 / 새 디자인 패턴을 *생성하지 않음*.
|
||||||
|
|
||||||
|
> 참조 : `CLAUDE.md` 디자인 원칙 + `feedback_no_hardcoding` + `feedback_blocks_must_be_css`
|
||||||
|
|
||||||
|
### 3. 원문 무손실
|
||||||
|
|
||||||
|
MDX 원문 *삭제 / 요약 / 압축 금지*. AI 호출이 normal path 에서 콘텐츠를 *재작성하지 않음*. 원문은 본문 preview 또는 details/popup 어딘가에 *반드시* 보존.
|
||||||
|
|
||||||
|
> 참조 : `feedback_ai_isolation_contract` + `PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §5.2
|
||||||
|
|
||||||
|
### 4. 그릇 변경 원칙 (positive form)
|
||||||
|
|
||||||
|
콘텐츠가 안 맞을 때 *콘텐츠를 줄이지 않음*. 대신 *그릇* (layout / zone / internal region / frame / display strategy) 을 변경하여 수용. 공통 CSS / padding / tolerance 임의 축소는 *그릇 변경* 이 아님 → 금지.
|
||||||
|
|
||||||
|
> 참조 : `feedback_phase_z_spacing_direction`
|
||||||
|
|
||||||
|
### 5. preview / details 원칙
|
||||||
|
|
||||||
|
inline preview = 원문의 *일부* 만 빌려 보여주는 것. 원문은 details / popup 에 *반드시* 보존. preview 자체가 원문을 대체하지 않음.
|
||||||
|
|
||||||
|
> 참조 : `PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §5.1 / §5.5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Heritage / Current State (참고)
|
||||||
|
|
||||||
|
본 섹션 = 시간 따라 변할 수 있는 *history / state* 기록. *원칙* 아님. frame DB 확장 / vocabulary 진화 시 mechanical 갱신.
|
||||||
|
|
||||||
|
### 1. Type A / B / B' / B'' → 8-layout vocabulary 진화
|
||||||
|
|
||||||
|
기존 *Type A / B / B' / B''* 의 4 preset 은 사라진 게 아니라 **8-layout vocabulary** (single / horizontal-2 / vertical-2 / top-1-bottom-2 / top-2-bottom-1 / left-1-right-2 / left-2-right-1 / grid-2x2) 로 *일반화* 된 *전신*. Step 7 의 8 vocabulary 는 이 진화의 결과.
|
||||||
|
|
||||||
|
### 2. 현재 runtime-verified frame set 은 text-frame 중심
|
||||||
|
|
||||||
|
현재 *runtime contract-registered / verified* frame set = `F13` (three_parallel_requirements) / `F29` (process_product_two_way) / `F16` (bim_issues_quadrant_four) — *모두 text 전용 성격*. `figma_to_html_agent/blocks` 의 전체 frame inventory 가 image / table / mixed frame 을 얼마나 포함하는지는 *전수 audit 전까지 미확정*. 따라서 현재 runtime 기준 :
|
||||||
|
|
||||||
|
- *text region* → frame 매칭 (현 거동)
|
||||||
|
- *image region* / *table region* / *details region* → 현재 contract-registered frame set 안에서는 frame 매칭 근거가 부족하므로 *display strategy* 로 처리 (image area 직접 배치 / table preview / details button 등)
|
||||||
|
|
||||||
|
**Step 9 의 region 단위 매칭은 현재 *runtime-verified frame set 기준으로 text region 만 frame 매칭 가능*** 함을 인지. 전체 frame inventory audit 또는 contract 등록 상태가 바뀌면 본 항목은 갱신 대상.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 22-step 상세
|
||||||
|
|
||||||
|
각 step 의 형식 :
|
||||||
|
> **Step N. 이름** — purpose (1-2 줄)
|
||||||
|
> **Status** : ✅ implemented / ⚠ partial / ❌ missing
|
||||||
|
> **Code 위치** : (해당 시)
|
||||||
|
> **Gap** : (해당 시)
|
||||||
|
|
||||||
|
### Block A — PRE-RENDER PLANNING
|
||||||
|
|
||||||
|
#### Step 0. 사전 준비
|
||||||
|
파이프라인 가동 전 준비되어 있어야 하는 정적 자료들. catalog / contract / matching data / template / asset.
|
||||||
|
|
||||||
|
- **포함** : Figma/BEP frame → HTML 변환물 / frame catalog / frame contract / V4 matching data + ontology / slide-base template / render assets
|
||||||
|
- **frame contract 필수 필드** : frame_id, template_id, accepted_content_types, slots, sub_zones, capacity, visual_hints, asset paths
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `templates/phase_z2/catalog/frame_contracts.yaml` / `tests/matching/v4_full32_result.yaml` / `templates/phase_z2/slide_base.html` / `templates/phase_z2/families/*.html` / `figma_to_html_agent/blocks/`
|
||||||
|
- **Gap** : `accepted_content_types` 와 `sub_zones` 필드 contract 에 미선언. *visual_hints* 는 일부만 (min_height_px). *density envelope* 미선언.
|
||||||
|
|
||||||
|
#### Step 1. MDX 업로드
|
||||||
|
사용자 MDX 파일 입력. 목표 : *MDX 1 → 자동 슬라이드 1 장*.
|
||||||
|
|
||||||
|
- **Status** : ✅ implemented
|
||||||
|
- **Code 위치** : `src/phase_z2_pipeline.py` 의 CLI entry (`run_phase_z2_mvp1(mdx_path, run_id)`)
|
||||||
|
|
||||||
|
#### Step 2. MDX 정규화
|
||||||
|
업로드된 MDX 를 파이프라인 표준 구조로 변환. frontmatter 분리 / slide title / heading tree / section id / 대중소 목차 관계 / note·footer·details 분리 / **raw content 보존**.
|
||||||
|
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `parse_mdx()` (frontmatter, ## sections, footer 추출) + `align_sections_to_v4_granularity()` (### drilling)
|
||||||
|
- **Gap** : heading tree 자체는 미생성 (현재 flat list). note / details 분리 미완. 대중소 목차 관계도 implicit. 정규화 결과가 *단순 문자열 + section_id* 수준 — heading tree 가 있는 *정규화 MDX 모델* 이 아직 아님.
|
||||||
|
|
||||||
|
#### Step 3. Content Object 추출
|
||||||
|
각 section 의 raw content 를 type 별 객체로 분해. text_block / bullet_list / numbered_list / table / image / diagram / jsx_block / note / details / long_original. *MDX 원문 보존, AI 요약 X*.
|
||||||
|
|
||||||
|
- **Status** : ❌ missing
|
||||||
|
- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §1 (content_object 정규화 schema)
|
||||||
|
|
||||||
|
#### Step 4. Section Internal Composition Planning
|
||||||
|
각 section 을 어떻게 다룰지 결정 — *whole-section 단일 frame 매칭* / *child-section grouping* / *content-type split* 의 3-way decision. split 인 경우 *Internal Region* 들로 분해 + region 비율 산정. **이 단계가 frame matching 보다 *먼저* 와야 함**.
|
||||||
|
|
||||||
|
- **3-way decision** :
|
||||||
|
```
|
||||||
|
section 전체 → 1 frame 매칭 가능?
|
||||||
|
├ YES → whole-section frame match (single-region zone)
|
||||||
|
└ NO → child-section grouping 가능?
|
||||||
|
├ YES → group merge → 1 frame 매칭 (single-region zone)
|
||||||
|
└ NO → content-type split
|
||||||
|
→ text region / table region / image region / details region
|
||||||
|
→ region 비율 산정 (예: text 80% / table 20%)
|
||||||
|
→ multi-region zone
|
||||||
|
```
|
||||||
|
- **출력** :
|
||||||
|
- `section_layout_signature` = text_only / text_plus_table / text_plus_image / table_heavy / image_with_caption / mixed_visual_text / details_heavy
|
||||||
|
- `composition_decision` = whole / group / split
|
||||||
|
- `internal_regions` (split 인 경우) = [{region_id, role, content_type, ratio_estimate}, ...]
|
||||||
|
- **Status** : ❌ missing
|
||||||
|
- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §2 (Internal Region schema, Layer A — entity / Universal Region Model / 3-way decision tree / 비율 산정 / topology vocabulary / region → frame·display interface). §1 의 content_object size_estimate / role 도 입력 자료.
|
||||||
|
|
||||||
|
#### Step 5. Matching Evidence 생성
|
||||||
|
정규화된 section + layout need 기반으로 V4 매칭 evidence 수집. *최종 선택이 아니라 후보 evidence*.
|
||||||
|
|
||||||
|
- **대상** : 소목차 section / 중목차 parent / 필요 시 sibling group 후보
|
||||||
|
- **V4 출력** : top-k frame candidates (frame_id, template_id, confidence, label, axes score)
|
||||||
|
- **Label** : use_as_is / light_edit / restructure / reject
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `lookup_v4_match()` in `phase_z2_pipeline.py`
|
||||||
|
- **Gap** : 현재 *rank-1 만* 반환. top-k 사용 안 됨. sibling group 후보도 없음.
|
||||||
|
|
||||||
|
#### Step 6. Composition Planning
|
||||||
|
어떤 MDX 덩어리를 하나의 *slide-level zone unit* 으로 볼지 결정. child 따로 / sibling 묶기 / parent 단위.
|
||||||
|
|
||||||
|
- **판단 기준** : heading 관계 / content_object 구조 / section_layout_signature / V4 top-k evidence / frame compatibility / capacity fit / content density
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `src/phase_z2_composition.py` (`plan_composition`, `parent_merged_inferred`, `capacity_fit` integration)
|
||||||
|
- **Gap** : section_layout_signature / content_object 구조 input 부재 (step 3, 4 가 없어서). frame compatibility 도 rank-1 매칭만 활용.
|
||||||
|
|
||||||
|
#### Step 7. Slide-Level Layout Planning
|
||||||
|
composition unit 개수와 성격을 보고 slide 전체 layout 선택. *기존 Type A/B/B'/B'' 의 후속 — 8-vocabulary 로 명시화*.
|
||||||
|
|
||||||
|
- **8 layout vocabulary** : single, horizontal-2, vertical-2, top-1-bottom-2, top-2-bottom-1, left-1-right-2, left-2-right-1, grid-2x2
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `src/phase_z2_composition.py` 의 `select_layout_preset()` + `LAYOUT_PRESETS`
|
||||||
|
- **Gap** : 현재 *count-based 만* (1→single, 2→horizontal-2, 3→top-1-bottom-2, 4→grid-2x2). "성격" (content_object 분포 / section_layout_signature) 미반영. 8 preset 중 horizontal-2 + single 만 실제 검증됨.
|
||||||
|
|
||||||
|
#### Step 8. Zone + Internal Region Ratio Planning
|
||||||
|
선택된 layout 안에서 각 zone 의 크기 / 비율 결정 + 각 zone *내부의* Internal Region 비율 결정. *두 단계 ratio* 산정 (zone-level + region-level). *50/50 고정 X*. *slide-base / title / divider / footer / gap 임의 축소 금지*.
|
||||||
|
|
||||||
|
- **두 단계 ratio** :
|
||||||
|
- zone-level : layout 의 각 zone 크기 (slide-body 안 분배)
|
||||||
|
- region-level : 각 zone 안 Internal Region 비율 (single-region 이면 100%, multi-region 이면 Step 4 의 ratio_estimate)
|
||||||
|
- **기준** : composition unit 중요도 / content_object 분량 / text·table·image 비중 / frame aspect / capacity / min·max zone / region 별 content type
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `compute_zone_layout()` (min_height + content_weight 분배) + `build_layout_css()` in `phase_z2_pipeline.py`
|
||||||
|
- **Gap** : horizontal-2 만 zone-level dynamic. 나머지 7 preset 은 fr-default. **region-level ratio 미구현** (Internal Region 자체가 Step 4 부재로 입력 X). content_object 분량 기반 정밀화 미반영.
|
||||||
|
|
||||||
|
#### Step 9. Region-Level Frame / Display Selection
|
||||||
|
각 *Internal Region* 에 들어갈 frame 또는 display strategy 확정. step 5 evidence 위에 composition / layout / region 제약 반영해 *최종* 선택. *unit of analysis = region*. single-region zone 은 자연스럽게 zone 1:1 frame 선택과 같음.
|
||||||
|
|
||||||
|
- **region 별 처리** :
|
||||||
|
- text region → text frame 매칭 (현 runtime-verified contract set 기준 F13 / F29 / F16 등)
|
||||||
|
- table region → table preview / details / table frame
|
||||||
|
- image region → image area / image frame
|
||||||
|
- details region → details / popup 전용 region
|
||||||
|
- **Label 처리** (region 단위) :
|
||||||
|
- use_as_is → deterministic slot mapping
|
||||||
|
- light_edit → 같은 frame contract 유지, minor adaptation 가능
|
||||||
|
- restructure → frame 후보 유지하되 content-to-slot 재배치 proposal 필요
|
||||||
|
- reject → 자동 적용 X
|
||||||
|
- **Status** : ⚠ partial — *step 5 와 분리되지 않음 + region-level 미구현 (zone 단위 만)*
|
||||||
|
- **Code 위치** : `plan_composition()` 이 V4 rank-1 즉시 선택 (step 5 와 conflate, zone 단위)
|
||||||
|
- **Gap** : top-k 활용 / composition 제약 반영한 final 단계가 없음. *region-level 매칭 부재* (현재 zone 단위만). restructure label 은 현재 *filter* (선택 X). MVP1_ALLOWED_STATUSES = {matched_zone, adapt_matched_zone} 만 통과.
|
||||||
|
|
||||||
|
#### Step 10. Frame Contract 확인
|
||||||
|
선택된 frame 의 contract 읽어서 accepted_content_types / slots / sub_zones / cardinality / capacity / visual_hints / density envelope / asset 확인.
|
||||||
|
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `get_contract()` + `frame_contracts.yaml` (F13/F29/F16)
|
||||||
|
- **Gap** : `accepted_content_types` 미선언. `sub_zones` 미선언. `density envelope` 미선언.
|
||||||
|
- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §3 (frame contract + Frame Slot, Layer B)
|
||||||
|
|
||||||
|
#### Step 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping
|
||||||
|
각 zone 안에서 *Internal Region 별로* content unit 또는 child group 을 배치 → 그 region 의 frame 내부 *Frame Slot* 에 매핑. 표 작으면 inline / 크면 preview + 자세히보기 / image aspect 유지 / 긴 원문 details / text capacity 내.
|
||||||
|
|
||||||
|
- **2 단계 매핑** :
|
||||||
|
- Layer A : content unit / child group → Internal Region (Step 4 의 region 분할 결과 소비)
|
||||||
|
- Layer B : Internal Region 안 → Frame Slot (frame contract 의 sub_zone 선언 소비)
|
||||||
|
- **Status** : ❌ missing
|
||||||
|
- **Cross-reference** : `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` §4 (placement algorithm 2-stage: Stage A → Stage B) + §5 (display strategy). *해당 SPEC 의 `sub_zones` (YAML 필드명) = Frame Slot (Layer B). Internal Region (Layer A) 는 §2 에 정의됨.*
|
||||||
|
|
||||||
|
#### Step 12. Slot Payload 생성
|
||||||
|
frame partial 에 주입할 데이터 생성. *deterministic mapper* 가 기본. *AI 는 normal path 에 없음*.
|
||||||
|
|
||||||
|
- **AI 가능 위치 (제한적)** : light_edit / restructure 에서 content_object → slot 배치 proposal 필요 시
|
||||||
|
- **AI 금지** : MDX 원문 요약·삭제 / HTML·CSS 직접 생성 / 새 디자인 임의 / layout·frame 임의 선택
|
||||||
|
- **Status** : ✅ implemented (deterministic 부분)
|
||||||
|
- **Code 위치** : `src/phase_z2_mapper.py` (`map_with_contract`, PAYLOAD_BUILDERS, ITEM_PARSERS)
|
||||||
|
- **Gap** : restructure label 의 AI proposal path 미구현 (현재 restructure 는 filter). content_object → sub_zone 매핑이 step 11 부재로 *implicit*.
|
||||||
|
|
||||||
|
### Block B — RENDER
|
||||||
|
|
||||||
|
#### Step 13. Render
|
||||||
|
Jinja2 로 HTML 생성. **고정** : slide-base / slide size / title / divider / footer / slide-body. **가변** : layout / zone ratio / frame partial / slot payload / assets.
|
||||||
|
|
||||||
|
- **산출** : final.html / assets/ / debug.json / preview.png
|
||||||
|
- **Status** : ✅ implemented
|
||||||
|
- **Code 위치** : `render_slide()` in `phase_z2_pipeline.py` + `templates/phase_z2/slide_base.html` + `templates/phase_z2/families/*.html`
|
||||||
|
|
||||||
|
### Block C — POST-RENDER TELEMETRY / EXCEPTION HANDLING
|
||||||
|
|
||||||
|
> 본 block 의 핵심 — *A block (planning) 이 정밀하면 거의 trigger 안 일어남*. 이상적으로 대기 상태. exception 케이스의 *진단 + 다음 capability 안내*.
|
||||||
|
|
||||||
|
#### Step 14. Selenium Visual Runtime Check
|
||||||
|
브라우저 렌더링 기준 실제 결과 검사. slide size / zone overflow / frame internal clipping / text·table·image clipping / content truncation.
|
||||||
|
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `run_overflow_check()` in `phase_z2_pipeline.py`
|
||||||
|
- **Gap** : 현재 *text / structural element overflow* 만 검사. image aspect mismatch / table clipping / under-fill 검사 미구현. clipped_inner 의 inner_content_signals 는 추가됨 (A1 step).
|
||||||
|
|
||||||
|
#### Step 15. Fit Classification
|
||||||
|
visual fail 발생 시 원인 분류.
|
||||||
|
|
||||||
|
- **카테고리** : minor_overflow / structural_minor_overflow / structural_major_overflow / tabular_overflow / image_aspect_mismatch / frame_capacity_mismatch / layout_zone_mismatch / hard_visual_fail
|
||||||
|
- **Status** : ✅ implemented (text / structural 도메인)
|
||||||
|
- **Code 위치** : `src/phase_z2_classifier.py` (`classify_visual_runtime_check`, `CONTENT_TYPE_PATTERNS`)
|
||||||
|
- **Cross-reference** : `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md` §1 / §2 / §3
|
||||||
|
- **Gap** : image_aspect_mismatch / tabular_overflow 분류는 정의됐지만 *실제 trigger 가 step 14 의 검사 부재로 일어나지 않음*.
|
||||||
|
|
||||||
|
#### Step 16. Overflow Router
|
||||||
|
fit classification 결과를 action 후보로 매핑.
|
||||||
|
|
||||||
|
- **매핑 예** : structural_minor_overflow → zone_ratio_retry / tabular_overflow → details_popup_candidate / image_aspect_mismatch → image_fit_candidate / frame_capacity_mismatch → frame_internal_fit_candidate
|
||||||
|
- **Status** : ✅ implemented
|
||||||
|
- **Code 위치** : `src/phase_z2_router.py` (`route_fit_classification`, `ACTION_BY_CATEGORY`)
|
||||||
|
- **Cross-reference** : `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md` §4
|
||||||
|
|
||||||
|
#### Step 17. Implemented Action 실행
|
||||||
|
구현된 action 만 실행. retry budget 제한 / 성공시만 final.html promote / 실패 candidate 는 final.html 아님 / 공통 CSS·padding·tolerance 변경 X / MDX 내용 삭제·요약 X.
|
||||||
|
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Implemented** : `zone_ratio_retry` (A3)
|
||||||
|
- **Code 위치** : `src/phase_z2_retry.py` (`plan_zone_ratio_retry`, `apply_retry_to_layout_css`) + `_attempt_zone_ratio_retry` orchestrator in `phase_z2_pipeline.py`
|
||||||
|
- **Missing actions** : `layout_adjust` / `frame_reselect` / `details_popup_escalation` / `image_fit_candidate` / `frame_internal_fit_candidate`
|
||||||
|
- **Note (사용자 잠금)** : `frame_internal_fit_candidate` 가 *허용할 수 있는 내부 sub-mechanism* (density envelope / line rhythm / internal grid row / text block allocation 등) 은 *frame contract 가 declare 한 envelope 안* 에서만 동작하는 *내부 영역*. **별도 action label 로 등재하지 않음** — `density_adjust_candidate` 같은 이름은 *공통 CSS/padding 축소 antipattern* 을 초대할 위험이 있어 *unified label `frame_internal_fit_candidate` 하나* 로 묶음.
|
||||||
|
|
||||||
|
#### Step 18. Failure Classification
|
||||||
|
action 실패 시 원인 분류.
|
||||||
|
|
||||||
|
- **Failure types** : donor_slack_insufficient / no_donor_candidates / rerender_still_fails / not_attempted
|
||||||
|
- **Status** : ✅ implemented
|
||||||
|
- **Code 위치** : `src/phase_z2_failure_router.py` (`classify_retry_failure`, `FAILURE_TYPE_DESCRIPTIONS`)
|
||||||
|
|
||||||
|
#### Step 19. Next Action Proposal
|
||||||
|
실패 원인 + 원래 overflow severity *함께* 보고 다음 후보 기록. failure_type 단독 X. **overflow_category + line_equivalent + failure_type 의 결합**으로 결정.
|
||||||
|
|
||||||
|
- **예시 (severity-aware)** :
|
||||||
|
- structural_minor_overflow + donor_slack_insufficient → frame_internal_fit_candidate
|
||||||
|
- structural_major_overflow + * → details_popup_candidate
|
||||||
|
- tabular_overflow + * → table_preview_or_details_candidate
|
||||||
|
- frame mismatch → frame_reselect_candidate
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `src/phase_z2_failure_router.py` (`route_retry_failure`, `NEXT_ACTION_BY_FAILURE`)
|
||||||
|
- **Gap** : 현재 *failure_type 단독* mapping (1-차원). severity (overflow_category × line_equivalent) 와의 *2-차원* mapping 미구현. `frame_internal_fit_candidate` 의 *execution contract / internal envelope* 미정의 (label 자체는 router/failure routing 에 등장하지만 *실제로 어떻게 동작하는지 + frame contract 가 declare 할 envelope 의 형식* 은 미정).
|
||||||
|
|
||||||
|
#### Step 20. Slide Status 결정
|
||||||
|
final.html 존재 ≠ PASS. 정확한 상태 분류.
|
||||||
|
|
||||||
|
- **Status enum** : PASS / RENDERED_WITH_VISUAL_REGRESSION / PARTIAL_COVERAGE / ABORTED
|
||||||
|
- **판단** : 모든 section coverage + visual ok → PASS / visual fail 있음 → RENDERED_WITH_VISUAL_REGRESSION / 일부 section 만 렌더 → PARTIAL_COVERAGE / 필수 단계 실패 → ABORTED
|
||||||
|
- **Status** : ✅ implemented
|
||||||
|
- **Code 위치** : `compute_slide_status()` in `phase_z2_pipeline.py`
|
||||||
|
|
||||||
|
#### Step 21. Debug / Trace 기록
|
||||||
|
전체 의사결정을 debug.json 에 기록. 정규화 MDX / content_objects / section_layout_signature / V4 evidence / composition_units / layout / zone sizes / frames / contracts / sub_zone mapping / slot_payload / render result / visual check / fit classification / router decision / action trace / failure classification / next action proposal / slide_status.
|
||||||
|
|
||||||
|
- **Status** : ⚠ partial
|
||||||
|
- **Code 위치** : `write_debug_json()` in `phase_z2_pipeline.py`
|
||||||
|
- **Gap** : content_objects / section_layout_signature / sub_zone mapping 항목은 step 3, 4, 11 부재로 미기록. *region-level telemetry* (region count / region ratios / region-level frame matching / region-level display strategy) 도 Internal Region (Layer A) 부재로 미기록. 그 외 항목은 모두 기록됨.
|
||||||
|
|
||||||
|
#### Step 22. 사용자 확인 / Export
|
||||||
|
사용자가 결과 확인. 현재 목표 = MDX → 자동 슬라이드 1 장 → status / debug. 향후 = layout 재선택 UI / top3 frame 선택 UI / zone 이동 / HTML 다운 / Gitea push.
|
||||||
|
|
||||||
|
- **Status** : ❌ missing (UI 영역 — 현재 범위 외)
|
||||||
|
- **Code 위치** : 없음 (CLI 만)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status matrix 요약
|
||||||
|
|
||||||
|
| Block | Step | Status |
|
||||||
|
|---|---|---|
|
||||||
|
| A | 0. 사전 준비 | ⚠ partial |
|
||||||
|
| A | 1. MDX 업로드 | ✅ |
|
||||||
|
| A | 2. MDX 정규화 | ⚠ partial |
|
||||||
|
| A | 3. Content Object 추출 | ❌ |
|
||||||
|
| A | 4. Section Internal Composition Planning | ❌ |
|
||||||
|
| A | 5. Matching Evidence | ⚠ partial (rank-1 only) |
|
||||||
|
| A | 6. Composition Planning | ⚠ partial |
|
||||||
|
| A | 7. Slide-Level Layout Planning | ⚠ partial (count-based) |
|
||||||
|
| A | 8. Zone + Internal Region Ratio Planning | ⚠ partial (zone-level horizontal-2 만 dynamic, region-level 미구현) |
|
||||||
|
| A | 9. Region-Level Frame / Display Selection | ⚠ merged with step 5 + region-level 미구현 |
|
||||||
|
| A | 10. Frame Contract 확인 | ⚠ partial (no sub_zones) |
|
||||||
|
| A | 11. Content Unit / Child Group → Internal Region → Frame Slot Mapping | ❌ |
|
||||||
|
| A | 12. Slot Payload 생성 | ✅ (deterministic) |
|
||||||
|
| B | 13. Render | ✅ |
|
||||||
|
| C | 14. Selenium Visual Runtime Check | ⚠ partial (text/structural only) |
|
||||||
|
| C | 15. Fit Classification | ✅ |
|
||||||
|
| C | 16. Overflow Router | ✅ |
|
||||||
|
| C | 17. Implemented Action 실행 | ⚠ partial (zone_ratio_retry only) |
|
||||||
|
| C | 18. Failure Classification | ✅ |
|
||||||
|
| C | 19. Next Action Proposal | ⚠ partial (1-D mapping) |
|
||||||
|
| C | 20. Slide Status 결정 | ✅ |
|
||||||
|
| C | 21. Debug / Trace 기록 | ⚠ partial (planning trace 누락) |
|
||||||
|
| C | 22. 사용자 확인 / Export | ❌ (UI 미구현) |
|
||||||
|
|
||||||
|
**핵심 gap 위치 (❌ 표시)** :
|
||||||
|
- Step 3 — Content Object 추출
|
||||||
|
- Step 4 — Section Internal Composition Planning (3-way decision + Internal Region 분할)
|
||||||
|
- Step 11 — Content Unit / Child Group → Internal Region → Frame Slot Mapping
|
||||||
|
- Step 22 — 사용자 UI
|
||||||
|
|
||||||
|
**부분 구현 위치 (⚠) 의 주요 결손** :
|
||||||
|
- Step 5 — top-k 미사용
|
||||||
|
- Step 8 — region-level ratio 미구현 (zone-level horizontal-2 만 dynamic)
|
||||||
|
- Step 9 — Step 5 와 conflate + region-level 매칭 부재
|
||||||
|
- Step 10 — sub_zones 미선언 (frame contract / Layer B)
|
||||||
|
- Step 14 — image / table 검사 부재
|
||||||
|
- Step 17 — `zone_ratio_retry` 외 action 모두 미구현
|
||||||
|
- Step 19 — severity-aware 2-차원 매핑 미구현
|
||||||
|
- Step 21 — planning trace 누락 (step 3, 4, 11 부재 종속) + region-level telemetry 미기록 (Layer A 부재 종속)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 기존 spec 문서 cross-reference
|
||||||
|
|
||||||
|
| Spec 문서 | 다루는 step |
|
||||||
|
|---|---|
|
||||||
|
| `docs/architecture/PHASE-Z-CATALOG-RUNTIME-DESIGN.md` | Step 0 (catalog 룰), Step 10 (frame contract), Step 12 (mapper) |
|
||||||
|
| `docs/architecture/PHASE-Z-FRAME-STYLE-INVENTORY.md` | Step 0 (frame inventory) |
|
||||||
|
| `docs/architecture/FRAME-INTEGRATION-MAP.md` | Step 0 (frame inventory) |
|
||||||
|
| `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md` | Step 14, 15, 16, 17, 18, 19 |
|
||||||
|
| `docs/architecture/PHASE-Z-CONTENT-OBJECT-SUBZONE-SPEC.md` | Step 3 (§1), Step 4 (§2 Internal Region / Layer A), Step 10 (§3 frame contract + Frame Slot / Layer B), Step 11 (§4 placement 2-stage + §5 display strategy). *해당 SPEC 의 `sub_zones` (YAML 필드명) = Frame Slot (Layer B). Internal Region (Layer A) 는 §2 에 정의됨.* |
|
||||||
|
|
||||||
|
## Memory feedback rules cross-reference
|
||||||
|
|
||||||
|
| Memory rule | 적용 step / 의사결정 |
|
||||||
|
|---|---|
|
||||||
|
| `feedback_one_step_per_turn` | 모든 step (작업 분할 discipline) |
|
||||||
|
| `feedback_no_hardcoding` | 모든 step (특히 9, 11, 12, 17) |
|
||||||
|
| `feedback_ai_role_separation` | Step 12 (AI 위치 제한) |
|
||||||
|
| `feedback_ai_isolation_contract` | Step 12 (normal path AI 금지) |
|
||||||
|
| `feedback_phase_z_spacing_direction` | Step 17 (CSS 공통 spacing 변경 금지) |
|
||||||
|
| `feedback_artifact_status_naming` | Step 20 (slide_status enum) |
|
||||||
|
| `feedback_auto_pipeline_first` | Block C 전체 (review/UI 개념 끼우지 말 것) |
|
||||||
|
| `feedback_sample_budget` | Step 1 (미사용 sample 분리 보존) |
|
||||||
|
| `feedback_detail_quality` | 모든 step (self-check) |
|
||||||
|
| `feedback_blocks_must_be_css` | Step 13 (frame partial CSS 원칙) |
|
||||||
|
| `feedback_recipe_variety` | Step 7, 9 (vocabulary 표현 범주) |
|
||||||
|
| `feedback_absolute_paths` | 보고 / 문서 작성 시 |
|
||||||
|
| `feedback_html_preview_whitebg` | Step 13 (slide-base 배경) |
|
||||||
|
| `feedback_figma_*` | Step 0 (figma frame 변환 / asset 작업) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to use this document
|
||||||
|
|
||||||
|
새 작업 시작 시 :
|
||||||
|
1. *어느 step* 의 작업인지 식별
|
||||||
|
2. 그 step 의 *Status* 확인 (✅ / ⚠ / ❌)
|
||||||
|
3. 해당 step 의 cross-reference 된 spec 문서 / memory rule 확인
|
||||||
|
4. 작업 결과가 *다른 step 에 영향* 주는지 확인 (block A 변경 → block C 의 trace 자동 변동)
|
||||||
|
|
||||||
|
새 spec 문서 추가 시 :
|
||||||
|
- 본 문서의 *cross-reference 표* 에 등록 (어느 step 영역인지)
|
||||||
|
|
||||||
|
새 memory rule 추가 시 :
|
||||||
|
- 본 문서의 *Memory feedback rules cross-reference* 표에 등록 (어느 step 의 의사결정인지)
|
||||||
|
|
||||||
|
작업 도중 — *어느 step 에 속하는지 모르는 작업이 들어오면* — 본 22-step 도면에 매핑이 안 된다는 것 자체가 *작업이 over-scoped 되었거나 새 step 정의가 필요* 하다는 신호.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 본 문서의 보존 / 변경 정책
|
||||||
|
|
||||||
|
- 본 문서는 *기준점*. 가벼운 정정 / status 갱신은 진행 가능 (예: ❌ → ⚠ → ✅ 변동)
|
||||||
|
- *22 step 의 추가 / 제거 / 순서 변경* 은 사용자 명시 잠금 후에만
|
||||||
|
- 본 문서의 *3-block 구조* 는 architectural reframe lock 의 직접 반영. 변경 시 reframe 자체를 다시 봄
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# Phase Z — pipeline status board
|
||||||
|
|
||||||
|
**Snapshot date** : 2026-05-04 (B1~B5 + trace-only runtime 연결 closure 반영 — Layer A telemetry first activation)
|
||||||
|
**역할** : 현재 위치표 / grading snapshot. *지도 본문* 은 [`PHASE-Z-PIPELINE-OVERVIEW.md`](PHASE-Z-PIPELINE-OVERVIEW.md).
|
||||||
|
|
||||||
|
| 문서 | 역할 | 변동 |
|
||||||
|
|---|---|---|
|
||||||
|
| `PHASE-Z-PIPELINE-OVERVIEW.md` | 고정 지도 (22-step 도면) | 거의 안 바뀜 |
|
||||||
|
| `PHASE-Z-PIPELINE-STATUS-BOARD.md` | 현재 진행 snapshot | 자주 갱신 |
|
||||||
|
|
||||||
|
본 문서가 *하지 않는* 것 :
|
||||||
|
- 새 구현 제안 X
|
||||||
|
- next step 추천 X
|
||||||
|
- 우선순위 결정 X
|
||||||
|
- A/B/C 선택지 X
|
||||||
|
- MDX03 / MDX04 추가 분석 X
|
||||||
|
- 코드 변경 X
|
||||||
|
- OVERVIEW 구조 수정 X
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Counting rule
|
||||||
|
|
||||||
|
```
|
||||||
|
Step 0 = precondition (파이프라인 가동 전 사전 준비)
|
||||||
|
Step 1~22 = runtime pipeline ("22-step pipeline" = 이 범위)
|
||||||
|
총 항목 수 = 23 (Step 0 + Step 1~22)
|
||||||
|
명명 = "22-step" (runtime 기준)
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 0 은 본체가 아닌 *준비 조건*. Step 1 (MDX 업로드) 부터가 runtime entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 22-step status board
|
||||||
|
|
||||||
|
| Block | Step | 이름 | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| — | 0 | 사전 준비 (catalog / contract / V4 / template / asset) | ⚠ partial |
|
||||||
|
| A | 1 | MDX 업로드 | ✅ |
|
||||||
|
| A | 2 | MDX 정규화 | ⚠ partial |
|
||||||
|
| A | 3 | Content Object 추출 | ⚠ partial (B1 v0 dormant module + trace-only runtime 호출, render path 미연결) |
|
||||||
|
| A | 4 | Section Internal Composition Planning | ⚠ partial (B2 v0 dormant module + trace-only runtime 호출, render path 미연결) |
|
||||||
|
| A | 5 | Matching Evidence 생성 | ⚠ partial (rank-1 only) |
|
||||||
|
| A | 6 | Composition Planning | ⚠ partial |
|
||||||
|
| A | 7 | Slide-Level Layout Planning | ⚠ partial (count-based) |
|
||||||
|
| A | 8 | Zone + Internal Region Ratio Planning | ⚠ partial (zone-level horizontal-2 만 dynamic, region-level 은 B2 안 partial) |
|
||||||
|
| A | 9 | Region-Level Frame / Display Selection | ⚠ partial (B4 가 catalog cover + declaration order 로 frame 선택 분담 / V4 evidence 미통합 / Step 5 와 conflate 잔존) |
|
||||||
|
| A | 10 | Frame Contract 확인 | ⚠ partial (B3 의 accepted_content_types + sub_zones 선언 추가 — B4 만 읽음, mapper 미읽음 / density envelope 별 axis) |
|
||||||
|
| A | 11 | Content Unit / Child Group → Internal Region → Frame Slot Mapping | ⚠ partial (B4 v0 dormant 2-stage + region 1:1 sub_zone + narrowest first + trace-only runtime 호출, render path 미연결) |
|
||||||
|
| A | 12 | Slot Payload 생성 | ✅ (deterministic) |
|
||||||
|
| B | 13 | Render | ✅ |
|
||||||
|
| C | 14 | Selenium Visual Runtime Check | ⚠ partial (text/structural overflow + B5 frame_slot_metrics F29 만 / image / table 검사 부재) |
|
||||||
|
| C | 15 | Fit Classification (A1) | ✅ |
|
||||||
|
| C | 16 | Overflow Router (A2) | ✅ |
|
||||||
|
| C | 17 | Implemented Action 실행 (A3) | ⚠ partial (zone_ratio_retry only) |
|
||||||
|
| C | 18 | Failure Classification (A4-1) | ✅ |
|
||||||
|
| C | 19 | Next Action Proposal (A4-2) | ⚠ partial (1-D mapping) |
|
||||||
|
| C | 20 | Slide Status 결정 | ✅ |
|
||||||
|
| C | 21 | Debug / Trace 기록 | ⚠ partial (placement_trace per-zone 기록 + frame_slot_metrics F29 기록 — render path 활성화 X / region marker partial 미주입) |
|
||||||
|
| C | 22 | 사용자 확인 / Export | ⚠ future (UI 영역 — 현재 범위 외) |
|
||||||
|
|
||||||
|
범례 :
|
||||||
|
- ✅ implemented
|
||||||
|
- ⚠ partial
|
||||||
|
- ❌ missing
|
||||||
|
- ⚠ future (현 범위 외 — 후속)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 핵심 missing (전이 후)
|
||||||
|
|
||||||
|
이전 Step 3 / 4 / 11 = ❌ missing → **본 session 작업으로 ⚠ partial 로 전이**.
|
||||||
|
|
||||||
|
**현재 *남은* gap** :
|
||||||
|
|
||||||
|
```
|
||||||
|
1. render path 의 placement_trace 활용 X
|
||||||
|
- B4 PlacementPlan 이 trace-only — render_slide() / mapper 가 미사용
|
||||||
|
- region-id / content_unit-id marker 가 partial template 에 미주입 (B5 후속 axis)
|
||||||
|
|
||||||
|
2. B4 frame_selection 의 V4 evidence 미통합
|
||||||
|
- B4 v0 = catalog declaration order 만 (cover + first-match)
|
||||||
|
- composition_planner 의 V4 rank-1 와 *cross-axis 비교 자료* 만 — 통합 미완
|
||||||
|
|
||||||
|
3. region-level / Frame Slot-level partial 측정
|
||||||
|
- B5 v0 frame_slot_metrics = F29 1 partial 만
|
||||||
|
- F13 / F16 marker 미적용
|
||||||
|
|
||||||
|
4. 그 외 잔존
|
||||||
|
- rules 2~5 (region-preview-details / region-grid-2x2 / region-main-support /
|
||||||
|
region-horizontal-split) 의 algorithm 미구현 (SPEC v1 §2.5 deferred)
|
||||||
|
- frame contract 의 density envelope 미선언
|
||||||
|
- tabular_overflow / image_aspect_mismatch 검사 부재 (Step 14)
|
||||||
|
- layout_adjust / frame_reselect / details_popup_escalation / image_fit /
|
||||||
|
frame_internal_fit_candidate (Step 17 missing actions) 미구현
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cross-cutting Layer A — 진전 단계 정리** :
|
||||||
|
|
||||||
|
| 단계 | 상태 |
|
||||||
|
|---|---|
|
||||||
|
| (a) OVERVIEW reframe (Layer A first-class lock + Universal Region Model) | ✓ |
|
||||||
|
| (b) SPEC v1 schema (Internal Region §2 + topology vocabulary §2.5 + 2-stage placement §4) | ✓ |
|
||||||
|
| (c) PLAN v1 (cross-ref sync) | ✓ |
|
||||||
|
| (d) B1 v0 ContentObject extractor (dormant) | ✓ |
|
||||||
|
| (e) B2 v0 InternalRegion planner (dormant) | ✓ |
|
||||||
|
| (f) B3 frame_contracts.yaml extension (dormant catalog 면) | ✓ |
|
||||||
|
| (g) B4 v0 placement planner (dormant) | ✓ |
|
||||||
|
| (h) B5 v0 Frame Slot telemetry markers (F29 만) | ✓ |
|
||||||
|
| (i) trace-only runtime 연결 (B1→B2→B4 real data 첫 호출 / debug.json placement_trace) | ✓ |
|
||||||
|
| (j) **render path 활성화 (region marker partial 주입 / B4 → mapper 통합 / V4 evidence 통합)** | **❌ pending** |
|
||||||
|
|
||||||
|
= (a)~(i) 완료 + (j) 가 *남은 핵심 axis* (B5 후속 / runtime 통합).
|
||||||
|
|
||||||
|
**Step 22** 는 별도 범주 (UI 영역 — 현재 자동 파이프라인 범위 외).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 구조 적절성 검토 (brief)
|
||||||
|
|
||||||
|
> snapshot — 22-step *재구성 / 합치기 / 쪼개기 제안 X*. OVERVIEW 영역.
|
||||||
|
|
||||||
|
- **3-block 구조 (A 계획 / B 렌더 / C 사후 telemetry) 적절**. 위계 추가 (Zone Internal Region) 후에도 block 경계는 변동 없음
|
||||||
|
- **Step 3~4 가 Step 5 보다 앞** 인 순서 적절. content_object 와 internal composition decision (3-way) 이 frame matching 의 입력이어야 함
|
||||||
|
- **Step 5 (evidence 생성) 와 Step 9 (final frame / display 선택) 가 분리** 된 구조 적절. 현재 conflate 된 건 구현 결손이지 도면 결손 아님. Step 9 의 unit of analysis = *region* 으로 reframe (OVERVIEW)
|
||||||
|
- **A1~A4 는 post-render telemetry layer**. 진짜 fit policy 의 자리는 Block A (composition planning, region 분할 포함). C block 은 *exception 처리 + 진단 안내*
|
||||||
|
- **Universal Region Model 적용 후에도 step numbering 보존** : Layer A 도입은 step 추가가 아니라 Step 4 / 8 / 9 / 11 의 *granularity unit shift* 로 흡수됨. step 0 ~ 22 그대로
|
||||||
|
- **Layer A trace-only runtime 활성화 = boolean 차원 X** : B1~B4 가 *real MDX runtime 위에서 호출* 되나 *render path 미대체*. debug.json 의 placement_trace = *진단 telemetry only* — final.html / canonical SHA 미영향. *render 활성화* 는 별 axis (B5 후속)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. AI 사용 위치 (runtime 기준)
|
||||||
|
|
||||||
|
```
|
||||||
|
runtime AI = Step 12 의 light_edit / restructure 1 곳만
|
||||||
|
├ 입력 : content_object + frame contract + Internal Region 배치 + Frame Slot 명세
|
||||||
|
├ 출력 : content → Internal Region / Frame Slot proposal
|
||||||
|
└ 금지 : MDX 원문 요약·삭제 / HTML·CSS 직접 생성 / layout·zone·region·frame 임의 선택
|
||||||
|
```
|
||||||
|
|
||||||
|
Step 0 (사전 준비) 의 Figma → HTML 변환은 *precondition phase 의 작업* — runtime AI 아님.
|
||||||
|
|
||||||
|
다른 step 에서의 AI 호출은 본 도면 안에 *없음*.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 현재 병목 (한 줄)
|
||||||
|
|
||||||
|
> 현재 Phase Z 의 *Layer A pre-render planning* (Step 3 / 4 / 11) 은 본 session 작업으로 ❌ → ⚠ partial 전이 (B1/B2/B4 dormant module + trace-only runtime 호출). *Layer A telemetry 의 first activation* — debug.json 의 placement_trace per-zone + frame_slot_metrics F29 partial 기록. 단 **render path 활성화는 미완** : B4 PlacementPlan 이 mapper output 을 *대체하지 않고* trace-only / region-id / content_unit_id marker 가 partial template 에 *미주입* / B4 frame_selection 이 V4 evidence *미통합*. 핵심 다음 axis = **(B5 후속) render path 의 placement_trace 활용 + region marker runtime activation + V4 통합**. *runtime contract-registered / verified frame set 이 text-frame 중심* 한계는 잔존 (frame inventory audit / refinement 별 axis).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 사용 방법
|
||||||
|
|
||||||
|
- 새 작업 들어오면 → 본 board 의 *어느 step* 의 status 를 바꾸는 작업인지 식별
|
||||||
|
- 작업이 *Step 매핑이 안 되면* → over-scoped 또는 새 step 정의 필요 (OVERVIEW 영역)
|
||||||
|
- ✅ → ⚠ → ❌ status 전이 / 갱신 시 → 본 board 만 수정. OVERVIEW 는 step 추가/제거/순서 변경 시에만
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Typography Tokens — 초안 v1
|
||||||
|
|
||||||
|
## Global Hierarchy Tokens
|
||||||
|
|
||||||
|
슬라이드 전체의 공통 글자 위계. 모든 블록이 이 기준을 따른다.
|
||||||
|
|
||||||
|
| 위계 | 토큰명 | font-size | font-weight | line-height | 용도 |
|
||||||
|
|------|--------|-----------|-------------|-------------|------|
|
||||||
|
| 대목차 | `--font-slide-title` | 22px | 700 | 1.4 | 슬라이드 상단 제목 |
|
||||||
|
| 중목차 | `--font-zone-title` | 13px | 700 | 1.4 | zone 제목 (## 대목차 하위) |
|
||||||
|
| 소목차 | `--font-sub-title` | 12px | 700 | 1.45 | 블록 내 소제목, 카드 제목 |
|
||||||
|
| 본문 | `--font-body` | 11px | 400~500 | 1.55 | 블릿, 설명 텍스트 |
|
||||||
|
| 캡션 | `--font-caption` | 10px | 400 | 1.4 | 각주, 출처, 보조 텍스트 |
|
||||||
|
| footer | `--font-footer` | 20px | 700 | 1.2 | 핵심 인사이트 pill |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Component Semantic Token 후보
|
||||||
|
|
||||||
|
위계 6종으로 안 덮이는 역할. 가까운 위계에서 기본값을 가져오되, 필요시 override.
|
||||||
|
|
||||||
|
| 역할 | 기반 위계 | 예상 override | 비고 |
|
||||||
|
|------|-----------|---------------|------|
|
||||||
|
| body-strong (본문 강조) | 본문 (11px) | weight: 600~700 | 본문 heading, 인라인 강조 |
|
||||||
|
| detail-link (자세히보기) | 캡션 (10px) | weight: 500, color: muted | 링크 텍스트 |
|
||||||
|
| pill-label | 소목차 (12px) | weight: 700, color: white | pill/badge 안 라벨 |
|
||||||
|
| table-header | 소목차 (12px) | weight: 700, color: white, bg: dark | 표 헤더 셀 |
|
||||||
|
| table-cell | 본문 (11px) | - | 표 데이터 셀 |
|
||||||
|
| compare-badge | 소목차 (12px) | weight: 700 | 비교 블록 VS 뱃지 |
|
||||||
|
| callout | 소목차 (12px) | weight: 700, color: accent | 강조 인용 |
|
||||||
|
| overline | 캡션 (10px) | weight: 600, letter-spacing | 상단 라벨 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Source
|
||||||
|
|
||||||
|
현재 코드에서 추출한 실제 사용값 기준.
|
||||||
|
|
||||||
|
| 출처 | 대목차 | 중목차 | 소목차 | 본문 | 캡션 | footer |
|
||||||
|
|------|--------|--------|--------|------|------|--------|
|
||||||
|
| slide-base.html | 22px | - | - | - | - | 20px |
|
||||||
|
| block_assembler.py zone title | - | 13px | - | - | - | - |
|
||||||
|
| block_assembler.py direct render | - | - | 12px | 11px | - | - |
|
||||||
|
| block_assembler.py .bul | - | - | - | 11px | - | - |
|
||||||
|
| FontHierarchy (legacy) | - | - | key_msg 14 | core 12 | sidebar 10 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FontHierarchy 매핑 (보조 체계)
|
||||||
|
|
||||||
|
기존 FontHierarchy는 역할 기반이므로, 위계 기반으로 매핑하여 보조로 유지.
|
||||||
|
|
||||||
|
| FontHierarchy | 위계 매핑 | 비고 |
|
||||||
|
|---------------|-----------|------|
|
||||||
|
| key_msg (14px) | 소목차~중목차 사이 | 강조 메시지용, component token 후보 |
|
||||||
|
| core (12px) | 소목차 | 기본 블록 제목급 |
|
||||||
|
| bg (11px) | 본문 | 배경/보조 텍스트 |
|
||||||
|
| sidebar (10px) | 캡션 | 사이드바/첨부 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 상태
|
||||||
|
|
||||||
|
- [x] inventory 수집 완료
|
||||||
|
- [x] global hierarchy v1 초안
|
||||||
|
- [x] component token 후보 분리
|
||||||
|
- [ ] typography.css 파일 작성
|
||||||
|
- [ ] spacing tokens 정의
|
||||||
|
- [ ] color tokens 정의 (공통 + 의미색)
|
||||||
+10
-3
@@ -1688,9 +1688,16 @@ def assemble_slide_html_final(ctx: "PipelineContext", title_text: str = "", meas
|
|||||||
"{% block body %}{% endblock %}",
|
"{% block body %}{% endblock %}",
|
||||||
body_html,
|
body_html,
|
||||||
)
|
)
|
||||||
# Jinja2로 title, footer_text 등 변수만 렌더링
|
# Jinja2 Environment로 렌더 ({% include %} 지원을 위해)
|
||||||
from jinja2 import Template
|
from jinja2 import Environment, FileSystemLoader, BaseLoader
|
||||||
template = Template(slide_base_raw)
|
include_env = Environment(loader=FileSystemLoader(str(templates_dir)))
|
||||||
|
# slide_base_raw를 문자열 템플릿으로 렌더하되, include는 templates_dir 기준
|
||||||
|
from jinja2 import DictLoader
|
||||||
|
combined_loader = FileSystemLoader(str(templates_dir))
|
||||||
|
include_env = Environment(loader=combined_loader)
|
||||||
|
# slide-base 내용을 임시 템플릿으로 등록
|
||||||
|
include_env.loader = FileSystemLoader(str(templates_dir))
|
||||||
|
template = include_env.from_string(slide_base_raw)
|
||||||
slide_html = template.render(
|
slide_html = template.render(
|
||||||
title=title,
|
title=title,
|
||||||
footer_text=conclusion,
|
footer_text=conclusion,
|
||||||
|
|||||||
@@ -0,0 +1,452 @@
|
|||||||
|
"""Phase Z-2 fit_classifier v0 (A1 — 분류 layer 만).
|
||||||
|
|
||||||
|
Selenium visual_runtime_check 의 결과 (clipped_inner / zone overflow) 를
|
||||||
|
spec `docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md` §3 taxonomy
|
||||||
|
의 *category* 로 분류하는 layer.
|
||||||
|
|
||||||
|
본 모듈은 ***분류만***. action / router / rerender / behavior 변경 X.
|
||||||
|
출력 = debug.json 의 `fit_classification` trace.
|
||||||
|
|
||||||
|
원칙 :
|
||||||
|
- className 이라는 raw 문자열 → semantic content_type 매핑은 *registry* 가 담당
|
||||||
|
- excess_y (px) → line_equivalent 환산은 content_type 별 line-height 기준
|
||||||
|
- category 결정은 spec §3.2 우선순위 그대로 적용 (frame_capacity_mismatch →
|
||||||
|
tabular → structural_major → layout_zone_mismatch → structural_minor →
|
||||||
|
moderate → minor → hard_visual_fail)
|
||||||
|
- 모든 결정은 trace 에 명시 — *어느 룰이 왜 적용됐는지* debug 로 검증 가능
|
||||||
|
|
||||||
|
다음 step (별도 — A2) :
|
||||||
|
overflow_router 가 본 module 의 category 를 받아 action 으로 매핑.
|
||||||
|
본 step 에서 router 는 X.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ─── §2 className → semantic content_type registry ───────────────
|
||||||
|
# spec PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md §2 의 registry 그대로.
|
||||||
|
# 패턴은 *위에서 아래로* 첫 매칭 우선. 더 specific 한 패턴이 위에 와야 함.
|
||||||
|
|
||||||
|
CONTENT_TYPE_PATTERNS: list[tuple[str, str, str]] = [
|
||||||
|
# (regex pattern, semantic_content_type, description)
|
||||||
|
|
||||||
|
# transform-block / transform-row → structural_unit
|
||||||
|
# spec : "paired comparison (AS-IS/TO-BE 한 쌍이 의미 단위)"
|
||||||
|
(r"^transform-block(__|$)", "structural_unit",
|
||||||
|
"transform-block — paired comparison container"),
|
||||||
|
(r"^transform-row(__|$)", "structural_unit",
|
||||||
|
"transform-row — AS-IS/TO-BE pair row"),
|
||||||
|
(r"^transform-rows$", "structural_unit",
|
||||||
|
"transform-rows wrapper"),
|
||||||
|
|
||||||
|
# tabular — table 클래스 또는 native <table>
|
||||||
|
(r"(^|[-_])table($|[-_])", "tabular",
|
||||||
|
"table — tabular content"),
|
||||||
|
|
||||||
|
# text-line family → text_flow
|
||||||
|
(r"^text-line(--|$)", "text_flow",
|
||||||
|
"text-line — free-flowing text/bullet"),
|
||||||
|
|
||||||
|
# frame internal cell (frame 내부의 단위 cell)
|
||||||
|
(r"^f\d+b__cell(--|$)", "frame_internal_cell",
|
||||||
|
"frame internal cell"),
|
||||||
|
(r"^f\d+b__pillar(--|$)", "frame_internal_cell",
|
||||||
|
"frame internal pillar"),
|
||||||
|
(r"^f\d+b__quadrant(--|$)", "frame_internal_cell",
|
||||||
|
"frame internal quadrant"),
|
||||||
|
|
||||||
|
# frame label / title / banner / ribbon
|
||||||
|
(r"^f\d+b__title$", "frame_label",
|
||||||
|
"frame title"),
|
||||||
|
(r"^f\d+b__section-title", "frame_label",
|
||||||
|
"frame section-title"),
|
||||||
|
(r"^f\d+b__banner", "frame_label",
|
||||||
|
"frame banner"),
|
||||||
|
(r"^f\d+b__ribbon", "frame_label",
|
||||||
|
"frame ribbon"),
|
||||||
|
(r"__label", "frame_label",
|
||||||
|
"frame label"),
|
||||||
|
|
||||||
|
# frame root (f29b, f13b, f16b 자체)
|
||||||
|
(r"^f\d+b$", "frame_internal",
|
||||||
|
"frame family root"),
|
||||||
|
|
||||||
|
# visual asset
|
||||||
|
(r"__bg(\b|$)", "visual_asset", "background asset"),
|
||||||
|
(r"^bg-", "visual_asset", "background asset"),
|
||||||
|
(r"__icon(\b|$)", "visual_asset", "icon asset"),
|
||||||
|
(r"^img-", "visual_asset", "image asset"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def classify_content_type(class_name: str) -> tuple[str, str]:
|
||||||
|
"""className 문자열 (공백 구분 multiple tokens 가능) → (semantic_content_type, match_reason).
|
||||||
|
|
||||||
|
공백으로 split 한 후 각 token 에 대해 CONTENT_TYPE_PATTERNS 순차 매칭.
|
||||||
|
*첫 매칭* 이 우선 (registry 의 순서가 우선순위).
|
||||||
|
매칭 안 되면 ('unknown', '').
|
||||||
|
|
||||||
|
예 :
|
||||||
|
'f29b__cell f29b__cell--left' → ('frame_internal_cell', "...")
|
||||||
|
'transform-block' → ('structural_unit', "...")
|
||||||
|
'text-line text-line--bullet' → ('text_flow', "...")
|
||||||
|
"""
|
||||||
|
if not class_name:
|
||||||
|
return ("unknown", "")
|
||||||
|
|
||||||
|
tokens = class_name.strip().split()
|
||||||
|
for token in tokens:
|
||||||
|
for pattern, ctype, desc in CONTENT_TYPE_PATTERNS:
|
||||||
|
if re.search(pattern, token):
|
||||||
|
return (ctype, f"token '{token}' matched pattern '{pattern}' ({desc})")
|
||||||
|
return ("unknown", f"no pattern matched any of tokens {tokens}")
|
||||||
|
|
||||||
|
|
||||||
|
# ─── line_equivalent 환산 ─────────────────────────────────────────
|
||||||
|
# content_type 별 *대표 단위 height* — excess_y 를 줄(또는 단위) 단위로 환산.
|
||||||
|
# structural_unit / tabular 의 경우는 "1 단위" = transform-row 또는 table-row.
|
||||||
|
|
||||||
|
DEFAULT_UNIT_HEIGHTS: dict[str, float] = {
|
||||||
|
# transform-row : padding 3+3 + line-height 11×1.45=15.95 ≈ 21.95
|
||||||
|
"structural_unit": 21.95,
|
||||||
|
# text-line : font 11 × line-height 1.6 = 17.6
|
||||||
|
"text_flow": 17.6,
|
||||||
|
# tabular row : 추정치 (실제 표 case 들어오면 calibration)
|
||||||
|
"tabular": 22.0,
|
||||||
|
# frame label / title : font 13 × line-height 1.3 = 16.9
|
||||||
|
"frame_label": 16.9,
|
||||||
|
# frame_internal* : 보수적 default (text-line 기준)
|
||||||
|
"frame_internal": 17.6,
|
||||||
|
"frame_internal_cell": 17.6,
|
||||||
|
# visual asset : crop 가능, 단위는 의미 없음 (line_eq 사용 안 됨)
|
||||||
|
"visual_asset": 17.6,
|
||||||
|
# unknown : text-line default
|
||||||
|
"unknown": 17.6,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compute_line_equivalent(excess_y: float, content_type: str) -> float:
|
||||||
|
"""excess_y (px) → line_equivalent (몇 줄 / 단위 분량인가).
|
||||||
|
|
||||||
|
content_type 별 default unit height 사용. 단위 height 가 0 이거나 없으면 0 반환.
|
||||||
|
소수점 2 자리 round.
|
||||||
|
"""
|
||||||
|
unit_h = DEFAULT_UNIT_HEIGHTS.get(content_type, 17.6)
|
||||||
|
if unit_h <= 0:
|
||||||
|
return 0.0
|
||||||
|
return round(float(excess_y) / unit_h, 2)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── §3 taxonomy classifier ──────────────────────────────────────
|
||||||
|
|
||||||
|
# spec §3.2 우선순위 :
|
||||||
|
# 1. frame_capacity_mismatch (composition 결과 우선)
|
||||||
|
# 2. tabular_overflow
|
||||||
|
# 3. structural_major_overflow
|
||||||
|
# 4. layout_zone_mismatch
|
||||||
|
# 5. structural_minor_overflow
|
||||||
|
# 6. moderate_overflow
|
||||||
|
# 7. minor_overflow
|
||||||
|
# 8. hard_visual_fail (fallback)
|
||||||
|
|
||||||
|
|
||||||
|
def classify_overflow(
|
||||||
|
*,
|
||||||
|
excess_y: float,
|
||||||
|
excess_x: float,
|
||||||
|
class_name: str,
|
||||||
|
inner_content_signals: Optional[list[str]] = None,
|
||||||
|
capacity_fit_status: Optional[str] = None,
|
||||||
|
) -> dict:
|
||||||
|
"""단일 overflow event (clipped_inner 또는 zone-self) 를 spec §3 category 로 분류.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
excess_y / excess_x : Selenium 측정 overflow px
|
||||||
|
class_name : Selenium 이 캡처한 className 문자열 (multi-token 가능)
|
||||||
|
inner_content_signals : Selenium 이 추가로 보고한 *내부 콘텐츠 신호* list
|
||||||
|
(예: ['structural_unit'] — clipped cell 안에 transform-block 이 있음).
|
||||||
|
className 이 frame_internal_cell 같은 *컨테이너* 일 때 *실제 overflow 한
|
||||||
|
content 의 type* 을 추론하기 위해 사용.
|
||||||
|
capacity_fit_status : composition v0.2 의 capacity_fit.fit_status (있으면 우선)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with inputs / derived / category / rule_applied
|
||||||
|
"""
|
||||||
|
inner_content_signals = list(inner_content_signals or [])
|
||||||
|
raw_type, type_match = classify_content_type(class_name)
|
||||||
|
|
||||||
|
# 컨테이너 (frame_internal_cell / frame_internal) 의 경우 inner signal 로 refine.
|
||||||
|
# 이유 : Selenium 이 overflow:hidden 컨테이너 (cell) 를 잡지만, 실제 *overflow 한
|
||||||
|
# content* 는 그 안의 transform-block / table / text-line. 컨테이너 className 만
|
||||||
|
# 보고는 *어떤 종류의 content 가 잘리고 있는지* 모름. inner signal 이 그걸 알려줌.
|
||||||
|
refined_via_inner = None
|
||||||
|
if raw_type in {"frame_internal_cell", "frame_internal", "unknown"} and inner_content_signals:
|
||||||
|
# spec §3.2 우선순위 따라 — tabular > structural_unit > text_flow
|
||||||
|
if "tabular" in inner_content_signals:
|
||||||
|
content_type, refined_via_inner = "tabular", "tabular (inner_signal)"
|
||||||
|
elif "structural_unit" in inner_content_signals:
|
||||||
|
content_type, refined_via_inner = "structural_unit", "structural_unit (inner_signal)"
|
||||||
|
elif "text_flow" in inner_content_signals:
|
||||||
|
content_type, refined_via_inner = "text_flow", "text_flow (inner_signal)"
|
||||||
|
else:
|
||||||
|
content_type = raw_type
|
||||||
|
else:
|
||||||
|
content_type = raw_type
|
||||||
|
|
||||||
|
line_equivalent = compute_line_equivalent(excess_y, content_type)
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
"excess_y": float(excess_y),
|
||||||
|
"excess_x": float(excess_x),
|
||||||
|
"class_name": class_name,
|
||||||
|
"inner_content_signals": inner_content_signals,
|
||||||
|
"capacity_fit_status": capacity_fit_status,
|
||||||
|
}
|
||||||
|
derived = {
|
||||||
|
"container_content_type": raw_type, # className 만 본 결과
|
||||||
|
"container_match": type_match,
|
||||||
|
"content_type": content_type, # inner signal 로 refine 된 *최종* 분류
|
||||||
|
"content_type_refined_via_inner": refined_via_inner,
|
||||||
|
"line_equivalent": line_equivalent,
|
||||||
|
"unit_height_used": DEFAULT_UNIT_HEIGHTS.get(content_type, 17.6),
|
||||||
|
}
|
||||||
|
|
||||||
|
def result(category: str, rule: str) -> dict:
|
||||||
|
return {
|
||||||
|
"inputs": inputs,
|
||||||
|
"derived": derived,
|
||||||
|
"category": category,
|
||||||
|
"rule_applied": rule,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. frame_capacity_mismatch — composition 결과가 이미 mismatch 신호
|
||||||
|
if capacity_fit_status in {"strict_mismatch", "exceeds_max", "below_min", "exceeds_truncate"}:
|
||||||
|
return result(
|
||||||
|
"frame_capacity_mismatch",
|
||||||
|
f"capacity_fit_status='{capacity_fit_status}' — composition 단계의 "
|
||||||
|
f"capacity_fit 가 이미 mismatch 신호 (spec §3.2 우선순위 1)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. tabular_overflow — 표는 어떤 양이든 popup 영역
|
||||||
|
if content_type == "tabular":
|
||||||
|
return result(
|
||||||
|
"tabular_overflow",
|
||||||
|
f"content_type=tabular — 표는 행 단위 자르면 의미 손실 (spec §3.2 우선순위 2)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. structural_major_overflow — 1 개 이상 *완전 단위* 잘림
|
||||||
|
if content_type == "structural_unit" and line_equivalent >= 1.0:
|
||||||
|
return result(
|
||||||
|
"structural_major_overflow",
|
||||||
|
f"content_type=structural_unit AND line_equivalent={line_equivalent} >= 1.0 — "
|
||||||
|
f"의미 단위 1+ 완전 잘림 (spec §3.2 우선순위 3)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. layout_zone_mismatch — frame root 자체 overflow
|
||||||
|
if content_type == "frame_internal":
|
||||||
|
return result(
|
||||||
|
"layout_zone_mismatch",
|
||||||
|
f"content_type=frame_internal — frame root 자체가 zone 안에 못 들어감 "
|
||||||
|
f"(spec §3.2 우선순위 4)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. structural_minor_overflow — boundary spill (부분만 잘림)
|
||||||
|
if content_type == "structural_unit":
|
||||||
|
return result(
|
||||||
|
"structural_minor_overflow",
|
||||||
|
f"content_type=structural_unit AND line_equivalent={line_equivalent} < 1.0 — "
|
||||||
|
f"boundary spill (부분 단위 잘림, 완전 단위 손실 아님) (spec §3.2 우선순위 5)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. moderate_overflow — text/label flow 의 중간 양
|
||||||
|
if content_type in {"text_flow", "frame_label"} and 1.5 < line_equivalent <= 4.0:
|
||||||
|
return result(
|
||||||
|
"moderate_overflow",
|
||||||
|
f"content_type={content_type} AND line_equivalent={line_equivalent} ∈ (1.5, 4] "
|
||||||
|
f"(spec §3.2 우선순위 6)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 7. minor_overflow — text/label flow 의 작은 양
|
||||||
|
if content_type in {"text_flow", "frame_label"} and line_equivalent <= 1.5:
|
||||||
|
return result(
|
||||||
|
"minor_overflow",
|
||||||
|
f"content_type={content_type} AND line_equivalent={line_equivalent} ≤ 1.5 "
|
||||||
|
f"(spec §3.2 우선순위 7)",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 8. hard_visual_fail — fallback (위 어디에도 안 잡힘)
|
||||||
|
return result(
|
||||||
|
"hard_visual_fail",
|
||||||
|
f"위 매핑 모두 미적용 (content_type={content_type}, line_equivalent="
|
||||||
|
f"{line_equivalent}) — fallback (spec §3.2 우선순위 8)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── visual_runtime_check 결과 → 전체 fit_classification trace ────
|
||||||
|
|
||||||
|
|
||||||
|
def _build_placement_diagnostic_for_zone(
|
||||||
|
zone_position: str,
|
||||||
|
placement_trace: Optional[dict],
|
||||||
|
mapper_template_id: Optional[str],
|
||||||
|
) -> dict:
|
||||||
|
"""zone 별 placement diagnostic 빌더 — placement_trace from B4 → 진단 dict.
|
||||||
|
|
||||||
|
phase_z2_pipeline.py 의 trace-only B1→B2→B4 chain 결과 (debug_zones[i].placement_trace)
|
||||||
|
를 per-zone surface 한 진단. classifier 는 *consume only* — placement_trace
|
||||||
|
raw 구조는 미변경.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
zone_position : zone position ("top" / "bottom_l" 등)
|
||||||
|
placement_trace : phase_z2_pipeline.py 의 placement_trace dict 또는 None
|
||||||
|
mapper_template_id : 기존 mapper / V4 가 선택한 frame template_id
|
||||||
|
(placement_trace 에 mapper_frame_template_id 누락 시 fallback)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
per-zone placement diagnostic dict (shape-stable, missing fields = None / 0).
|
||||||
|
"""
|
||||||
|
if placement_trace is None:
|
||||||
|
return {
|
||||||
|
"zone_position": zone_position,
|
||||||
|
"mapper_frame_template_id": mapper_template_id,
|
||||||
|
"b4_selected_template_id": None,
|
||||||
|
"region_count": 0,
|
||||||
|
"slot_assignment_count": 0,
|
||||||
|
"rejection_count": 0,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"zone_position": zone_position,
|
||||||
|
"mapper_frame_template_id": (
|
||||||
|
placement_trace.get("mapper_frame_template_id") or mapper_template_id
|
||||||
|
),
|
||||||
|
"b4_selected_template_id": placement_trace.get("selected_template_id"),
|
||||||
|
"region_count": len(placement_trace.get("internal_regions") or []),
|
||||||
|
"slot_assignment_count": len(placement_trace.get("slot_assignments") or []),
|
||||||
|
"rejection_count": len(placement_trace.get("rejection") or []),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def classify_visual_runtime_check(overflow: dict, debug_zones: list[dict]) -> dict:
|
||||||
|
"""Selenium overflow + composition 의 zone debug → 전체 fit_classification 산출.
|
||||||
|
|
||||||
|
각 overflow event (zone-self overflow / cell-level clipped_inner) 를 개별 분류.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
overflow : run_overflow_check 결과 (passed, slide, zones[], ...)
|
||||||
|
debug_zones : pipeline 의 debug_zones list (zone 별 capacity_fit / template_id 등)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict :
|
||||||
|
visual_check_passed : Selenium 통과 여부
|
||||||
|
classifications : 각 overflow event 의 분류 결과 list
|
||||||
|
summary : 텍스트 요약 (n events, categories seen)
|
||||||
|
categories_seen : 등장한 카테고리 unique list
|
||||||
|
unclassified_signals : 미분류 신호 (raw Selenium 결과 중 분류 안 된 것)
|
||||||
|
placement_diagnostics : per-zone placement_trace 진단 (B4 vs mapper
|
||||||
|
divergence + region / slot_assignment / rejection
|
||||||
|
count) — passed 여부 무관 항상 surface
|
||||||
|
"""
|
||||||
|
# placement_diagnostics — debug_zones[i].placement_trace 를 per-zone diagnostic 으로 surface.
|
||||||
|
# passed 여부 무관 항상 빌드 (B4 vs mapper divergence 가 passed 에서도 진단 가치).
|
||||||
|
placement_diagnostics = [
|
||||||
|
_build_placement_diagnostic_for_zone(
|
||||||
|
zone_position=dz.get("position", "?"),
|
||||||
|
placement_trace=dz.get("placement_trace"),
|
||||||
|
mapper_template_id=dz.get("v4_template_id"),
|
||||||
|
)
|
||||||
|
for dz in (debug_zones or [])
|
||||||
|
]
|
||||||
|
|
||||||
|
if overflow.get("passed", False):
|
||||||
|
return {
|
||||||
|
"visual_check_passed": True,
|
||||||
|
"classifications": [],
|
||||||
|
"summary": "visual check passed — no overflow to classify",
|
||||||
|
"categories_seen": [],
|
||||||
|
"unclassified_signals": [],
|
||||||
|
"placement_diagnostics": placement_diagnostics,
|
||||||
|
}
|
||||||
|
|
||||||
|
# zone position → debug_zones 매핑 (capacity_fit_status 추출용)
|
||||||
|
capacity_status_by_position: dict[str, Optional[str]] = {}
|
||||||
|
template_id_by_position: dict[str, Optional[str]] = {}
|
||||||
|
for dz in (debug_zones or []):
|
||||||
|
pos = dz.get("position")
|
||||||
|
capacity_status_by_position[pos] = (
|
||||||
|
(dz.get("composition_rationale") or {})
|
||||||
|
.get("capacity_fit", {})
|
||||||
|
.get("fit_status")
|
||||||
|
)
|
||||||
|
template_id_by_position[pos] = dz.get("v4_template_id")
|
||||||
|
|
||||||
|
classifications: list[dict] = []
|
||||||
|
|
||||||
|
for z in overflow.get("zones", []):
|
||||||
|
zone_position = z.get("position", "?")
|
||||||
|
zone_template_id = z.get("template_id") or template_id_by_position.get(zone_position)
|
||||||
|
capacity_fit_status = capacity_status_by_position.get(zone_position)
|
||||||
|
|
||||||
|
# zone-self overflow (frame root 자체)
|
||||||
|
if z.get("overflowed"):
|
||||||
|
cls = classify_overflow(
|
||||||
|
excess_y=z.get("excess_y", 0),
|
||||||
|
excess_x=z.get("excess_x", 0),
|
||||||
|
class_name=zone_template_id and f"f{re.sub(r'[^0-9]', '', str(zone_template_id))[:2] or '0'}b" or "f?b",
|
||||||
|
# zone 자체는 frame root 패턴 매칭 → frame_internal 으로 분류 의도
|
||||||
|
capacity_fit_status=capacity_fit_status,
|
||||||
|
)
|
||||||
|
cls["source"] = "zone_self_overflow"
|
||||||
|
cls["zone_position"] = zone_position
|
||||||
|
cls["zone_template_id"] = zone_template_id
|
||||||
|
classifications.append(cls)
|
||||||
|
|
||||||
|
# cell-level clipped_inner
|
||||||
|
for c in z.get("clipped_inner", []):
|
||||||
|
cls = classify_overflow(
|
||||||
|
excess_y=c.get("excess_y", 0),
|
||||||
|
excess_x=c.get("excess_x", 0),
|
||||||
|
class_name=c.get("class_name", ""),
|
||||||
|
inner_content_signals=c.get("inner_content_signals") or [],
|
||||||
|
capacity_fit_status=capacity_fit_status,
|
||||||
|
)
|
||||||
|
cls["source"] = "clipped_inner"
|
||||||
|
cls["zone_position"] = zone_position
|
||||||
|
cls["zone_template_id"] = zone_template_id
|
||||||
|
cls["client_height"] = c.get("clientHeight")
|
||||||
|
cls["scroll_height"] = c.get("scrollHeight")
|
||||||
|
classifications.append(cls)
|
||||||
|
|
||||||
|
# slide-level / slide-body overflow (zones 외부) 도 분류 시도 (보통 zone-level 에서 잡히지만 보조)
|
||||||
|
unclassified: list[dict] = []
|
||||||
|
slide_m = overflow.get("slide") or {}
|
||||||
|
if slide_m.get("overflowed"):
|
||||||
|
unclassified.append({
|
||||||
|
"level": "slide",
|
||||||
|
"excess_y": slide_m.get("excess_y"),
|
||||||
|
"excess_x": slide_m.get("excess_x"),
|
||||||
|
"note": "slide-level overflow — 보통 zone 단위 분류로 충분, 미분류 보고만",
|
||||||
|
})
|
||||||
|
body_m = overflow.get("slide_body") or {}
|
||||||
|
if body_m.get("overflowed"):
|
||||||
|
unclassified.append({
|
||||||
|
"level": "slide_body",
|
||||||
|
"excess_y": body_m.get("excess_y"),
|
||||||
|
"excess_x": body_m.get("excess_x"),
|
||||||
|
"note": "slide_body overflow — 위와 같음",
|
||||||
|
})
|
||||||
|
|
||||||
|
categories = sorted({c["category"] for c in classifications})
|
||||||
|
return {
|
||||||
|
"visual_check_passed": False,
|
||||||
|
"classifications": classifications,
|
||||||
|
"summary": (
|
||||||
|
f"{len(classifications)} overflow event(s) classified, "
|
||||||
|
f"categories: {categories or 'none'}"
|
||||||
|
),
|
||||||
|
"categories_seen": categories,
|
||||||
|
"unclassified_signals": unclassified,
|
||||||
|
"placement_diagnostics": placement_diagnostics,
|
||||||
|
}
|
||||||
@@ -0,0 +1,571 @@
|
|||||||
|
"""Phase Z-2 Composition Planner v0.
|
||||||
|
|
||||||
|
Pipeline 의 빠진 layer = MDX 덩어리들을 *최종 zone unit* 으로 묶는 결정 layer.
|
||||||
|
|
||||||
|
위치 :
|
||||||
|
parse_mdx → align_sections_to_v4_granularity → [본 모듈] → render
|
||||||
|
|
||||||
|
원칙 (절대 룰) :
|
||||||
|
- 특정 MDX / frame / section 하드코딩 X (예: "04-2 면" / "F16 이면")
|
||||||
|
- 모든 결정 = catalog 메타 + V4 evidence parametric
|
||||||
|
- 같은 코드가 MDX 02/03/04/05/06... 모두 처리 — 결과는 케이스마다 다름
|
||||||
|
- drilling 결과 = 입력 (재료), composition planner 결과 = 출력 (zone units)
|
||||||
|
- slide-level layout = zone 까지만 나눔. zone 내부 분할은 frame partial 책임
|
||||||
|
|
||||||
|
8 layout preset vocabulary :
|
||||||
|
L1 single / L2 horizontal-2 / L3 vertical-2
|
||||||
|
L4 top-1-bottom-2 / L5 top-2-bottom-1
|
||||||
|
L6 left-1-right-2 / L7 left-2-right-1
|
||||||
|
L8 grid-2x2
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 8 Layout Preset Vocabulary ────────────────────────────────
|
||||||
|
|
||||||
|
LAYOUT_PRESETS: dict[str, dict] = {
|
||||||
|
"single": {
|
||||||
|
"zones": 1,
|
||||||
|
"topology": "single",
|
||||||
|
"positions": ["primary"],
|
||||||
|
"css_areas": '"primary"',
|
||||||
|
"css_cols": "1fr",
|
||||||
|
"css_rows": "1fr",
|
||||||
|
},
|
||||||
|
"horizontal-2": {
|
||||||
|
"zones": 2,
|
||||||
|
"topology": "rows",
|
||||||
|
"positions": ["top", "bottom"],
|
||||||
|
"css_areas": '"top" "bottom"',
|
||||||
|
"css_cols": "1fr",
|
||||||
|
"css_rows": "1fr 1fr",
|
||||||
|
},
|
||||||
|
"vertical-2": {
|
||||||
|
"zones": 2,
|
||||||
|
"topology": "cols",
|
||||||
|
"positions": ["left", "right"],
|
||||||
|
"css_areas": '"left right"',
|
||||||
|
"css_cols": "1fr 1fr",
|
||||||
|
"css_rows": "1fr",
|
||||||
|
},
|
||||||
|
"top-1-bottom-2": {
|
||||||
|
"zones": 3,
|
||||||
|
"topology": "T",
|
||||||
|
"positions": ["top", "bottom-left", "bottom-right"],
|
||||||
|
"css_areas": '"top top" "bottom-left bottom-right"',
|
||||||
|
"css_cols": "1fr 1fr",
|
||||||
|
"css_rows": "1fr 1fr",
|
||||||
|
},
|
||||||
|
"top-2-bottom-1": {
|
||||||
|
"zones": 3,
|
||||||
|
"topology": "inverted-T",
|
||||||
|
"positions": ["top-left", "top-right", "bottom"],
|
||||||
|
"css_areas": '"top-left top-right" "bottom bottom"',
|
||||||
|
"css_cols": "1fr 1fr",
|
||||||
|
"css_rows": "1fr 1fr",
|
||||||
|
},
|
||||||
|
"left-1-right-2": {
|
||||||
|
"zones": 3,
|
||||||
|
"topology": "side-T-left",
|
||||||
|
"positions": ["left", "right-top", "right-bottom"],
|
||||||
|
"css_areas": '"left right-top" "left right-bottom"',
|
||||||
|
"css_cols": "1fr 1fr",
|
||||||
|
"css_rows": "1fr 1fr",
|
||||||
|
},
|
||||||
|
"left-2-right-1": {
|
||||||
|
"zones": 3,
|
||||||
|
"topology": "side-T-right",
|
||||||
|
"positions": ["left-top", "right", "left-bottom"],
|
||||||
|
"css_areas": '"left-top right" "left-bottom right"',
|
||||||
|
"css_cols": "1fr 1fr",
|
||||||
|
"css_rows": "1fr 1fr",
|
||||||
|
},
|
||||||
|
"grid-2x2": {
|
||||||
|
"zones": 4,
|
||||||
|
"topology": "2x2",
|
||||||
|
"positions": ["top-left", "top-right", "bottom-left", "bottom-right"],
|
||||||
|
"css_areas": '"top-left top-right" "bottom-left bottom-right"',
|
||||||
|
"css_cols": "1fr 1fr",
|
||||||
|
"css_rows": "1fr 1fr",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── CompositionUnit ────────────────────────────────────────────
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CompositionUnit:
|
||||||
|
"""Slide 내 1 zone 후보 = MDX section(s) + 매칭된 frame.
|
||||||
|
|
||||||
|
source_section_ids : 1 개 = single, 2+ = merged
|
||||||
|
merge_type :
|
||||||
|
- "single" : 단일 section
|
||||||
|
- "parent_merged" : parent V4 entry 존재 (v0)
|
||||||
|
- "parent_merged_inferred" : parent V4 entry 없음, child evidence 로 추론 (v0.1)
|
||||||
|
frame_* : V4 evidence 그대로 (catalog 메타 X 하드코딩 X)
|
||||||
|
score : 종합 점수
|
||||||
|
rationale : score breakdown 추적
|
||||||
|
review_required : True 면 자동 선택 X — debug 에만 노출, 사용자/AI 검토 후
|
||||||
|
별도 path (light_edit / restructure / AI restructuring) 로 처리
|
||||||
|
review_reasons : 왜 review_required 가 True 인지 (자가검증용 — child label mix /
|
||||||
|
template_id 불일치 / cardinality 불호환 등)
|
||||||
|
"""
|
||||||
|
source_section_ids: list[str]
|
||||||
|
merge_type: str
|
||||||
|
frame_template_id: str
|
||||||
|
frame_id: str
|
||||||
|
frame_number: int
|
||||||
|
confidence: float
|
||||||
|
label: str # use_as_is / light_edit / restructure / reject
|
||||||
|
phase_z_status: str
|
||||||
|
raw_content: str
|
||||||
|
title: str
|
||||||
|
score: float = 0.0
|
||||||
|
rationale: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
# 자동 파이프라인 단계 상태 (review/UI 개념 X — 현재는 자동 결정 + 명확한 실패 기록만)
|
||||||
|
# auto_selectable=False 면 자동 선택 단계에서 제외. filter_reasons 가 그 이유.
|
||||||
|
# 예: parent_merged_inferred 의 W1/W2/W3 (rep status / all reject / majority not-auto-renderable)
|
||||||
|
# 사용자/AI 검토는 별 layer (interactive editor) 에서 처리. 본 dataclass 는 자동 결정 완결.
|
||||||
|
auto_selectable: bool = True
|
||||||
|
filter_reasons: list[str] = field(default_factory=list)
|
||||||
|
# informational signals — auto_selectable 여부와 무관. future axis 가 점수화할 영역.
|
||||||
|
# 예: "children disagree on rank-1 template_id" / "minority of children non-auto-renderable"
|
||||||
|
notes: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Heading Tree ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
def derive_parent_id(section_id: str) -> Optional[str]:
|
||||||
|
"""section_id 에서 parent 도출 — V4 키 컨벤션 기반.
|
||||||
|
|
||||||
|
예시 (코멘트, 룰 X) :
|
||||||
|
- "04-2.1" → "04-2" (decimal suffix → strip)
|
||||||
|
- "04-1" → None (top-level, no parent)
|
||||||
|
- "04" → None
|
||||||
|
"""
|
||||||
|
parts = section_id.split("-", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
return None
|
||||||
|
mdx_id, suffix = parts
|
||||||
|
if "." in suffix:
|
||||||
|
parent_suffix = suffix.split(".")[0]
|
||||||
|
return f"{mdx_id}-{parent_suffix}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_heading_tree(sections) -> dict:
|
||||||
|
"""Section list → tree {section_id: {section, children}}."""
|
||||||
|
tree = {s.section_id: {"section": s, "children": []} for s in sections}
|
||||||
|
for s in sections:
|
||||||
|
parent = derive_parent_id(s.section_id)
|
||||||
|
if parent and parent in tree:
|
||||||
|
tree[parent]["children"].append(s.section_id)
|
||||||
|
return tree
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Candidate Generation ──────────────────────────────────────
|
||||||
|
|
||||||
|
def _apply_capacity_fit(candidate: CompositionUnit, capacity_fit_fn) -> None:
|
||||||
|
"""capacity_fit_fn 결과를 candidate 의 rationale + auto_selectable + filter_reasons 에 반영.
|
||||||
|
|
||||||
|
fit_status 가 'ok' / 'no_contract' / 'unknown_source_shape' 이면 auto_selectable 영향 X
|
||||||
|
(no_contract 는 catalog-only mapper 가 별도로 ValueError 처리).
|
||||||
|
그 외 (strict_mismatch / exceeds_max / below_min / exceeds_truncate) 는 silent loss 또는
|
||||||
|
mapper FitError 가 발생할 후보 → auto_selectable=False + filter_reasons 'C1: ...'.
|
||||||
|
"""
|
||||||
|
if capacity_fit_fn is None:
|
||||||
|
return
|
||||||
|
fit = capacity_fit_fn(candidate.frame_template_id, candidate.raw_content)
|
||||||
|
candidate.rationale["capacity_fit"] = fit
|
||||||
|
if fit["fit_status"] in {"ok", "no_contract", "unknown_source_shape"}:
|
||||||
|
return
|
||||||
|
candidate.auto_selectable = False
|
||||||
|
candidate.filter_reasons.append(
|
||||||
|
f"C1: capacity mismatch ({fit['fit_status']}) — {fit['mismatch_reason']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_candidates(sections, v4_lookup_fn, v4_label_to_status: dict,
|
||||||
|
auto_renderable_statuses: Optional[set[str]] = None,
|
||||||
|
capacity_fit_fn=None):
|
||||||
|
"""Generate composition candidates.
|
||||||
|
|
||||||
|
v0.1 candidate types :
|
||||||
|
1. single : per leaf section (V4 entry 필수)
|
||||||
|
2. parent_merged : parent 자체에 V4 entry 존재 (parent 가 직접 매칭됨)
|
||||||
|
3. parent_merged_inferred : parent V4 없음. child evidence 로 representative
|
||||||
|
template_id 추론
|
||||||
|
|
||||||
|
원칙 :
|
||||||
|
- 특정 section_id / template_id / frame 하드코딩 X
|
||||||
|
- 모든 결정 = derive_parent_id() + V4 evidence + v4_label_to_status mapping + 주입된 fn (파라메트릭)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sections : align 결과
|
||||||
|
v4_lookup_fn : (section_id) → V4Match | None
|
||||||
|
v4_label_to_status : V4 label → Phase Z status mapping
|
||||||
|
auto_renderable_statuses : 자동 렌더 허용 status set (W1/W3 판정 입력)
|
||||||
|
capacity_fit_fn : Optional (template_id, content) → fit dict.
|
||||||
|
제공되면 모든 candidate 에 적용 — capacity mismatch 시 auto_selectable=False
|
||||||
|
(silent truncate / mapper FitError 사전 차단).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[CompositionUnit]
|
||||||
|
"""
|
||||||
|
if auto_renderable_statuses is None:
|
||||||
|
auto_renderable_statuses = set()
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
# 1. Separate
|
||||||
|
for s in sections:
|
||||||
|
match = v4_lookup_fn(s.section_id)
|
||||||
|
if match is None:
|
||||||
|
continue
|
||||||
|
c = CompositionUnit(
|
||||||
|
source_section_ids=[s.section_id],
|
||||||
|
merge_type="single",
|
||||||
|
frame_template_id=match.template_id,
|
||||||
|
frame_id=match.frame_id,
|
||||||
|
frame_number=match.frame_number,
|
||||||
|
confidence=match.confidence,
|
||||||
|
label=match.label,
|
||||||
|
phase_z_status=v4_label_to_status.get(match.label, "unknown"),
|
||||||
|
raw_content=s.raw_content,
|
||||||
|
title=s.title,
|
||||||
|
)
|
||||||
|
_apply_capacity_fit(c, capacity_fit_fn)
|
||||||
|
candidates.append(c)
|
||||||
|
|
||||||
|
# parent → children 그룹화
|
||||||
|
parent_to_children: dict[str, list] = {}
|
||||||
|
for s in sections:
|
||||||
|
pid = derive_parent_id(s.section_id)
|
||||||
|
if pid:
|
||||||
|
parent_to_children.setdefault(pid, []).append(s)
|
||||||
|
|
||||||
|
# 2. parent_merged (parent 자체가 V4 에 매칭된 경우)
|
||||||
|
for pid, children in parent_to_children.items():
|
||||||
|
parent_match = v4_lookup_fn(pid)
|
||||||
|
if parent_match is None:
|
||||||
|
continue # branch 3 가 처리
|
||||||
|
if len(children) < 2:
|
||||||
|
continue # merge 의미 없음
|
||||||
|
merged_raw = "\n\n".join(c.raw_content for c in children)
|
||||||
|
c_pm = CompositionUnit(
|
||||||
|
source_section_ids=[c.section_id for c in children],
|
||||||
|
merge_type="parent_merged",
|
||||||
|
frame_template_id=parent_match.template_id,
|
||||||
|
frame_id=parent_match.frame_id,
|
||||||
|
frame_number=parent_match.frame_number,
|
||||||
|
confidence=parent_match.confidence,
|
||||||
|
label=parent_match.label,
|
||||||
|
phase_z_status=v4_label_to_status.get(parent_match.label, "unknown"),
|
||||||
|
raw_content=merged_raw,
|
||||||
|
title=pid,
|
||||||
|
)
|
||||||
|
_apply_capacity_fit(c_pm, capacity_fit_fn)
|
||||||
|
candidates.append(c_pm)
|
||||||
|
|
||||||
|
# 3. parent_merged_inferred (v0.1) — parent V4 없음, child evidence 기반
|
||||||
|
for pid, children in parent_to_children.items():
|
||||||
|
if v4_lookup_fn(pid) is not None:
|
||||||
|
continue # branch 2 가 이미 처리
|
||||||
|
if len(children) < 2:
|
||||||
|
continue
|
||||||
|
# children 중 V4 매칭 있는 것들만 evidence 로 사용
|
||||||
|
child_matches: list[tuple] = []
|
||||||
|
for c in children:
|
||||||
|
m = v4_lookup_fn(c.section_id)
|
||||||
|
if m is not None:
|
||||||
|
child_matches.append((c, m))
|
||||||
|
if len(child_matches) < 2:
|
||||||
|
continue # 최소 2 child evidence 필요
|
||||||
|
|
||||||
|
# representative = 가장 confidence 높은 child match (v0.1.1 단순 룰)
|
||||||
|
# 향후 axes : top-k convergence, template family agreement, cardinality_fit 등
|
||||||
|
rep_child, rep_match = max(child_matches, key=lambda cm: cm[1].confidence)
|
||||||
|
|
||||||
|
# 자동 선택 가능 여부 = auto_selectable. default True (strong inferred merge).
|
||||||
|
# 다음 weak 신호 중 하나라도 있으면 auto_selectable=False (filter_reasons 에 사유) :
|
||||||
|
# W1 : representative status 가 auto-renderable 아님 → 자동 렌더 자체가 막힘
|
||||||
|
# W2 : 모든 child 가 reject → merge 의미 자체가 없음
|
||||||
|
# W3 : auto-renderable 아닌 child label 이 majority (>50%)
|
||||||
|
# informational notes (auto_selectable 영향 X, future axis 점수화 영역) :
|
||||||
|
# N1 : children 의 rank-1 template_id 가 서로 다름 → top-k / family compat
|
||||||
|
# N2 : non-auto-renderable child label 이 일부 (소수) 존재
|
||||||
|
rep_status = v4_label_to_status.get(rep_match.label, "unknown")
|
||||||
|
child_labels = [m.label for _, m in child_matches]
|
||||||
|
child_template_ids_unique = sorted({m.template_id for _, m in child_matches})
|
||||||
|
n_children = len(child_matches)
|
||||||
|
n_not_auto = sum(
|
||||||
|
1 for l in child_labels
|
||||||
|
if v4_label_to_status.get(l) not in auto_renderable_statuses
|
||||||
|
)
|
||||||
|
|
||||||
|
filter_reasons: list[str] = []
|
||||||
|
notes: list[str] = []
|
||||||
|
|
||||||
|
if rep_status not in auto_renderable_statuses:
|
||||||
|
filter_reasons.append(
|
||||||
|
f"W1: representative status '{rep_status}' (label={rep_match.label}) "
|
||||||
|
f"not in auto_renderable_statuses={sorted(auto_renderable_statuses)}."
|
||||||
|
)
|
||||||
|
if all(l == "reject" for l in child_labels):
|
||||||
|
filter_reasons.append(
|
||||||
|
"W2: all children labeled 'reject' — merge has no fit basis."
|
||||||
|
)
|
||||||
|
if n_children > 0 and n_not_auto * 2 > n_children:
|
||||||
|
non_auto_labels = sorted({
|
||||||
|
l for l in child_labels
|
||||||
|
if v4_label_to_status.get(l) not in auto_renderable_statuses
|
||||||
|
})
|
||||||
|
filter_reasons.append(
|
||||||
|
f"W3: majority of children ({n_not_auto}/{n_children}) have "
|
||||||
|
f"non-auto-renderable labels {non_auto_labels}."
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(child_template_ids_unique) > 1:
|
||||||
|
notes.append(
|
||||||
|
f"N1: children's rank-1 template_id differs ({child_template_ids_unique}). "
|
||||||
|
f"representative='{rep_match.template_id}' (highest child confidence). "
|
||||||
|
f"top-k / family compatibility 평가는 future axis."
|
||||||
|
)
|
||||||
|
if 0 < n_not_auto <= n_children // 2:
|
||||||
|
non_auto_labels_minority = sorted({
|
||||||
|
l for l in child_labels
|
||||||
|
if v4_label_to_status.get(l) not in auto_renderable_statuses
|
||||||
|
})
|
||||||
|
notes.append(
|
||||||
|
f"N2: minority ({n_not_auto}/{n_children}) of children non-auto-renderable "
|
||||||
|
f"({non_auto_labels_minority}). representative is auto-renderable, merge proceeds."
|
||||||
|
)
|
||||||
|
|
||||||
|
auto_selectable = len(filter_reasons) == 0
|
||||||
|
|
||||||
|
merged_raw = "\n\n".join(c.raw_content for c, _ in child_matches)
|
||||||
|
c_inf = CompositionUnit(
|
||||||
|
source_section_ids=[c.section_id for c, _ in child_matches],
|
||||||
|
merge_type="parent_merged_inferred",
|
||||||
|
frame_template_id=rep_match.template_id,
|
||||||
|
frame_id=rep_match.frame_id,
|
||||||
|
frame_number=rep_match.frame_number,
|
||||||
|
confidence=rep_match.confidence,
|
||||||
|
label=rep_match.label,
|
||||||
|
phase_z_status=rep_status,
|
||||||
|
raw_content=merged_raw,
|
||||||
|
title=pid,
|
||||||
|
auto_selectable=auto_selectable,
|
||||||
|
filter_reasons=filter_reasons,
|
||||||
|
notes=notes,
|
||||||
|
)
|
||||||
|
_apply_capacity_fit(c_inf, capacity_fit_fn)
|
||||||
|
candidates.append(c_inf)
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Scoring ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# v0 label weights — V4 label → score multiplier.
|
||||||
|
# 향후 axes 추가 (cardinality_fit / hierarchy_coherence / density) 시 확장.
|
||||||
|
V0_LABEL_WEIGHT = {
|
||||||
|
"use_as_is": 1.0,
|
||||||
|
"light_edit": 0.7,
|
||||||
|
"restructure": 0.4,
|
||||||
|
"reject": 0.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def score_candidate(c: CompositionUnit) -> CompositionUnit:
|
||||||
|
"""v0 scoring : confidence × label_weight.
|
||||||
|
|
||||||
|
추후 추가될 axes (rationale 에 자리만 잡아둠) :
|
||||||
|
- cardinality_fit : item_count vs frame ideal/min/max
|
||||||
|
- hierarchy_coherence : merge_type 적합도
|
||||||
|
- density_score : content 밀도 vs zone 크기
|
||||||
|
"""
|
||||||
|
label_weight = V0_LABEL_WEIGHT.get(c.label, 0.0)
|
||||||
|
frame_compat = c.confidence * label_weight
|
||||||
|
c.score = frame_compat
|
||||||
|
# 기존 rationale 보존 (예: collect_candidates 가 넣은 capacity_fit)
|
||||||
|
c.rationale.update({
|
||||||
|
"frame_compat": round(frame_compat, 4),
|
||||||
|
"confidence": c.confidence,
|
||||||
|
"label": c.label,
|
||||||
|
"label_weight": label_weight,
|
||||||
|
"merge_type": c.merge_type,
|
||||||
|
# placeholders for future axes
|
||||||
|
"hierarchy_coherence": None,
|
||||||
|
"density_score": None,
|
||||||
|
})
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Selection ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def select_composition_units(candidates, allowed_statuses: set[str]) -> list[CompositionUnit]:
|
||||||
|
"""Greedy non-overlapping selection by score, with coverage tiebreak.
|
||||||
|
|
||||||
|
1. 모든 candidate 점수 매김
|
||||||
|
2. filter :
|
||||||
|
- phase_z_status ∈ allowed_statuses
|
||||||
|
- auto_selectable=True (W1/W2/W3 신호 통과)
|
||||||
|
3. 정렬 키 = (score desc, source_section_ids 수 desc)
|
||||||
|
— 동점이면 더 많은 section 을 cover 하는 후보 우선.
|
||||||
|
parent_merged_inferred 가 같은 점수의 single 후보를 *coverage 우위* 로 이김.
|
||||||
|
4. greedy : 이미 covered 된 section 을 가진 후보는 skip
|
||||||
|
5. 최종 선택 = covered set 채워나감
|
||||||
|
|
||||||
|
auto_selectable=False candidate 는 자동 선택 X. debug 의 candidates_summary 에는 남음.
|
||||||
|
UI/editor layer 에서 사용자가 별도 처리 가능 (현 v0 범위 X).
|
||||||
|
"""
|
||||||
|
scored = [score_candidate(c) for c in candidates]
|
||||||
|
viable = [
|
||||||
|
c for c in scored
|
||||||
|
if c.phase_z_status in allowed_statuses and c.auto_selectable
|
||||||
|
]
|
||||||
|
viable.sort(key=lambda c: (c.score, len(c.source_section_ids)), reverse=True)
|
||||||
|
|
||||||
|
selected = []
|
||||||
|
covered = set()
|
||||||
|
for c in viable:
|
||||||
|
if any(sid in covered for sid in c.source_section_ids):
|
||||||
|
continue
|
||||||
|
selected.append(c)
|
||||||
|
covered.update(c.source_section_ids)
|
||||||
|
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Layout Preset Selection ───────────────────────────────────
|
||||||
|
|
||||||
|
def select_layout_preset(units: list[CompositionUnit]) -> Optional[str]:
|
||||||
|
"""v0 : count-based default selection.
|
||||||
|
|
||||||
|
1 unit → single
|
||||||
|
2 units → horizontal-2 (default. vertical-2 는 aspect signal 추가 시 분기)
|
||||||
|
3 units → top-1-bottom-2 (default. 다른 3-zone variant 는 content-weight signal 추가 시 분기)
|
||||||
|
4 units → grid-2x2
|
||||||
|
|
||||||
|
v0 한계 :
|
||||||
|
- aspect / content-weight 신호 미반영 → 2 units 는 항상 horizontal, 3 units 는 항상 top-1-bottom-2
|
||||||
|
- 향후 unit.raw_content 기반 weight 산정 시 정교화
|
||||||
|
"""
|
||||||
|
n = len(units)
|
||||||
|
if n == 0:
|
||||||
|
return None
|
||||||
|
if n == 1:
|
||||||
|
return "single"
|
||||||
|
if n == 2:
|
||||||
|
return "horizontal-2"
|
||||||
|
if n == 3:
|
||||||
|
return "top-1-bottom-2"
|
||||||
|
if n == 4:
|
||||||
|
return "grid-2x2"
|
||||||
|
raise ValueError(
|
||||||
|
f"Composition v0 : layout for {n} units not supported (max 4). "
|
||||||
|
"Larger counts require split-into-multiple-slides decision (future)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Public entry — composition pipeline ───────────────────────
|
||||||
|
|
||||||
|
def plan_composition(sections, v4_lookup_fn, v4_label_to_status: dict,
|
||||||
|
allowed_statuses: set[str],
|
||||||
|
capacity_fit_fn=None) -> tuple[list[CompositionUnit], Optional[str], dict]:
|
||||||
|
"""Composition planner v0.2 entry.
|
||||||
|
|
||||||
|
v0.2 변경 :
|
||||||
|
- capacity_fit_fn 주입 시 모든 candidate 에 capacity 사전 검사
|
||||||
|
(silent truncate / mapper FitError 사전 차단). 불일치 시 auto_selectable=False
|
||||||
|
+ filter_reason 'C1: ...'.
|
||||||
|
|
||||||
|
v0.1 / v0.1.1 동작 (유지) :
|
||||||
|
- parent_merged_inferred candidate 생성 (parent V4 없어도)
|
||||||
|
- review 개념 X. auto_selectable + filter_reasons 만으로 자동 결정
|
||||||
|
- selection : score desc + coverage 우세 tiebreak
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
units : 자동 선택된 composition units
|
||||||
|
layout_preset : 8 vocabulary 중 하나 (또는 None)
|
||||||
|
debug : 후보 전체 + capacity_fit + filter_reasons + preset 결정 근거
|
||||||
|
"""
|
||||||
|
candidates = collect_candidates(
|
||||||
|
sections, v4_lookup_fn, v4_label_to_status,
|
||||||
|
auto_renderable_statuses=allowed_statuses,
|
||||||
|
capacity_fit_fn=capacity_fit_fn,
|
||||||
|
)
|
||||||
|
scored_all = [score_candidate(c) for c in candidates]
|
||||||
|
|
||||||
|
units = select_composition_units(candidates, allowed_statuses)
|
||||||
|
preset = select_layout_preset(units)
|
||||||
|
|
||||||
|
def _candidate_state(c: CompositionUnit) -> str:
|
||||||
|
if c in units:
|
||||||
|
return "selected"
|
||||||
|
if c.phase_z_status not in allowed_statuses:
|
||||||
|
return "filtered_status" # V4 label → status not auto-renderable
|
||||||
|
if not c.auto_selectable:
|
||||||
|
# filter_reasons prefix 로 capacity 와 weak 구분
|
||||||
|
if any(r.startswith("C") for r in c.filter_reasons):
|
||||||
|
return "filtered_capacity" # C1 (capacity mismatch)
|
||||||
|
return "filtered_weak" # W1/W2/W3 (parent_merged_inferred only)
|
||||||
|
return "filtered_lost" # viable 였지만 coverage 충돌로 밀림
|
||||||
|
|
||||||
|
candidates_summary = [
|
||||||
|
{
|
||||||
|
"source_section_ids": c.source_section_ids,
|
||||||
|
"merge_type": c.merge_type,
|
||||||
|
"template_id": c.frame_template_id,
|
||||||
|
"label": c.label,
|
||||||
|
"phase_z_status": c.phase_z_status,
|
||||||
|
"score": c.score,
|
||||||
|
"selection_state": _candidate_state(c),
|
||||||
|
"auto_selectable": c.auto_selectable,
|
||||||
|
"filter_reasons": list(c.filter_reasons),
|
||||||
|
"notes": list(c.notes),
|
||||||
|
"capacity_fit": c.rationale.get("capacity_fit"),
|
||||||
|
}
|
||||||
|
for c in scored_all
|
||||||
|
]
|
||||||
|
|
||||||
|
merge_candidates = [
|
||||||
|
s for s in candidates_summary
|
||||||
|
if s["merge_type"] in {"parent_merged", "parent_merged_inferred"}
|
||||||
|
]
|
||||||
|
capacity_mismatches = [
|
||||||
|
s for s in candidates_summary
|
||||||
|
if s["selection_state"] == "filtered_capacity"
|
||||||
|
]
|
||||||
|
|
||||||
|
debug = {
|
||||||
|
"planner_version": "v0.2",
|
||||||
|
"selection_rule": (
|
||||||
|
"score desc, then source_section_ids count desc (coverage tiebreak). "
|
||||||
|
"filter = phase_z_status ∉ allowed_statuses OR auto_selectable=False. "
|
||||||
|
"auto_selectable=False 사유 : C1 (capacity mismatch — silent truncate / FitError 차단), "
|
||||||
|
"W1 (rep not auto-renderable), W2 (all children reject), W3 (majority children non-auto-renderable)."
|
||||||
|
),
|
||||||
|
"candidates_total": len(scored_all),
|
||||||
|
"candidates_viable_auto": len([
|
||||||
|
c for c in scored_all
|
||||||
|
if c.phase_z_status in allowed_statuses and c.auto_selectable
|
||||||
|
]),
|
||||||
|
"candidates_summary": candidates_summary,
|
||||||
|
"merge_candidates": merge_candidates,
|
||||||
|
"capacity_mismatches": capacity_mismatches,
|
||||||
|
"selected_units_count": len(units),
|
||||||
|
"layout_preset": preset,
|
||||||
|
"layout_preset_rationale": (
|
||||||
|
f"v0 count-based: {len(units)} units → {preset}"
|
||||||
|
if preset else "no viable units"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return units, preset, debug
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
"""Phase Z-2 Content Object extractor (B1 v0 — dormant module).
|
||||||
|
|
||||||
|
SPEC v1 §1 의 typed content_object schema 만족하는 dedicated extractor.
|
||||||
|
|
||||||
|
v0 minimal :
|
||||||
|
- 지원 type : text_block, transform_table 2 개 만 (table / image / diagram / details 제외)
|
||||||
|
- role : 모두 "summary" (v0 default — role 정밀화는 별 axis)
|
||||||
|
- dormant — runtime path 미연결 (pipeline / composition / mapper 미터치)
|
||||||
|
- mapper 미수정, 기존 helper move / promote / copy 없음
|
||||||
|
- transform_table 은 *arrow column 보존* 위해 B1 *local helper* 로 구현
|
||||||
|
(regex / parsing 일부가 mapper helper 와 유사 — 단 mapper helper 는 arrow 폐기.
|
||||||
|
향후 helper promote / 통합 refactor 는 별 axis)
|
||||||
|
|
||||||
|
v0 흐름 :
|
||||||
|
section.raw_content
|
||||||
|
→ 3-column markdown table 감지 (arrow glyph 포함) → transform_table
|
||||||
|
→ 나머지 content → text_block (format / bullet_count / has_emphasis 분석)
|
||||||
|
→ list[ContentObject]
|
||||||
|
|
||||||
|
검증 :
|
||||||
|
- dormancy : MDX 03 final.html SHA = canonical 유지 (runtime path 미연결)
|
||||||
|
- correctness : __main__ self-test (text_block 1 case + transform_table 1 case)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# B1 v0 helper 처리 정직 기록 (기존 보고 정정 — 2026-04-30) :
|
||||||
|
# - `phase_z2_mapper` 미수정. 기존 mapper helper (`_extract_markdown_table` 등) move /
|
||||||
|
# promote / copy 없음.
|
||||||
|
# - 단 SPEC v1 §1.2 transform_table.rows = [{from, arrow, to}] schema 가
|
||||||
|
# mapper 의 helper 출력 (from/to 만, arrow 폐기) 와 호환 안 됨.
|
||||||
|
# - 따라서 *arrow column 보존* 이 필요한 transform_table 추출 부분은 본 module 의
|
||||||
|
# *layer-agnostic local helper* (`_capture_3col_transform_table`) 로 *별도 구현*.
|
||||||
|
# - mapper helper 와 regex / parsing 일부 유사 — 향후 *promote / 통합 refactor* 는
|
||||||
|
# 별 axis (B1 안정 후 layer-agnostic helper module 통합 검토 가능).
|
||||||
|
|
||||||
|
|
||||||
|
# ─── ContentObject schema (SPEC v1 §1.1) ────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ContentObject:
|
||||||
|
"""SPEC v1 §1.1 base schema. v0 = text_block + transform_table 만 지원.
|
||||||
|
|
||||||
|
Fields :
|
||||||
|
id : section 내 unique id (예: '03-2.transform-1' / '03-2.text-1')
|
||||||
|
type : "text_block" | "transform_table"
|
||||||
|
role : v0 = "summary" 만 (정밀화는 별 axis)
|
||||||
|
raw_payload : 원본 markdown (자름 / 변형 X — 원문 보존 룰)
|
||||||
|
size_estimate : type 별 (line_count / rows 등)
|
||||||
|
type_specific : type 별 detail (SPEC v1 §1.2)
|
||||||
|
source_shape_index : positional index within source_shape (Option 1, optional)
|
||||||
|
source_shape_kind : "top_bullets" | "h3_subsections" | ... (Option 1, optional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
type: str
|
||||||
|
role: str
|
||||||
|
raw_payload: str
|
||||||
|
size_estimate: dict = field(default_factory=dict)
|
||||||
|
type_specific: dict = field(default_factory=dict)
|
||||||
|
source_shape_index: Optional[int] = None
|
||||||
|
source_shape_kind: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Transform table extraction ─────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
_ARROW_GLYPHS = ("➜", "➠", "→", "->", "=>")
|
||||||
|
|
||||||
|
_TABLE_PATTERN = re.compile(
|
||||||
|
r"(^[ \t]*\|[^\n]+\|\n[ \t]*\|[\s\-:|]+\|\n(?:[ \t]*\|[^\n]+\|\n?)+)",
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _capture_3col_transform_table(content: str) -> tuple[dict | None, str]:
|
||||||
|
"""3-column markdown table 에서 (from / arrow / to) 캡처 → transform_table.
|
||||||
|
|
||||||
|
본 함수 = B1 v0 의 *layer-agnostic extractor helper*. mapper 의
|
||||||
|
`_extract_markdown_table` 와 regex / parsing 의 일부가 유사하나, mapper helper 는
|
||||||
|
arrow column 을 폐기 (from/to 만 추출) — SPEC v1 §1.2 의
|
||||||
|
`transform_table.rows = [{from, arrow, to}]` schema 를 직접 만족 못 함.
|
||||||
|
따라서 arrow column 보존 필요해 본 module 안에 *별도 구현*. mapper 미수정 유지.
|
||||||
|
|
||||||
|
*향후 helper promote / 통합 refactor 는 별 axis* — B1 안정 후 mapper 와
|
||||||
|
*layer-agnostic helper module* 통합 검토 가능.
|
||||||
|
|
||||||
|
arrow column 에 arrow glyph 가 있어야 transform 으로 인정.
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
({"type_specific": ..., "raw_payload": <table markdown>}, content_without_table)
|
||||||
|
또는 (None, original_content) — transform 패턴 미감지 시
|
||||||
|
"""
|
||||||
|
m = _TABLE_PATTERN.search(content)
|
||||||
|
if not m:
|
||||||
|
return None, content
|
||||||
|
|
||||||
|
raw_lines = [r.strip() for r in m.group(1).strip().splitlines() if r.strip()]
|
||||||
|
if len(raw_lines) < 3: # header + separator + ≥1 data row
|
||||||
|
return None, content
|
||||||
|
|
||||||
|
data_rows = raw_lines[2:] # skip header + separator
|
||||||
|
pairs: list[dict] = []
|
||||||
|
arrow_glyph = ""
|
||||||
|
for r in data_rows:
|
||||||
|
cells = [c.strip() for c in r.strip("|").split("|")]
|
||||||
|
if len(cells) < 3:
|
||||||
|
continue
|
||||||
|
f = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[0])
|
||||||
|
a = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[1])
|
||||||
|
t = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[2])
|
||||||
|
if not arrow_glyph:
|
||||||
|
for g in _ARROW_GLYPHS:
|
||||||
|
if g in a:
|
||||||
|
arrow_glyph = g
|
||||||
|
break
|
||||||
|
pairs.append({"from": f, "arrow": a, "to": t})
|
||||||
|
|
||||||
|
if not pairs:
|
||||||
|
return None, content
|
||||||
|
|
||||||
|
# transform 인지 검증 — arrow glyph 가 *어느 row 든* 등장해야
|
||||||
|
has_arrow = any(any(g in p["arrow"] for g in _ARROW_GLYPHS) for p in pairs)
|
||||||
|
if not has_arrow:
|
||||||
|
return None, content
|
||||||
|
|
||||||
|
type_specific = {
|
||||||
|
"pair_count": len(pairs),
|
||||||
|
"arrow_glyph": arrow_glyph,
|
||||||
|
"rows": pairs,
|
||||||
|
}
|
||||||
|
raw_table = m.group(1)
|
||||||
|
remaining = content[: m.start()] + content[m.end() :]
|
||||||
|
return ({"type_specific": type_specific, "raw_payload": raw_table}, remaining)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Text block extraction ──────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_text_block_specific(content: str) -> tuple[dict, int]:
|
||||||
|
"""text_block 의 type_specific + line_count 추출.
|
||||||
|
|
||||||
|
format 결정 :
|
||||||
|
- top bullet 0 → paragraph
|
||||||
|
- top bullet 있음, nested 0 → bullet_list
|
||||||
|
- top bullet + nested → nested_list
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
(type_specific dict, line_count)
|
||||||
|
"""
|
||||||
|
lines = content.splitlines()
|
||||||
|
|
||||||
|
top_bullets = sum(1 for l in lines if re.match(r"^[\*\-]\s", l))
|
||||||
|
nested_bullets = sum(1 for l in lines if re.match(r"^\s+[\*\-]\s", l))
|
||||||
|
|
||||||
|
# max_indent_level (2-space indent 단위)
|
||||||
|
max_indent = 0
|
||||||
|
for l in lines:
|
||||||
|
mm = re.match(r"^( *)[\*\-]\s", l)
|
||||||
|
if mm:
|
||||||
|
level = len(mm.group(1)) // 2
|
||||||
|
max_indent = max(max_indent, level)
|
||||||
|
|
||||||
|
if top_bullets == 0:
|
||||||
|
fmt = "paragraph"
|
||||||
|
elif nested_bullets > 0:
|
||||||
|
fmt = "nested_list"
|
||||||
|
else:
|
||||||
|
fmt = "bullet_list"
|
||||||
|
|
||||||
|
has_emphasis = bool(
|
||||||
|
re.search(r"\*\*[^*\n]+\*\*", content)
|
||||||
|
or re.search(r"(?<!\*)\*[^*\n]+\*(?!\*)", content)
|
||||||
|
)
|
||||||
|
|
||||||
|
line_count = sum(1 for l in lines if l.strip())
|
||||||
|
|
||||||
|
type_specific = {
|
||||||
|
"format": fmt,
|
||||||
|
"bullet_count": top_bullets,
|
||||||
|
"max_indent_level": max_indent,
|
||||||
|
"has_emphasis": has_emphasis,
|
||||||
|
}
|
||||||
|
return type_specific, line_count
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Public entry ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def extract_content_objects(section, source_shape: Optional[str] = None) -> list[ContentObject]:
|
||||||
|
"""MDX section.raw_content → typed content_object list (SPEC v1 §1).
|
||||||
|
|
||||||
|
v0 minimal :
|
||||||
|
- 1 section → 1~2 ContentObject (transform_table + text_block 또는 text_block 만)
|
||||||
|
- role = "summary" (모두 — v0 default)
|
||||||
|
- 미지원 type (table / image / diagram / details) = 무시 (별 axis)
|
||||||
|
- 원문 (raw_payload) = 자름 / 변형 X (원문 보존 룰)
|
||||||
|
|
||||||
|
Option 1 (source_shape-aware) :
|
||||||
|
- source_shape="top_bullets" : raw_content 를 mapper.split_source 로 N units 분할 →
|
||||||
|
unit 별 ContentObject 1 개 (text_block) with source_shape_index=i / source_shape_kind="top_bullets"
|
||||||
|
- source_shape=None 또는 미지원 값 (h3_subsections 등) : 기존 legacy 동작
|
||||||
|
|
||||||
|
Args :
|
||||||
|
section : MdxSection-like 객체 (section_id, raw_content 필드 필요)
|
||||||
|
source_shape : "top_bullets" 시 source_shape-aware 분기. None 이면 legacy.
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
list[ContentObject] — legacy 0~2 / top_bullets N (bullet 수)
|
||||||
|
"""
|
||||||
|
content = section.raw_content
|
||||||
|
section_id = section.section_id
|
||||||
|
|
||||||
|
if source_shape in ("top_bullets", "h3_subsections"):
|
||||||
|
from phase_z2_mapper import split_source
|
||||||
|
units = split_source(source_shape, content)
|
||||||
|
objects: list[ContentObject] = []
|
||||||
|
for i, unit in enumerate(units):
|
||||||
|
if source_shape == "h3_subsections" and isinstance(unit, tuple) and len(unit) == 2:
|
||||||
|
title, body = unit
|
||||||
|
raw_payload = f"### {title}\n{body}".strip()
|
||||||
|
else:
|
||||||
|
raw_payload = (unit if isinstance(unit, str) else str(unit)).strip()
|
||||||
|
if not raw_payload:
|
||||||
|
continue
|
||||||
|
text_specific, line_count = _detect_text_block_specific(raw_payload)
|
||||||
|
objects.append(
|
||||||
|
ContentObject(
|
||||||
|
id=f"{section_id}.text-{i + 1}",
|
||||||
|
type="text_block",
|
||||||
|
role="summary",
|
||||||
|
raw_payload=raw_payload,
|
||||||
|
size_estimate={"line_count": line_count},
|
||||||
|
type_specific=text_specific,
|
||||||
|
source_shape_index=i,
|
||||||
|
source_shape_kind=source_shape,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return objects
|
||||||
|
|
||||||
|
# legacy path (source_shape=None 또는 미지원 값)
|
||||||
|
objects: list[ContentObject] = []
|
||||||
|
|
||||||
|
# 1. transform_table 추출 시도 (3-col with arrow)
|
||||||
|
transform_result, remaining = _capture_3col_transform_table(content)
|
||||||
|
if transform_result is not None:
|
||||||
|
objects.append(
|
||||||
|
ContentObject(
|
||||||
|
id=f"{section_id}.transform-1",
|
||||||
|
type="transform_table",
|
||||||
|
role="summary",
|
||||||
|
raw_payload=transform_result["raw_payload"],
|
||||||
|
size_estimate={"rows": transform_result["type_specific"]["pair_count"]},
|
||||||
|
type_specific=transform_result["type_specific"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 2. text_block 추출 (transform 추출 후 남은 content, 또는 transform 없으면 전체)
|
||||||
|
text_remainder = remaining if transform_result is not None else content
|
||||||
|
if text_remainder.strip():
|
||||||
|
text_specific, line_count = _detect_text_block_specific(text_remainder)
|
||||||
|
objects.append(
|
||||||
|
ContentObject(
|
||||||
|
id=f"{section_id}.text-1",
|
||||||
|
type="text_block",
|
||||||
|
role="summary",
|
||||||
|
raw_payload=text_remainder.strip(),
|
||||||
|
size_estimate={"line_count": line_count},
|
||||||
|
type_specific=text_specific,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Self-test (B1 v0 correctness 검증) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _run_self_test():
|
||||||
|
"""v0 unit test : text_block 1 case + transform_table 1 case.
|
||||||
|
|
||||||
|
scope-lock 의 검증 (b) correctness — 추출기 정확성 확인.
|
||||||
|
fixed input 기반, MDX 01/02/04 미사용.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class MockSection:
|
||||||
|
def __init__(self, section_id: str, raw_content: str):
|
||||||
|
self.section_id = section_id
|
||||||
|
self.raw_content = raw_content
|
||||||
|
|
||||||
|
# ─── Test 1 : text_block (nested_list 형태, F13 style) ───────
|
||||||
|
text_section = MockSection(
|
||||||
|
"test-1",
|
||||||
|
"* **기술 부족**\n"
|
||||||
|
" * 디지털 도구 미숙\n"
|
||||||
|
" * BIM 활용 제한\n"
|
||||||
|
"* **인력 부족**\n"
|
||||||
|
" * 전문가 부재\n"
|
||||||
|
"* **자연 환경**\n"
|
||||||
|
" * 지역적 제약\n",
|
||||||
|
)
|
||||||
|
objs1 = extract_content_objects(text_section)
|
||||||
|
assert len(objs1) == 1, f"text-only section → 1 obj 기대, got {len(objs1)}"
|
||||||
|
o = objs1[0]
|
||||||
|
assert o.type == "text_block", f"type=text_block 기대, got {o.type}"
|
||||||
|
assert o.role == "summary"
|
||||||
|
assert o.id == "test-1.text-1"
|
||||||
|
assert o.type_specific["format"] == "nested_list", f"format=nested_list 기대, got {o.type_specific['format']}"
|
||||||
|
assert o.type_specific["bullet_count"] == 3, f"top bullet=3 기대, got {o.type_specific['bullet_count']}"
|
||||||
|
assert o.type_specific["max_indent_level"] >= 1, "nested 가 있으니 max_indent ≥ 1"
|
||||||
|
assert o.type_specific["has_emphasis"] is True, "**bold** 존재 → has_emphasis=True"
|
||||||
|
assert o.size_estimate["line_count"] >= 6
|
||||||
|
assert "기술 부족" in o.raw_payload, "원문 보존 — '기술 부족' 잔존 필요"
|
||||||
|
print("[OK] Test 1 (text_block) passed.")
|
||||||
|
|
||||||
|
# ─── Test 2 : transform_table (3-col, arrow 포함) + 잔여 text ─
|
||||||
|
transform_section = MockSection(
|
||||||
|
"test-2",
|
||||||
|
"**프로세스 변환**\n"
|
||||||
|
"\n"
|
||||||
|
"| AS-IS | ➜ | TO-BE |\n"
|
||||||
|
"|---|---|---|\n"
|
||||||
|
"| 도면 중심 | ➜ | BIM 모델 중심 |\n"
|
||||||
|
"| 단계별 분리 | ➜ | 통합 협업 |\n"
|
||||||
|
"| 사후 검토 | ➜ | 실시간 검증 |\n"
|
||||||
|
"\n"
|
||||||
|
"추가 설명 : 위 변환이 핵심.\n",
|
||||||
|
)
|
||||||
|
objs2 = extract_content_objects(transform_section)
|
||||||
|
assert len(objs2) == 2, f"transform+text → 2 obj 기대, got {len(objs2)}"
|
||||||
|
|
||||||
|
# transform_table 검증
|
||||||
|
t = objs2[0]
|
||||||
|
assert t.type == "transform_table", f"첫 obj=transform_table 기대, got {t.type}"
|
||||||
|
assert t.role == "summary"
|
||||||
|
assert t.id == "test-2.transform-1"
|
||||||
|
assert t.type_specific["pair_count"] == 3, f"pair_count=3 기대, got {t.type_specific['pair_count']}"
|
||||||
|
assert t.type_specific["arrow_glyph"] == "➜", f"arrow_glyph=➜ 기대, got {t.type_specific['arrow_glyph']}"
|
||||||
|
assert len(t.type_specific["rows"]) == 3
|
||||||
|
assert t.type_specific["rows"][0]["from"] == "도면 중심"
|
||||||
|
assert t.type_specific["rows"][0]["to"] == "BIM 모델 중심"
|
||||||
|
assert t.size_estimate["rows"] == 3
|
||||||
|
assert "도면 중심" in t.raw_payload, "raw_payload 에 원본 table 보존"
|
||||||
|
|
||||||
|
# text_block 검증 (transform 제거 후 남은 content)
|
||||||
|
tb = objs2[1]
|
||||||
|
assert tb.type == "text_block", f"두번째 obj=text_block 기대, got {tb.type}"
|
||||||
|
assert tb.id == "test-2.text-1"
|
||||||
|
assert "프로세스 변환" in tb.raw_payload, "transform 제거 후 surrounding text 보존 — '프로세스 변환'"
|
||||||
|
assert "추가 설명" in tb.raw_payload, "transform 뒤 잔여 text 보존 — '추가 설명'"
|
||||||
|
print("[OK] Test 2 (transform_table + text_block) passed.")
|
||||||
|
|
||||||
|
print("\n=== B1 v0 self-test PASS ===")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_run_self_test()
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
"""Phase Z-2 retry_failure_classifier + next_action_router (A4 — 분류 / 매핑만).
|
||||||
|
|
||||||
|
A3 (zone_ratio_retry) 의 결과 (retry_trace) 를 받아 :
|
||||||
|
1. **retry_failure_classifier** : 실패 type 을 4 종 중 하나로 분류
|
||||||
|
2. **next_action_router** : failure_type → next_proposed_action 매핑
|
||||||
|
|
||||||
|
본 module 은 ***분류 + 매핑까지만***. layout_adjust / frame_reselect / details_popup
|
||||||
|
실행 X. retry_trace 에 `failure_classification` + `next_action_proposal` 두 필드 추가.
|
||||||
|
|
||||||
|
**잠근 매핑** (사용자 잠금 — 2026-04-29) :
|
||||||
|
|
||||||
|
| failure_type | next_proposed_action |
|
||||||
|
|---|---|
|
||||||
|
| donor_slack_insufficient | layout_adjust |
|
||||||
|
| no_donor_candidates | layout_adjust |
|
||||||
|
| rerender_still_fails | frame_reselect |
|
||||||
|
| not_attempted | none |
|
||||||
|
|
||||||
|
**escalation 단계 hierarchy** (이번 기본 매핑이 따르는 원칙) :
|
||||||
|
```
|
||||||
|
layout_adjust (가장 가벼움 — zone 배치만 변경)
|
||||||
|
↓ 그래도 안 되면
|
||||||
|
frame_reselect (중간 — frame 자체 변경)
|
||||||
|
↓ 그래도 안 되면
|
||||||
|
details_popup_escalation (가장 invasive — content popup, 마지막 resort)
|
||||||
|
```
|
||||||
|
|
||||||
|
`details_popup_escalation` 은 본 매핑에 *없음* — tabular_overflow / structural_major_overflow /
|
||||||
|
frame_reselect 실패 이후 단계에서 다룸 (별 step).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ─── §A4-1 failure_type registry ──────────────────────────────────
|
||||||
|
|
||||||
|
FAILURE_TYPE_DESCRIPTIONS: dict[str, str] = {
|
||||||
|
"not_attempted": (
|
||||||
|
"retry was not attempted (router_active=False or zone_ratio_retry "
|
||||||
|
"not in proposed actions). 정상 path 의 일부 — 실패 X"
|
||||||
|
),
|
||||||
|
"donor_slack_insufficient": (
|
||||||
|
"primary donor 의 slack 이 target_added_px 보다 작음. 현재 layout 안 "
|
||||||
|
"redistribution 한도 도달"
|
||||||
|
),
|
||||||
|
"no_donor_candidates": (
|
||||||
|
"donor 후보 자체 없음 — single layout / sibling visual fail / capacity "
|
||||||
|
"mismatch / slack 0 등의 이유로 zone redistribution 불가"
|
||||||
|
),
|
||||||
|
"rerender_still_fails": (
|
||||||
|
"redistribution 실행 + rerender 까지 했는데도 visual_check 실패. "
|
||||||
|
"현재 frame/zone 조합이 content 와 맞지 않음"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── §A4-2 next_action mapping (사용자 잠금) ──────────────────────
|
||||||
|
|
||||||
|
NEXT_ACTION_BY_FAILURE: dict[str, str] = {
|
||||||
|
"donor_slack_insufficient": "layout_adjust",
|
||||||
|
"no_donor_candidates": "layout_adjust",
|
||||||
|
"rerender_still_fails": "frame_reselect",
|
||||||
|
"not_attempted": "none",
|
||||||
|
}
|
||||||
|
|
||||||
|
NEXT_ACTION_RATIONALE: dict[str, str] = {
|
||||||
|
"donor_slack_insufficient": (
|
||||||
|
"현재 layout 안 redistribution 끝남 → 다른 layout topology 검토 "
|
||||||
|
"(layout_adjust). frame 자체는 아직 의심 대상 X"
|
||||||
|
),
|
||||||
|
"no_donor_candidates": (
|
||||||
|
"donor 자체 없거나 모두 막힘 → layout topology 부터 재구성하여 "
|
||||||
|
"sibling/space 다시 만들어 보는 게 우선 (layout_adjust). frame 변경은 그 다음"
|
||||||
|
),
|
||||||
|
"rerender_still_fails": (
|
||||||
|
"redistribution + rerender 까지 했는데도 visual fail → 현재 "
|
||||||
|
"frame/zone 조합 자체 부적합, V4 top-k 의 다른 frame 평가 (frame_reselect). "
|
||||||
|
"popup 직행은 아직 빠름 (tabular / structural_major 가 아닌 한)"
|
||||||
|
),
|
||||||
|
"not_attempted": (
|
||||||
|
"retry 시도 자체가 없었음 (visual ok 등) — escalation 불필요"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 본 매핑이 가리키는 next action 들의 *현재 코드* 구현 상태
|
||||||
|
NEXT_ACTION_IMPLEMENTATION_STATUS: dict[str, str] = {
|
||||||
|
"layout_adjust": "MISSING",
|
||||||
|
"frame_reselect": "MISSING",
|
||||||
|
"none": "n/a",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── classifier ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def classify_retry_failure(retry_trace: dict) -> Optional[dict]:
|
||||||
|
"""retry_trace → failure classification.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None : retry 가 *성공* 한 case (retry_passed=True). 분류할 failure 없음.
|
||||||
|
dict : {failure_type, classification_rule}
|
||||||
|
"""
|
||||||
|
# case 0 : retry 성공 — failure 없음
|
||||||
|
if retry_trace.get("retry_passed"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# case 1 : retry 시도 자체 안 됨 (router_active=False 또는 다른 action)
|
||||||
|
if not retry_trace.get("retry_attempted"):
|
||||||
|
return {
|
||||||
|
"failure_type": "not_attempted",
|
||||||
|
"classification_rule": (
|
||||||
|
"retry_attempted=False — router_active=False or zone_ratio_retry "
|
||||||
|
"not in proposed_actions"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# case 2 : plan 단계 실패 (rerender 안 일어남)
|
||||||
|
plan = retry_trace.get("plan") or {}
|
||||||
|
if plan and not plan.get("feasible"):
|
||||||
|
reason = (plan.get("failure_reason") or "")
|
||||||
|
reason_lower = reason.lower()
|
||||||
|
|
||||||
|
# donor slack insufficient — primary donor 가 있으나 slack 부족
|
||||||
|
if (
|
||||||
|
"primary donor" in reason_lower
|
||||||
|
and "slack" in reason_lower
|
||||||
|
and "target_added_px" in reason_lower
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"failure_type": "donor_slack_insufficient",
|
||||||
|
"classification_rule": (
|
||||||
|
"plan.feasible=False AND failure_reason matches "
|
||||||
|
"'primary donor ... slack ... target_added_px ...'"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# no donor candidates — sibling 자체 없거나 모두 자격 미달
|
||||||
|
if "no donor candidates" in reason_lower:
|
||||||
|
return {
|
||||||
|
"failure_type": "no_donor_candidates",
|
||||||
|
"classification_rule": (
|
||||||
|
"plan.feasible=False AND failure_reason matches "
|
||||||
|
"'no donor candidates'"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 위 두 패턴 미매칭 — 보수적으로 no_donor_candidates 로 분류
|
||||||
|
# (donor 가 거의 모두 막힌 경우 와 구조적으로 비슷)
|
||||||
|
return {
|
||||||
|
"failure_type": "no_donor_candidates",
|
||||||
|
"classification_rule": (
|
||||||
|
f"plan.feasible=False, failure_reason did not match known patterns. "
|
||||||
|
f"defaulting to 'no_donor_candidates'. raw failure_reason: {reason!r}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# case 3 : plan feasible AND rerender 했는데 visual fail
|
||||||
|
if retry_trace.get("rerender_attempted") and not retry_trace.get("retry_passed"):
|
||||||
|
return {
|
||||||
|
"failure_type": "rerender_still_fails",
|
||||||
|
"classification_rule": (
|
||||||
|
"plan.feasible=True AND rerender_attempted=True AND retry_passed=False"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# case 4 (defensive) : 어떤 case 에도 안 잡힘 — 보수적 fallback
|
||||||
|
return {
|
||||||
|
"failure_type": "not_attempted",
|
||||||
|
"classification_rule": (
|
||||||
|
"no failure pattern matched (defensive fallback). retry_trace 구조 "
|
||||||
|
"예상과 다름 — 검토 필요"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── router ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def route_retry_failure(failure_type: str) -> dict:
|
||||||
|
"""failure_type → next_proposed_action mapping.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict :
|
||||||
|
next_proposed_action
|
||||||
|
next_action_rationale
|
||||||
|
next_action_implementation_status
|
||||||
|
mapping_source
|
||||||
|
"""
|
||||||
|
next_action = NEXT_ACTION_BY_FAILURE.get(failure_type)
|
||||||
|
if next_action is None:
|
||||||
|
return {
|
||||||
|
"next_proposed_action": None,
|
||||||
|
"next_action_rationale": (
|
||||||
|
f"failure_type '{failure_type}' has no mapping in NEXT_ACTION_BY_FAILURE"
|
||||||
|
),
|
||||||
|
"next_action_implementation_status": "unknown",
|
||||||
|
"mapping_source": "no mapping (unknown failure_type)",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"next_proposed_action": next_action,
|
||||||
|
"next_action_rationale": NEXT_ACTION_RATIONALE.get(failure_type, ""),
|
||||||
|
"next_action_implementation_status": NEXT_ACTION_IMPLEMENTATION_STATUS.get(
|
||||||
|
next_action, "unknown"
|
||||||
|
),
|
||||||
|
"mapping_source": "A4 NEXT_ACTION_BY_FAILURE (사용자 잠금 2026-04-29)",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── enrichment wrapper ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def enrich_retry_trace_with_failure_classification(retry_trace: dict) -> dict:
|
||||||
|
"""retry_trace 에 `failure_classification` + `next_action_proposal` 두 필드 추가.
|
||||||
|
|
||||||
|
Mutates retry_trace in place AND returns it.
|
||||||
|
|
||||||
|
retry_passed=True 인 경우 → 두 필드 모두 None (failure 없음, escalation 없음).
|
||||||
|
"""
|
||||||
|
fc = classify_retry_failure(retry_trace)
|
||||||
|
if fc is None:
|
||||||
|
# retry succeeded — no failure to classify
|
||||||
|
retry_trace["failure_classification"] = None
|
||||||
|
retry_trace["next_action_proposal"] = None
|
||||||
|
return retry_trace
|
||||||
|
|
||||||
|
failure_type = fc["failure_type"]
|
||||||
|
nr = route_retry_failure(failure_type)
|
||||||
|
|
||||||
|
retry_trace["failure_classification"] = {
|
||||||
|
"failure_type": failure_type,
|
||||||
|
"failure_type_description": FAILURE_TYPE_DESCRIPTIONS.get(failure_type, ""),
|
||||||
|
"classification_rule": fc["classification_rule"],
|
||||||
|
}
|
||||||
|
retry_trace["next_action_proposal"] = nr
|
||||||
|
return retry_trace
|
||||||
@@ -0,0 +1,442 @@
|
|||||||
|
"""Phase Z-2 Internal Region planner (B2 v0 — dormant module).
|
||||||
|
|
||||||
|
SPEC v1 §2 의 Layer A planner — ContentObject[] → InternalRegion[] + region_layout.
|
||||||
|
|
||||||
|
v0 minimal :
|
||||||
|
- 지원 case : text_block only / text_block + transform_table 2 가지
|
||||||
|
- 3-way decision : whole + split (group merge 미지원 — 별 axis)
|
||||||
|
- topology vocabulary 출력 : `region-single` + `region-vertical-stack` 2 entry 만
|
||||||
|
- SPEC v1 §2.5 algorithm = rule 1 + rule 6 만 구현 (rules 2~5 명시 deferred)
|
||||||
|
- role 할당 : type 기반 (text_block → primary / transform_table → supporting)
|
||||||
|
- split 결정 : distinct content type 기준 (같은 type 만 → single region)
|
||||||
|
- frame_match_strategy : kind="frame_match" / frame_id=None (Step 9 / B4 책임)
|
||||||
|
- dormant — runtime path 미연결 (pipeline / composition / mapper 미터치)
|
||||||
|
|
||||||
|
책임 boundary :
|
||||||
|
- B2 = region 생성 (split / role / ratio / topology)
|
||||||
|
- Step 9 / B4 = frame compatibility / frame selection / display strategy
|
||||||
|
- accepted_content_types 기반 compatibility 판단은 B2 책임 *아님*
|
||||||
|
|
||||||
|
frame_contracts 인자 :
|
||||||
|
- signature 에 둠 (future hook). v0 에서는 *output 결정에 사용 X*.
|
||||||
|
- 향후 display_only 활성화 / B4 통합 시 hook 자리.
|
||||||
|
|
||||||
|
검증 :
|
||||||
|
- dormancy : MDX 03 final.html SHA = canonical 유지 (runtime path 미연결)
|
||||||
|
- correctness : __main__ self-test (text-only 1 case + text+transform 1 case)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
# B2 v0 input contract = B1 의 ContentObject (phase_z2_content_extractor).
|
||||||
|
# 두 module 모두 dormant — runtime path 와 무관한 *layer-agnostic 의존*.
|
||||||
|
from phase_z2_content_extractor import ContentObject
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Constants (B2 v0 lock) ──────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
# transform_table 의 size proxy 환산 계수 (SPEC v1 §2.4 size proxy).
|
||||||
|
# pair 1 개 = 1.5 line 등가 (heuristic, content 기반 ratio 산정용).
|
||||||
|
# 정밀화는 향후 axis (visual_hints / content density signal).
|
||||||
|
_PAIR_HEIGHT_FACTOR = 1.5
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Output schema (SPEC v1 §2.1 + §2.5) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class InternalRegion:
|
||||||
|
"""SPEC v1 §2.1 Internal Region entity schema.
|
||||||
|
|
||||||
|
Fields :
|
||||||
|
region_id : zone 내 unique id (예: '{section_id}.region-1')
|
||||||
|
role : 'primary' | 'supporting' | (B2 v0 = 두 개만 사용)
|
||||||
|
content_type : 'text_block' | 'transform_table' (v0 supported)
|
||||||
|
ratio_estimate : zone 내 비율 (sum normalize = 1.0)
|
||||||
|
content_unit_ids : 본 region 에 묶인 content_object id list
|
||||||
|
frame_match_strategy : {kind, frame_id, display_strategy}
|
||||||
|
— B2 v0 에서 kind="frame_match" / frame_id=None /
|
||||||
|
display_strategy="inline_full" 고정.
|
||||||
|
실제 frame 결정은 Step 9 / B4 책임.
|
||||||
|
source_shape_index : positional index from B1 source_shape split (Option 1, optional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
region_id: str
|
||||||
|
role: str
|
||||||
|
content_type: str
|
||||||
|
ratio_estimate: float
|
||||||
|
content_unit_ids: list[str]
|
||||||
|
frame_match_strategy: dict
|
||||||
|
source_shape_index: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RegionLayout:
|
||||||
|
"""SPEC v1 §2.5 region_layout — zone 내 region 들의 *공간 배치 패턴*.
|
||||||
|
|
||||||
|
B2 v0 가 출력하는 vocabulary :
|
||||||
|
- 'region-single' (rule 1 — region_count == 1)
|
||||||
|
- 'region-vertical-stack' (rule 6 fallback — 그 외)
|
||||||
|
|
||||||
|
rules 2~5 (region-preview-details / region-grid-2x2 / region-main-support /
|
||||||
|
region-horizontal-split) 는 SPEC 정의 있으나 *B2 v0 deferred*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
region_layout_type: str # 'region-single' | 'region-vertical-stack'
|
||||||
|
region_order: list[str] # region_id 의 배치 순서
|
||||||
|
region_placement: str # 'single' | 'vertical'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ZoneRegionPlan:
|
||||||
|
"""B2 v0 의 출력 — 1 zone 의 region 분할 결과 + layout.
|
||||||
|
|
||||||
|
Fields :
|
||||||
|
internal_regions : list[InternalRegion]
|
||||||
|
region_layout : RegionLayout
|
||||||
|
"""
|
||||||
|
|
||||||
|
internal_regions: list[InternalRegion] = field(default_factory=list)
|
||||||
|
region_layout: Optional[RegionLayout] = None
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
# B2 v0 지원 type 별 (role, size proxy 추출 함수).
|
||||||
|
# - text_block : role=primary, size = size_estimate.line_count
|
||||||
|
# - transform_table : role=supporting, size = size_estimate.rows × _PAIR_HEIGHT_FACTOR
|
||||||
|
_TYPE_ROLE: dict[str, str] = {
|
||||||
|
"text_block": "primary",
|
||||||
|
"transform_table": "supporting",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _size_proxy(obj: ContentObject) -> float:
|
||||||
|
"""Content object 의 *공간 크기 proxy* (SPEC v1 §2.4).
|
||||||
|
|
||||||
|
text_block : line_count
|
||||||
|
transform_table : rows × _PAIR_HEIGHT_FACTOR
|
||||||
|
그 외 : 0 (B2 v0 미지원 type)
|
||||||
|
"""
|
||||||
|
if obj.type == "text_block":
|
||||||
|
return float(obj.size_estimate.get("line_count", 0))
|
||||||
|
if obj.type == "transform_table":
|
||||||
|
return float(obj.size_estimate.get("rows", 0)) * _PAIR_HEIGHT_FACTOR
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def _group_by_type_preserving_order(
|
||||||
|
content_objects: list[ContentObject],
|
||||||
|
) -> dict[str, list[ContentObject]]:
|
||||||
|
"""content_objects 를 type 별로 grouping. 등장 순서 보존 (dict 의 ordered 특성)."""
|
||||||
|
groups: dict[str, list[ContentObject]] = {}
|
||||||
|
for obj in content_objects:
|
||||||
|
groups.setdefault(obj.type, []).append(obj)
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
# region_order 결정 시 type 우선순위 — primary 먼저, supporting 다음.
|
||||||
|
# B2 v0 type 만 등록. 향후 axis 에서 secondary / reference 추가 가능.
|
||||||
|
_TYPE_ORDER_PRIORITY: dict[str, int] = {
|
||||||
|
"text_block": 0, # primary
|
||||||
|
"transform_table": 1, # supporting
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Option 1 source_shape-aware planner ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _plan_by_source_shape_index(
|
||||||
|
content_objects: list[ContentObject],
|
||||||
|
section_id: str,
|
||||||
|
) -> ZoneRegionPlan:
|
||||||
|
"""source_shape_index 기준 *positional* region grouping (Option 1).
|
||||||
|
|
||||||
|
같은 source_shape_index 끼리 1 region. mapper 의 split_source 와 cardinality align —
|
||||||
|
F13 의 top_bullets 3 개 → 3 region 으로 mapper pillar_1/2/3 와 1:1 positional.
|
||||||
|
"""
|
||||||
|
groups: dict[int, list[ContentObject]] = {}
|
||||||
|
for obj in content_objects:
|
||||||
|
if obj.source_shape_index is None:
|
||||||
|
continue
|
||||||
|
groups.setdefault(obj.source_shape_index, []).append(obj)
|
||||||
|
|
||||||
|
sorted_indices = sorted(groups.keys())
|
||||||
|
|
||||||
|
# size proxy + ratio (positional region 내부 size_estimate 합산)
|
||||||
|
index_sizes: dict[int, float] = {idx: sum(_size_proxy(o) for o in groups[idx]) for idx in sorted_indices}
|
||||||
|
total_size = sum(index_sizes.values())
|
||||||
|
if total_size <= 0:
|
||||||
|
equal_share = 1.0 / max(len(sorted_indices), 1)
|
||||||
|
index_sizes = {idx: equal_share for idx in sorted_indices}
|
||||||
|
total_size = sum(index_sizes.values()) or 1.0
|
||||||
|
|
||||||
|
regions: list[InternalRegion] = []
|
||||||
|
for ord_idx, sidx in enumerate(sorted_indices, start=1):
|
||||||
|
objs = groups[sidx]
|
||||||
|
# role / content_type : group 내 첫 obj 의 type 기반 (Option 1 pilot = text_block 동질)
|
||||||
|
primary_obj = objs[0]
|
||||||
|
ctype = primary_obj.type
|
||||||
|
regions.append(
|
||||||
|
InternalRegion(
|
||||||
|
region_id=f"{section_id}.region-{ord_idx}",
|
||||||
|
role=_TYPE_ROLE.get(ctype, "primary"),
|
||||||
|
content_type=ctype,
|
||||||
|
ratio_estimate=round(index_sizes[sidx] / total_size, 4),
|
||||||
|
content_unit_ids=[o.id for o in objs],
|
||||||
|
frame_match_strategy={
|
||||||
|
"kind": "frame_match",
|
||||||
|
"frame_id": None,
|
||||||
|
"display_strategy": "inline_full",
|
||||||
|
},
|
||||||
|
source_shape_index=sidx,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
region_count = len(regions)
|
||||||
|
if region_count == 1:
|
||||||
|
layout_type = "region-single"
|
||||||
|
placement = "single"
|
||||||
|
else:
|
||||||
|
layout_type = "region-vertical-stack"
|
||||||
|
placement = "vertical"
|
||||||
|
|
||||||
|
region_order = [r.region_id for r in regions]
|
||||||
|
return ZoneRegionPlan(
|
||||||
|
internal_regions=regions,
|
||||||
|
region_layout=RegionLayout(
|
||||||
|
region_layout_type=layout_type,
|
||||||
|
region_order=region_order,
|
||||||
|
region_placement=placement,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Public entry ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def plan_internal_regions(
|
||||||
|
content_objects: list[ContentObject],
|
||||||
|
frame_contracts: Optional[list[dict[str, Any]]] = None, # v0 unused, future hook
|
||||||
|
section_id: str = "",
|
||||||
|
) -> ZoneRegionPlan:
|
||||||
|
"""ContentObject[] → ZoneRegionPlan (region 분할 + topology + ratio + role).
|
||||||
|
|
||||||
|
B2 v0 algorithm :
|
||||||
|
1. content_objects 를 type 별로 grouping (등장 순서 보존)
|
||||||
|
2. distinct type 수 → region_count 결정 (split 결정)
|
||||||
|
3. region 별 :
|
||||||
|
- role = type 기반 (_TYPE_ROLE)
|
||||||
|
- ratio_estimate = type 내 size proxy 합 / 전체 합 (normalize=1.0)
|
||||||
|
- frame_match_strategy = {kind: 'frame_match', frame_id: None,
|
||||||
|
display_strategy: 'inline_full'} (Step 9 / B4 영역)
|
||||||
|
4. topology vocabulary 결정 — SPEC v1 §2.5 :
|
||||||
|
- rule 1 : region_count == 1 → region-single
|
||||||
|
- rules 2~5 : *deferred* (SPEC 정의만, B2 v0 미구현)
|
||||||
|
- rule 6 fallback : 그 외 → region-vertical-stack
|
||||||
|
5. region_order = type priority (primary → supporting) 순.
|
||||||
|
|
||||||
|
Args :
|
||||||
|
content_objects : list[ContentObject] — B1 v0 extractor 출력
|
||||||
|
frame_contracts : v0 unused (future hook). signature 에 두되 output 결정에 미사용.
|
||||||
|
section_id : region_id 생성용 prefix
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
ZoneRegionPlan (1 zone 의 plan, singular).
|
||||||
|
|
||||||
|
Note :
|
||||||
|
- frame_contracts 무시 — 본 v0 는 *frame compatibility 판단 안 함*.
|
||||||
|
compatibility 판단은 Step 9 / B4 책임.
|
||||||
|
- empty content_objects → empty plan (region_layout=None) — caller 가 사전 차단 권장.
|
||||||
|
"""
|
||||||
|
if not content_objects:
|
||||||
|
return ZoneRegionPlan()
|
||||||
|
|
||||||
|
# Option 1 source_shape-aware path : ContentObjects 가 source_shape_index 보유 시 *positional*
|
||||||
|
# grouping. 같은 index 끼리 1 region. mapper 의 split_source 와 cardinality align.
|
||||||
|
if any(o.source_shape_index is not None for o in content_objects):
|
||||||
|
return _plan_by_source_shape_index(content_objects, section_id)
|
||||||
|
|
||||||
|
# 1. type 별 grouping
|
||||||
|
groups = _group_by_type_preserving_order(content_objects)
|
||||||
|
|
||||||
|
# 2. region 별 size proxy 합 + 전체 합
|
||||||
|
type_sizes: dict[str, float] = {}
|
||||||
|
for ctype, objs in groups.items():
|
||||||
|
type_sizes[ctype] = sum(_size_proxy(o) for o in objs)
|
||||||
|
total_size = sum(type_sizes.values())
|
||||||
|
if total_size <= 0:
|
||||||
|
# 모든 size proxy = 0 인 edge case (예: 빈 content) → equal split fallback
|
||||||
|
equal_share = 1.0 / max(len(groups), 1)
|
||||||
|
for ctype in groups:
|
||||||
|
type_sizes[ctype] = equal_share
|
||||||
|
total_size = sum(type_sizes.values()) or 1.0
|
||||||
|
|
||||||
|
# 3. region 생성 (type 우선순위 순으로)
|
||||||
|
sorted_types = sorted(
|
||||||
|
groups.keys(),
|
||||||
|
key=lambda t: _TYPE_ORDER_PRIORITY.get(t, 99),
|
||||||
|
)
|
||||||
|
regions: list[InternalRegion] = []
|
||||||
|
for idx, ctype in enumerate(sorted_types, start=1):
|
||||||
|
objs = groups[ctype]
|
||||||
|
ratio = type_sizes[ctype] / total_size
|
||||||
|
regions.append(
|
||||||
|
InternalRegion(
|
||||||
|
region_id=f"{section_id}.region-{idx}",
|
||||||
|
role=_TYPE_ROLE.get(ctype, "primary"), # 미지원 type fallback = primary
|
||||||
|
content_type=ctype,
|
||||||
|
ratio_estimate=round(ratio, 4),
|
||||||
|
content_unit_ids=[o.id for o in objs],
|
||||||
|
frame_match_strategy={
|
||||||
|
"kind": "frame_match",
|
||||||
|
"frame_id": None, # Step 9 / B4 영역
|
||||||
|
"display_strategy": "inline_full", # v0 default
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. topology vocabulary 결정 (SPEC v1 §2.5 algorithm — rule 1 + rule 6 만)
|
||||||
|
region_count = len(regions)
|
||||||
|
if region_count == 1:
|
||||||
|
# rule 1
|
||||||
|
layout_type = "region-single"
|
||||||
|
placement = "single"
|
||||||
|
else:
|
||||||
|
# rules 2~5 = B2 v0 deferred (SPEC 정의만, 미구현) :
|
||||||
|
# - rule 2 region-preview-details : details_presence path 미구현
|
||||||
|
# - rule 3 region-grid-2x2 : 4 region 미지원
|
||||||
|
# - rule 4 region-main-support : role asymmetric trigger 미구현
|
||||||
|
# - rule 5 region-horizontal-split : visual element type 미지원
|
||||||
|
# rule 6 fallback
|
||||||
|
layout_type = "region-vertical-stack"
|
||||||
|
placement = "vertical"
|
||||||
|
|
||||||
|
# 5. region_order = 위 sorted_types 순 (primary → supporting)
|
||||||
|
region_order = [r.region_id for r in regions]
|
||||||
|
|
||||||
|
return ZoneRegionPlan(
|
||||||
|
internal_regions=regions,
|
||||||
|
region_layout=RegionLayout(
|
||||||
|
region_layout_type=layout_type,
|
||||||
|
region_order=region_order,
|
||||||
|
region_placement=placement,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Self-test (B2 v0 correctness 검증) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _run_self_test():
|
||||||
|
"""v0 unit test : text-only 1 case + text+transform 1 case.
|
||||||
|
|
||||||
|
scope-lock 의 검증 (b) correctness — planner 정확성 확인.
|
||||||
|
fixed input 기반, MDX 01/02/04 미사용.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ─── Test 1 : text-only (1 ContentObject) ────────────────────
|
||||||
|
text_obj = ContentObject(
|
||||||
|
id="test-1.text-1",
|
||||||
|
type="text_block",
|
||||||
|
role="summary",
|
||||||
|
raw_payload="* 본문\n * nested",
|
||||||
|
size_estimate={"line_count": 6},
|
||||||
|
type_specific={"format": "nested_list", "bullet_count": 1, "max_indent_level": 1, "has_emphasis": False},
|
||||||
|
)
|
||||||
|
plan1 = plan_internal_regions([text_obj], section_id="test-1")
|
||||||
|
assert plan1.region_layout is not None
|
||||||
|
assert plan1.region_layout.region_layout_type == "region-single", \
|
||||||
|
f"text-only → region-single 기대, got {plan1.region_layout.region_layout_type}"
|
||||||
|
assert plan1.region_layout.region_placement == "single"
|
||||||
|
assert len(plan1.internal_regions) == 1, f"1 region 기대, got {len(plan1.internal_regions)}"
|
||||||
|
r = plan1.internal_regions[0]
|
||||||
|
assert r.region_id == "test-1.region-1"
|
||||||
|
assert r.role == "primary", f"text-only role=primary 기대, got {r.role}"
|
||||||
|
assert r.content_type == "text_block"
|
||||||
|
assert r.ratio_estimate == 1.0, f"단일 region ratio=1.0 기대, got {r.ratio_estimate}"
|
||||||
|
assert r.content_unit_ids == ["test-1.text-1"]
|
||||||
|
assert r.frame_match_strategy["kind"] == "frame_match"
|
||||||
|
assert r.frame_match_strategy["frame_id"] is None, "B2 v0 frame_id=None lock"
|
||||||
|
assert r.frame_match_strategy["display_strategy"] == "inline_full"
|
||||||
|
assert plan1.region_layout.region_order == ["test-1.region-1"]
|
||||||
|
print("[OK] Test 1 (text-only) passed.")
|
||||||
|
|
||||||
|
# ─── Test 2 : text + transform_table (2 ContentObject) ────────
|
||||||
|
text_obj2 = ContentObject(
|
||||||
|
id="test-2.text-1",
|
||||||
|
type="text_block",
|
||||||
|
role="summary",
|
||||||
|
raw_payload="* 본문",
|
||||||
|
size_estimate={"line_count": 6},
|
||||||
|
type_specific={"format": "bullet_list", "bullet_count": 1, "max_indent_level": 0, "has_emphasis": False},
|
||||||
|
)
|
||||||
|
transform_obj = ContentObject(
|
||||||
|
id="test-2.transform-1",
|
||||||
|
type="transform_table",
|
||||||
|
role="summary",
|
||||||
|
raw_payload="| AS-IS | ➜ | TO-BE |\n|---|---|---|\n| a | ➜ | b |\n| c | ➜ | d |",
|
||||||
|
size_estimate={"rows": 2},
|
||||||
|
type_specific={"pair_count": 2, "arrow_glyph": "➜", "rows": [
|
||||||
|
{"from": "a", "arrow": "➜", "to": "b"},
|
||||||
|
{"from": "c", "arrow": "➜", "to": "d"},
|
||||||
|
]},
|
||||||
|
)
|
||||||
|
plan2 = plan_internal_regions([text_obj2, transform_obj], section_id="test-2")
|
||||||
|
assert plan2.region_layout is not None
|
||||||
|
assert plan2.region_layout.region_layout_type == "region-vertical-stack", \
|
||||||
|
f"text+transform → region-vertical-stack (rule 6 fallback) 기대, got {plan2.region_layout.region_layout_type}"
|
||||||
|
assert plan2.region_layout.region_placement == "vertical"
|
||||||
|
assert len(plan2.internal_regions) == 2, f"2 region 기대, got {len(plan2.internal_regions)}"
|
||||||
|
|
||||||
|
# region_order = primary first (text), supporting second (transform)
|
||||||
|
assert plan2.region_layout.region_order == ["test-2.region-1", "test-2.region-2"]
|
||||||
|
|
||||||
|
# text region (region-1, primary)
|
||||||
|
text_r = plan2.internal_regions[0]
|
||||||
|
assert text_r.region_id == "test-2.region-1"
|
||||||
|
assert text_r.role == "primary"
|
||||||
|
assert text_r.content_type == "text_block"
|
||||||
|
# ratio : 6 / (6 + 2*1.5) = 6/9 ≈ 0.667
|
||||||
|
expected_text_ratio = 6.0 / (6.0 + 2.0 * 1.5)
|
||||||
|
assert abs(text_r.ratio_estimate - expected_text_ratio) < 0.001, \
|
||||||
|
f"text ratio {expected_text_ratio:.4f} 기대, got {text_r.ratio_estimate}"
|
||||||
|
assert text_r.content_unit_ids == ["test-2.text-1"]
|
||||||
|
assert text_r.frame_match_strategy["kind"] == "frame_match"
|
||||||
|
assert text_r.frame_match_strategy["frame_id"] is None
|
||||||
|
|
||||||
|
# transform region (region-2, supporting)
|
||||||
|
tr_r = plan2.internal_regions[1]
|
||||||
|
assert tr_r.region_id == "test-2.region-2"
|
||||||
|
assert tr_r.role == "supporting", f"transform_table role=supporting 기대, got {tr_r.role}"
|
||||||
|
assert tr_r.content_type == "transform_table"
|
||||||
|
expected_tr_ratio = (2.0 * 1.5) / (6.0 + 2.0 * 1.5)
|
||||||
|
assert abs(tr_r.ratio_estimate - expected_tr_ratio) < 0.001, \
|
||||||
|
f"transform ratio {expected_tr_ratio:.4f} 기대, got {tr_r.ratio_estimate}"
|
||||||
|
assert tr_r.content_unit_ids == ["test-2.transform-1"]
|
||||||
|
assert tr_r.frame_match_strategy["frame_id"] is None
|
||||||
|
|
||||||
|
# ratio sum normalize = 1.0
|
||||||
|
ratio_sum = text_r.ratio_estimate + tr_r.ratio_estimate
|
||||||
|
assert abs(ratio_sum - 1.0) < 0.01, f"ratio sum=1.0 기대, got {ratio_sum}"
|
||||||
|
|
||||||
|
# frame_contracts 인자 unused 검증 — None 으로 호출 / dict 으로 호출 결과 동일해야 함
|
||||||
|
plan2_with_contracts = plan_internal_regions(
|
||||||
|
[text_obj2, transform_obj],
|
||||||
|
frame_contracts=[{"template_id": "dummy", "accepted_content_types": ["text_block"]}],
|
||||||
|
section_id="test-2",
|
||||||
|
)
|
||||||
|
assert plan2_with_contracts.region_layout.region_layout_type == plan2.region_layout.region_layout_type
|
||||||
|
assert len(plan2_with_contracts.internal_regions) == len(plan2.internal_regions)
|
||||||
|
print("[OK] Test 2 (text+transform, vertical-stack, ratio 6:3) passed.")
|
||||||
|
|
||||||
|
print("\n=== B2 v0 self-test PASS ===")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_run_self_test()
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
"""Phase Z-2 contract-based generic mapper (v0).
|
||||||
|
|
||||||
|
frame 별 hand-coded mapper 의 대체 — catalog `frame_contracts.yaml` 에 선언된
|
||||||
|
source_shape / cardinality / role_order / payload builder 를 읽고
|
||||||
|
MdxSection → slot_payload 변환.
|
||||||
|
|
||||||
|
원칙 :
|
||||||
|
- frame ↔ mapper 의 binding = catalog 가 결정 (Python registry hardcoded X)
|
||||||
|
- cardinality / role_order / payload 형태 = catalog
|
||||||
|
- reusable primitive : ITEM_PARSERS / COLUMN_BODY_PARSERS / PAYLOAD_BUILDERS named registry
|
||||||
|
- cardinality strict 위반 → FitError → fallback path 신호 (AI restructuring 후보)
|
||||||
|
|
||||||
|
dispatch 모델 :
|
||||||
|
contract.payload.builder = named entry of PAYLOAD_BUILDERS
|
||||||
|
builder 가 (section, units, contract) → slot_payload dict 산출
|
||||||
|
builder 내부에서 ITEM_PARSERS / COLUMN_BODY_PARSERS 등 sub-primitive 호출
|
||||||
|
|
||||||
|
v0 등록 frame :
|
||||||
|
- F13 (three_parallel_requirements) → builder=items_with_role / item_parser=pillar_item
|
||||||
|
- F29 (process_product_two_way) → builder=process_product_pair / column body parsers
|
||||||
|
F16 는 다음 step.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
CATALOG_PATH = PROJECT_ROOT / "templates" / "phase_z2" / "catalog" / "frame_contracts.yaml"
|
||||||
|
|
||||||
|
|
||||||
|
class FitError(Exception):
|
||||||
|
"""Contract 위반 — fallback path (AI restructuring) 로 넘어가야 하는 신호.
|
||||||
|
|
||||||
|
cardinality 위반 / source_shape mismatch 등. message 에 위반 이유 명시.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Catalog loading ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
_CATALOG_CACHE: dict | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def load_frame_contracts() -> dict:
|
||||||
|
global _CATALOG_CACHE
|
||||||
|
if _CATALOG_CACHE is None:
|
||||||
|
_CATALOG_CACHE = yaml.safe_load(CATALOG_PATH.read_text(encoding="utf-8")) or {}
|
||||||
|
return _CATALOG_CACHE
|
||||||
|
|
||||||
|
|
||||||
|
def get_contract(template_id: str) -> dict | None:
|
||||||
|
return load_frame_contracts().get(template_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Source-shape splitters ──────────────────────────────────────
|
||||||
|
|
||||||
|
def _split_top_bullets(content: str) -> list[tuple[str, list[str]]]:
|
||||||
|
"""top-level bullet groups → [(top_line, nested_lines), ...]."""
|
||||||
|
groups = []
|
||||||
|
cur_top, cur_nested = None, []
|
||||||
|
for line in content.splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
if re.match(r"^[\*\-]\s", line):
|
||||||
|
if cur_top is not None:
|
||||||
|
groups.append((cur_top, cur_nested))
|
||||||
|
cur_top, cur_nested = line, []
|
||||||
|
elif line.startswith(" ") and cur_top is not None:
|
||||||
|
cur_nested.append(line)
|
||||||
|
if cur_top is not None:
|
||||||
|
groups.append((cur_top, cur_nested))
|
||||||
|
return groups
|
||||||
|
|
||||||
|
|
||||||
|
def _split_h3_subsections(content: str) -> list[tuple[str, str]]:
|
||||||
|
"""### N(.N) TITLE 단위 split → [(title, body), ...].
|
||||||
|
|
||||||
|
body = subsection 내부 (### 다음 줄 ~ 다음 ### 직전).
|
||||||
|
"""
|
||||||
|
pattern = re.compile(r"^###\s+(\d+(?:\.\d+)?)\s+(.+?)$", re.MULTILINE)
|
||||||
|
matches = list(pattern.finditer(content))
|
||||||
|
units = []
|
||||||
|
for i, m in enumerate(matches):
|
||||||
|
title = m.group(2).strip()
|
||||||
|
start = m.end()
|
||||||
|
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
|
||||||
|
body = content[start:end].strip()
|
||||||
|
units.append((title, body))
|
||||||
|
return units
|
||||||
|
|
||||||
|
|
||||||
|
def split_source(source_shape: str, content: str) -> list:
|
||||||
|
if source_shape == "top_bullets":
|
||||||
|
return _split_top_bullets(content)
|
||||||
|
if source_shape == "h3_subsections":
|
||||||
|
return _split_h3_subsections(content)
|
||||||
|
raise ValueError(
|
||||||
|
f"Contract supports source_shape in (top_bullets, h3_subsections). "
|
||||||
|
f"got '{source_shape}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Shared text helpers ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def _split_label_for_bar(label: str) -> tuple[str, str]:
|
||||||
|
"""'기술(디지털)' → ('기술', '(디지털)'). 괄호 없으면 (label, '')."""
|
||||||
|
m = re.match(r"^([^(]+?)\s*(\([^)]+\))\s*$", label.strip())
|
||||||
|
if m:
|
||||||
|
return m.group(1).strip(), m.group(2).strip()
|
||||||
|
return label.strip(), ""
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_bold_or_plain(top_line: str) -> str:
|
||||||
|
bold = re.search(r"\*\*(.+?)\*\*", top_line)
|
||||||
|
if bold:
|
||||||
|
return bold.group(1).strip()
|
||||||
|
return top_line.strip().lstrip("*-").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _text_lines_with_indent(nested_lines: list[str], base_indent: int = 0) -> list[dict]:
|
||||||
|
text_lines = []
|
||||||
|
for line in nested_lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
s = line.strip()
|
||||||
|
if s in ("<br/>", "<br>", "---"):
|
||||||
|
continue
|
||||||
|
if not re.match(r"^[\*\-]\s", s):
|
||||||
|
continue
|
||||||
|
indent = len(line) - len(line.lstrip())
|
||||||
|
rel = max(0, indent - base_indent)
|
||||||
|
indent_level = max(0, rel // 2)
|
||||||
|
text = re.sub(r"^[\*\-]\s+", "", s)
|
||||||
|
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
|
||||||
|
text_lines.append({"text": text, "indent": indent_level})
|
||||||
|
return text_lines
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_markdown_table(content: str) -> tuple[list[dict] | None, str]:
|
||||||
|
"""Markdown 표 → [{from, to}] (column 1 = from, column 3 = to).
|
||||||
|
|
||||||
|
AS-IS / TO-BE 형식의 3-column 표 (from | arrow | to) 를 transforms 로 변환.
|
||||||
|
Returns (transforms_or_None, content_without_table).
|
||||||
|
"""
|
||||||
|
pattern = re.compile(
|
||||||
|
r"(^[ \t]*\|[^\n]+\|\n[ \t]*\|[\s\-:|]+\|\n(?:[ \t]*\|[^\n]+\|\n?)+)",
|
||||||
|
re.MULTILINE,
|
||||||
|
)
|
||||||
|
m = pattern.search(content)
|
||||||
|
if not m:
|
||||||
|
return None, content
|
||||||
|
rows = [r.strip() for r in m.group(1).strip().splitlines() if r.strip()]
|
||||||
|
transforms = []
|
||||||
|
for r in rows[2:]:
|
||||||
|
cells = [c.strip() for c in r.strip("|").split("|")]
|
||||||
|
if len(cells) >= 3:
|
||||||
|
f = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[0])
|
||||||
|
t = re.sub(r"\*\*(.+?)\*\*", r"\1", cells[2])
|
||||||
|
transforms.append({"from": f, "to": t})
|
||||||
|
remaining = content[:m.start()] + content[m.end():]
|
||||||
|
return (transforms or None), remaining
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Item parser primitives (top-bullet 단위) ─────────────────────
|
||||||
|
|
||||||
|
def _parse_nested_pillar_sections(nested_lines: list[str]) -> list[dict]:
|
||||||
|
"""Pillar nested → [{heading, text_lines}, ...]."""
|
||||||
|
sections = []
|
||||||
|
cur_heading = None
|
||||||
|
cur_text_lines: list[dict] = []
|
||||||
|
section_base_indent: int | None = None
|
||||||
|
|
||||||
|
for line in nested_lines:
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
indent = len(line) - len(line.lstrip())
|
||||||
|
stripped = line.strip()
|
||||||
|
if not re.match(r"^[\*\-]\s", stripped):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if section_base_indent is None or indent <= section_base_indent:
|
||||||
|
if cur_heading is not None:
|
||||||
|
sections.append({"heading": cur_heading, "text_lines": cur_text_lines})
|
||||||
|
bold = re.search(r"\*\*(.+?)\*\*", stripped)
|
||||||
|
cur_heading = (bold.group(1).strip() if bold
|
||||||
|
else stripped.lstrip("*-").strip())
|
||||||
|
cur_text_lines = []
|
||||||
|
section_base_indent = indent
|
||||||
|
else:
|
||||||
|
rel_indent = indent - section_base_indent
|
||||||
|
indent_level = max(0, (rel_indent - 2) // 2)
|
||||||
|
text = re.sub(r"^[\*\-]\s+", "", stripped)
|
||||||
|
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
|
||||||
|
cur_text_lines.append({"text": text, "indent": indent_level})
|
||||||
|
|
||||||
|
if cur_heading is not None:
|
||||||
|
sections.append({"heading": cur_heading, "text_lines": cur_text_lines})
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def parse_pillar_item(unit: tuple[str, list[str]]) -> dict:
|
||||||
|
"""F13 pillar — bold = label, label 분해, nested = sections."""
|
||||||
|
top_line, nested_lines = unit
|
||||||
|
label = _extract_bold_or_plain(top_line)
|
||||||
|
label_main, label_paren = _split_label_for_bar(label)
|
||||||
|
sections = _parse_nested_pillar_sections(nested_lines)
|
||||||
|
return {
|
||||||
|
"label": label,
|
||||||
|
"label_main": label_main,
|
||||||
|
"label_paren": label_paren,
|
||||||
|
"sections": sections,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_quadrant_item(unit: tuple[str, list[str]]) -> dict:
|
||||||
|
"""F16 quadrant — bold = label, nested = body (text_lines flat list, no heading).
|
||||||
|
|
||||||
|
F13 pillar 와의 차이 :
|
||||||
|
- pillar_item 은 nested 안에서 heading + text_lines 계층 분리
|
||||||
|
- quadrant_item 은 nested 전체를 하나의 text_lines list 로 (heading 없음)
|
||||||
|
Returns:
|
||||||
|
{label, body: [{text, indent}, ...]}
|
||||||
|
"""
|
||||||
|
top_line, nested_lines = unit
|
||||||
|
label = _extract_bold_or_plain(top_line)
|
||||||
|
non_empty = [l for l in nested_lines if l.strip()]
|
||||||
|
base = min((len(l) - len(l.lstrip()) for l in non_empty), default=0)
|
||||||
|
body = _text_lines_with_indent(nested_lines, base_indent=base)
|
||||||
|
return {"label": label, "body": body}
|
||||||
|
|
||||||
|
|
||||||
|
ITEM_PARSERS: dict[str, Callable] = {
|
||||||
|
"pillar_item": parse_pillar_item,
|
||||||
|
"quadrant_item": parse_quadrant_item,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Column body parsers (h3 subsection body 단위) ────────────────
|
||||||
|
|
||||||
|
def _parse_column_sections(body: str, transform_first: bool) -> list[dict]:
|
||||||
|
"""Column body → list of sections.
|
||||||
|
|
||||||
|
transform_first=True 면 첫 top-bullet 의 nested 안에 markdown table 이 있으면
|
||||||
|
text_lines 대신 transforms 로 산출 (AS-IS/TO-BE).
|
||||||
|
"""
|
||||||
|
groups = _split_top_bullets(body)
|
||||||
|
sections = []
|
||||||
|
for i, (top_line, nested_lines) in enumerate(groups):
|
||||||
|
title = _extract_bold_or_plain(top_line)
|
||||||
|
if i == 0 and transform_first:
|
||||||
|
nested_text = "\n".join(nested_lines)
|
||||||
|
transforms, _ = _extract_markdown_table(nested_text)
|
||||||
|
if transforms:
|
||||||
|
sections.append({"title": title, "transforms": transforms})
|
||||||
|
continue
|
||||||
|
non_empty = [l for l in nested_lines if l.strip()]
|
||||||
|
base = min((len(l) - len(l.lstrip()) for l in non_empty), default=0)
|
||||||
|
sections.append({
|
||||||
|
"title": title,
|
||||||
|
"text_lines": _text_lines_with_indent(nested_lines, base_indent=base),
|
||||||
|
})
|
||||||
|
return sections
|
||||||
|
|
||||||
|
|
||||||
|
def parse_column_with_transform(body: str) -> list[dict]:
|
||||||
|
"""첫 top-bullet 이 AS-IS/TO-BE 표 가능 (F29 process column)."""
|
||||||
|
return _parse_column_sections(body, transform_first=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_column_plain(body: str) -> list[dict]:
|
||||||
|
"""모두 일반 text_lines section (F29 product column)."""
|
||||||
|
return _parse_column_sections(body, transform_first=False)
|
||||||
|
|
||||||
|
|
||||||
|
COLUMN_BODY_PARSERS: dict[str, Callable] = {
|
||||||
|
"column_with_transform": parse_column_with_transform,
|
||||||
|
"column_plain": parse_column_plain,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Payload builders (named registry — top-level dispatch) ───────
|
||||||
|
|
||||||
|
def _resolve_title(section, payload_spec: dict, contract: dict) -> dict:
|
||||||
|
"""payload.title.source 처리 — v0 = section.title 만 지원."""
|
||||||
|
title_spec = payload_spec.get("title", {}) or {}
|
||||||
|
src = title_spec.get("source")
|
||||||
|
if src is None:
|
||||||
|
return {}
|
||||||
|
if src == "section.title":
|
||||||
|
return {"title": section.title}
|
||||||
|
raise ValueError(
|
||||||
|
f"Contract '{contract['template_id']}' has unsupported title source "
|
||||||
|
f"'{src}'. v0 supports 'section.title' only."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_items_with_role(section, units, contract) -> dict:
|
||||||
|
"""F13-style — top_bullets 각 → array item, role_order[i] 가 item.role_field 채움.
|
||||||
|
|
||||||
|
builder_options :
|
||||||
|
item_parser : ITEM_PARSERS key
|
||||||
|
array_root : payload[array_root] 에 list 부착
|
||||||
|
role_field : item dict 에 role 부착할 key (선택)
|
||||||
|
"""
|
||||||
|
options = contract["payload"]["builder_options"]
|
||||||
|
parser_name = options["item_parser"]
|
||||||
|
parser = ITEM_PARSERS.get(parser_name)
|
||||||
|
if parser is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Contract '{contract['template_id']}' references item_parser='{parser_name}' "
|
||||||
|
f"but ITEM_PARSERS has no such entry."
|
||||||
|
)
|
||||||
|
|
||||||
|
role_order = contract.get("role_order", []) or []
|
||||||
|
role_field = options.get("role_field")
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for i, unit in enumerate(units):
|
||||||
|
item = parser(unit)
|
||||||
|
if role_field and i < len(role_order):
|
||||||
|
item[role_field] = role_order[i]
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
payload: dict = {}
|
||||||
|
payload.update(_resolve_title(section, contract["payload"], contract))
|
||||||
|
payload[options["array_root"]] = items
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _build_process_product_pair(section, units, contract) -> dict:
|
||||||
|
"""F29-style — h3 subsections 2 개 = 2 명명 column.
|
||||||
|
|
||||||
|
builder_options :
|
||||||
|
pad_sections_to : N (sections list 길이 강제 — 미달 시 빈 section 으로 채움)
|
||||||
|
columns : list of
|
||||||
|
- title_to : subsection title → payload[title_to]
|
||||||
|
body_to : parsed sections → payload[body_to] = {"sections": [...]}
|
||||||
|
body_parser : COLUMN_BODY_PARSERS key
|
||||||
|
pad_empty : empty section template (선택, default = {"title": "", "text_lines": []})
|
||||||
|
"""
|
||||||
|
options = contract["payload"]["builder_options"]
|
||||||
|
pad_to = options.get("pad_sections_to")
|
||||||
|
cols = options["columns"]
|
||||||
|
|
||||||
|
if len(units) < len(cols):
|
||||||
|
raise FitError(
|
||||||
|
f"Contract '{contract['template_id']}' builder process_product_pair needs "
|
||||||
|
f"{len(cols)} subsection units, got {len(units)} in section "
|
||||||
|
f"'{getattr(section, 'section_id', '?')}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
payload: dict = {}
|
||||||
|
payload.update(_resolve_title(section, contract["payload"], contract))
|
||||||
|
|
||||||
|
for i, col in enumerate(cols):
|
||||||
|
sub_title, sub_body = units[i]
|
||||||
|
parser_name = col["body_parser"]
|
||||||
|
parser = COLUMN_BODY_PARSERS.get(parser_name)
|
||||||
|
if parser is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Contract '{contract['template_id']}' references column body_parser="
|
||||||
|
f"'{parser_name}' but COLUMN_BODY_PARSERS has no such entry."
|
||||||
|
)
|
||||||
|
sections_list = parser(sub_body)
|
||||||
|
if pad_to is not None:
|
||||||
|
empty_template = col.get("pad_empty", {"title": "", "text_lines": []})
|
||||||
|
while len(sections_list) < pad_to:
|
||||||
|
sections_list.append(dict(empty_template))
|
||||||
|
sections_list = sections_list[:pad_to]
|
||||||
|
payload[col["title_to"]] = sub_title
|
||||||
|
payload[col["body_to"]] = {"sections": sections_list}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def _build_quadrant_flat_slots(section, units, contract) -> dict:
|
||||||
|
"""F16-style — top_bullets 각 → flat keyed slots (quadrant_N_label / quadrant_N_body).
|
||||||
|
|
||||||
|
F13/F29 와의 차이 = output shape 가 array 도 named columns 도 아닌 flat keyed.
|
||||||
|
role/position 은 index 1..N 으로 implicit (1=TL, 2=TR, 3=BL, 4=BR — partial template 결정).
|
||||||
|
|
||||||
|
builder_options :
|
||||||
|
item_parser : ITEM_PARSERS key (각 unit → {label, body} dict 산출)
|
||||||
|
pad_to : N (units 수 < N 이면 빈 slot 으로 채움)
|
||||||
|
truncate_at : M (units 수 > M 이면 M+1 부터 무시 + _truncated_count 기록)
|
||||||
|
label_key_pattern : "quadrant_{n}_label" (n = 1-based index)
|
||||||
|
body_key_pattern : "quadrant_{n}_body"
|
||||||
|
empty_label : pad slot 의 label 값 (default = "")
|
||||||
|
empty_body : pad slot 의 body 값 (default = [])
|
||||||
|
"""
|
||||||
|
options = contract["payload"]["builder_options"]
|
||||||
|
parser_name = options["item_parser"]
|
||||||
|
parser = ITEM_PARSERS.get(parser_name)
|
||||||
|
if parser is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Contract '{contract['template_id']}' references item_parser='{parser_name}' "
|
||||||
|
f"but ITEM_PARSERS has no such entry."
|
||||||
|
)
|
||||||
|
|
||||||
|
pad_to = options.get("pad_to", 4)
|
||||||
|
truncate_at = options.get("truncate_at", pad_to)
|
||||||
|
label_key = options.get("label_key_pattern", "quadrant_{n}_label")
|
||||||
|
body_key = options.get("body_key_pattern", "quadrant_{n}_body")
|
||||||
|
empty_label = options.get("empty_label", "")
|
||||||
|
empty_body = options.get("empty_body", [])
|
||||||
|
|
||||||
|
payload: dict = {}
|
||||||
|
payload.update(_resolve_title(section, contract["payload"], contract))
|
||||||
|
|
||||||
|
visible_units = list(units[:truncate_at])
|
||||||
|
parsed = [parser(u) for u in visible_units]
|
||||||
|
|
||||||
|
for i in range(pad_to):
|
||||||
|
n = i + 1
|
||||||
|
if i < len(parsed):
|
||||||
|
payload[label_key.format(n=n)] = parsed[i]["label"]
|
||||||
|
payload[body_key.format(n=n)] = parsed[i]["body"]
|
||||||
|
else:
|
||||||
|
payload[label_key.format(n=n)] = empty_label
|
||||||
|
# list / dict default 는 항상 새 객체 — shared reference 방지
|
||||||
|
payload[body_key.format(n=n)] = list(empty_body) if isinstance(empty_body, list) else empty_body
|
||||||
|
|
||||||
|
if len(units) > truncate_at:
|
||||||
|
payload["_truncated_count"] = len(units) - truncate_at
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
PAYLOAD_BUILDERS: dict[str, Callable] = {
|
||||||
|
"items_with_role": _build_items_with_role,
|
||||||
|
"process_product_pair": _build_process_product_pair,
|
||||||
|
"quadrant_flat_slots": _build_quadrant_flat_slots,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Generic mapper (single dispatch via builder) ────────────────
|
||||||
|
|
||||||
|
def _check_cardinality(contract: dict, units: list, section) -> None:
|
||||||
|
card = contract.get("cardinality", {}) or {}
|
||||||
|
n = len(units)
|
||||||
|
strict = card.get("strict")
|
||||||
|
if strict is not None and n != strict:
|
||||||
|
raise FitError(
|
||||||
|
f"Contract '{contract['template_id']}' expects strict {strict} units "
|
||||||
|
f"(source_shape={contract['source_shape']}), got {n} "
|
||||||
|
f"in section '{getattr(section, 'section_id', '?')}'. "
|
||||||
|
f"overflow_policy={card.get('overflow_policy', 'abort_or_review')}."
|
||||||
|
)
|
||||||
|
mn = card.get("min")
|
||||||
|
if mn is not None and n < mn:
|
||||||
|
raise FitError(
|
||||||
|
f"Contract '{contract['template_id']}' expects min {mn} units, got {n} "
|
||||||
|
f"in section '{getattr(section, 'section_id', '?')}'."
|
||||||
|
)
|
||||||
|
mx = card.get("max")
|
||||||
|
if mx is not None and n > mx:
|
||||||
|
raise FitError(
|
||||||
|
f"Contract '{contract['template_id']}' expects max {mx} units, got {n} "
|
||||||
|
f"in section '{getattr(section, 'section_id', '?')}'."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_capacity_fit(template_id: str, content: str) -> dict:
|
||||||
|
"""Content 의 item_count vs template contract capacity 비교 (planner 단계 사전 검사).
|
||||||
|
|
||||||
|
목적 : 자동 파이프라인이 "이 frame 에 이 content 넣으면 잘린다 / 안 맞는다" 를
|
||||||
|
render 전에 미리 알도록. silent truncate / FitError 차단의 입력 신호.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict with :
|
||||||
|
item_count : source_shape 으로 split 한 unit 수
|
||||||
|
source_shape : contract 의 source_shape ('top_bullets' / 'h3_subsections' / ...)
|
||||||
|
capacity : {strict, min, max, truncate_at, pad_to} (없는 키는 None)
|
||||||
|
fit_status : 'ok' / 'strict_mismatch' / 'exceeds_max' / 'below_min' /
|
||||||
|
'exceeds_truncate' / 'no_contract' / 'unknown_source_shape'
|
||||||
|
mismatch_reason : str | None — fit_status != 'ok' 일 때 이유
|
||||||
|
|
||||||
|
fit 룰 (자동 파이프라인이 silent loss 방지하기 위한 보수적 규칙):
|
||||||
|
1. strict cardinality 가 있으면 정확히 일치해야 함
|
||||||
|
2. max 가 있으면 그 이하
|
||||||
|
3. min 이 있으면 그 이상
|
||||||
|
4. truncate_at 이 있으면 그 이하 (초과 시 builder 가 자르므로 = 콘텐츠 손실)
|
||||||
|
5. pad_to 만 있고 item_count 가 부족 → mismatch 아님 (빈 slot 으로 채워질 뿐, 손실 X)
|
||||||
|
"""
|
||||||
|
contract = get_contract(template_id)
|
||||||
|
if contract is None:
|
||||||
|
return {
|
||||||
|
"item_count": None,
|
||||||
|
"source_shape": None,
|
||||||
|
"capacity": {"strict": None, "min": None, "max": None,
|
||||||
|
"truncate_at": None, "pad_to": None},
|
||||||
|
"fit_status": "no_contract",
|
||||||
|
"mismatch_reason": (
|
||||||
|
f"no contract for template_id='{template_id}' — capacity check skipped. "
|
||||||
|
f"이 candidate 는 catalog-only dispatch 의 ValueError 가 mapper 단계에서 발생할 것."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
source_shape = contract.get("source_shape")
|
||||||
|
try:
|
||||||
|
units = split_source(source_shape, content)
|
||||||
|
except ValueError:
|
||||||
|
return {
|
||||||
|
"item_count": None,
|
||||||
|
"source_shape": source_shape,
|
||||||
|
"capacity": {"strict": None, "min": None, "max": None,
|
||||||
|
"truncate_at": None, "pad_to": None},
|
||||||
|
"fit_status": "unknown_source_shape",
|
||||||
|
"mismatch_reason": f"source_shape='{source_shape}' is not supported by split_source().",
|
||||||
|
}
|
||||||
|
item_count = len(units)
|
||||||
|
|
||||||
|
cardinality = contract.get("cardinality") or {}
|
||||||
|
strict = cardinality.get("strict")
|
||||||
|
mn = cardinality.get("min")
|
||||||
|
mx = cardinality.get("max")
|
||||||
|
|
||||||
|
builder_options = (contract.get("payload") or {}).get("builder_options") or {}
|
||||||
|
truncate_at = builder_options.get("truncate_at")
|
||||||
|
pad_to = builder_options.get("pad_to")
|
||||||
|
|
||||||
|
capacity = {
|
||||||
|
"strict": strict,
|
||||||
|
"min": mn,
|
||||||
|
"max": mx,
|
||||||
|
"truncate_at": truncate_at,
|
||||||
|
"pad_to": pad_to,
|
||||||
|
}
|
||||||
|
|
||||||
|
if strict is not None and item_count != strict:
|
||||||
|
return {
|
||||||
|
"item_count": item_count,
|
||||||
|
"source_shape": source_shape,
|
||||||
|
"capacity": capacity,
|
||||||
|
"fit_status": "strict_mismatch",
|
||||||
|
"mismatch_reason": (
|
||||||
|
f"strict cardinality {strict}, content has {item_count} items. "
|
||||||
|
f"mapper 가 FitError 를 raise 할 것."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if mx is not None and item_count > mx:
|
||||||
|
return {
|
||||||
|
"item_count": item_count,
|
||||||
|
"source_shape": source_shape,
|
||||||
|
"capacity": capacity,
|
||||||
|
"fit_status": "exceeds_max",
|
||||||
|
"mismatch_reason": f"max cardinality {mx}, content has {item_count} items.",
|
||||||
|
}
|
||||||
|
if mn is not None and item_count < mn:
|
||||||
|
return {
|
||||||
|
"item_count": item_count,
|
||||||
|
"source_shape": source_shape,
|
||||||
|
"capacity": capacity,
|
||||||
|
"fit_status": "below_min",
|
||||||
|
"mismatch_reason": f"min cardinality {mn}, content has {item_count} items.",
|
||||||
|
}
|
||||||
|
if truncate_at is not None and item_count > truncate_at:
|
||||||
|
return {
|
||||||
|
"item_count": item_count,
|
||||||
|
"source_shape": source_shape,
|
||||||
|
"capacity": capacity,
|
||||||
|
"fit_status": "exceeds_truncate",
|
||||||
|
"mismatch_reason": (
|
||||||
|
f"builder truncate_at {truncate_at}, content has {item_count} items "
|
||||||
|
f"({item_count - truncate_at} would be silently dropped). "
|
||||||
|
f"silent truncate 방지 위해 자동 선택 X."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"item_count": item_count,
|
||||||
|
"source_shape": source_shape,
|
||||||
|
"capacity": capacity,
|
||||||
|
"fit_status": "ok",
|
||||||
|
"mismatch_reason": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def map_with_contract(section, contract: dict) -> dict:
|
||||||
|
"""MdxSection + contract → slot_payload via named PAYLOAD_BUILDERS dispatch.
|
||||||
|
|
||||||
|
Steps :
|
||||||
|
1. source_shape 따라 raw_content split → units
|
||||||
|
2. cardinality check (위반 → FitError)
|
||||||
|
3. payload.builder 의 named entry 조회 → builder(section, units, contract)
|
||||||
|
"""
|
||||||
|
units = split_source(contract["source_shape"], section.raw_content)
|
||||||
|
_check_cardinality(contract, units, section)
|
||||||
|
|
||||||
|
payload_spec = contract["payload"]
|
||||||
|
builder_name = payload_spec.get("builder")
|
||||||
|
if not builder_name:
|
||||||
|
raise ValueError(
|
||||||
|
f"Contract '{contract['template_id']}' missing payload.builder. "
|
||||||
|
f"available: {sorted(PAYLOAD_BUILDERS.keys())}"
|
||||||
|
)
|
||||||
|
builder = PAYLOAD_BUILDERS.get(builder_name)
|
||||||
|
if builder is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Contract '{contract['template_id']}' references payload.builder="
|
||||||
|
f"'{builder_name}' but PAYLOAD_BUILDERS has no such entry. "
|
||||||
|
f"available: {sorted(PAYLOAD_BUILDERS.keys())}"
|
||||||
|
)
|
||||||
|
return builder(section, units, contract)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,370 @@
|
|||||||
|
"""Phase Z-2 Placement planner (B4 v0 — dormant module).
|
||||||
|
|
||||||
|
SPEC v1 §4 의 2-stage placement (Layer A → Layer B) 통합 module.
|
||||||
|
|
||||||
|
v0 minimal :
|
||||||
|
- 지원 case : text_block only / text_block + transform_table 2 가지 (B1/B2 정합)
|
||||||
|
- dormant — runtime path 미연결 (pipeline / composition / mapper / partial / yaml 미터치)
|
||||||
|
- Stage A = B2 plan_internal_regions() *호출만* (logic 중복 X)
|
||||||
|
- frame 선택 = accepted_content_types cover + frame_contracts 입력 순서 first
|
||||||
|
- Stage B 매핑 단위 = region 1:1 sub_zone (단순화)
|
||||||
|
- sub_zone 선택 = narrowest accepts first + declaration order tie-break (deadlock 방지)
|
||||||
|
- cardinality.strict 초과 → rejection / under-fill → 허용
|
||||||
|
- display_strategy = 모두 inline_full / overflow_buffer = []
|
||||||
|
- partial_target_path = sub_zone 으로부터 *읽어서 보존만* (실제 marker 미적용)
|
||||||
|
- F13 multi-pillar distribution 미지원 (1 ContentObject per region 만)
|
||||||
|
- V4 rank / multi-frame ranking / display_only path 미활성
|
||||||
|
|
||||||
|
책임 boundary :
|
||||||
|
- B4 = Stage A wrapping + frame 선택 + Stage B (region 1:1 sub_zone) 매핑
|
||||||
|
- 별 axis = display_only path / preview·details 활성 / partial template marker /
|
||||||
|
telemetry 연동 / runtime pipeline 연결
|
||||||
|
|
||||||
|
검증 :
|
||||||
|
- dormancy : MDX 03 final.html SHA = canonical 유지 (runtime path 미연결)
|
||||||
|
- correctness : __main__ self-test (text-only 1 case + text+transform 1 case)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
# B4 v0 input contract = B1 (ContentObject) + B2 (InternalRegion / ZoneRegionPlan).
|
||||||
|
# 세 module 모두 dormant — runtime path 와 무관한 layer-agnostic 의존.
|
||||||
|
from phase_z2_content_extractor import ContentObject
|
||||||
|
from phase_z2_internal_region_planner import (
|
||||||
|
InternalRegion,
|
||||||
|
ZoneRegionPlan,
|
||||||
|
plan_internal_regions,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Output schema (SPEC v1 §4.1) ────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SlotAssignment:
|
||||||
|
"""SPEC v1 §4.3 Stage B 의 slot_assignment.
|
||||||
|
|
||||||
|
region 의 content_unit 이 frame 의 어느 Frame Slot 으로 가는지 명시.
|
||||||
|
partial_target_path = B3 catalog 의 sub_zone.partial_target_path 그대로 보존
|
||||||
|
(실제 marker 적용은 별 axis B5 영역).
|
||||||
|
"""
|
||||||
|
|
||||||
|
region_id: str
|
||||||
|
content_unit_id: str
|
||||||
|
frame_slot_id: str
|
||||||
|
partial_target_path: str
|
||||||
|
display_strategy: str # v0 = "inline_full" 만
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlacementPlan:
|
||||||
|
"""B4 v0 의 출력 — section 의 *전체 placement* 결과 (Stage A + Stage B 통합).
|
||||||
|
|
||||||
|
Fields :
|
||||||
|
section_id : section 식별자 (region_id prefix 와 일관)
|
||||||
|
selected_frame_id : frame.frame_id (or None — frame cover 실패 시)
|
||||||
|
selected_template_id : frame.template_id (or None)
|
||||||
|
internal_regions : Stage A 결과 (B2 planner 출력 그대로)
|
||||||
|
slot_assignments : Stage B 결과 (region.content_unit → Frame Slot)
|
||||||
|
overflow_buffer : v0 = 빈 list (preview/details path 미활성)
|
||||||
|
rejection : 매칭 안 된 / cardinality 초과 등 — 자동 렌더 X 신호
|
||||||
|
"""
|
||||||
|
|
||||||
|
section_id: str
|
||||||
|
selected_frame_id: Optional[str] = None
|
||||||
|
selected_template_id: Optional[str] = None
|
||||||
|
internal_regions: list[InternalRegion] = field(default_factory=list)
|
||||||
|
slot_assignments: list[SlotAssignment] = field(default_factory=list)
|
||||||
|
overflow_buffer: list[dict] = field(default_factory=list)
|
||||||
|
rejection: list[dict] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Sub_zone assignment (Stage B) ───────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_region_to_sub_zone(
|
||||||
|
region: InternalRegion,
|
||||||
|
frame_sub_zones: list[dict[str, Any]],
|
||||||
|
assigned_sub_zone_ids: set[str],
|
||||||
|
) -> Optional[dict[str, Any]]:
|
||||||
|
"""region 에 매칭할 sub_zone 선택.
|
||||||
|
|
||||||
|
Option 1 (source_shape-aware) :
|
||||||
|
region.source_shape_index 보유 시 *positional 1:1* — frame_sub_zones[index] 채택.
|
||||||
|
accepts mismatch 또는 already assigned 시 None (rejection).
|
||||||
|
|
||||||
|
Legacy (B4 v0 narrowest-first heuristic) :
|
||||||
|
1. not-yet-assigned 중 region.content_type 을 accepts 하는 후보 수집
|
||||||
|
2. 후보 중 accepts list 가장 *좁은* sub_zone 우선
|
||||||
|
3. 동률이면 declaration order (Python sort 의 stability 활용)
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
sub_zone dict 또는 None (compatible 후보 없음 / positional accepts mismatch)
|
||||||
|
"""
|
||||||
|
# Option 1 positional matching
|
||||||
|
if region.source_shape_index is not None:
|
||||||
|
if region.source_shape_index >= len(frame_sub_zones):
|
||||||
|
return None
|
||||||
|
sz = frame_sub_zones[region.source_shape_index]
|
||||||
|
if sz["id"] in assigned_sub_zone_ids:
|
||||||
|
return None
|
||||||
|
accepts = sz.get("accepts") or []
|
||||||
|
if region.content_type not in accepts:
|
||||||
|
return None
|
||||||
|
return sz
|
||||||
|
|
||||||
|
# Legacy narrowest-first
|
||||||
|
candidates: list[dict[str, Any]] = []
|
||||||
|
for sz in frame_sub_zones:
|
||||||
|
if sz["id"] in assigned_sub_zone_ids:
|
||||||
|
continue
|
||||||
|
accepts = sz.get("accepts") or []
|
||||||
|
if region.content_type in accepts:
|
||||||
|
candidates.append(sz)
|
||||||
|
|
||||||
|
if not candidates:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# narrowest first — accepts size 작을수록 우선. Python sort stable → 동률은 declaration order 보존.
|
||||||
|
candidates.sort(key=lambda sz: len(sz.get("accepts") or []))
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Public entry ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def plan_placement(
|
||||||
|
content_objects: list[ContentObject],
|
||||||
|
contract: dict[str, Any],
|
||||||
|
section_id: str = "",
|
||||||
|
) -> PlacementPlan:
|
||||||
|
"""ContentObject[] + selected contract → PlacementPlan.
|
||||||
|
|
||||||
|
B4 role (post role-reframe) :
|
||||||
|
- frame selection authority = V4/composition (caller); B4 receives the selected contract
|
||||||
|
- B4 책임 = contract validation + slot assignment + rejection generation
|
||||||
|
|
||||||
|
v0 algorithm :
|
||||||
|
1. Stage A = plan_internal_regions() → internal_regions
|
||||||
|
2. Contract validation : accepted_content_types ⊇ content_type_set
|
||||||
|
- mismatch → rejection (selected_contract_content_type_mismatch) + early return
|
||||||
|
3. contract 의 sub_zones 읽음 (B3 catalog 의 Frame Slot 선언)
|
||||||
|
4. Stage B (region → sub_zone 매핑) — Option 1 positional or legacy narrowest-first
|
||||||
|
5. display_strategy / overflow_buffer = v0 default
|
||||||
|
|
||||||
|
Args :
|
||||||
|
content_objects : list[ContentObject] — B1 v0 extractor 출력
|
||||||
|
contract : dict — V4/composition 결정 frame 의 contract (frame_contracts.yaml entry)
|
||||||
|
section_id : region_id / 결과 식별자 prefix
|
||||||
|
|
||||||
|
Returns :
|
||||||
|
PlacementPlan
|
||||||
|
"""
|
||||||
|
plan = PlacementPlan(section_id=section_id)
|
||||||
|
|
||||||
|
if not content_objects:
|
||||||
|
return plan
|
||||||
|
|
||||||
|
# 1. Stage A — B2 호출
|
||||||
|
zone_plan: ZoneRegionPlan = plan_internal_regions(
|
||||||
|
content_objects=content_objects,
|
||||||
|
section_id=section_id,
|
||||||
|
)
|
||||||
|
plan.internal_regions = list(zone_plan.internal_regions)
|
||||||
|
|
||||||
|
# 2. Contract validation — selected contract 가 content_type_set 을 수용?
|
||||||
|
content_type_set = {o.type for o in content_objects}
|
||||||
|
accepted = set(contract.get("accepted_content_types") or [])
|
||||||
|
if not (content_type_set <= accepted):
|
||||||
|
plan.rejection.append({
|
||||||
|
"reason": "selected_contract_content_type_mismatch",
|
||||||
|
"content_types": sorted(content_type_set),
|
||||||
|
"accepted_content_types": sorted(accepted),
|
||||||
|
"template_id": contract.get("template_id"),
|
||||||
|
})
|
||||||
|
return plan
|
||||||
|
|
||||||
|
plan.selected_template_id = contract.get("template_id")
|
||||||
|
fid = contract.get("frame_id")
|
||||||
|
plan.selected_frame_id = str(fid) if fid is not None else None
|
||||||
|
|
||||||
|
# 3. contract 의 sub_zones (B3 catalog 의 Frame Slot 선언)
|
||||||
|
sub_zones = list(contract.get("sub_zones") or [])
|
||||||
|
|
||||||
|
# 4. Stage B — region 1:1 sub_zone 매핑
|
||||||
|
assigned_sub_zone_ids: set[str] = set()
|
||||||
|
|
||||||
|
for region in plan.internal_regions:
|
||||||
|
sub_zone = _assign_region_to_sub_zone(region, sub_zones, assigned_sub_zone_ids)
|
||||||
|
if sub_zone is None:
|
||||||
|
plan.rejection.append({
|
||||||
|
"reason": "no_compatible_sub_zone",
|
||||||
|
"region_id": region.region_id,
|
||||||
|
"region_content_type": region.content_type,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
assigned_sub_zone_ids.add(sub_zone["id"])
|
||||||
|
|
||||||
|
# cardinality 검사 (v0 = strict only)
|
||||||
|
cardinality = sub_zone.get("cardinality") or {}
|
||||||
|
strict = cardinality.get("strict")
|
||||||
|
unit_count = len(region.content_unit_ids)
|
||||||
|
if strict is not None and unit_count > strict:
|
||||||
|
plan.rejection.append({
|
||||||
|
"reason": "cardinality_strict_exceeded",
|
||||||
|
"region_id": region.region_id,
|
||||||
|
"frame_slot_id": sub_zone["id"],
|
||||||
|
"cardinality_strict": strict,
|
||||||
|
"unit_count": unit_count,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# SlotAssignment 생성 (under-fill 허용 — strict 보다 적어도 OK)
|
||||||
|
partial_path = sub_zone.get("partial_target_path") or ""
|
||||||
|
for content_unit_id in region.content_unit_ids:
|
||||||
|
plan.slot_assignments.append(
|
||||||
|
SlotAssignment(
|
||||||
|
region_id=region.region_id,
|
||||||
|
content_unit_id=content_unit_id,
|
||||||
|
frame_slot_id=sub_zone["id"],
|
||||||
|
partial_target_path=partial_path,
|
||||||
|
display_strategy="inline_full", # v0 default
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. v0 = overflow_buffer 미활성 (빈 list 그대로)
|
||||||
|
return plan
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Self-test (B4 v0 correctness 검증) ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def _run_self_test():
|
||||||
|
"""v0 unit test : Test 1 (text-only → F13) + Test 2 (text+transform → F29).
|
||||||
|
|
||||||
|
fixed input + 실제 frame_contracts.yaml 로드해서 검증.
|
||||||
|
YAML declaration order = F13 / F29 / F16.
|
||||||
|
"""
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
catalog_path = PROJECT_ROOT / "templates" / "phase_z2" / "catalog" / "frame_contracts.yaml"
|
||||||
|
catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8"))
|
||||||
|
# B4 role reframe : caller (V4/composition) 가 frame 선택, B4 는 contract 받음.
|
||||||
|
f13_contract = catalog["three_parallel_requirements"]
|
||||||
|
f29_contract = catalog["process_product_two_way"]
|
||||||
|
|
||||||
|
# ─── Test 1 : 1 text_block → F13 → pillar_1 ─────────────────
|
||||||
|
text_obj = ContentObject(
|
||||||
|
id="t1.text-1",
|
||||||
|
type="text_block",
|
||||||
|
role="summary",
|
||||||
|
raw_payload="* 본문",
|
||||||
|
size_estimate={"line_count": 6},
|
||||||
|
type_specific={
|
||||||
|
"format": "bullet_list", "bullet_count": 1,
|
||||||
|
"max_indent_level": 0, "has_emphasis": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
plan1 = plan_placement([text_obj], f13_contract, section_id="t1")
|
||||||
|
|
||||||
|
# frame 선택 = F13 (declaration order first, content_type_set={text_block} cover)
|
||||||
|
assert plan1.selected_template_id == "three_parallel_requirements", \
|
||||||
|
f"Test 1 frame=F13 기대, got {plan1.selected_template_id}"
|
||||||
|
assert plan1.selected_frame_id == "1171281190", \
|
||||||
|
f"Test 1 frame_id=1171281190 기대, got {plan1.selected_frame_id}"
|
||||||
|
|
||||||
|
# 1 region (region-single)
|
||||||
|
assert len(plan1.internal_regions) == 1, \
|
||||||
|
f"Test 1 1 region 기대, got {len(plan1.internal_regions)}"
|
||||||
|
|
||||||
|
# 1 SlotAssignment — region 1 → pillar_1 (declaration order tie-break, 모두 size=1)
|
||||||
|
assert len(plan1.slot_assignments) == 1, \
|
||||||
|
f"Test 1 slot_assignments=1 기대, got {len(plan1.slot_assignments)}"
|
||||||
|
sa = plan1.slot_assignments[0]
|
||||||
|
assert sa.frame_slot_id == "pillar_1", \
|
||||||
|
f"Test 1 sub_zone=pillar_1 기대, got {sa.frame_slot_id}"
|
||||||
|
assert sa.region_id == "t1.region-1"
|
||||||
|
assert sa.content_unit_id == "t1.text-1"
|
||||||
|
assert sa.display_strategy == "inline_full"
|
||||||
|
assert "f13b__col" in sa.partial_target_path, \
|
||||||
|
f"Test 1 partial_target_path 보존 기대, got {sa.partial_target_path}"
|
||||||
|
|
||||||
|
# under-fill 허용 — pillar_2 / pillar_3 미할당, rejection 0
|
||||||
|
assert len(plan1.rejection) == 0, f"Test 1 rejection=0 기대, got {plan1.rejection}"
|
||||||
|
assert plan1.overflow_buffer == []
|
||||||
|
print("[OK] Test 1 (text-only → F13 → pillar_1) passed.")
|
||||||
|
|
||||||
|
# ─── Test 2 : 1 text + 1 transform → F29 → product_column / process_column ─
|
||||||
|
text_obj2 = ContentObject(
|
||||||
|
id="t2.text-1",
|
||||||
|
type="text_block",
|
||||||
|
role="summary",
|
||||||
|
raw_payload="* 본문",
|
||||||
|
size_estimate={"line_count": 6},
|
||||||
|
type_specific={
|
||||||
|
"format": "bullet_list", "bullet_count": 1,
|
||||||
|
"max_indent_level": 0, "has_emphasis": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
transform_obj = ContentObject(
|
||||||
|
id="t2.transform-1",
|
||||||
|
type="transform_table",
|
||||||
|
role="summary",
|
||||||
|
raw_payload="| AS-IS | ➜ | TO-BE |\n|---|---|---|\n| a | ➜ | b |",
|
||||||
|
size_estimate={"rows": 1},
|
||||||
|
type_specific={
|
||||||
|
"pair_count": 1, "arrow_glyph": "➜",
|
||||||
|
"rows": [{"from": "a", "arrow": "➜", "to": "b"}],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
plan2 = plan_placement([text_obj2, transform_obj], f29_contract, section_id="t2")
|
||||||
|
|
||||||
|
# frame 선택 = F29 (transform_table 수용 유일)
|
||||||
|
assert plan2.selected_template_id == "process_product_two_way", \
|
||||||
|
f"Test 2 frame=F29 기대, got {plan2.selected_template_id}"
|
||||||
|
|
||||||
|
# 2 regions (text=primary / transform=supporting)
|
||||||
|
assert len(plan2.internal_regions) == 2, \
|
||||||
|
f"Test 2 2 regions 기대, got {len(plan2.internal_regions)}"
|
||||||
|
|
||||||
|
# 2 SlotAssignments
|
||||||
|
assert len(plan2.slot_assignments) == 2, \
|
||||||
|
f"Test 2 slot_assignments=2 기대, got {len(plan2.slot_assignments)}"
|
||||||
|
|
||||||
|
# narrowest-first 검증 (F29 deadlock 방지 핵심) :
|
||||||
|
# text region → product_column (accepts=[text], size 1, narrowest)
|
||||||
|
# transform region → process_column (accepts=[text+transform], size 2, 남은 candidate)
|
||||||
|
text_sa = next(
|
||||||
|
(sa for sa in plan2.slot_assignments if sa.content_unit_id == "t2.text-1"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
transform_sa = next(
|
||||||
|
(sa for sa in plan2.slot_assignments if sa.content_unit_id == "t2.transform-1"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert text_sa is not None, "text content_unit SlotAssignment 존재 기대"
|
||||||
|
assert text_sa.frame_slot_id == "product_column", \
|
||||||
|
f"Test 2 text → product_column (narrowest) 기대, got {text_sa.frame_slot_id}"
|
||||||
|
assert text_sa.region_id == "t2.region-1"
|
||||||
|
|
||||||
|
assert transform_sa is not None, "transform content_unit SlotAssignment 존재 기대"
|
||||||
|
assert transform_sa.frame_slot_id == "process_column", \
|
||||||
|
f"Test 2 transform → process_column 기대, got {transform_sa.frame_slot_id}"
|
||||||
|
assert transform_sa.region_id == "t2.region-2"
|
||||||
|
|
||||||
|
# rejection 없음 / overflow_buffer 빈 list (under-fill 허용)
|
||||||
|
assert len(plan2.rejection) == 0, f"Test 2 rejection=0 기대, got {plan2.rejection}"
|
||||||
|
assert plan2.overflow_buffer == []
|
||||||
|
print("[OK] Test 2 (text+transform → F29 → product_column / process_column) passed.")
|
||||||
|
|
||||||
|
print("\n=== B4 v0 self-test PASS ===")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_run_self_test()
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
"""Phase Z-2 zone_ratio_retry action v0 (A3 — 실제 zone redistribution 구현).
|
||||||
|
|
||||||
|
router 가 *제안* 한 zone_ratio_retry action 의 **실행 layer**.
|
||||||
|
|
||||||
|
원칙 (A3 locked rules — 사용자 잠금 7+1) :
|
||||||
|
1. retry budget = 1 — 한 번만 시도
|
||||||
|
2. slide / slide-body / title / divider / footer / zone gap 모두 고정
|
||||||
|
(공통 spacing 깎기 금지)
|
||||||
|
3. 조정 대상 = router 가 지목한 *target zone* 만 height 증가
|
||||||
|
4. donor 선택 기준 :
|
||||||
|
- 같은 layout 의 sibling zone
|
||||||
|
- visual_check 통과 (이 zone 자체엔 overflow 없음)
|
||||||
|
- capacity_fit 가 ok
|
||||||
|
- 현재 height > min_height_px (slack > 0)
|
||||||
|
- donor min_height_px 아래로 줄지 X
|
||||||
|
- 여러 후보면 slack 가장 큰 것부터 (greedy)
|
||||||
|
- 부족 시 retry 실패
|
||||||
|
5. target_added_px = observed excess_y + safety_margin (small fixed)
|
||||||
|
— donor 가 min_height 아래로 가면 실패
|
||||||
|
6. retry 후 status :
|
||||||
|
- 성공 → PASS 가능
|
||||||
|
- 실패 → RENDERED_WITH_VISUAL_REGRESSION 유지 (CSS/padding/tolerance 보정 X)
|
||||||
|
7. debug trace 필수 (retry_attempted / target / donor / before/after / passed / reason)
|
||||||
|
8. revert 정책 ((b)) :
|
||||||
|
- redistribution check 실패 → rerender 안 함, original final.html 유지
|
||||||
|
- rerender 후 visual_check 실패 → original 로 revert (final.html 변경 X),
|
||||||
|
retried_candidate.html 은 *진단 artifact* 로만 별도 보관
|
||||||
|
- retry 성공 시에만 final.html = retried version
|
||||||
|
|
||||||
|
본 module 은 *plan + apply layer*. rerender / final.html 갱신 / revert 는 pipeline 이.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# 작은 고정 safety margin — 실험적 default. debug 에 기록.
|
||||||
|
DEFAULT_SAFETY_MARGIN_PX = 4
|
||||||
|
|
||||||
|
|
||||||
|
def plan_zone_ratio_retry(
|
||||||
|
*,
|
||||||
|
debug_zones: list[dict],
|
||||||
|
overflow: dict,
|
||||||
|
fit_classification: dict,
|
||||||
|
router_decision: dict,
|
||||||
|
safety_margin_px: int = DEFAULT_SAFETY_MARGIN_PX,
|
||||||
|
) -> Optional[dict]:
|
||||||
|
"""zone_ratio_retry 의 redistribution plan 을 산출.
|
||||||
|
|
||||||
|
*plan 만*. 실제 height 적용 / rerender X (caller 가 처리).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None : retry 시도 자체가 불필요 (router 가 zone_ratio_retry 제안 X)
|
||||||
|
dict : retry attempt 정보 (feasible 여부 + 상세)
|
||||||
|
|
||||||
|
feasible=True 이면 caller 가 zones_after 로 layout_css 재구성 + rerender 시도.
|
||||||
|
feasible=False 이면 caller 는 retry 포기 (original final.html 유지).
|
||||||
|
"""
|
||||||
|
if not router_decision.get("router_active"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# zone_ratio_retry 가 router 제안에 포함된 첫 classification 을 target 으로
|
||||||
|
target_cls = None
|
||||||
|
for cls in fit_classification.get("classifications", []) or []:
|
||||||
|
if cls.get("proposed_action") == "zone_ratio_retry":
|
||||||
|
target_cls = cls
|
||||||
|
break
|
||||||
|
if target_cls is None:
|
||||||
|
return None # 다른 action (popup / reselect) — 본 retry 대상 아님
|
||||||
|
|
||||||
|
target_zone_position = target_cls.get("zone_position")
|
||||||
|
target_excess_y = float(target_cls.get("inputs", {}).get("excess_y", 0))
|
||||||
|
# round up to integer (subpixel 끼면 부족할 수 있음)
|
||||||
|
target_added_px = int(math.ceil(target_excess_y)) + int(safety_margin_px)
|
||||||
|
|
||||||
|
# zones_before — debug_zones 의 height_px 를 모음
|
||||||
|
zones_before: dict[str, int] = {}
|
||||||
|
zone_min_by_pos: dict[str, int] = {}
|
||||||
|
for dz in debug_zones:
|
||||||
|
pos = dz.get("position")
|
||||||
|
if pos is None:
|
||||||
|
continue
|
||||||
|
h = dz.get("height_px")
|
||||||
|
m = dz.get("min_height_px")
|
||||||
|
if h is None or m is None:
|
||||||
|
continue
|
||||||
|
zones_before[pos] = int(h)
|
||||||
|
zone_min_by_pos[pos] = int(m)
|
||||||
|
|
||||||
|
# overflow zone 별 visual fail 정보
|
||||||
|
overflow_zone_status: dict[str, dict] = {}
|
||||||
|
for z in overflow.get("zones", []) or []:
|
||||||
|
overflow_zone_status[z.get("position")] = z
|
||||||
|
|
||||||
|
# donor 후보 식별
|
||||||
|
donor_candidates: list[dict] = []
|
||||||
|
for dz in debug_zones:
|
||||||
|
pos = dz.get("position")
|
||||||
|
if pos is None or pos == target_zone_position:
|
||||||
|
continue
|
||||||
|
# rule 4-(a) sibling 확인은 layout 내 sibling = 같은 zones list 안에 있으면 OK
|
||||||
|
# (본 함수는 1 layout 내 zones 만 받음)
|
||||||
|
|
||||||
|
# rule 4-(b) visual_check 통과 — 이 zone 에 자체 overflow / clipped_inner 없음
|
||||||
|
zinfo = overflow_zone_status.get(pos, {})
|
||||||
|
zone_self_overflow = bool(zinfo.get("overflowed"))
|
||||||
|
zone_inner_clipped = bool(zinfo.get("clipped_inner"))
|
||||||
|
if zone_self_overflow or zone_inner_clipped:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# rule 4-(c) capacity_fit 가 ok
|
||||||
|
cap_status = (
|
||||||
|
(dz.get("composition_rationale") or {}).get("capacity_fit", {}).get("fit_status")
|
||||||
|
)
|
||||||
|
# 'ok' 아니거나 missing/unknown 이면 보수적으로 제외 (no_contract 는 허용 — capacity_fit 자체 부재)
|
||||||
|
if cap_status not in {"ok", "no_contract", None}:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# rule 4-(d) 현재 height > min_height
|
||||||
|
height = zones_before.get(pos)
|
||||||
|
min_h = zone_min_by_pos.get(pos)
|
||||||
|
if height is None or min_h is None:
|
||||||
|
continue
|
||||||
|
slack = height - min_h
|
||||||
|
if slack <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
donor_candidates.append({
|
||||||
|
"position": pos,
|
||||||
|
"current_height": height,
|
||||||
|
"min_height": min_h,
|
||||||
|
"slack": slack,
|
||||||
|
"capacity_fit_status": cap_status,
|
||||||
|
})
|
||||||
|
|
||||||
|
# rule 4-(f) 여러 후보면 slack 가장 큰 것부터
|
||||||
|
donor_candidates.sort(key=lambda d: d["slack"], reverse=True)
|
||||||
|
|
||||||
|
# base plan dict (failure / success 공용)
|
||||||
|
base_plan = {
|
||||||
|
"target_zone_position": target_zone_position,
|
||||||
|
"target_excess_y": target_excess_y,
|
||||||
|
"target_added_px": target_added_px,
|
||||||
|
"safety_margin_px_used": int(safety_margin_px),
|
||||||
|
"donor_candidates_considered": donor_candidates,
|
||||||
|
"zones_before": dict(zones_before),
|
||||||
|
}
|
||||||
|
|
||||||
|
if not donor_candidates:
|
||||||
|
return {
|
||||||
|
**base_plan,
|
||||||
|
"feasible": False,
|
||||||
|
"donor_zone_position": None,
|
||||||
|
"donor_reduced_px": 0,
|
||||||
|
"zones_after": dict(zones_before),
|
||||||
|
"failure_reason": (
|
||||||
|
f"no donor candidates eligible (sibling visual_check OK + "
|
||||||
|
f"capacity_fit ok/no_contract + slack > 0)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# A3 minimal : single primary donor (multi-donor 는 future)
|
||||||
|
primary_donor = donor_candidates[0]
|
||||||
|
if primary_donor["slack"] < target_added_px:
|
||||||
|
return {
|
||||||
|
**base_plan,
|
||||||
|
"feasible": False,
|
||||||
|
"donor_zone_position": primary_donor["position"],
|
||||||
|
"donor_max_slack": primary_donor["slack"],
|
||||||
|
"donor_reduced_px": 0,
|
||||||
|
"zones_after": dict(zones_before),
|
||||||
|
"failure_reason": (
|
||||||
|
f"primary donor '{primary_donor['position']}' slack {primary_donor['slack']}px "
|
||||||
|
f"< target_added_px {target_added_px}px (excess_y {target_excess_y} + "
|
||||||
|
f"safety_margin {safety_margin_px}). multi-donor aggregation is future axis."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# feasible
|
||||||
|
zones_after = dict(zones_before)
|
||||||
|
zones_after[target_zone_position] = zones_before[target_zone_position] + target_added_px
|
||||||
|
zones_after[primary_donor["position"]] = (
|
||||||
|
zones_before[primary_donor["position"]] - target_added_px
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
**base_plan,
|
||||||
|
"feasible": True,
|
||||||
|
"donor_zone_position": primary_donor["position"],
|
||||||
|
"donor_reduced_px": target_added_px,
|
||||||
|
"zones_after": zones_after,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def apply_retry_to_layout_css(layout_css: dict, plan: dict, zones_data: list[dict],
|
||||||
|
total_height: int, gap_px: int) -> dict:
|
||||||
|
"""retry plan 의 zones_after 를 반영한 *새* layout_css 반환 (mutation X).
|
||||||
|
|
||||||
|
horizontal-2 같은 dynamic_rows 인 경우만 해당. fr-default layout 은 retry target 아님
|
||||||
|
(왜냐하면 dynamic heights 가 없으면 redistribution 의미 없음).
|
||||||
|
"""
|
||||||
|
new_layout_css = dict(layout_css)
|
||||||
|
# zone position 순서대로 height_px 추출
|
||||||
|
new_heights_px = [plan["zones_after"][zd["position"]] for zd in zones_data]
|
||||||
|
new_layout_css["heights_px"] = new_heights_px
|
||||||
|
new_layout_css["rows"] = " ".join(f"{h}px" for h in new_heights_px)
|
||||||
|
new_layout_css["ratios"] = [round(h / total_height, 3) for h in new_heights_px]
|
||||||
|
new_layout_css["computation"] = "zone_ratio_retry override (A3)"
|
||||||
|
new_layout_css["dynamic_rows"] = True
|
||||||
|
new_layout_css["raw_zone_layout"] = (layout_css.get("raw_zone_layout") or {}).copy()
|
||||||
|
new_layout_css["raw_zone_layout"]["retry_applied"] = True
|
||||||
|
return new_layout_css
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"""Phase Z-2 overflow_router v0 (A2 — 정책 매핑 layer 만).
|
||||||
|
|
||||||
|
fit_classifier 의 출력 (category) 를 spec §4 의 *proposed_action* 으로 매핑하는 layer.
|
||||||
|
|
||||||
|
본 module 은 ***매핑까지만***. 실제 action 실행은 별도 step (A3+).
|
||||||
|
출력 = 각 classification 에 proposed_action 추가 + router 전체 summary.
|
||||||
|
|
||||||
|
원칙 :
|
||||||
|
- classifier = 사실 분류 (category 결정)
|
||||||
|
- router = 정책 결정 (그 category 면 무엇을 *제안* 할 것인가)
|
||||||
|
- 본 단계는 *제안 trace* 만. pipeline behavior / abort 정책 / rerender 변경 X
|
||||||
|
- 실행 안 됨 → 현재 코드는 여전히 visual_check_passed=False 시 sys.exit(1)
|
||||||
|
그러나 debug.json 에 *어떤 action 이 제안됐는지* 가 기록됨
|
||||||
|
|
||||||
|
다음 step (별도 — A3) :
|
||||||
|
zone_ratio_retry action 의 *실제 구현* — 지금 spec §4 mapping 의 가장 자주
|
||||||
|
트리거되는 action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
# ─── §4 mapping table (spec PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC §4) ──
|
||||||
|
|
||||||
|
# category → proposed_action (primary)
|
||||||
|
ACTION_BY_CATEGORY: dict[str, str] = {
|
||||||
|
"minor_overflow": "zone_ratio_retry",
|
||||||
|
"moderate_overflow": "layout_adjust",
|
||||||
|
"structural_minor_overflow": "zone_ratio_retry",
|
||||||
|
"structural_major_overflow": "details_popup_escalation",
|
||||||
|
"tabular_overflow": "details_popup_escalation",
|
||||||
|
"frame_capacity_mismatch": "frame_reselect",
|
||||||
|
"layout_zone_mismatch": "layout_adjust",
|
||||||
|
"hard_visual_fail": "abort",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 매핑 근거 — *왜 이 category 면 이 action 인가* trace 용
|
||||||
|
ACTION_RATIONALE: dict[str, str] = {
|
||||||
|
"minor_overflow":
|
||||||
|
"1.5 줄 미만 text/label flow → zone 양보 / spacing 재계산으로 fit 가능",
|
||||||
|
"moderate_overflow":
|
||||||
|
"1.5~4 줄 text/label → layout/zone ratio 재분배 필요",
|
||||||
|
"structural_minor_overflow":
|
||||||
|
"structural unit boundary spill (<1 unit drop) → zone 양보로 fit, 단위 자르기 X",
|
||||||
|
"structural_major_overflow":
|
||||||
|
"1+ structural unit 완전 잘림 → 의미 손실, popup 으로 escalate",
|
||||||
|
"tabular_overflow":
|
||||||
|
"표는 행 단위로 잘리면 의미 손실 → popup escalate (또는 table-friendly frame reselect)",
|
||||||
|
"frame_capacity_mismatch":
|
||||||
|
"composition capacity_fit 가 이미 mismatch 신호 → V4 top-k 의 다른 frame 평가",
|
||||||
|
"layout_zone_mismatch":
|
||||||
|
"frame root 자체 overflow → layout preset 변경 또는 zone 키움",
|
||||||
|
"hard_visual_fail":
|
||||||
|
"위 매핑 모두 미적용 — 마지막 fallback (현재 코드는 sys.exit 으로 abort)",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 각 action 의 *현재 코드* 구현 상태 (2026-04-29 기준)
|
||||||
|
# A2 단계에서 이 매핑이 *어디까지 자동 처리되고 어디서 막히는지* trace 확보용
|
||||||
|
ACTION_IMPLEMENTATION_STATUS: dict[str, str] = {
|
||||||
|
"zone_ratio_retry": "IMPLEMENTED", # A3 (2026-04-29) phase_z2_retry.plan_zone_ratio_retry + pipeline orchestration
|
||||||
|
"layout_adjust": "MISSING",
|
||||||
|
"details_popup_escalation": "MISSING", # CLAUDE.md 의 <details> 원칙은 있음, runtime 미구현
|
||||||
|
"frame_reselect": "MISSING", # V4 top-k 자료는 있음, planner 가 rank-1 만
|
||||||
|
"adapter_needed": "PARTIAL", # composition v0.1.1 의 mapper FitError catch
|
||||||
|
"abort": "IMPLEMENTED", # sys.exit(1) — pipeline 의 현재 default
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── 단일 분류 → routing 결과 ─────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def route_action(category: str) -> dict:
|
||||||
|
"""category → proposed_action mapping 결과 (단일).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict :
|
||||||
|
proposed_action : action 이름 (또는 None)
|
||||||
|
rationale : *왜* 이 action 인가
|
||||||
|
implementation_status : implemented / partial / missing / unknown
|
||||||
|
mapping_source : "spec §4 ACTION_BY_CATEGORY" 또는 "no mapping"
|
||||||
|
"""
|
||||||
|
action = ACTION_BY_CATEGORY.get(category)
|
||||||
|
if action is None:
|
||||||
|
return {
|
||||||
|
"proposed_action": None,
|
||||||
|
"rationale": f"category '{category}' has no mapping in ACTION_BY_CATEGORY",
|
||||||
|
"implementation_status": "unknown",
|
||||||
|
"mapping_source": "no mapping (unknown category)",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"proposed_action": action,
|
||||||
|
"rationale": ACTION_RATIONALE.get(category, ""),
|
||||||
|
"implementation_status": ACTION_IMPLEMENTATION_STATUS.get(action, "unknown"),
|
||||||
|
"mapping_source": "spec §4 ACTION_BY_CATEGORY",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ─── fit_classification 전체 → router decision ──────────────────
|
||||||
|
|
||||||
|
|
||||||
|
def route_fit_classification(fit_classification: dict) -> dict:
|
||||||
|
"""fit_classification 의 모든 classifications 에 proposed_action 추가 + summary.
|
||||||
|
|
||||||
|
각 classification 에 다음 필드를 *추가* (기존 필드 보존) :
|
||||||
|
- proposed_action
|
||||||
|
- proposed_action_rationale
|
||||||
|
- proposed_action_implementation_status
|
||||||
|
- proposed_action_mapping_source
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
router decision summary dict :
|
||||||
|
router_active : True/False (visual_check_passed=False 일 때만 True)
|
||||||
|
proposed_actions_summary : unique action 들 sorted list
|
||||||
|
implementation_status_summary : {status: count} dict
|
||||||
|
routed_count : 처리된 classification 수
|
||||||
|
routed_details : per-classification routing trace
|
||||||
|
missing_actions_pending_impl : 본 routing 에서 *현재 미구현* 인 action 모음
|
||||||
|
note : 사용자 안내 텍스트
|
||||||
|
"""
|
||||||
|
if fit_classification.get("visual_check_passed", True):
|
||||||
|
return {
|
||||||
|
"router_active": False,
|
||||||
|
"proposed_actions_summary": [],
|
||||||
|
"implementation_status_summary": {},
|
||||||
|
"routed_count": 0,
|
||||||
|
"routed_details": [],
|
||||||
|
"missing_actions_pending_impl": [],
|
||||||
|
"note": "visual check passed — no overflow to route",
|
||||||
|
}
|
||||||
|
|
||||||
|
classifications = fit_classification.get("classifications", []) or []
|
||||||
|
routed_details = []
|
||||||
|
|
||||||
|
for cls in classifications:
|
||||||
|
category = cls.get("category", "hard_visual_fail")
|
||||||
|
routing = route_action(category)
|
||||||
|
|
||||||
|
# classification entry 에 proposed_action 정보 *추가* (기존 필드 보존)
|
||||||
|
cls["proposed_action"] = routing["proposed_action"]
|
||||||
|
cls["proposed_action_rationale"] = routing["rationale"]
|
||||||
|
cls["proposed_action_implementation_status"] = routing["implementation_status"]
|
||||||
|
cls["proposed_action_mapping_source"] = routing["mapping_source"]
|
||||||
|
|
||||||
|
routed_details.append({
|
||||||
|
"source": cls.get("source"),
|
||||||
|
"zone_position": cls.get("zone_position"),
|
||||||
|
"category": category,
|
||||||
|
"proposed_action": routing["proposed_action"],
|
||||||
|
"implementation_status": routing["implementation_status"],
|
||||||
|
})
|
||||||
|
|
||||||
|
# summary
|
||||||
|
actions_seen = sorted({
|
||||||
|
r["proposed_action"] for r in routed_details
|
||||||
|
if r["proposed_action"] is not None
|
||||||
|
})
|
||||||
|
status_breakdown: dict[str, int] = {}
|
||||||
|
missing_actions: list[str] = []
|
||||||
|
for r in routed_details:
|
||||||
|
s = r["implementation_status"]
|
||||||
|
status_breakdown[s] = status_breakdown.get(s, 0) + 1
|
||||||
|
if s == "MISSING" and r["proposed_action"] not in missing_actions:
|
||||||
|
missing_actions.append(r["proposed_action"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"router_active": True,
|
||||||
|
"proposed_actions_summary": actions_seen,
|
||||||
|
"implementation_status_summary": status_breakdown,
|
||||||
|
"routed_count": len(routed_details),
|
||||||
|
"routed_details": routed_details,
|
||||||
|
"missing_actions_pending_impl": sorted(missing_actions),
|
||||||
|
"note": (
|
||||||
|
"router 는 category → proposed_action 매핑까지 담당. 실제 action 실행은 "
|
||||||
|
"pipeline 의 별도 orchestrator 가 처리 (예: zone_ratio_retry 는 "
|
||||||
|
"_attempt_zone_ratio_retry 에서 실행). proposed_action 의 implementation_status "
|
||||||
|
"가 IMPLEMENTED 이면 pipeline 이 시도하고 결과는 retry_trace 에 기록, "
|
||||||
|
"MISSING 이면 그 action 은 실행 X 이고 기존 abort/status 흐름 (sys.exit(1)) 으로 종료."
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -5,13 +5,7 @@
|
|||||||
용도: 16:9 슬라이드의 고정 배경. 상단 제목 + 구분선 + 본문 영역 + 하단 결론 pill.
|
용도: 16:9 슬라이드의 고정 배경. 상단 제목 + 구분선 + 본문 영역 + 하단 결론 pill.
|
||||||
각 블록(new/, cards/, emphasis/ 등)은 본문 영역(.slide-body) 안에 들어간다.
|
각 블록(new/, cards/, emphasis/ 등)은 본문 영역(.slide-body) 안에 들어간다.
|
||||||
Figma 원본: Frame 1171281207 (node 107:22, 3848×2165, Page 2)
|
Figma 원본: Frame 1171281207 (node 107:22, 3848×2165, Page 2)
|
||||||
수학적 계산:
|
CSS: styles/tokens/ + styles/base/slide-base.css 로 분리됨.
|
||||||
원본 3848×2165 → 16:9 (1280×720)
|
|
||||||
scale = 1280 / 3848 = 0.33264
|
|
||||||
상단 제목: top 125px × S ≈ 42px, left 155px × S ≈ 52px, font 65.8px × S ≈ 22px
|
|
||||||
구분선: top 218px × S ≈ 72px
|
|
||||||
하단 pill: top 1980px × S ≈ 659px, height 123px × S ≈ 41px
|
|
||||||
본문 영역: top ~72px ~ bottom ~659px = 약 587px 가용
|
|
||||||
슬롯:
|
슬롯:
|
||||||
title — 슬라이드 제목 (gradient text)
|
title — 슬라이드 제목 (gradient text)
|
||||||
body — {% block body %}{% endblock %} 또는 include 대상
|
body — {% block body %}{% endblock %} 또는 include 대상
|
||||||
@@ -20,10 +14,6 @@
|
|||||||
svg/bg_slide_texture.png — 배경 텍스처 (opacity 2%)
|
svg/bg_slide_texture.png — 배경 텍스처 (opacity 2%)
|
||||||
svg/line_divider.svg — 구분선
|
svg/line_divider.svg — 구분선
|
||||||
svg/pill_scroll.png — 하단 pill 배경 (두루마리, 이미지 유지)
|
svg/pill_scroll.png — 하단 pill 배경 (두루마리, 이미지 유지)
|
||||||
CSS 요소:
|
|
||||||
배경 gradient: linear-gradient(180deg, #f0f0f0, #fff)
|
|
||||||
제목 gradient text: linear-gradient(180deg, #296b55, #123328)
|
|
||||||
하단 pill: 캡슐 border-radius 50px
|
|
||||||
-->
|
-->
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ko">
|
<html lang="ko">
|
||||||
@@ -33,136 +23,18 @@
|
|||||||
<title>{{ title|default("슬라이드") }}</title>
|
<title>{{ title|default("슬라이드") }}</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500;700;900&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@500;700;900&display=swap" rel="stylesheet">
|
||||||
|
<!--
|
||||||
|
CSS 로딩 순서:
|
||||||
|
1. tokens (typography, spacing, colors) — 변수 정의
|
||||||
|
2. base (slide-base.css) — 프레임 스타일
|
||||||
|
3. blocks (블록별 CSS) — 블록 스타일
|
||||||
|
4. themes (선택) — 톤 변형
|
||||||
|
-->
|
||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
{% include "styles/tokens/typography.css" %}
|
||||||
body {
|
{% include "styles/tokens/spacing.css" %}
|
||||||
font-family: 'Noto Sans KR', sans-serif;
|
{% include "styles/tokens/colors.css" %}
|
||||||
background: #e8ecf0;
|
{% include "styles/base/slide-base.css" %}
|
||||||
display: flex; justify-content: center; align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
word-break: keep-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── 16:9 슬라이드 컨테이너 ── */
|
|
||||||
.slide {
|
|
||||||
width: 1280px; height: 720px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
배경: Figma gradient(#f0f0f0→#fff) + 텍스처(opacity 2%)
|
|
||||||
MCP 확인: bg-gradient-to-b from-[#f0f0f0] to-white, opacity-2
|
|
||||||
*/
|
|
||||||
.slide-bg {
|
|
||||||
position: absolute; inset: 0;
|
|
||||||
background: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
.slide-bg-texture {
|
|
||||||
position: absolute; inset: 0;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
opacity: 0.02;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
상단 제목: Figma 65.8px Bold, gradient(#296b55→#123328)
|
|
||||||
top: 125/2165 × 720 ≈ 42px, left: 155/3848 × 1280 ≈ 52px
|
|
||||||
text-shadow: 0 0 5px #322c1e (Figma 5.263px × S)
|
|
||||||
*/
|
|
||||||
.slide-title {
|
|
||||||
position: absolute;
|
|
||||||
left: 52px; top: 22px;
|
|
||||||
width: calc(100% - 104px);
|
|
||||||
font-weight: 700;
|
|
||||||
font-size: 22px;
|
|
||||||
line-height: 1.4;
|
|
||||||
background-image: linear-gradient(180deg, #296b55 0%, #123328 100%);
|
|
||||||
-webkit-background-clip: text; background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
text-shadow: 0 0 2px #322c1e;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
구분선: Figma top 218px → 72px, width 3553/3848 × 1280 ≈ 1181px
|
|
||||||
SVG 이미지 또는 CSS border
|
|
||||||
*/
|
|
||||||
.slide-divider {
|
|
||||||
position: absolute;
|
|
||||||
left: 50px; top: 58px;
|
|
||||||
width: calc(100% - 100px);
|
|
||||||
height: 2px;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
.slide-divider img {
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
본문 영역: 구분선 아래 ~ 하단 pill 위
|
|
||||||
top: ~65px, bottom: ~60px (pill 영역)
|
|
||||||
→ 가용 높이 약 595px
|
|
||||||
*/
|
|
||||||
.slide-body {
|
|
||||||
position: absolute;
|
|
||||||
left: 40px; top: 65px;
|
|
||||||
width: calc(100% - 80px);
|
|
||||||
height: 590px;
|
|
||||||
z-index: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
하단 pill: Figma top 1980/2165 × 720 ≈ 659px, height 123 × S ≈ 41px
|
|
||||||
캡슐 border-radius: 50px × S ≈ 17px → 999px (캡슐)
|
|
||||||
pill 배경: 이미지 (두루마리, R16 crop) 또는 CSS gradient
|
|
||||||
*/
|
|
||||||
.slide-footer {
|
|
||||||
position: absolute;
|
|
||||||
left: 50px; bottom: 8px;
|
|
||||||
width: calc(100% - 100px);
|
|
||||||
height: 41px;
|
|
||||||
border-radius: 999px;
|
|
||||||
overflow: hidden;
|
|
||||||
z-index: 2;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
}
|
|
||||||
.slide-footer-bg {
|
|
||||||
position: absolute; inset: 0;
|
|
||||||
width: 100%; height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 999px;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
/* pill 배경이 이미지 없을 때 CSS gradient fallback */
|
|
||||||
.slide-footer--css {
|
|
||||||
background: linear-gradient(90deg, #3b3523 5%, #263a2a 50%, #113f31 95%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
footer 텍스트: Figma 60px × S ≈ 20px, Bold white
|
|
||||||
<em> = 노란색 #fe3
|
|
||||||
*/
|
|
||||||
.slide-footer-text {
|
|
||||||
position: relative; z-index: 1;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #ffffff;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.2;
|
|
||||||
text-shadow: 0 0 4px rgba(0,0,0,0.5);
|
|
||||||
letter-spacing: -0.01em;
|
|
||||||
}
|
|
||||||
.slide-footer-text em {
|
|
||||||
color: #ffee33;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
# Phase Z-2 frame contracts catalog (executable v0).
|
||||||
|
#
|
||||||
|
# 목적 :
|
||||||
|
# Python mapper 에 박혀있던 slot 구조 / cardinality / visual role 을 catalog 로 끌어냄.
|
||||||
|
# generic mapper (src/phase_z2_mapper.py:map_with_contract) 가 본 catalog 를 읽고
|
||||||
|
# MdxSection → slot_payload 변환. frame 별 hand-coded mapper 를 제거 방향.
|
||||||
|
#
|
||||||
|
# 원칙 :
|
||||||
|
# - frame 추가 = 본 yaml 에 entry 추가 (Python 수정은 새 builder/parser 필요시에만)
|
||||||
|
# - role_order = 순서 기반 visual role (label 키워드 기반 X — 다른 MDX 도 깨지지 않게)
|
||||||
|
# - cardinality strict 위반 → FitError → fallback path (AI restructuring 후보)
|
||||||
|
# - payload.builder = PAYLOAD_BUILDERS named registry 의 entry
|
||||||
|
# - builder 가 ITEM_PARSERS / COLUMN_BODY_PARSERS 같은 sub-primitive 호출
|
||||||
|
#
|
||||||
|
# v0 등록 frame :
|
||||||
|
# F13 three_parallel_requirements → builder=items_with_role
|
||||||
|
# F29 process_product_two_way → builder=process_product_pair
|
||||||
|
# F16 bim_issues_quadrant_four → builder=quadrant_flat_slots
|
||||||
|
# 이 셋이 현재 runtime 에서 실제 쓰는 주요 frame 의 모든 mapper.
|
||||||
|
|
||||||
|
three_parallel_requirements:
|
||||||
|
template_id: three_parallel_requirements
|
||||||
|
frame_id: 1171281190
|
||||||
|
family: three_parallel
|
||||||
|
|
||||||
|
source_shape: top_bullets
|
||||||
|
cardinality:
|
||||||
|
strict: 3
|
||||||
|
overflow_policy: abort_or_review
|
||||||
|
|
||||||
|
# 순서 기반 visual role (color_class 대체 — label 키워드 의존 제거)
|
||||||
|
role_order:
|
||||||
|
- tech
|
||||||
|
- people
|
||||||
|
- nature
|
||||||
|
|
||||||
|
# zone-level layout 힌트 — pipeline 의 compute_zone_layout 이 본 값을 lower bound 로 사용.
|
||||||
|
# 230 = 본 frame 의 token-based font 기준 최소 가독 높이 (Figma → CSS adapt 검증값).
|
||||||
|
visual_hints:
|
||||||
|
min_height_px: 230
|
||||||
|
|
||||||
|
# NEW (SPEC v1 §3) : 본 frame 이 받을 수 있는 content_object type (Layer A → Layer B 매칭 입력).
|
||||||
|
# 현재 mapper 는 본 필드를 *읽지 않음* — dormant. Layer A planner (B2) 가 도입 시 소비 예정.
|
||||||
|
accepted_content_types:
|
||||||
|
- text_block
|
||||||
|
|
||||||
|
# NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언 — frame 내부 자리.
|
||||||
|
# YAML field name 'sub_zones' = 코드/catalog reality 유지. 의미 = Frame Slot.
|
||||||
|
# partial_target_path = path *식별자* 만 (값). 실제 marker attribute (data-frame-slot 등) 은 별 axis (B5).
|
||||||
|
sub_zones:
|
||||||
|
- id: pillar_1
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
partial_target_path: ".f13b__cols > .f13b__col:nth-child(1)"
|
||||||
|
- id: pillar_2
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
partial_target_path: ".f13b__cols > .f13b__col:nth-child(2)"
|
||||||
|
- id: pillar_3
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
partial_target_path: ".f13b__cols > .f13b__col:nth-child(3)"
|
||||||
|
|
||||||
|
payload:
|
||||||
|
title:
|
||||||
|
source: section.title
|
||||||
|
builder: items_with_role
|
||||||
|
builder_options:
|
||||||
|
item_parser: pillar_item # ITEM_PARSERS entry
|
||||||
|
array_root: pillars # payload[array_root] = list of items
|
||||||
|
role_field: color_class # role_order[i] → item[role_field]
|
||||||
|
|
||||||
|
|
||||||
|
process_product_two_way:
|
||||||
|
template_id: process_product_two_way
|
||||||
|
frame_id: 1171281210
|
||||||
|
family: two_column_h3
|
||||||
|
|
||||||
|
source_shape: h3_subsections
|
||||||
|
cardinality:
|
||||||
|
strict: 2 # F29 frame = 2 visual columns. ≠2 → fallback.
|
||||||
|
overflow_policy: abort_or_review
|
||||||
|
|
||||||
|
# transform-block + 3 sections 의 breathing 위해 345 (이전 hardcoded 동일).
|
||||||
|
visual_hints:
|
||||||
|
min_height_px: 345
|
||||||
|
|
||||||
|
# NEW (SPEC v1 §3) : 본 frame 이 받을 수 있는 content_object type.
|
||||||
|
# process column 은 transform_table (AS-IS/TO-BE) 도 수용. product column 은 text_block 만.
|
||||||
|
# 현재 mapper 는 본 필드를 *읽지 않음* — dormant.
|
||||||
|
accepted_content_types:
|
||||||
|
- text_block
|
||||||
|
- transform_table
|
||||||
|
|
||||||
|
# NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언.
|
||||||
|
# 2 column × 3 section = 6 cell, but Frame Slot 단위 = column.
|
||||||
|
# partial_target_path = path 식별자 만 (값). marker attribute 결정은 별 axis.
|
||||||
|
sub_zones:
|
||||||
|
- id: process_column
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block, transform_table]
|
||||||
|
cardinality: { strict: 3 } # 3 sections per column
|
||||||
|
partial_target_path: ".f29b__grid .f29b__cell--left"
|
||||||
|
- id: product_column
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block] # product 쪽은 transform 안 받음 (현재 frame 의 시각 구분)
|
||||||
|
cardinality: { strict: 3 }
|
||||||
|
partial_target_path: ".f29b__grid .f29b__cell--right"
|
||||||
|
|
||||||
|
payload:
|
||||||
|
title:
|
||||||
|
source: section.title
|
||||||
|
builder: process_product_pair
|
||||||
|
builder_options:
|
||||||
|
pad_sections_to: 3 # 각 column.sections 길이 = 3 (legacy 와 동일)
|
||||||
|
columns:
|
||||||
|
- title_to: banner_left
|
||||||
|
body_to: process
|
||||||
|
body_parser: column_with_transform # 첫 top-bullet AS-IS/TO-BE 표 인식
|
||||||
|
- title_to: banner_right
|
||||||
|
body_to: product
|
||||||
|
body_parser: column_plain # 모든 section = 일반 text_lines
|
||||||
|
|
||||||
|
|
||||||
|
bim_issues_quadrant_four:
|
||||||
|
template_id: bim_issues_quadrant_four
|
||||||
|
frame_id: 1171281193
|
||||||
|
family: bim_issues_quadrant
|
||||||
|
|
||||||
|
source_shape: top_bullets
|
||||||
|
# F16 정책 = pad_to=4 + truncate>4 (legacy 와 동일). cardinality strict 화는 본 transition 범위 외.
|
||||||
|
# 향후 normal path 안정 후 strict 적용 + 위반 시 fallback path (FitError) 검토.
|
||||||
|
|
||||||
|
# NEW (SPEC v1 §3) : 본 frame 이 받을 수 있는 content_object type.
|
||||||
|
# 현재 mapper 는 본 필드를 *읽지 않음* — dormant.
|
||||||
|
accepted_content_types:
|
||||||
|
- text_block
|
||||||
|
|
||||||
|
# NEW (SPEC v1 §3) : Frame Slot (Layer B) 선언 — 4 quadrant.
|
||||||
|
# sub_zone 별 cardinality strict: 1 = 각 Frame Slot 의 *capacity* (Layer B schema).
|
||||||
|
# 기존 payload.builder_options.pad_to=4 / truncate_at=4 = mapper runtime 거동 (input mismatch 시).
|
||||||
|
# 두 layer 는 *공존* — 본 axis 에서 builder_options 는 미변경.
|
||||||
|
sub_zones:
|
||||||
|
- id: quadrant_1
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
partial_target_path: ".f16b__quadrant--tl"
|
||||||
|
- id: quadrant_2
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
partial_target_path: ".f16b__quadrant--tr"
|
||||||
|
- id: quadrant_3
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
partial_target_path: ".f16b__quadrant--bl"
|
||||||
|
- id: quadrant_4
|
||||||
|
role: main_text
|
||||||
|
accepts: [text_block]
|
||||||
|
cardinality: { strict: 1 }
|
||||||
|
partial_target_path: ".f16b__quadrant--br"
|
||||||
|
|
||||||
|
payload:
|
||||||
|
title:
|
||||||
|
source: section.title
|
||||||
|
builder: quadrant_flat_slots
|
||||||
|
builder_options:
|
||||||
|
item_parser: quadrant_item # ITEM_PARSERS entry
|
||||||
|
pad_to: 4 # quadrant_1..4 모두 채움 (부족 시 empty pad)
|
||||||
|
truncate_at: 4 # 5번째 이후 무시 + _truncated_count 기록
|
||||||
|
label_key_pattern: "quadrant_{n}_label"
|
||||||
|
body_key_pattern: "quadrant_{n}_body"
|
||||||
|
empty_label: ""
|
||||||
|
empty_body: []
|
||||||
|
# implicit slot_order = [quadrant_1=TL, quadrant_2=TR, quadrant_3=BL, quadrant_4=BR]
|
||||||
|
# 위치(TL/TR/BL/BR) 매핑은 partial template (families/bim_issues_quadrant_four.html) 결정.
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
<!-- Phase Z-2 frame-derived adapted block.
|
||||||
|
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt. -->
|
||||||
|
{#
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Visual Provenance — figma_to_html_agent/blocks/1171281193/index.html
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
PROMOTED (Figma 1:1 변환물 → 그대로 사용):
|
||||||
|
- card-bg gradient (TL/TR top→bottom) : line 49 → linear-gradient(180deg, #f2f2f2 0%, #ffffff 100%)
|
||||||
|
- card-bg gradient (BL/BR bottom→top) : line 53-54 → linear-gradient(0deg, #f2f2f2 0%, #ffffff 100%)
|
||||||
|
- header-ribbon (TL/BL — 갈색) : line 75, 79 → linear-gradient(270deg, rgba(165,161,150,0.5), #39321E 73%)
|
||||||
|
- header-ribbon (TR/BR — 청록) : line 83, 87 → linear-gradient(90deg, rgba(41,107,85,0.5), #032118)
|
||||||
|
- red headline color : line 127 → #ff0000
|
||||||
|
- body-text color : line 136 → #000
|
||||||
|
- center-quote (Rome wasn't Built in a day!) : line 233 (decorative, MDX 4-2.2 의 핵심 메시지로 대체 가능)
|
||||||
|
- bullet marker : line 241 → "•"
|
||||||
|
- card 모서리 radius : line 51-54 → 50px (top corners TL/TR, bottom corners BL/BR)
|
||||||
|
|
||||||
|
NOT PROMOTED (Figma 데코지만 MDX 04-2.2 에 없는 콘텐츠):
|
||||||
|
- center-quote 의 영문 ("Rome wasn't Built in a day!") — slot_payload.center_quote 로 동적 (없으면 omit)
|
||||||
|
|
||||||
|
ADAPTED (Figma px / native size 무시, zone-compatible):
|
||||||
|
- 2226×1766 inner + zoom 0.575 → CSS grid 2×2 (no zoom, no fixed inner)
|
||||||
|
- Figma 60px ribbon font / 55px headline / 42px body → token-fixed (zone-title 13 / sub-title 12 / body 11)
|
||||||
|
- 1080px card width / 119px ribbon height → grid 1fr 1fr / 22px ribbon
|
||||||
|
- 644×584 center 원 → 90px 원 (zone-fit)
|
||||||
|
- absolute positioning (4 quadrants) → grid-template-areas
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
slots: title, center_quote (옵션), quadrant_{1,2,3,4}_{label, body}
|
||||||
|
- quadrant 1 = TL (정책 집행)
|
||||||
|
- quadrant 2 = TR (수행 개념)
|
||||||
|
- quadrant 3 = BL (근본 취지의 이해부족)
|
||||||
|
- quadrant 4 = BR (지속적 투자 의지 부재)
|
||||||
|
- body 형식 = text-line family 호환 HTML (text_lines list 또는 pre-rendered string)
|
||||||
|
#}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.f16b {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||||
|
word-break: keep-all;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.f16b__title {
|
||||||
|
font-size: var(--font-zone-title);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: var(--lh-zone-title);
|
||||||
|
color: #1e293b;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4 quadrants — 2×2 grid (CSS grid-template-areas) */
|
||||||
|
.f16b__quadrants {
|
||||||
|
flex: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f16b__quadrant {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 8px 6px;
|
||||||
|
/* PROMOTED — card-bg gradient */
|
||||||
|
}
|
||||||
|
.f16b__quadrant--tl {
|
||||||
|
background: linear-gradient(180deg, #f2f2f2 0%, #ffffff 100%);
|
||||||
|
border-radius: 14px 14px 0 0; /* PROMOTED 50px → adapted 14px */
|
||||||
|
}
|
||||||
|
.f16b__quadrant--tr {
|
||||||
|
background: linear-gradient(180deg, #f2f2f2 0%, #ffffff 100%);
|
||||||
|
border-radius: 14px 14px 0 0;
|
||||||
|
}
|
||||||
|
.f16b__quadrant--bl {
|
||||||
|
background: linear-gradient(0deg, #f2f2f2 0%, #ffffff 100%);
|
||||||
|
border-radius: 0 0 14px 14px;
|
||||||
|
}
|
||||||
|
.f16b__quadrant--br {
|
||||||
|
background: linear-gradient(0deg, #f2f2f2 0%, #ffffff 100%);
|
||||||
|
border-radius: 0 0 14px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header ribbon — PROMOTED gradient + token font */
|
||||||
|
.f16b__ribbon {
|
||||||
|
height: 22px;
|
||||||
|
margin: 0 -4px 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: var(--font-sub-title);
|
||||||
|
font-weight: 900;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 0 3px #322c1e;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
/* PROMOTED ribbon gradient — TL/BL 갈색, TR/BR 청록 (figma line 75,79,83,87) */
|
||||||
|
.f16b__quadrant--tl .f16b__ribbon,
|
||||||
|
.f16b__quadrant--bl .f16b__ribbon {
|
||||||
|
background: linear-gradient(270deg, rgba(165,161,150,0.5) -26.87%, #39321E 73.13%);
|
||||||
|
}
|
||||||
|
.f16b__quadrant--tr .f16b__ribbon,
|
||||||
|
.f16b__quadrant--br .f16b__ribbon {
|
||||||
|
background: linear-gradient(90deg, rgba(41,107,85,0.5) 0%, #032118 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Body — red headline + text-line bullets */
|
||||||
|
.f16b__body {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
font-size: var(--font-body);
|
||||||
|
line-height: var(--lh-body);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.f16b__headline {
|
||||||
|
color: #ff0000; /* PROMOTED line 127 */
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--font-sub-title);
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
/* text-line family 은 slide_base.html 의 공통 contract 사용 */
|
||||||
|
|
||||||
|
/* Center quote (옵션 — MDX 에 있을 때만) */
|
||||||
|
.f16b__center {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%; top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 80px; height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, rgba(60,50,30,0.85), rgba(20,15,5,0.92));
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.f16b__center-quote {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="f16b" data-frame-id="1171281193" data-template-id="bim_issues_quadrant_four">
|
||||||
|
{% if slot_payload.title %}
|
||||||
|
<div class="f16b__title">{{ slot_payload.title }}</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="f16b__quadrants">
|
||||||
|
|
||||||
|
{# Quadrant 1 (TL) — 정책 집행 (갈색 ribbon) #}
|
||||||
|
<div class="f16b__quadrant f16b__quadrant--tl">
|
||||||
|
{% if slot_payload.quadrant_1_label %}
|
||||||
|
<div class="f16b__ribbon">{{ slot_payload.quadrant_1_label | safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if slot_payload.quadrant_1_headline or slot_payload.quadrant_1_body %}
|
||||||
|
<div class="f16b__body">
|
||||||
|
{% if slot_payload.quadrant_1_headline %}<div class="f16b__headline">{{ slot_payload.quadrant_1_headline | safe }}</div>{% endif %}
|
||||||
|
{% for line in slot_payload.quadrant_1_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Quadrant 2 (TR) — 수행 개념 (청록 ribbon) #}
|
||||||
|
<div class="f16b__quadrant f16b__quadrant--tr">
|
||||||
|
{% if slot_payload.quadrant_2_label %}
|
||||||
|
<div class="f16b__ribbon">{{ slot_payload.quadrant_2_label | safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if slot_payload.quadrant_2_headline or slot_payload.quadrant_2_body %}
|
||||||
|
<div class="f16b__body">
|
||||||
|
{% if slot_payload.quadrant_2_headline %}<div class="f16b__headline">{{ slot_payload.quadrant_2_headline | safe }}</div>{% endif %}
|
||||||
|
{% for line in slot_payload.quadrant_2_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Quadrant 3 (BL) — 근본 취지의 이해부족 (갈색 ribbon) #}
|
||||||
|
<div class="f16b__quadrant f16b__quadrant--bl">
|
||||||
|
{% if slot_payload.quadrant_3_label %}
|
||||||
|
<div class="f16b__ribbon">{{ slot_payload.quadrant_3_label | safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if slot_payload.quadrant_3_headline or slot_payload.quadrant_3_body %}
|
||||||
|
<div class="f16b__body">
|
||||||
|
{% if slot_payload.quadrant_3_headline %}<div class="f16b__headline">{{ slot_payload.quadrant_3_headline | safe }}</div>{% endif %}
|
||||||
|
{% for line in slot_payload.quadrant_3_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Quadrant 4 (BR) — 지속적 투자 의지 부재 (청록 ribbon) #}
|
||||||
|
<div class="f16b__quadrant f16b__quadrant--br">
|
||||||
|
{% if slot_payload.quadrant_4_label %}
|
||||||
|
<div class="f16b__ribbon">{{ slot_payload.quadrant_4_label | safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if slot_payload.quadrant_4_headline or slot_payload.quadrant_4_body %}
|
||||||
|
<div class="f16b__body">
|
||||||
|
{% if slot_payload.quadrant_4_headline %}<div class="f16b__headline">{{ slot_payload.quadrant_4_headline | safe }}</div>{% endif %}
|
||||||
|
{% for line in slot_payload.quadrant_4_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Center quote — 옵션 #}
|
||||||
|
{% if slot_payload.center_quote %}
|
||||||
|
<div class="f16b__center">
|
||||||
|
<div class="f16b__center-quote">{{ slot_payload.center_quote | safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block.
|
||||||
|
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt. -->
|
||||||
|
{#
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Visual Provenance — figma_to_html_agent/blocks/1171281210/index.html
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
PROMOTED (Figma 1:1 변환물 → 그대로 사용):
|
||||||
|
- banner-left text gradient : lines 101-104 → linear-gradient(90deg, rgb(62,53,35), rgb(62,53,35)) on linear-gradient(180deg, rgb(41,107,85), rgb(18,51,40))
|
||||||
|
- banner-right text gradient : lines 110-113 → linear-gradient(90deg, rgb(34,95,74), rgb(34,95,74)) on linear-gradient(180deg, rgb(41,107,85), rgb(18,51,40))
|
||||||
|
- section-title-left color : line 125 → #5C3714 (갈)
|
||||||
|
- section-title-right color : line 126 → #084C56 (짙은 청록)
|
||||||
|
- bullets text : line 136 → 700 / #000 / list-disc
|
||||||
|
- banner SVG asset (left) : line 196 → assets/header_left_bg.svg (좌측 180° rotate)
|
||||||
|
- banner SVG asset (right) : line 197 → assets/header_right_bg.svg
|
||||||
|
- body bg SVG asset : line 193 → assets/body_bg.svg (3845×933 상단)
|
||||||
|
- arrow PNG asset : line 220 → assets/arrow.png (AS-IS → TO-BE)
|
||||||
|
- footnote (*) inline : lines 144, 281-283 → 25.8px small + indent
|
||||||
|
|
||||||
|
ADAPTED (Figma px / native 무시, zone flex 재구성):
|
||||||
|
- 3848×1388 inner + zoom 0.33270 → flex layout (no zoom, no fixed inner)
|
||||||
|
- 70px banner / 50px section / 40px body→ var(--font-sub-title) / 8-10px / var(--font-caption)
|
||||||
|
- absolute left/top px → flex grid (no absolute)
|
||||||
|
- banner heights 142px + body 933px → 비율 기반 flex (banner ~28%, body ~72%)
|
||||||
|
- assets 1921×142 / 3845×933 sizes → object-fit cover, 비율 유지
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
slots: title (omitted), banner_left, banner_right,
|
||||||
|
process.sections[3] (s[0]: AS-IS/TO-BE, s[1-2]: bullets),
|
||||||
|
product.sections[3] (bullets, optional footnote/sub_title)
|
||||||
|
#}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.f29b {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
position: relative;
|
||||||
|
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||||
|
word-break: keep-all;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.f29b__title {
|
||||||
|
font-size: var(--font-zone-title); /* 13px — frame 13 의 f13b__title 과 동일 위계 */
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: var(--lh-zone-title);
|
||||||
|
background-image: linear-gradient(180deg, #000 0%, #883700 100%);
|
||||||
|
-webkit-background-clip: text; background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* banners — top */
|
||||||
|
.f29b__banners {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
position: relative;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.f29b__banner {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.f29b__banner-bg {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
object-fit: fill;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.f29b__banner--left .f29b__banner-bg { transform: rotate(180deg); } /* PROMOTED line 71 (좌 180° rotate) */
|
||||||
|
.f29b__banner-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: var(--font-sub-title);
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 0 0 2px #322C1E;
|
||||||
|
-webkit-background-clip: text; background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
/* PROMOTED gradient — lines 101-104 / 110-113 */
|
||||||
|
.f29b__banner--left .f29b__banner-text {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(90deg, rgb(62,53,35) 0%, rgb(62,53,35) 100%),
|
||||||
|
linear-gradient(180deg, rgb(41,107,85) 0%, rgb(18,51,40) 100%);
|
||||||
|
}
|
||||||
|
.f29b__banner--right .f29b__banner-text {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(90deg, rgb(34,95,74) 0%, rgb(34,95,74) 100%),
|
||||||
|
linear-gradient(180deg, rgb(41,107,85) 0%, rgb(18,51,40) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* body — 6 cell grid (3 rows × 2 cols) — 좌우 row divider Y 정렬 */
|
||||||
|
.f29b__body {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
padding-top: 4px;
|
||||||
|
}
|
||||||
|
.f29b__body-bg {
|
||||||
|
position: absolute;
|
||||||
|
left: 0; top: 0;
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
object-fit: fill;
|
||||||
|
z-index: 0;
|
||||||
|
opacity: 0.85;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6 cell grid: 3 rows (section 1/2/3), 2 cols (process/product). 각 row 의 height = max(좌, 우) */
|
||||||
|
.f29b__grid {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-auto-rows: auto;
|
||||||
|
gap: 6px 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
height: 100%;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
.f29b__cell {
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
/* row 2/3 (3 번째 cell 이후) 위에 dashed divider — 좌우 같은 Y */
|
||||||
|
.f29b__cell:nth-child(n+3) {
|
||||||
|
border-top: 1px dashed rgba(0,0,0,0.15);
|
||||||
|
padding-top: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
/* feedback-3: section title 에 border-left accent 로 hierarchy 강화 */
|
||||||
|
.f29b__section-title {
|
||||||
|
font-size: var(--font-zone-title); /* 13px */
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.3;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
padding-left: 6px;
|
||||||
|
border-left: 3px solid currentColor;
|
||||||
|
}
|
||||||
|
.f29b__cell--left .f29b__section-title { color: #5C3714; } /* PROMOTED line 125 */
|
||||||
|
.f29b__cell--right .f29b__section-title { color: #084C56; } /* PROMOTED line 126 */
|
||||||
|
.f29b__section-title__sub {
|
||||||
|
font-size: var(--font-caption); /* 10px */
|
||||||
|
margin-left: 3px;
|
||||||
|
}
|
||||||
|
/* f29b 의 본문 = .text-line 공통 contract 사용. bullet color override. */
|
||||||
|
.f29b__cell .text-line--bullet { color: #000; font-weight: 700; }
|
||||||
|
|
||||||
|
/* AS-IS/TO-BE 좌우 + arrow (PROMOTED 구조) */
|
||||||
|
.f29b__transform {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
.f29b__transform__list {
|
||||||
|
/* .text-line 공통 contract 그대로. wrapper 만 grid 안의 cell 역할 */
|
||||||
|
}
|
||||||
|
.f29b__transform__arrow {
|
||||||
|
align-self: center;
|
||||||
|
width: 28px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.f29b__transform__arrow img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f29b__footnote {
|
||||||
|
font-size: var(--font-caption); /* 10px */
|
||||||
|
line-height: var(--lh-caption);
|
||||||
|
color: #333;
|
||||||
|
padding-left: 14px;
|
||||||
|
margin-top: 1px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
.f29b__aster-small { font-size: 9px; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="f29b" data-frame-id="1171281210" data-template-id="process_product_two_way">
|
||||||
|
|
||||||
|
{# H2 mid-heading — section.title 렌더 (frame 13 의 f13b__title 과 동일 위계) #}
|
||||||
|
<div class="f29b__title">{{ slot_payload.title }}</div>
|
||||||
|
|
||||||
|
{# Banners (PROMOTED 구조 + assets line 196,197 + gradient lines 101-113) #}
|
||||||
|
<div class="f29b__banners">
|
||||||
|
<div class="f29b__banner f29b__banner--left">
|
||||||
|
<img class="f29b__banner-bg" src="assets/process_product_two_way/header_left_bg.svg" alt="">
|
||||||
|
<div class="f29b__banner-text">{{ slot_payload.banner_left }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="f29b__banner f29b__banner--right">
|
||||||
|
<img class="f29b__banner-bg" src="assets/process_product_two_way/header_right_bg.svg" alt="">
|
||||||
|
<div class="f29b__banner-text">{{ slot_payload.banner_right }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Body — 6 cell grid (row-major: row 1 좌/우, row 2 좌/우, row 3 좌/우).
|
||||||
|
row 별 height = max(좌, 우) → divider Y 정렬 #}
|
||||||
|
<div class="f29b__body">
|
||||||
|
<img class="f29b__body-bg" src="assets/process_product_two_way/body_bg.svg" alt="">
|
||||||
|
|
||||||
|
<div class="f29b__grid">
|
||||||
|
|
||||||
|
{# ───── Row 1: section 1 ───── #}
|
||||||
|
{% set s1 = slot_payload.process.sections[0] %}
|
||||||
|
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||||
|
{% if s1.title %}
|
||||||
|
<div class="f29b__section-title">{{ s1.title | safe }}</div>
|
||||||
|
{% if s1.transforms %}
|
||||||
|
<div class="transform-block">
|
||||||
|
<div class="transform-block__header">
|
||||||
|
<span class="transform-block__label--from">AS-IS</span>
|
||||||
|
<span></span>
|
||||||
|
<span class="transform-block__label--to">TO-BE</span>
|
||||||
|
</div>
|
||||||
|
<div class="transform-rows">
|
||||||
|
{% for t in s1.transforms %}
|
||||||
|
<div class="transform-row">
|
||||||
|
<div class="transform-row__from">{{ t.from | safe }}</div>
|
||||||
|
<div class="transform-row__arrow">➜</div>
|
||||||
|
<div class="transform-row__to">{{ t.to | safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% elif s1.text_lines %}
|
||||||
|
{% for line in s1.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set p1 = slot_payload.product.sections[0] %}
|
||||||
|
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||||
|
{% if p1.title %}
|
||||||
|
<div class="f29b__section-title">{{ p1.title | safe }}</div>
|
||||||
|
{% for line in p1.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ───── Row 2: section 2 ───── #}
|
||||||
|
{% set s2 = slot_payload.process.sections[1] %}
|
||||||
|
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||||
|
{% if s2.title %}
|
||||||
|
<div class="f29b__section-title">{{ s2.title | safe }}</div>
|
||||||
|
{% for line in s2.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set p2 = slot_payload.product.sections[1] %}
|
||||||
|
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||||
|
{% if p2.title %}
|
||||||
|
<div class="f29b__section-title">{{ p2.title | safe }}</div>
|
||||||
|
{% for line in p2.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
{% if p2.footnote %}
|
||||||
|
<div class="f29b__footnote"><span class="f29b__aster-small">*</span> {{ p2.footnote | safe }}</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ───── Row 3: section 3 ───── #}
|
||||||
|
{% set s3 = slot_payload.process.sections[2] %}
|
||||||
|
<div class="f29b__cell f29b__cell--left" data-frame-slot-id="process_column">
|
||||||
|
{% if s3.title %}
|
||||||
|
<div class="f29b__section-title">{{ s3.title | safe }}</div>
|
||||||
|
{% for line in s3.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set p3 = slot_payload.product.sections[2] %}
|
||||||
|
<div class="f29b__cell f29b__cell--right" data-frame-slot-id="product_column">
|
||||||
|
{% if p3.title %}
|
||||||
|
<div class="f29b__section-title">{{ p3.title | safe }}{% if p3.sub_title %}<span class="f29b__section-title__sub">{{ p3.sub_title | safe }}</span>{% endif %}</div>
|
||||||
|
{% for line in p3.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block.
|
||||||
|
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt.
|
||||||
|
Legacy templates/blocks/structures/prerequisites-3col.html 의 token-fixed font 접근 학습. -->
|
||||||
|
{#
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
Visual Provenance — figma_to_html_agent/blocks/1171281190/index.html
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
PROMOTED (Figma 1:1 변환물 → 그대로 사용):
|
||||||
|
- tech bar gradient : line 71 → linear-gradient(180deg, #0D78D0 0%, #023056 100%)
|
||||||
|
- people bar gradient : line 72 → linear-gradient(180deg, #FF9A23 0%, #CC5200 100%)
|
||||||
|
- nature bar gradient : line 73 → linear-gradient(180deg, #39BE49 21%, #23742C 100%)
|
||||||
|
- tech heading text : line 123 → linear-gradient(180deg, #0D78D0, #134D7F)
|
||||||
|
- people heading text : line 124 → linear-gradient(180deg, #FE900C, #D18B37)
|
||||||
|
- nature heading text : line 125 → linear-gradient(180deg, #3CA649, #23742C)
|
||||||
|
- title gradient : line 40 → linear-gradient(180deg, #000, #883700)
|
||||||
|
- desc color : line 118 → #3E3523
|
||||||
|
- col 외곽 border : line 57 → 2px solid #000 (top + bottom)
|
||||||
|
- col-section divider : line 103 → 2px dashed #000
|
||||||
|
|
||||||
|
NOT PROMOTED (Figma 데코지만 MDX 에 없으므로 주입 X):
|
||||||
|
- 한자 (技/術 / 人/材 / 天/地) ← MDX 에 없는 content. Figma frame 의 visual 데코일 뿐.
|
||||||
|
|
||||||
|
ADAPTED:
|
||||||
|
- Figma px / native size 무시, flex layout
|
||||||
|
- Figma 70/50/45/35px fonts → token-fixed (zone-title 13 / sub-title 12 / body 11 / caption 10)
|
||||||
|
- desc <br>-joined → nested <ul><li> (구조 보존)
|
||||||
|
─────────────────────────────────────────────────────────────────────────────
|
||||||
|
slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
|
||||||
|
#}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.f13b {
|
||||||
|
width: 100%; height: 100%;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
.f13b__title {
|
||||||
|
font-size: var(--font-zone-title);
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: var(--lh-zone-title);
|
||||||
|
background-image: linear-gradient(180deg, #000 0%, #883700 100%);
|
||||||
|
-webkit-background-clip: text; background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.f13b__cols {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.f13b__col {
|
||||||
|
display: flex;
|
||||||
|
border-top: 2px solid #000; /* PROMOTED line 57 */
|
||||||
|
border-bottom: 2px solid #000;
|
||||||
|
overflow: hidden;
|
||||||
|
min-width: 0;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
.f13b__bar {
|
||||||
|
flex: 0 0 50px;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
align-items: center; justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
padding: 6px 2px;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
.f13b__col--tech .f13b__bar { background: linear-gradient(180deg, #0D78D0 0%, #023056 100%); }
|
||||||
|
.f13b__col--people .f13b__bar { background: linear-gradient(180deg, #FF9A23 0%, #CC5200 100%); }
|
||||||
|
.f13b__col--nature .f13b__bar { background: linear-gradient(180deg, #39BE49 21%, #23742C 100%); }
|
||||||
|
/* PROMOTED Figma frame 13 line 92-93 — main + parenthetical stacked vertically */
|
||||||
|
.f13b__bar-main {
|
||||||
|
font-size: var(--font-sub-title); /* 12px — main word "기술/사람/자연" */
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.f13b__bar-paren {
|
||||||
|
font-size: var(--font-caption); /* 10px — "(디지털/역량/여건)" */
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.2;
|
||||||
|
display: block;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.f13b__body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
padding: 8px 12px; /* breathing (was 6px 10px) */
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.f13b__section {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 0; /* breathing (was 4px) */
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.f13b__section + .f13b__section {
|
||||||
|
border-top: 2px dashed #000; /* PROMOTED line 103 */
|
||||||
|
margin-top: 6px; /* visible section gap (was 4px) */
|
||||||
|
padding-top: 8px; /* breathing (was 6px) */
|
||||||
|
}
|
||||||
|
/* heading — visible hierarchy (zone-title 13px, bigger than body 11px) */
|
||||||
|
.f13b__heading {
|
||||||
|
font-size: var(--font-zone-title); /* 13px (was 12px) — heading 위계 강화 */
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: var(--lh-zone-title);
|
||||||
|
-webkit-background-clip: text; background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
margin-bottom: 6px; /* visible heading-body gap (was 4px) */
|
||||||
|
}
|
||||||
|
.f13b__col--tech .f13b__heading { background-image: linear-gradient(180deg, #0D78D0 0%, #134D7F 100%); }
|
||||||
|
.f13b__col--people .f13b__heading { background-image: linear-gradient(180deg, #FE900C 0%, #D18B37 100%); }
|
||||||
|
.f13b__col--nature .f13b__heading { background-image: linear-gradient(180deg, #3CA649 21%, #23742C 100%); }
|
||||||
|
|
||||||
|
/* desc — frame-family text layout contract (.text-line 공통 클래스 사용)
|
||||||
|
feedback (mvp1.5b_test5): browser-native ul/li 폐기. */
|
||||||
|
.f13b__desc {
|
||||||
|
color: #3E3523; /* PROMOTED line 118 */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
/* desc 안 .text-line 색 override */
|
||||||
|
.f13b__desc .text-line { color: #3E3523; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="f13b" data-frame-id="1171281190" data-template-id="three_parallel_requirements">
|
||||||
|
<div class="f13b__title">{{ slot_payload.title }}</div>
|
||||||
|
<div class="f13b__cols">
|
||||||
|
{% for pillar in slot_payload.pillars %}
|
||||||
|
<div class="f13b__col f13b__col--{{ pillar.color_class }}">
|
||||||
|
<div class="f13b__bar">
|
||||||
|
<span class="f13b__bar-main">{{ pillar.label_main | safe }}</span>
|
||||||
|
{% if pillar.label_paren %}
|
||||||
|
<span class="f13b__bar-paren">{{ pillar.label_paren | safe }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="f13b__body">
|
||||||
|
{% for section in pillar.sections %}
|
||||||
|
<div class="f13b__section">
|
||||||
|
<div class="f13b__heading">{{ section.heading | safe }}</div>
|
||||||
|
{% if section.text_lines %}
|
||||||
|
<div class="f13b__desc">
|
||||||
|
{% for line in section.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<!-- Phase Z-2 MVP-1.5b — single slide + Type B layout (top/bottom zones).
|
||||||
|
원래 Phase Z 설계 복귀: MDX 1 = slide 1, layout preset = zone 분할, frame-derived block ⊂ zone. -->
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1280">
|
||||||
|
<title>{{ slide_title }}</title>
|
||||||
|
<style>
|
||||||
|
/* ── existing tokens (inlined) ── */
|
||||||
|
{{ token_css | safe }}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
|
||||||
|
background: #e8ecf0;
|
||||||
|
display: flex; justify-content: center; align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
word-break: keep-all;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 16:9 슬라이드 (single, 1280×720) ── */
|
||||||
|
.slide {
|
||||||
|
width: 1280px; height: 720px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,.15);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.slide-bg {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
background: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.slide-title {
|
||||||
|
position: absolute;
|
||||||
|
left: 52px; top: 22px;
|
||||||
|
width: calc(100% - 104px);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: var(--font-slide-title);
|
||||||
|
line-height: var(--lh-slide-title);
|
||||||
|
background-image: linear-gradient(180deg, #296b55 0%, #123328 100%);
|
||||||
|
-webkit-background-clip: text; background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
text-shadow: 0 0 2px #322c1e;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.slide-divider {
|
||||||
|
position: absolute;
|
||||||
|
left: 50px; top: 58px;
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
height: 2px;
|
||||||
|
background: #1e293b;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.slide-body {
|
||||||
|
position: absolute;
|
||||||
|
left: 40px; top: 65px;
|
||||||
|
width: calc(100% - 80px); /* 1200 */
|
||||||
|
height: 590px;
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Layout preset (composition planner v0 — 8 vocabulary) ──
|
||||||
|
단일 .layout-{preset} 클래스에 grid CSS 주입.
|
||||||
|
layout_css = pipeline build_layout_css() 결과 :
|
||||||
|
- horizontal-2 : rows = dynamic px (content_weight + frame_min_height)
|
||||||
|
- 그 외 : rows / cols = fr default (LAYOUT_PRESETS)
|
||||||
|
positions (single / top / bottom / left / right / top-left / ... / grid-2x2 9개)
|
||||||
|
은 .zone 의 inline grid-area 로 지정 — preset CSS 는 areas 만 정의. */
|
||||||
|
.layout-{{ layout_preset }} {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: {{ layout_css.cols }};
|
||||||
|
grid-template-rows: {{ layout_css.rows }};
|
||||||
|
grid-template-areas: {{ layout_css.areas | safe }};
|
||||||
|
gap: {{ gap_px }}px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Zone — block 가용 공간 최대화. 프레임 자체 styling 이 boundary 역할 ── */
|
||||||
|
.zone {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Frame-family text layout contract (shared, reusable) ──
|
||||||
|
feedback-1 (mvp1.5b_test7): visible improvement 강화.
|
||||||
|
Stronger hanging indent + breathing line spacing + visible hierarchy. */
|
||||||
|
.text-line {
|
||||||
|
font-size: var(--font-body); /* 11px */
|
||||||
|
font-weight: var(--weight-body);
|
||||||
|
line-height: 1.6; /* breathing room */
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.text-line + .text-line {
|
||||||
|
margin-top: 4px; /* visible gap between lines (was 2px) */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bullet line — hanging indent via padding + absolute marker (안정적) */
|
||||||
|
.text-line--bullet {
|
||||||
|
padding-left: 14px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.text-line--bullet::before {
|
||||||
|
content: "•"; /* visible bullet 마커 */
|
||||||
|
position: absolute;
|
||||||
|
left: 2px;
|
||||||
|
top: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: inherit;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
/* indent levels — visible nested hierarchy */
|
||||||
|
.text-line--indent-1 {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
.text-line--indent-1::before {
|
||||||
|
content: "◦"; /* sub-bullet 다른 마커 */
|
||||||
|
left: 18px;
|
||||||
|
}
|
||||||
|
.text-line--indent-2 {
|
||||||
|
padding-left: 46px;
|
||||||
|
}
|
||||||
|
.text-line--indent-2::before {
|
||||||
|
content: "▪";
|
||||||
|
left: 34px;
|
||||||
|
}
|
||||||
|
/* body line — 마커 없음 */
|
||||||
|
.text-line--body {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
.text-line strong { color: #000; font-weight: 700; }
|
||||||
|
|
||||||
|
/* ── Transform-block family (frame 29 AS-IS/TO-BE dedicated comparison component) ──
|
||||||
|
feedback-3: bullet rows 가 아닌 *전용 비교 component*. Header (AS-IS / TO-BE) + paired rows.
|
||||||
|
color-coded cells, left-border accent → 단번에 *비교 구조* 인지. */
|
||||||
|
.transform-block {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 6px 8px 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
.transform-block__header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 22px 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-bottom: 3px;
|
||||||
|
border-bottom: 1px dashed rgba(0,0,0,0.12);
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.transform-block__label--from { color: #6b5444; }
|
||||||
|
.transform-block__label--to { color: #2563eb; text-align: left; }
|
||||||
|
.transform-rows {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
.transform-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 22px 1fr;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: stretch;
|
||||||
|
font-size: var(--font-body);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
.transform-row__from {
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: linear-gradient(90deg, rgba(140,120,90,0.16), rgba(140,120,90,0.08));
|
||||||
|
border-left: 2px solid #b8a98a;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #4b3f30;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.transform-row__arrow {
|
||||||
|
text-align: center;
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 14px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
.transform-row__to {
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: linear-gradient(90deg, rgba(37,99,235,0.10), rgba(37,99,235,0.18));
|
||||||
|
border-left: 2px solid #2563eb;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #1e40af;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer pill — feedback-3: body 와 weight balance, 살짝 가벼워짐 ── */
|
||||||
|
.slide-footer {
|
||||||
|
position: absolute;
|
||||||
|
left: 60px; bottom: 8px;
|
||||||
|
width: calc(100% - 120px);
|
||||||
|
height: 32px; /* was 41px */
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
background: linear-gradient(90deg, #3b3523 5%, #263a2a 50%, #113f31 95%);
|
||||||
|
opacity: 0.92; /* slightly lighter */
|
||||||
|
}
|
||||||
|
.slide-footer-text {
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
font-size: 15px; /* was 20px (--font-footer) */
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 0 3px rgba(0,0,0,.4);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.slide-footer-text em { color: #fe3; font-style: normal; }
|
||||||
|
|
||||||
|
.phase-z2-marker {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px; right: 8px;
|
||||||
|
font-size: 9px; color: #94a3b8;
|
||||||
|
font-family: monospace;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="slide" data-page="1">
|
||||||
|
<div class="slide-bg"></div>
|
||||||
|
<div class="phase-z2-marker">phase_z2 / mvp-1.5b / {{ layout_preset }} / single slide</div>
|
||||||
|
<div class="slide-title">{{ slide_title }}</div>
|
||||||
|
<div class="slide-divider"></div>
|
||||||
|
<div class="slide-body">
|
||||||
|
<div class="layout-{{ layout_preset }}">
|
||||||
|
{% for zone in zones %}
|
||||||
|
<div class="zone" data-zone-position="{{ zone.position }}" data-template-id="{{ zone.template_id }}" style="grid-area: {{ zone.position }};">
|
||||||
|
{{ zone.partial_html | safe }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if slide_footer %}
|
||||||
|
<div class="slide-footer">
|
||||||
|
<div class="slide-footer-text">{{ slide_footer | safe }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/* ══════════════════════════════════════
|
||||||
|
slide-base.css
|
||||||
|
══════════════════════════════════════
|
||||||
|
슬라이드 공통 프레임 스타일.
|
||||||
|
slide-base.html에서 분리.
|
||||||
|
직접값 대신 토큰 변수 참조.
|
||||||
|
══════════════════════════════════════ */
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans KR', sans-serif;
|
||||||
|
background: var(--color-bg-page);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
word-break: keep-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 16:9 슬라이드 컨테이너 ── */
|
||||||
|
.slide {
|
||||||
|
width: 1280px;
|
||||||
|
height: 720px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--color-bg);
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 배경 ── */
|
||||||
|
.slide-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.slide-bg-texture {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
opacity: 0.02;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 대목차 제목 ── */
|
||||||
|
.slide-title {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--slide-title-left);
|
||||||
|
top: var(--slide-title-top);
|
||||||
|
width: calc(100% - 104px);
|
||||||
|
font-weight: var(--weight-slide-title);
|
||||||
|
font-size: var(--font-slide-title);
|
||||||
|
line-height: var(--lh-slide-title);
|
||||||
|
background-image: linear-gradient(180deg, var(--color-slide-title-from) 0%, var(--color-slide-title-to) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
text-shadow: 0 0 2px #322c1e;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 구분선 ── */
|
||||||
|
.slide-divider {
|
||||||
|
position: absolute;
|
||||||
|
left: 50px;
|
||||||
|
top: var(--slide-divider-top);
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
height: 2px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.slide-divider img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 본문 영역 ── */
|
||||||
|
.slide-body {
|
||||||
|
position: absolute;
|
||||||
|
left: var(--slide-padding-x);
|
||||||
|
top: var(--slide-body-top);
|
||||||
|
width: calc(100% - var(--slide-padding-x) * 2);
|
||||||
|
height: var(--slide-body-height);
|
||||||
|
z-index: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 하단 footer pill ── */
|
||||||
|
.slide-footer {
|
||||||
|
position: absolute;
|
||||||
|
left: 50px;
|
||||||
|
bottom: var(--slide-footer-bottom);
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
height: var(--slide-footer-height);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.slide-footer-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 999px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.slide-footer--css {
|
||||||
|
background: linear-gradient(90deg, var(--color-footer-bg-from) 5%, var(--color-footer-bg-mid) 50%, var(--color-footer-bg-to) 95%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── footer 텍스트 ── */
|
||||||
|
.slide-footer-text {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
font-size: var(--font-footer);
|
||||||
|
font-weight: var(--weight-footer);
|
||||||
|
line-height: var(--lh-footer);
|
||||||
|
color: var(--color-footer-text);
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 0 0 4px rgba(0,0,0,0.5);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.slide-footer-text em {
|
||||||
|
color: var(--color-footer-accent);
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/* ══════════════════════════════════════
|
||||||
|
Color Tokens v1
|
||||||
|
══════════════════════════════════════
|
||||||
|
2층 구조: 공통 테마색 + 블록 의미색.
|
||||||
|
══════════════════════════════════════ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* ══ 1층: 공통 테마색 ══ */
|
||||||
|
/* 모든 블록이 공유하는 기본 팔레트 */
|
||||||
|
|
||||||
|
/* 텍스트 */
|
||||||
|
--color-title: #1e293b; /* 제목 (진한 남색) */
|
||||||
|
--color-zone-title: #1a365d; /* zone 중목차 제목 */
|
||||||
|
--color-body: #475569; /* 본문 */
|
||||||
|
--color-body-strong: #1e293b; /* 본문 강조 */
|
||||||
|
--color-caption: #94a3b8; /* 캡션, 보조 */
|
||||||
|
--color-muted: #64748b; /* 약한 텍스트 */
|
||||||
|
|
||||||
|
/* 배경 */
|
||||||
|
--color-bg: #ffffff; /* 슬라이드 배경 */
|
||||||
|
--color-bg-subtle: #f8fafc; /* 카드/영역 배경 */
|
||||||
|
--color-bg-page: #e8ecf0; /* 페이지 배경 (슬라이드 바깥) */
|
||||||
|
|
||||||
|
/* 보더 */
|
||||||
|
--color-border: #e2e8f0; /* 기본 보더 */
|
||||||
|
--color-border-strong: #cbd5e1; /* 강한 보더 */
|
||||||
|
|
||||||
|
/* 강조 */
|
||||||
|
--color-accent: #2563eb; /* 링크, 강조 (파랑) */
|
||||||
|
--color-accent-hover: #eff6ff; /* 링크 hover 배경 */
|
||||||
|
--color-danger: #dc2626; /* 경고, 에러 */
|
||||||
|
|
||||||
|
/* slide-base 전용 */
|
||||||
|
--color-slide-title-from: #296b55; /* 대목차 gradient 시작 */
|
||||||
|
--color-slide-title-to: #123328; /* 대목차 gradient 끝 */
|
||||||
|
--color-footer-bg-from: #3b3523; /* footer gradient 시작 */
|
||||||
|
--color-footer-bg-mid: #263a2a; /* footer gradient 중간 */
|
||||||
|
--color-footer-bg-to: #113f31; /* footer gradient 끝 */
|
||||||
|
--color-footer-text: #ffffff; /* footer 텍스트 */
|
||||||
|
--color-footer-accent: #ffee33; /* footer 강조 (em) */
|
||||||
|
|
||||||
|
/* ══ 2층: 블록 의미색 ══ */
|
||||||
|
/* 특정 블록 유형에서 의미를 가지는 색 */
|
||||||
|
|
||||||
|
/* 3열 비교 (prerequisites-3col) */
|
||||||
|
--color-col-1-from: #0D78D0; /* 1열 gradient 시작 */
|
||||||
|
--color-col-1-to: #023056; /* 1열 gradient 끝 */
|
||||||
|
--color-col-2-from: #FF9A23; /* 2열 gradient 시작 */
|
||||||
|
--color-col-2-to: #CC5200; /* 2열 gradient 끝 */
|
||||||
|
--color-col-3-from: #39BE49; /* 3열 gradient 시작 */
|
||||||
|
--color-col-3-to: #23742C; /* 3열 gradient 끝 */
|
||||||
|
|
||||||
|
/* 비교 블록 (compare) */
|
||||||
|
--color-compare-left: #5c3714; /* 좌측 (AS-IS 계열) */
|
||||||
|
--color-compare-right: #285b4a; /* 우측 (TO-BE 계열) */
|
||||||
|
--color-compare-badge: #ae3607; /* VS 뱃지 */
|
||||||
|
|
||||||
|
/* 다크 카드 (direct render) */
|
||||||
|
--color-dark-card-1: #1a365d; /* 카드 1 배경 */
|
||||||
|
--color-dark-card-2: #1e3a2f; /* 카드 2 배경 */
|
||||||
|
--color-dark-card-3: #3b1f2b; /* 카드 3 배경 */
|
||||||
|
--color-dark-card-title: #fbbf24; /* 다크카드 제목 (금색) */
|
||||||
|
--color-dark-card-body: #e2e8f0; /* 다크카드 본문 */
|
||||||
|
|
||||||
|
/* 표 */
|
||||||
|
--color-table-header-bg: #64748b; /* 표 헤더 배경 */
|
||||||
|
--color-table-header-text: #ffffff; /* 표 헤더 텍스트 */
|
||||||
|
|
||||||
|
/* 블록 제목 gradient (Figma 원본) */
|
||||||
|
--color-block-title-from: #CC5200; /* 주황 gradient 시작 */
|
||||||
|
--color-block-title-to: #883700; /* 주황 gradient 끝 */
|
||||||
|
|
||||||
|
/* pill */
|
||||||
|
--color-pill-bg: #1e293b; /* pill 배경 */
|
||||||
|
--color-pill-text: #ffffff; /* pill 텍스트 */
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/* ══════════════════════════════════════
|
||||||
|
Spacing Tokens v1
|
||||||
|
══════════════════════════════════════
|
||||||
|
여백/간격/들여쓰기 공통 기준.
|
||||||
|
══════════════════════════════════════ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* ── 기본 스케일 ── */
|
||||||
|
--space-xs: 4px;
|
||||||
|
--space-sm: 8px;
|
||||||
|
--space-md: 12px;
|
||||||
|
--space-lg: 16px;
|
||||||
|
--space-xl: 24px;
|
||||||
|
|
||||||
|
/* ── slide-base 레이아웃 ── */
|
||||||
|
--slide-padding-x: 40px; /* 본문 좌우 여백 */
|
||||||
|
--slide-title-left: 52px; /* 대목차 시작 */
|
||||||
|
--slide-title-top: 22px; /* 대목차 위치 */
|
||||||
|
--slide-divider-top: 58px; /* 구분선 위치 */
|
||||||
|
--slide-body-top: 65px; /* 본문 시작 */
|
||||||
|
--slide-body-height: 575px; /* 본문 높이 */
|
||||||
|
--slide-footer-bottom: 18px; /* footer 하단 여백 */
|
||||||
|
--slide-footer-height: 41px; /* footer 높이 */
|
||||||
|
|
||||||
|
/* ── zone ── */
|
||||||
|
--zone-gap: 8px; /* zone 간 간격 */
|
||||||
|
--zone-padding-left: 12px; /* zone 안쪽 좌측 여백 */
|
||||||
|
--zone-padding-right: 8px; /* zone 안쪽 우측 여백 */
|
||||||
|
--zone-title-mb: 8px; /* zone 제목 아래 여백 */
|
||||||
|
|
||||||
|
/* ── 블록 공통 ── */
|
||||||
|
--card-padding: 16px; /* 카드 내부 여백 */
|
||||||
|
--card-padding-sm: 8px; /* 작은 카드 내부 여백 */
|
||||||
|
--card-gap: 12px; /* 카드 간 간격 */
|
||||||
|
--card-radius: 6px; /* 카드 모서리 */
|
||||||
|
|
||||||
|
/* ── 블릿/텍스트 ── */
|
||||||
|
--bullet-indent: 14px; /* 불릿 hanging indent */
|
||||||
|
--bullet-gap: 2px; /* 불릿 항목 간 간격 */
|
||||||
|
--heading-gap: 4px; /* 소제목 아래 간격 */
|
||||||
|
--section-gap: 10px; /* 섹션 간 간격 */
|
||||||
|
|
||||||
|
/* ── flex/grid 기본 ── */
|
||||||
|
--flex-gap: 12px; /* 기본 flex gap */
|
||||||
|
--flex-gap-sm: 6px; /* 좁은 flex gap */
|
||||||
|
--grid-gap: 16px; /* 기본 grid gap */
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/* ══════════════════════════════════════
|
||||||
|
Typography Tokens v1
|
||||||
|
══════════════════════════════════════
|
||||||
|
슬라이드 전체의 공통 글자 위계.
|
||||||
|
모든 블록은 이 토큰을 참조한다.
|
||||||
|
직접값(font-size: 11px) 금지 → var(--font-body) 사용.
|
||||||
|
══════════════════════════════════════ */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* ── 1층: Global Hierarchy ── */
|
||||||
|
|
||||||
|
/* 대목차: 슬라이드 상단 제목 */
|
||||||
|
--font-slide-title: 22px;
|
||||||
|
--weight-slide-title: 700;
|
||||||
|
--lh-slide-title: 1.4;
|
||||||
|
|
||||||
|
/* 중목차: zone 제목 */
|
||||||
|
--font-zone-title: 13px;
|
||||||
|
--weight-zone-title: 700;
|
||||||
|
--lh-zone-title: 1.4;
|
||||||
|
|
||||||
|
/* 소목차: 블록 내 소제목, 카드 제목 */
|
||||||
|
--font-sub-title: 12px;
|
||||||
|
--weight-sub-title: 700;
|
||||||
|
--lh-sub-title: 1.45;
|
||||||
|
|
||||||
|
/* 본문: 블릿, 설명 텍스트 */
|
||||||
|
--font-body: 11px;
|
||||||
|
--weight-body: 500;
|
||||||
|
--lh-body: 1.55;
|
||||||
|
|
||||||
|
/* 캡션: 각주, 출처, 보조 텍스트 */
|
||||||
|
--font-caption: 10px;
|
||||||
|
--weight-caption: 400;
|
||||||
|
--lh-caption: 1.4;
|
||||||
|
|
||||||
|
/* footer: 핵심 인사이트 pill */
|
||||||
|
--font-footer: 20px;
|
||||||
|
--weight-footer: 700;
|
||||||
|
--lh-footer: 1.2;
|
||||||
|
|
||||||
|
/* ── 2층: Component Semantic Tokens ── */
|
||||||
|
/* 가까운 위계에서 기본값, 필요시 override */
|
||||||
|
|
||||||
|
/* 본문 강조 (인라인 heading) */
|
||||||
|
--font-body-strong: var(--font-body);
|
||||||
|
--weight-body-strong: 600;
|
||||||
|
|
||||||
|
/* 자세히보기 링크 */
|
||||||
|
--font-detail-link: var(--font-caption);
|
||||||
|
--weight-detail-link: 500;
|
||||||
|
|
||||||
|
/* pill/badge 라벨 */
|
||||||
|
--font-pill-label: var(--font-sub-title);
|
||||||
|
--weight-pill-label: 700;
|
||||||
|
|
||||||
|
/* 표 헤더 */
|
||||||
|
--font-table-header: var(--font-sub-title);
|
||||||
|
--weight-table-header: 700;
|
||||||
|
|
||||||
|
/* 표 셀 */
|
||||||
|
--font-table-cell: var(--font-body);
|
||||||
|
--weight-table-cell: 400;
|
||||||
|
|
||||||
|
/* 비교 뱃지 */
|
||||||
|
--font-compare-badge: var(--font-sub-title);
|
||||||
|
--weight-compare-badge: 700;
|
||||||
|
|
||||||
|
/* 강조 인용 */
|
||||||
|
--font-callout: var(--font-sub-title);
|
||||||
|
--weight-callout: 700;
|
||||||
|
|
||||||
|
/* 상단 라벨 (overline) */
|
||||||
|
--font-overline: var(--font-caption);
|
||||||
|
--weight-overline: 600;
|
||||||
|
--ls-overline: 0.05em;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user