figma_to_html_agent 추가 + MCP/Claude 설정

figma_to_html_agent/:
- Figma MCP 기반 블록 추출 에이전트 (CLAUDE.md, PLAN.md, PROCESS.md 등)
- block-tests/: Figma→HTML 변환 결과물 (bim-3roles-cards 등)
- templates_staging/: Jinja2 템플릿 + meta.yaml + example.yaml
- figma-analysis/, figma-assets/: Figma 분석 데이터 + 에셋
- scripts/: gradient_math.py 등 유틸리티

설정:
- .mcp.json: Figma MCP 서버 연결 설정
- .claude/settings.json: Claude Code 프로젝트 설정

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-13 11:00:31 +09:00
parent 360cd8e44c
commit 51548fdc41
467 changed files with 25280 additions and 10 deletions

362
figma_to_html_agent/MATH.md Normal file
View File

@@ -0,0 +1,362 @@
# 수학 공식 레퍼런스
Figma → HTML 변환에서 사용하는 모든 수학 공식. 이 문서의 공식만 사용하고, 직관/감으로 보정하지 않는다.
---
## §1. 스케일 팩터
### 정의
```
S = 1280 / W_원본_프레임
```
`1280`은 16:9 슬라이드 가로 폭. 모든 프레임은 가로 1280에 맞춰 축소된다.
### 적용 방법: CSS transform scale (권장)
```html
<div class="block">
<div class="inner"> <!-- 원본 W × H 좌표계 그대로 -->
... 모든 요소 (Figma 원본 px) ...
</div>
</div>
```
```css
.block {
width: 1280px;
height: {H × S}px;
overflow: hidden;
position: relative;
}
.inner {
position: absolute;
left: 0; top: 0;
width: {W}px; /* 원본 그대로 */
height: {H}px;
transform: scale({S});
transform-origin: top left;
}
```
**왜 transform이 좋은가:**
- 위치/크기/폰트/그림자/스트로크/blur radius 모두 한 번에 균일 축소
- 매 값 수동 곱셈하면 누적 오차 + 검증 어려움
- transform은 GPU 가속, 계산 정확
### 적용 대상
| 적용 | 미적용 |
|------|------|
| 위치 (x, y) | 색상 |
| 크기 (width, height) | 그라데이션 방향 (각도 그대로) |
| 폰트 크기 | 그라데이션 stop 퍼센트 (그대로) |
| 스트로크 너비 | 폰트 굵기 |
| 간격 (gap, padding) | line-height 비율 (1.5 등) |
| 그림자 (blur, offset) | border-radius 비율 (50% 등) |
| border-radius (px) | |
---
## §2. SVG `<linearGradient>` → CSS `linear-gradient()`
### 입력
SVG에서:
```xml
<linearGradient id="..." gradientUnits="userSpaceOnUse"
x1="..." y1="..." x2="..." y2="...">
<stop offset="0" stop-color="..."/>
<stop offset="1" stop-color="..."/>
</linearGradient>
```
### 변환 공식
```
1. dx = x2 - x1
dy = y2 - y1
L_svg = √(dx² + dy²)
2. SVG 벡터 각도 (y-down 좌표계, 0°=오른쪽, +CW):
svg_angle = atan2(dy, dx) (단위: 라디안)
3. CSS 각도 (12시 방향=0°, +CW):
css_angle = degrees(svg_angle) + 90
css_angle = css_angle mod 360
4. CSS 그라데이션 선 길이 (W×H 박스 안):
α = radians(css_angle)
L_css = |W × sin(α)| + |H × cos(α)|
5. 박스 중심의 t 파라미터 (SVG 벡터 위, 0=시작, 1=끝):
t_center = ((W/2 - x1)·dx + (H/2 - y1)·dy) / L_svg²
6. CSS 0% / 100%가 SVG t-space의 어디에 매핑되는지:
half = (L_css / 2) / L_svg
t0 = t_center - half ← CSS 0%
t1 = t_center + half ← CSS 100%
7. SVG 각 stop offset (0~1)을 CSS percent로:
pct = (offset - t0) / (t1 - t0) × 100
```
### 예시
SVG:
```xml
<linearGradient x1="110.833" y1="18.2292" x2="219.479" y2="175"
gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#FDC69E"/>
<stop offset="1" stop-color="#E0782C"/>
</linearGradient>
```
박스 W=H=350일 때:
```
dx = 108.65, dy = 156.77
L_svg = √(108.65² + 156.77²) = 190.74
svg_angle = atan2(156.77, 108.65) = 0.9646 rad = 55.27°
css_angle = 55.27 + 90 = 145.27°
α = 2.535 rad
L_css = 350 × |sin 145.27°| + 350 × |cos 145.27°|
= 350 × 0.5696 + 350 × 0.8220
= 487.06
t_center = ((175 - 110.833)·108.65 + (175 - 18.229)·156.77) / 190.74²
= (6971.7 + 24577.3) / 36382
= 0.8672
half = (487.06 / 2) / 190.74 = 1.2767
t0 = 0.8672 - 1.2767 = -0.4095
t1 = 0.8672 + 1.2767 = 2.1439
SVG offset 0 → pct = (0 - (-0.4095)) / 2.5534 × 100 = 16.04%
SVG offset 1 → pct = (1 - (-0.4095)) / 2.5534 × 100 = 55.20%
```
CSS:
```css
background: linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%);
```
### 코드: `scripts/gradient_math.py`
```python
from scripts.gradient_math import svg_to_css
svg_to_css(W=350, H=350,
x1=110.833, y1=18.2292, x2=219.479, y2=175,
stops=[(0, '#FDC69E'), (1, '#E0782C')])
# → "linear-gradient(145.27deg, #FDC69E 16.04%, #E0782C 55.20%)"
```
---
## §3. 회전 감지 (bbox 비율 검사)
Figma MCP는 `rotation` 속성을 출력하지 않으므로 bbox 비율로 추론:
```
단일 문자 텍스트:
width > height × 1.5 → 90° 회전 (가로로 누움)
일반 텍스트:
width < fontSize × 0.8 → 좁은 박스 세로 배치 (writing-mode 아님, <br>로 줄바꿈)
```
CSS 적용:
```css
.rotated {
transform: rotate(90deg); /* 또는 -90deg */
}
```
---
## §4. Descender 보정 (padding-bottom)
CSS `line-height: 1`이거나 `< font_content_area_ratio`이면 글리프 하강부(g, y, p, 쉼표)가 잘림.
### 폰트별 메트릭
| 폰트 | UPM | typoAscender | typoDescender | content_area_ratio |
|------|-----|------|------|------|
| Noto Sans KR | 1000 | 1160 | 288 | 1.448 |
| Pretendard | 1000 | 1100 | 300 | 1.400 |
### 공식
```
content_area_ratio = (typoAscender + |typoDescender|) / UPM
half_leading = (line_height_ratio - content_area_ratio) / 2
↑ 음수면 잘림 발생
clipped_px = |half_leading| × font_size
padding-bottom = ceil(clipped_px)
```
### 예시 (Noto Sans KR, font 27.1px, lh 1)
```
half_leading = (1.0 - 1.448) / 2 = -0.224
clipped = 0.224 × 27.1 = 6.07 px
→ padding-bottom: 7px
```
### 예시 (Noto Sans KR, font 30px, lh 35px → ratio 1.167)
```
half_leading = (1.167 - 1.448) / 2 = -0.1405
clipped = 0.1405 × 30 = 4.215 px
→ padding-bottom: 5px
```
---
## §5. SVG viewBox padding → CSS box-sizing 매핑
SVG가 drop-shadow blur 여백을 위해 viewBox를 확장해놓은 경우 (예: 280×280 fill을 310 viewBox에 넣음):
### 케이스 A — Stroke가 fill 외부 (안전과 품질 ring 같은 케이스)
```
SVG: viewBox 310, fill r=140 (d=280), stroke r=142.5 width=5 (extends r=140 to 145)
visible: 290×290 (fill 280 + 5px stroke 외부 확장)
viewBox padding: 310 - 290 = 20 (각 변 10이 drop-shadow blur 패딩, 추가 5는 stroke)
CSS:
div W=H=290
border: 5px solid white
box-sizing: border-box
→ border-box 290, padding-box 280 ← fill 영역
position: Figma fill 위치 - (5, 5) ← stroke 외부 확장 보정
```
### 케이스 B — Stroke가 fill 내부 (생산성/소통 ring 같은 케이스)
```
SVG: viewBox 300, fill r=140, stroke r=137.5 width=5 (extends r=135 to 140 — fill 외곽 5px overlap)
visible: 280×280 (stroke가 fill 외곽 5px를 덮음)
viewBox padding: 300 - 280 = 20 (전부 drop-shadow blur)
CSS:
div W=H=280
border: 5px solid white
box-sizing: border-box
background-origin: border-box ← gradient를 border-box 280에 매핑
background-clip: border-box
→ border 5가 외곽 fill을 덮어 그라데이션 가시 영역은 270
position: Figma 위치 그대로 (offset 없음)
```
### 그라데이션 좌표 remap
SVG `<linearGradient>` 좌표는 viewBox 공간 기준. CSS box로 매핑할 때:
```
viewBox padding이 P (예: 15 또는 10)이라면:
CSS_x = SVG_x - P
CSS_y = SVG_y - P
```
이렇게 보정한 좌표를 §2의 svg_to_css 공식에 W=H=fill_size로 넣는다.
---
## §6. Drop shadow: SVG `feGaussianBlur` ↔ CSS `box-shadow`
SVG:
```xml
<filter>
<feGaussianBlur stdDeviation="5"/>
<feColorMatrix .../>
</filter>
```
CSS 근사:
```css
box-shadow: 0 0 {2 × stdDeviation}px {color};
```
`stdDeviation=5` → CSS `box-shadow: 0 0 10px black`
**주의:** 정확한 픽셀 일치는 아님. 시각적으로 매우 유사하지만 SVG 가우시안과 CSS 블러 알고리즘이 다름. ±2px 차이는 허용.
---
## §7. Blend mode 호환
### Figma가 사용하는 blend mode → CSS 호환 매핑
| Figma | CSS 정확 | CSS 호환 (Chrome/Firefox) | 비고 |
|-------|---------|----------------------|------|
| Normal | normal | normal | 기본 |
| Multiply | multiply | multiply | OK |
| **Plus darker** | plus-darker | **multiply** | plus-darker는 Safari 전용 |
| Darken | darken | darken | OK |
| Screen | screen | screen | OK |
| Overlay | overlay | overlay | OK |
### Plus-darker vs Multiply 차이
```
plus-darker(src, dst) = max(0, src + dst - 1)
multiply(src, dst) = src × dst
```
- 흰 배경: 둘 다 동일 (효과 없음)
- 어두운 배경: multiply가 plus-darker보다 강하게 어두워짐
- 밝은 그라데이션 + 흰 배경 조합: 시각적 차이 거의 없음 (이 프로젝트 디자인 대부분 해당)
**Chrome/Firefox 호환 위해 multiply로 통일.** RULES.md R10 참조.
---
## §8. CSS `border-radius` 비율 변환
Figma `cornerRadius`는 px 단위. CSS도 px 단위 그대로 사용 + scale 적용.
특수 케이스:
- 완전 원: `border-radius: 50%`
- 캡슐: `border-radius: {height/2}px`
- 한쪽만 둥근 사각: `border-radius: {tl} {tr} {br} {bl}` (개별 4값)
스케일링 시: scale transform이 자동으로 px 값을 비율 유지하며 축소함. 별도 계산 불필요.
---
## §9. 글자 수 추정 (블록 안에 들어갈 텍스트 양)
블록 너비/높이에서 들어갈 수 있는 한글 글자 수를 미리 계산:
```
한 줄 글자 수 = 블록 너비(px) / (font_size × 한글_글자_너비_계수)
줄 수 = 블록 높이(px) / (font_size × line_height_ratio)
총 글자 수 = 한 줄 × 줄 수 × 안전계수(0.85)
```
### Pretendard / Noto Sans KR 한글 글자 너비 계수 = 0.97
| font-size | 한글 글자 너비 | line_height 1.6 줄 높이 |
|-----------|-------------|---------------------|
| 12px | 11.6px | 19.2px |
| 16px | 15.5px | 25.6px |
| 20px | 19.4px | 32.0px |
| 24px | 23.3px | 38.4px |
| 30px | 29.1px | 48.0px |
이는 design_agent 텍스트 편집 단계에서 사용. 변환 단계에서는 직접 사용하지 않음.
---
## 검증 체크리스트
변환 후 매번 확인:
- [ ] §1 스케일 — `transform: scale(S)` 한 번만 사용했는가, 매 값 수동 곱셈은 없는가
- [ ] §2 그라데이션 — gradient_math.py로 도출한 값을 그대로 사용했는가, 눈대중 각도/stop은 없는가
- [ ] §3 회전 — bbox 비율로 회전 감지했는가
- [ ] §4 descender — `line_height < content_area_ratio`인 텍스트에 padding-bottom 추가했는가
- [ ] §5 viewBox — stroke 정렬 확인 (외부/내부)에 따라 box-sizing 적용했는가
- [ ] §6 shadow — `box-shadow blur = 2 × stdDeviation`인가
- [ ] §7 blend — `plus-darker``multiply`로 교체했는가