# CSS 보정 규칙 Figma → HTML 변환 시 Figma와 CSS 렌더링 차이를 수학적으로 보정하는 규칙 모음. **모든 규칙은 수학적 근거가 있어야 한다. 감으로 보정하지 않는다.** --- ## R1. Descender 보정 (padding-bottom) **문제:** CSS `line-height: 1`이면 글리프 하강부(g, y, p, 쉼표)가 잘림. Figma는 line-height에 관계없이 글리프를 항상 표시하지만, CSS는 line box 밖을 자른다. **원인:** 폰트의 content area > line box일 때 half-leading이 음수가 되어 잘림 발생. **계산:** ``` content_area_ratio = (typoAscender + |typoDescender|) / UPM half_leading = (line_height - content_area_ratio) / 2 ← 음수이면 잘림 clipped_px = |half_leading| × font_size padding-bottom = ceil(clipped_px) ``` **폰트별 값:** | 폰트 | UPM | Ascender | Descender | content_area_ratio | |------|-----|----------|-----------|-------------------| | Noto Sans KR | 1000 | 1160 | 288 | 1.448 | | Pretendard | 1000 | 1100 | 300 | 1.400 | **예시 (Noto Sans KR, font-size 27.1px, line-height 1):** ``` half_leading = (1 - 1.448) / 2 = -0.224 clipped = 0.224 × 27.1 = 6.07px → padding-bottom: 7px ``` **적용:** `line-height < content_area_ratio`인 모든 텍스트 요소에 padding-bottom 추가. --- ## R2. 회전 감지 (bbox 비율) **문제:** Figma MCP는 `rotation`/`transform` 속성을 출력하지 않음. **감지 방법:** 바운딩 박스의 가로세로 비율이 해당 글자의 정상 비율과 반대이면 회전. ``` 단일 문자 "(" 정상: ~18×50 (세로가 김) Figma bbox: 60×19 (가로가 김) → 가로:세로 = 3.2:1 → 90° 회전 확정 ``` **규칙:** - 단일 문자 텍스트에서 `width > height × 1.5` → 90° 회전 - 일반 텍스트에서 `width < fontSize × 0.8` → 세로 배치용 좁은 박스 (writing-mode 아님,
줄바꿈) **CSS 구현:** ```css .rotated-bracket { transform: rotate(90deg); } /* 여는 괄호 */ .rotated-bracket-close { transform: rotate(-90deg); } /* 닫는 괄호 */ ``` --- ## R3. 세로 텍스트 (좁은 박스) **문제:** Figma에서 좁은 박스(width < fontSize) 안에 텍스트를 넣으면 글자가 한 줄에 하나씩 배치됨. **감지:** `bbox.width < fontSize × 0.8` + 2글자 이상 **CSS 구현:** `writing-mode` 사용하지 않음. HTML에서 `
`로 글자마다 줄바꿈. ```html
``` 이유: `writing-mode: vertical-rl`은 Figma 원본과 다른 간격/정렬을 만듦. --- ## R4. 그라데이션 텍스트 **Figma:** 텍스트 fills에 GRADIENT_LINEAR이 있으면 그라데이션 텍스트. **CSS:** ```css .gradient-text { background: linear-gradient(...); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } ``` **주의:** 흰 텍스트 스트로크(`-webkit-text-stroke: white`) 사용 금지. HTML에서 보기 불편하므로 제거한다. --- ## R5. 다중 fills 처리 **Figma:** 하나의 노드에 여러 fill이 쌓일 수 있음 (리스트 순서 = 위에서 아래). **규칙:** 첫 번째 fill이 불투명(opacity 1)이면 나머지는 가려짐 → 첫 번째만 사용. --- ## R6. 중복 노드 **감지:** 동일 좌표 + 동일 내용 + 동일 크기 → Figma 복사 흔적. **처리:** 1개만 렌더링, 나머지 무시. flat 목록에 [중복] 표기. --- ## R7. 미리보기 배경 **슬라이드 배경:** 항상 `#ffffff` (흰색) **블록 배경:** 항상 `#ffffff` (미리보기용). 원본 배경색은 주석으로 기록. 이유: 다크 배경에서 요소가 안 보이는 문제 방지. 위치/크기 확인이 우선. --- ## R8. 스케일 팩터 **계산:** `Scale = 1280 / 원본_width` **적용 대상:** - 위치 (x, y) - 크기 (width, height) - 폰트 크기 (fontSize) - 스트로크 너비 (strokeWeight) - 간격 (gap, padding) - 그림자 (blur, offset) **적용하지 않는 것:** - 색상 (그대로 유지) - 그라데이션 방향/퍼센트 (그대로 유지) - 폰트 굵기 (그대로 유지) - line-height 비율 (그대로 유지) - border-radius 비율 (스케일 적용) **구현 권장:** 매 값 수동 곱셈 대신 `transform: scale(S)` 한 번으로 균일 축소. MATH.md §1 참조. --- ## R9. 순수 CSS 우선, SVG는 곡선/필터에만 블록 라이브러리의 동적 재구성을 위해 가능한 한 **HTML div + CSS**로 구현한다. | 요소 | 구현 | |------|------| | 원/사각형 + linear-gradient | `
` + `border-radius` + `background: linear-gradient(...)` | | Stroke (경계선) | `border` + `box-sizing: border-box` | | Drop shadow blur | `box-shadow: 0 0 {2×stdDev}px {color}` | | **곡선 (아크, 비원형 path)** | **SVG `` 또는 PNG** ← CSS 불가능 | | **복잡한 SVG filter chain** | **SVG ``** ← CSS 근사 불가 시 | | 텍스트 | HTML `
` 절대 배치 | **이유:** 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개 | 첫 적용. 상/하 배치 반전 패턴 발견. |