` 절대 배치 |
**이유:** SVG ``는 정적 파일. 색상/개수/위치 변경 시 매번 재export 필요. CSS는 변수/Jinja로 즉시 파라미터화 가능.
---
## R10. Blend mode 호환 (plus-darker → multiply)
**문제:** Figma의 `plus darker` blend mode는 Apple CoreGraphics 전용. CSS 스펙엔 `plus-darker`가 있지만 **Safari/WebKit만 지원**, Chrome/Firefox에서는 무시되어 효과 사라짐.
**규칙:**
1. SVG/CSS에 `mix-blend-mode: plus-darker` 발견 시 → **`multiply`로 교체**
2. SVG 파일 내부의 `style="mix-blend-mode:plus-darker"`도 함께 교체
3. 시각 차이 검증: 흰 배경 위 밝은 그라데이션은 거의 동일. 어두운 영역은 multiply가 더 강함
```
plus-darker(src, dst) = max(0, src + dst - 1) [Safari only]
multiply(src, dst) = src × dst [모든 브라우저]
```
자세한 비교: MATH.md §7
---
## R11. Stroke 정렬: viewBox padding 처리
SVG는 stroke가 fill의 안/밖으로 확장될 수 있어 viewBox에 padding이 들어감. CSS 변환 시 두 케이스로 나뉨:
### 케이스 A — Stroke가 fill **외부**
예: `r=140 fill` + `r=142.5 stroke-width=5` → stroke가 r=140~145 (외부)
```css
.ring {
width: 290px; height: 290px; /* fill 280 + 외부 stroke 5×2 */
border: 5px solid white;
box-sizing: border-box; /* border 안쪽 padding-box = 280 = fill */
background: linear-gradient(...); /* default origin: padding-box 280 */
border-radius: 50%;
}
/* 위치: Figma fill 위치에서 (-5, -5) 오프셋 */
```
### 케이스 B — Stroke가 fill **내부** (overlap)
예: `r=140 fill` + `r=137.5 stroke-width=5` → stroke가 r=135~140 (fill 외곽 overlap)
```css
.ring {
width: 280px; height: 280px; /* fill 280 그대로 */
border: 5px solid white;
box-sizing: border-box; /* padding-box 270 */
background: linear-gradient(...);
background-origin: border-box; /* gradient는 280 영역에 매핑 */
background-clip: border-box;
border-radius: 50%;
}
/* 위치: Figma fill 위치 그대로 */
```
판별: SVG 안의 stroke `r` 값이 fill `r`보다 **크면** 외부 (케이스 A), **작거나 같으면** 내부 (케이스 B).
---
## R12. viewBox padding gradient remap
viewBox padding이 있는 SVG의 그라데이션 좌표는 viewBox 공간 기준이므로, CSS 박스로 매핑할 때 **각 좌표에서 padding 만큼 빼야** 한다.
```python
# SVG viewBox 310, 실제 fill 280, padding 15
css_x1 = svg_x1 - 15
css_y1 = svg_y1 - 15
css_x2 = svg_x2 - 15
css_y2 = svg_y2 - 15
# 그 다음 svg_to_css(W=280, H=280, ...)
```
또는 `scripts/gradient_math.py`의 `svg_to_css_remap()` 사용:
```python
svg_to_css_remap(css_W=280, css_H=280, viewbox_padding=15,
x1=..., y1=..., x2=..., y2=..., stops=[...])
```
---
## R14. 한글 줄바꿈은 word-break: keep-all (전역 default)
**문제:** Chrome 기본 동작은 한글을 글자 단위로 wrap (예: "수행공정의 쉬운이해로 관리 편의성 증" / "진"). Figma는 단어 단위 wrap이라 시각이 다름.
**규칙:** 모든 변환물의 base CSS에 `word-break: keep-all` 적용.
```css
body {
font-family: 'Noto Sans KR', sans-serif;
...
word-break: keep-all; /* 한글 단어 단위 wrap (Figma matching) */
}
```
또는 텍스트 컨테이너 단위로:
```css
.bullet-text, .left-text, .right-text, .body-text {
word-break: keep-all;
}
```
**언제 빼나:**
- `white-space: nowrap` 단일 라인 텍스트 (영향 없음, 안 빼도 무방)
- 코드/숫자 등 단어 경계가 없는 콘텐츠
**예외:** 영문/기호 혼합 텍스트는 `word-break: keep-all` 만으로는 부족할 수 있음. 그 경우 `overflow-wrap: anywhere` 또는 ` ` 명시 split.
---
## R15. 박스 vertical center align (Figma flex justify-center 모방)
**문제:** Figma React 코드에서 자주 보이는 패턴:
```jsx
```
이는 **컨테이너 박스의 vertical center에 텍스트를 정렬**한다는 의미. 단순히 `top` 값만 받아서 박는 건 잘못 — 텍스트가 박스 top에 붙어 다른 요소(예: cat pill의 vertical center)와 어긋남.
**올바른 변환:**
```css
.text-box {
position: absolute;
top: ; /* Figma top - height/2 */
height: ;
width: ;
display: flex;
flex-direction: column;
justify-content: center; /* vertical center */
}
```
**또는 인접 박스(예: 옆에 있는 cat pill)와 동일한 top + height를 박고 flex justify-center 적용**하면 자동으로 가운데 align. 1:1 변환에서 가장 안전.
**검증:** 인접 박스 center y 와 텍스트 박스 center y 가 같은지 측정. 차이 > 5px이면 잘못된 것.
---
## R13. Custom-Marker Bullet List 패턴 (sub-pattern)
**감지 조건 (3가지 모두 충족):**
1. 여러 텍스트 항목이 세로로 나열됨
2. 각 항목 앞에 **장식 마커**가 있음 (체크박스 아이콘, 점, 화살표, 숫자, 원, PNG 등)
3. 마커는 인터랙티브하지 않고 순수 시각 요소 (실제 `` 가 아님)
**Figma 원본에서는** 마커와 텍스트가 별도 요소로 평면 배치돼있을 수 있다. 그래도 **시맨틱적으로는 하나의 list item**으로 봐야 한다.
### 구조 (CSS Flex Pair Pattern)
```html
텍스트 항목
긴 텍스트가 두 줄로
```
### CSS
```css
.bullet-list {
display: flex;
flex-direction: column;
/* 동일 top/bottom 정렬을 위해 컨테이너에 fixed height + space-between */
justify-content: space-between;
}
.bullet-row {
display: flex;
align-items: flex-start;
--lh: 85px; /* 기본 라인 높이 */
}
.bullet-row.compact {
--lh: 50px; /* 2-line 항목용 타이트 lh */
}
.bullet-icon {
flex: none;
width: var(--icon-w);
height: var(--icon-h);
/* 핵심: 아이콘 vertical center를 첫 줄 vertical center에 align */
margin-top: calc(var(--lh) / 2 - var(--icon-h) / 2);
/* 컬럼별 figma gap (text_left − icon_left − icon_w) */
margin-right: var(--icon-gap);
}
.bullet-text {
flex: 1;
line-height: var(--lh);
white-space: normal;
word-break: keep-all; /* 한글: 단어 단위 줄바꿈 */
}
```
### 핵심 수학
```
icon margin-top = lh / 2 − icon_h / 2 (첫 줄 vertical center)
icon margin-right = text_left − icon_left − icon_w (Figma 데이터)
```
### 절대 하지 말 것
- 마커와 텍스트를 별도 요소로 절대 배치 (`
` × N)
- row에 fixed `height` 설정 (wrap 시 overlap)
- `white-space: nowrap` (텍스트가 컨테이너 밖으로 overflow)
- 모든 row에 동일한 top/bottom margin 강제 (텍스트 길이가 결정해야 함)
### 정렬 원칙
3개 이상의 평행한 컬럼이 있을 때:
- **모든 컬럼은 동일한 top + 동일한 height** 로 시작
- 컬럼별 자연 콘텐츠 합 중 **가장 큰 값**을 height로 사용
- `justify-content: space-between` 으로 내부 균등 분포
- 결과: 컬럼별 spacing은 다르지만 vertical extent는 동일
### 적용 사례
| 프레임 | 사용 | 비고 |
|--------|------|------|
| 1171281191 (cards-3col-persona) | 3 컬럼 × 6~7 마커-text 페어 | 첫 적용 |
| (앞으로 비슷한 패턴 발견 시 추가) | | |
### 1:1 변환 단계의 임시 보정 (템플릿화 시 제거)
다음은 1:1 시각 fidelity를 위한 **임시 보정**이며, 템플릿화 시 모두 제거해야 한다 (자연 wrap이 처리):
- `letter-spacing: -1.5px` 등 — Chrome Noto Sans KR 너비가 Figma보다 약간 넓어 wrap이 일어나는 것을 방지하기 위한 보정
- ` ` 명시적 줄바꿈 — Figma의 의도된 split 위치 보존용. 템플릿화 시 자연 wrap이 알아서 처리
- `class="compact"` 수동 지정 — 어떤 항목이 2-line인지 1:1 단계에선 수동, 템플릿화 시 텍스트 길이 자동 판정
이 보정들은 HTML 코멘트로 `` 표시한다.
---
## R16. 이미지 프레임 배치 — overflow:hidden으로 부분 표시
**상황:** 하나의 원본 이미지에 양쪽 끝 모두 디자인 요소(곡선, 말림, 장식 등)가 있고, Figma에서 프레임(컨테이너)보다 이미지를 크게 배치하여 **한쪽만 보이게** 하는 경우.
**Figma가 하는 것:**
- 프레임: 457.96px (표시 영역)
- 이미지: 664px (원본, 프레임보다 큼)
- 이미지를 프레임 안에서 `left`, `width`로 위치/크기 지정
- 프레임에 `overflow: hidden` → 프레임 밖으로 나간 부분 안 보임
- 결과: 이미지의 **원하는 쪽만** 프레임 안에 보임
**Figma가 주는 값의 의미:**
```
left: -45.3%; width: 145.3%
→ 이미지를 좌측으로 45.3% 밀어서 배치
→ 좌측 끝이 프레임 밖으로 나감 → 좌측 디자인 요소 안 보임
→ 우측 디자인 요소만 프레임 안에 보임
left: 0; width: 151.25%
→ 이미지를 좌측 정렬, 우측이 프레임 밖으로 넘침
→ 우측 디자인 요소 안 보임
→ 좌측 디자인 요소만 프레임 안에 보임
```
**이것은 crop이 아니다.** 이미지를 자르는 것이 아니라, 프레임 안에서 이미지의 **위치**를 조절하는 것. 이미지 원본은 그대로 유지.
**CSS 구현:**
```css
.pill-frame {
position: relative; /* 또는 absolute */
width: 457.96px; /* 프레임 크기 */
height: 95.62px;
overflow: hidden; /* 핵심: 프레임 밖 숨김 */
}
.pill-frame img {
position: absolute;
top: 0;
left: -45.3%; /* Figma 값 그대로 */
width: 145.3%; /* Figma 값 그대로 */
height: 100%;
}
```
**절대 하지 말 것:**
- `width: 100%; object-fit: fill` — 이미지가 찌그러져 양쪽 디자인 요소가 다 보임
- `scaleX(-1)` 임의 추가 — Figma에 없는 변환
- `object-fit: cover/contain` — 이미지 비율/위치가 달라짐
- "crop"이라 부르기 — 이미지를 자르는 게 아니라 위치를 조절하는 것
**rotate(180deg) + 이미지 배치 주의:**
부모에 `rotate(180deg)`가 적용된 경우 (예: 하단 pill), 이미지가 상하좌우 모두 뒤집힘. 이때 **이미지 배치(left/width)를 상단과 반대로** 적용해야 최종 결과가 올바른 방향이 됨.
```
상단 left-pill: left: -45.3%; width: 145.3% → 우측 보임
하단 left-pill: rotate(180) + left: 0; width: 151.25% → 결과적으로 우측 보임 (뒤집혀서)
상단 right-pill: left: 0; width: 151.25% → 좌측 보임
하단 right-pill: rotate(180) + left: -45.3%; width: 145.3% → 결과적으로 좌측 보임 (뒤집혀서)
```
**검증 방법:** 각 pill을 개별 screenshot으로 뽑아서 Figma 원본 pill screenshot과 **곡선/직선 위치**를 1:1 대조. 양쪽 다 곡선이 보이면 이미지 배치가 잘못된 것.
**적용 사례:**
| 프레임 | 사용 | 비고 |
|--------|------|------|
| 1171281194 (issues-paired-rows) | 두루마리 pill 8개 | 첫 적용. 상/하 배치 반전 패턴 발견. |
---
## R17. 콘텐츠 주도형 레이아웃 (absolute 금지, flex/flow 필수)
**문제:** 블록 내 모든 요소를 absolute + 고정 top/left로 배치하면, 텍스트 분량이 바뀔 때 겹치거나 빈 공간이 생긴다. overflow:hidden으로 숨기는 건 해결이 아니라 더 큰 문제.
**원칙:**
- CLAUDE.md: "텍스트를 자르지 않는다. 디자인이 텍스트에 맞춘다."
- CLAUDE.md: "빈 공간 방치 안 함"
- blocks_index.md I-1: "모든 픽셀 위치 절대 배치 금지"
**구조 (2단계):**
```
.inner (zoom: S — 뷰포트 맞춤)
.title (flow)
.rows (display: flex; flex-direction: column)
section.row (flex column — pill → body → pill)
.pill-area (flex: none)
.body-area (flex: 1, display: flex)
.body-left (flex: 1, 자연 flow)
.divider (flex: none)
.body-right (flex: 1, 자연 flow)
.pill-area (flex: none)
section.row ...
```
**1단계:** row 내부만 flex/flow (row 위치는 absolute top 유지)
**2단계:** row 간 배치도 flex column (행이 커지면 아래 행이 밀림)
**절대 하지 말 것:**
- 본문 텍스트에 position: absolute + 고정 top
- 텍스트 겹침을 overflow: hidden으로 숨기기
- 텍스트 높이를 고정값으로 제한
**적용 사례:**
| 프레임 | 전환 | 결과 |
|--------|------|------|
| 1171281194 | absolute → flex 2단계 | 텍스트-pill 겹침 해결, 행 간 자연 flow |
---
## R18. Crop variant / Label position 분리 (pill 2축 구조)
**문제:** pill 이미지의 crop 방향(어떤 곡면을 보여줄지)과 라벨 위치(행에서 좌/우 어디에 있는지)를 같은 클래스로 묶으면, 하단 pill(flip 적용)에서 라벨 위치가 뒤집힌다.
**원인:** crop-left/right는 이미지 방향 축, pos-left/right는 행 내 위치 축. 이 둘은 독립.
**규칙:** 두 축을 분리한다.
```css
/* 축1: 이미지 crop (어떤 곡면 보일지) */
.crop-left { } /* 우측 곡선 보임 */
.crop-right { } /* 좌측 곡선 보임 */
/* 축2: 라벨 위치 (행에서 좌/우) */
.pos-left .label { left: ...; }
.pos-right .label { right: ...; text-align: right; }
```
**4가지 조합:**
| 상황 | crop | pos | 예 |
|------|------|-----|-----|
| 상단 좌 pill | crop-left | pos-left | 개념 부재 |
| 상단 우 pill | crop-right | pos-right | 잘못된 접근방식 |
| 하단 좌 pill (flip) | crop-right | pos-left | 방향성 상실 |
| 하단 우 pill (flip) | crop-left | pos-right | 전제조건 오류 |
**라벨 offset:**
- 현재: designer-tuned fallback (Figma 원본 값)
- 장기 목표: seam-based computed anchor (pill_seam_meta.md 참조)
- 라벨 offset은 crop visible area 기준으로 별도 조정 가능
**적용 사례:** 1171281194 (issues-paired-rows) — 하단 pill 라벨 위치 오류 수정
---
## R19. zoom vs transform:scale
**문제:** `transform: scale(S)`는 시각적 크기만 줄이고 레이아웃 상의 높이는 원본 그대로. flex/flow 기반 콘텐츠와 함께 쓰면 하단에 큰 여백이 생긴다.
**규칙:** 콘텐츠 주도형 레이아웃(R17)에서는 `zoom`을 사용한다.
```css
/* ✗ 문제: layout height가 원본 크기 */
.inner {
transform: scale(0.69);
transform-origin: top left;
}
/* ✓ 해결: layout height도 실제로 줄어듦 */
.inner {
zoom: 0.69;
}
```
**차이:**
| 속성 | 시각 크기 | 레이아웃 크기 | flow 호환 |
|------|----------|-------------|----------|
| transform: scale | 줄어듦 | 원본 그대로 | ✗ |
| zoom | 줄어듦 | 줄어듦 | ✓ |
**주의:** 기존 absolute 기반 블록(1:1 reference)에서는 transform:scale이 문제없음. R17 구조로 전환한 블록에서만 zoom 적용.
**브라우저 지원:** Chrome, Safari, Firefox 126+ (2024~) 모두 지원.