Figma 대조 — 기기 · 원본 연결 · 엄격 차이 보고
목표는 화면마다 엄격 픽셀 차이 3% 미만이다(Figma 「Cocode Scaffold App v1.0」 페이지 49:6). 이 문서는 그
차이를 어떤 기기로 찍고, 무엇을 가리고, 어떻게 세고, 어디서 판정하는지를 정한다. 판정 규칙의 정본은
figma-fidelity.json 하나다 — 기준 · 세는 법을 바꾸는 것은 그 파일을 바꾸는 PR 이다.
| 구성 | 자리 | 하는 일 |
|---|---|---|
| 화면 대장 | docs/product/figma-screens.json |
Figma 프레임 56개 — 노드 · 크기 · 기기 · 원본 PNG · insets(띠) |
| 원본 PNG | docs/design/figma/reference/<키>.png |
Figma 에서 1배율로 내보낸 정본. 구현 캡처로 덮어쓰지 않는다 |
| Figma 기기 | FigmaDevices (package:test_driver/golden.dart) |
폰 · 태블릿 · 데스크톱 — DPR 1 |
| 대장 → 골든 | FigmaLedger · FigmaScreen |
키 하나 → 기기 · co_golden 원본(GoldenDesignReference) |
| 화면 등록 | FigmaGoldenScreens |
capture 전용 시나리오 + 대장 연결 기록 |
| 수락 보고 | tool/design/check_visual_acceptance.dart |
화면마다 엄격 차이를 다시 세어 판정 → build/visual-acceptance.json |
| 판정 정책 | docs/product/figma-fidelity.json |
기준 3%(미만) · 세는 법 · Δ 의 N · 글자 밖 판정(선택 textMask) |
| CI | .github/workflows/golden.yml 의 Figma 대조 (수락 보고) |
요약 · 아티팩트 — 지금은 보고만 |
기기
Figma 프레임 그대로 찍는다. 원본 PNG 가 1배율이므로 기기도 DPR 1 이다 — 캡처와 원본이 같은 픽셀 격자에 놓여야
co_golden 이 견준다(크기가 다르면 sizeMismatch 로 견주지 않는다).
| 기기 | 크기 | 플랫폼 | 기본 안전 영역 |
|---|---|---|---|
FigmaDevices.phone (figma-phone) |
390×844 | iOS | 위 44 · 아래 34 |
FigmaDevices.tablet (figma-tablet) |
1024×768 | iOS(가로) | 위 44 · 아래 34 |
FigmaDevices.desktop (figma-desktop) |
1440×900 | macOS(웹 화면) | 없음 |
- 화면마다 대장이 기기를 정한다 —
FigmaScreen.device는 위 기기를 바탕으로 크기를 프레임 크기로, 안전 영역(view padding)을 대장insets로 덮어쓴다. 그래서SafeArea아래 앱 내용이 Figma 와 같은 y 에서 시작하고 끝난다 (예: 폰 HP-01@390 은 위 44 · 아래 34, 태블릿 1024 프레임 8장은 위 44 · 아래 0, 데스크톱은 둘 다 0). - 긴 프레임은 그 크기 그대로 찍는다(HP-14@390 390×1409 등 — 이 앱의 폰 24장 가운데 9장이 844 가 아니다). 앱도 그 높이의 화면에 그려진다.
- 변형은 하나 — 그 기기 · 대장 색 모드(
modes.colorLight →light) ·ko· 글자 배율 1. - CoUI 배율은 1 이다. coui_flutter 0.157 부터
AdaptiveScaler.defaultScaling이 iOS · Android 를 포함한 모든 플랫폼에서 1 이고(AdaptiveScaling.mobile= 1), 앱 셸(app/app·app/console)과 골든 셸(kernelGoldenMatrix)의CoUIApp은scaling을 넘기지 않는다. co_golden 도 배율을 걸지 않는다(기기의 크기 · DPR · 안전 영역 · 플랫폼 · 글자 배율만 시험 화면에 건다). 그래서 iOS 로 찍어도 1배로 그려져 1배율 Figma 와 맞는다. coui 가 기본값을 바꾸거나 셸이 배율을 넘기면package/test_driver/test/figma/figma_scaling_test.dart가 실패한다 — 그때 Figma 기기에도 같은 배율을 줄지(그러면 원본과 어긋난다) 정한다. 큰 배율(1.25 × 글자 배율)의 레이아웃은testScaleInvariant가 따로 본다. - 글자 단계 — CoUI 는 화면 폭으로 글자 단계를 고른다(태블릿 ≥768 · 데스크톱 ≥1024). 390 은 mobile, 1024 · 1440 은 desktop 이다 — Figma 「Semantic/ Typography」 의 Mobile · Desktop 모드와 같다(같은 시험이 지킨다).
가림
가리는 것은 띠 둘뿐이다 — 위 상태 바(insets.top)와 아래 홈 인디케이터(insets.bottom). 둘 다 기기(OS)가
그리고 앱은 그리지 않는다. 띠마다 사유가 기록된다(상태 바 띠(위 44px) — 기기(OS)가 그리고 앱은 그리지 않는다).
- 띠 높이는 화면 대장이 정한다. 대장에 없는 값으로 가리지 않는다 — co_golden 의 기기 안전 영역 자동 가림
(
maskSafeArea)도 끄고 대장 띠만 넘긴다(FigmaGoldenScreens.designAnalysis). - 그 밖을 가리는 것은 금지다. 시계 · 지도 타일처럼 정말 그릴 수 없는 것이 생기면, 먼저 아래 「가림 예외」 표에
화면 · 영역 · 사유 · 이슈를 적는 PR 로 오너 승인을 받는다. 수락 보고 도구는 아직 예외를 받지 않는다 — 대장 띠
밖의 차이는 모두 센다. 첫 예외가 승인되면 그때
figma-fidelity.json에 자리를 만든다. 글자 잉크 상자를 빼는 「글자 밖」 수는 가림 예외가 아니라 세는 법이다(아래 「세는 법」) — 그 글자는 구조 대조가 대신 본다. - 하단 바 구성품(
BottomActionBar · 스캐폴드안·InputBar)은 안에HomeIndicator(바닥 34)를 품고 바탕을 그 밑까지 깐다. 그런 프레임도 바닥 34 가 홈 인디케이터 띠다 — 대장insets.bottom이 34 여야 앱이 그만큼 안전 영역을 두고 바를 그린다(대장이 0 이면 앱 바가 34 짧게 그려진다).
가림 예외
| 화면 | 영역(논리 px) | 사유 | 이슈 |
|---|---|---|---|
| (없음) |
세는 법
분모는 띠 밖의 픽셀(comparable = 전체 − 띠)이다. 두 이미지를 8비트 RGBA 로 풀어 픽셀마다 견준다.
| 이름 | 「다르다」로 세는 픽셀 | 보고 키 |
|---|---|---|
| exact | RGBA 네 채널 중 하나라도 다르다 — 엄격(정확) 차이 | exact |
| Δ≤N 무시 | 네 채널 차이의 최댓값이 N 을 넘는다(N 이하는 세지 않는다) — 기본 N = 16 | delta |
- 판정은 정책의
count(exact·delta)로 한 수만 쓴다. 둘 다 늘 보고한다. 지금 정책은delta(N = 8) · 글자 밖 · 3% 미만(maximumIsExclusive: true— 3.000% 는 넘음)이다 — 오너 확정 2026-11-02(아래 「글자 밖」 · 「보정」). frameExact는 띠를 빼지 않은 정확 차이다 — co_golden 의designComparison.differentPixels와 같아야 하고, 다르면 디코딩 검산 실패로 「판정 못 함」이다.- co_golden 디자인 분석(허용 오차 — CIEDE2000 · 위치 반경 · 안티에일리어싱 제외 · 글자 상자 완화)의 판정 · 비율 ·
안티에일리어싱 픽셀 수(
analysis.antialiasedPixels)는 참고로 싣는다. 판정에는 쓰지 않는다. - 왜 둘인가 — Figma 와 Flutter 는 글자 · 모서리 가장자리를 다르게 래스터한다. 같은 화면을 Figma 에서 두 번 내보내 견줘도 exact 14.1% · Δ≤16 4.7% 였다(오케스트레이터 측정, 2026-11-01). 엄격 3% 를 「모든 RGBA 변화」로 셀지 「작은 채널 차이는 빼고」 셀지는 HP-01 을 재서 정한다(아래 「보정」).
글자 밖 — 글자 잉크 상자를 빼고 세기
같은 글자를 같은 자리에 그려도 두 렌더러가 가장자리를 다르게 칠해, 글자 상자(+2 px) 픽셀의 26–32% 가 Δ>8 로 남는다
(아래 「글자 바닥값」). 그래서 글자는 구조로 엄격하게, 픽셀은 글자 밖에서 센다. 오너가 2026-11-02 에 판정에 쓰기로 했다 —
figma-fidelity.json 의 "textMask": {"padding": 2}.
- 빼는 것: 대장 연결 기록의
spec(docs/design/figma/spec/<키>.design.json)에 있는 TEXT 노드마다 잉크 상자(ink— FigmaabsoluteRenderBounds, 없으면rect)를 사방 N px 넓힌 사각형. 프레임 밖은 자르고, 숨긴 노드는 co_golden 처럼 읽지 않는다. 분모도 그만큼 준다(띠 밖 − 글자 가림). 대신 보는 것: 상자 안의 글자는 co_golden 구조 대조 findings (내용 · 서체 · 굵기 · 크기 · 색 토큰 · 위치)와expectRenderedText가 본다 — 글자 밖 「수락」은 글자 findings 0 과 함께 읽는다. - 보고: 사양이 있는 화면은 판정과 무관하게 늘 글자 밖 exact · Δ>N 을 함께 싣는다(JSON
outsideText·textMask, 표 열 「Δ≤N 글자 밖」, 콘솔Δ≤N글자밖). 사양이 없으면 「사양 없음」, 읽지 못하면 「사양 읽지 못함」이다. - 판정 스위치: 정책의 선택 항목
"textMask": {"padding": N}(N 0–16) 또는 명령줄--text-mask(N 은 정책 값, 없으면- ·
--text-mask-padding <N>이 켜면 판정이 글자 밖 비율(정책의count)을 쓴다. 명령줄로 켠 것은--count·--delta처럼overrides에 남는다(textMask.padding=N). 켜져 있는데 사양이 없는 화면은 띠 밖 전체로 판정하고 사유에 「사양 없음 — 글자 가림 없이 셌다」를 남기며, 사양을 읽지 못하면 「판정 못 함」이다. 정책에 항목이 없으면 판정은 그대로다(글자 밖 수만 참고로 더 보인다 — N 2).
- ·
- 한계: 상자 안의 차이는 원인과 무관하게 빠진다 — 글자에 겹친 다른 부품의 차이도 함께다. 예: 하단 탭처럼 글자 위를 덮는 CoUI 부품이 CoUI 와 Figma 사이에 높이가 다르면 그 글자 상자 안의 차이가 함께 빠진다(scaffold-booking SH-01@1024 의 실측은 그쪽 문서). 그런 차이는 상자 밖에 남은 몫과 구조 대조의 위치 · 크기 findings 로 본다.
- 어림: scaffold-booking 은 구현 전에 Figma 79장의 글자 잉크 면적과 SH-01 의 「글자 바닥값 ÷ 잉크 면적」으로 어느 화면이 완벽히 구현해도 3% 를 넘을지 어림했다(그쪽 문서 「글자 밖」). 이 앱의 어림은 HP-01 을 처음 잰 뒤 같은 방법으로 다시 낸다.
판정
| 상태 | 뜻 |
|---|---|
accepted ✅ |
정책의 세는 법으로 기준 미만 |
above-threshold ⛔ |
기준 이상 |
unverified ⚠️ |
판정 못 함 — 캡처 실패 · 넘침 · 크기 불일치 · 원본 · 대장 없음 · 대장 연결 기록 없는 디자인 대조 · 검산 실패 · 글자 밖으로 판정하는데 사양을 읽지 못함. 통과가 아니다 |
stale 🕰 |
낡음 — 캡처 뒤 원본 PNG 나 대장 항목(노드 · 크기 · 띠 …)이 바뀌었거나 --since 보다 이른 캡처. 다시 찍는다 |
비교는 대장 연결 기록(build/co_golden/figma/**)마다 한 줄이다. 기록은 그리기 전에 남으므로 그리다 실패한 화면도
보고에서 사라지지 않는다. coverage 는 대장 화면 중 몇 개를 견줬는지와 빠진 키를 보인다(대장이 없으면 세지 않는다).
수락은 비교마다의 지표다 — 커버리지 · 실제 앱 동작 · 디자인 승인은 따로 본다.
화면 등록
화면 패키지의 test/golden/ 에서 대장 키로 등록한다. 원본과 같은 상태(같은 데이터 · 선택 · 스크롤)를 만들어 찍는다.
import 'package:test_driver/golden.dart';
final FigmaGoldenScreens figma = FigmaGoldenScreens(suite: 'clinic');
void main() {
figma.screen('HP-01@390', build: (_) => const HomePage());
figma.screen('HP-03+HP-04@1440', build: (_) => const ServiceDetailPage(), interact: …);
}
- capture 전용이다. compare(기준 이미지 비교) · 보통
flutter test에서는 건너뛴다 — Figma 변형은 회귀 기준 이미지가 없고 필요 없다(회귀는 커널 매트릭스가 맡는다). 키는 모든 모드에서 대장에서 찾으므로 틀린 키는 바로 실패한다. - 사양 JSON 이
docs/design/figma/spec/<키>.design.json에 있으면 함께 넘겨, co_golden 이 서체 · 색 · 위치 · 컴포넌트 차이를 소스 줄과 함께 낸다(co_golden README 「Design analysis」).
로컬 실행
# 1) 캡처 — 모든 골든 패키지(Figma 대조 포함) → build/co_golden
tool/golden/capture.sh
# Figma 대조만, 한 패키지에서(출력을 저장소 build/co_golden 으로):
(cd <패키지> && TZ=Asia/Seoul CO_GOLDEN_MODE=capture \
CO_GOLDEN_OUTPUT="$(git rev-parse --show-toplevel)/build/co_golden" \
flutter test --no-pub --tags figma test/golden)
# 2) 수락 보고 — 보고만(종료 코드 0) · Markdown 요약까지
tool/design/check_visual_acceptance.sh --report-only --summary build/visual-acceptance.md
# 판정으로(수락이 아닌 비교가 있으면 1): --report-only 를 뺀다
# 보정 탐색 — 정책은 그대로 두고 보고에 덮어쓰기로 남긴다
tool/design/check_visual_acceptance.sh --report-only --count delta --delta 8
# 글자 밖으로 판정(정책에 textMask 가 있으면 이미 켜져 있다 — 「세는 법」의 「글자 밖」)
tool/design/check_visual_acceptance.sh --report-only --count delta --delta 8 --text-mask
# 3) 갤러리
tool/golden/gallery.sh # → build/golden-gallery/index.html
tool/design 은 별도 pub 루트다(PNG 를 푸는 image 의존 — 루트 워크스페이스 의존을 늘리지 않는다). 래퍼
check_visual_acceptance.sh 가 lock 그대로 받고 저장소 루트에서 돌린다. 도구 시험: (cd tool/design && dart test).
출력
build/co_golden/ # capture.sh (co_golden)
├── images/<suite>/<scenario>/<변형>.png 캡처
├── references/<suite>/<scenario>/<변형>.png 원본 사본(바이트 그대로)
├── differences/<suite>/<scenario>/<변형>.png co_golden 정확 차이(띠 안 포함, 자홍)
├── analysis/<suite>/<scenario>/<변형>.{json,png} co_golden 허용 오차 분석 · 지도
├── figma/<suite>/<scenario>/<변형>.json 대장 연결 기록(키 · 대장 · 띠) — FigmaGoldenScreens
└── runs/<suite>/<scenario>.json 실행 기록
build/visual-acceptance.json # 수락 보고(정책 · 수 · 커버리지 · 비교마다 exact · delta · 글자 밖 · 분석)
build/visual-acceptance.md # --summary
build/visual-acceptance/<suite>/<scenario>/<변형>.png # 보정 지도
build/golden-gallery/index.html # gallery.sh
보정 지도는 수락 보고가 쓴다 — 회색 같음 · 파랑 가린 띠 · 노랑 exact 로만 세는 픽셀(Δ≤N) · 자홍 두 규칙 모두 세는 픽셀(Δ>N) · 초록 글자 가림(사양이 있는 화면 — 진한 초록은 그 안의 Δ>N). 노랑이 글자 · 모서리 가장자리뿐이면 Δ 규칙이 래스터 차이만 빼는 것이다.
갤러리 읽는 법
gallery.sh 의 갤러리에서 Figma 대조가 있는 변형은 원본과 캡처를 함께 보인다.
- 디자인 차이만 으로 판정이
fail·undetermined인 변형만 남긴다. 머리의 판정 줄은 co_golden 허용 오차 판정(일치 · 불일치 · 판정 불가)이다 — 이 문서의 엄격 판정이 아니다. 엄격 판정은 수락 보고를 본다. - 보기: 50% 겹쳐 보기(불투명도 막대로 원본 ↔ 구현) · 좌우 비교 · 허용 오차 차이(빨강 차이 · 노랑 안티에일리어싱 · 파랑 가림) · RGBA 차이 이미지(co_golden 정확 차이 — 띠 안도 칠한다).
- 찾은 것(findings)을 고르면 Figma 위치(분홍)와 구현 위치(파랑 점선)가 표시된다. 고칠 곳이 앱 코드인지 디자인 시스템인지 알려 준다. 픽셀 영역·가림 표시 로 차이 영역과 띠를 함께 본다. 분석 리포트 JSON 이 전문이다.
- 화면을 고치면 다시 캡처한다. 원본은 Figma 가 바뀔 때만 다시 내보낸다(캡처로 덮어쓰지 않는다).
CI
golden.yml 의 골든 (compare) 가 PR(준비 완료) · dispatch 에서 확인용 capture 를 돌린 뒤 출력을 아티팩트
co-golden-capture 로 올리고, Figma 대조 (수락 보고) 잡이 그것을 받아 도구 시험 → 수락 보고를 돌려 잡 요약과
아티팩트 visual-acceptance(JSON · Markdown · 보정 지도)로 남긴다.
- 지금은 보고만 — 워크플로 env
FIGMA_ACCEPTANCE_BLOCKING: "false"."true"로 바꾸면 수락이 아닌 비교(기준 넘음 · 판정 못 함 · 낡음)가 하나라도 있을 때 그 잡이 실패한다. 기준 · 세는 법은 이 env 가 아니라figma-fidelity.json이 정한다. - 기존
골든 (compare)판정은 그대로다 — Figma 시나리오는 compare 에서 건너뛰므로 기준 이미지가 필요 없고, 아티팩트 올리기가 실패해도 compare 는 실패하지 않는다. main push 에는 capture 가 없어 이 잡도 없다. - 문서만 바뀐 PR(
docs/**)에는 골든 워크플로가 돌지 않는다 — 정책 · 원본만 바꿨으면 dispatch 로 다시 돌린다.
보정 — HP-01 첫 측정 (끝났다 — 오너 확정 2026-11-02)
엄격 3% 를 무엇으로 셀지는 HP-01 을 구현해 처음 잰 뒤 정했다(figma-fidelity.json 의 calibration). 세는 법은 오너가
확정했다(2026-11-02) — 「글자 밖 Δ≤8 3% 미만」(count: delta · maxChannelDelta: 8 · textMask.padding: 2, co_golden
구조 오류 0건 지향). exact · Δ≤8 전체는 참고로 계속 보고한다.
- HP-01@390 을 구현하고
FigmaGoldenScreens로 등록한다. 원본과 같은 데이터 · 상태로 찍는다(브랜드 테마 반영 뒤). - 수락 보고에서 exact · Δ≤16 · co_golden 허용 오차(안티에일리어싱 픽셀 수)를 함께 읽는다.
--count delta --delta N으로 N = 0 · 4 · 8 · 16 · 32 의 비율을 표로 만든다. 보정 지도에서 노랑(Δ≤N 이라 빠지는 픽셀)이 글자 · 모서리 가장자리뿐인지 본다 — 레이아웃 · 색 · 크기 차이가 노랑에 숨으면 N 이 너무 크다.- 기준선(Figma 끼리 exact 14.1% · Δ≤16 4.7%, 오케스트레이터 측정 2026-11-01)과 견준다 — 구현이 아무리 맞아도 exact 로는 3% 아래로 내려가지 않을 수 있다는 근거다.
- 오너가
count·maxChannelDelta를 정한다 →figma-fidelity.json을 바꾸는 PR(calibration.status: decided, 측정 표 링크). 그다음FIGMA_ACCEPTANCE_BLOCKING을"true"로 바꿀지 정한다(아직 아니다 — Figma NavigationBar 수정 뒤).
먼저 잰 앱 — scaffold-booking (2026-11-01)
단골 예약의 SH-01 세 크기를 같은 도구로 잰 결과(Δ≤N 표 · 원인별 분해 · 글자 바닥값 · 사진 보정 · CoUI NavigationBar 높이
차이)와 권고(exact 는 쓰지 않는다 · Δ≤8 을 권한다, 결정은 오너)는 coco-de/scaffold-booking
docs/product/figma-fidelity.md 「보정」에 있다. 같은 Figma 파일 · 같은 CoUI 부품이라 이 앱에도 그대로 들어맞는 것(글자
가장자리 래스터 차이 · 사진 ±1 잡음 · 하단 탭 높이 차이)이 많지만, 숫자는 이 앱의 화면으로 다시 잰다.
파일럿 (지웠다)
처음에는 package/test_driver/test/golden/figma_pilot_golden_test.dart 가 HP-01 로그인의 구현이 아닌 자리표시로 파이프라인
(캡처 → 원본 → 차이 → 분석 → 수락 보고)이 끝까지 도는지만 보였다(대장은 파일럿 픽스처
figma-screens.pilot.json). HP-01 로그인을 구현하면서(R2 ③) 그 화면 패키지(feature/clinic/app)의 골든에서
FigmaGoldenScreens 로 HP-01@390 · @1440 과 HP-02 두 크기를 등록했으므로 파일럿 시험 · 픽스처 · 원본 PNG 를 지웠다.
package/test_driver 의 대장 시험은 정본 대장(docs/product/figma-screens.json)을 읽는다. 측정 숫자는 이 앱의 화면으로 잰
값이 R2 ③ PR 본문의 「Figma 대조」 표에 있다.