Figma 대조 — 기기 · 원본 연결 · 엄격 차이 보고

목표는 화면마다 엄격 픽셀 차이 3% 미만이다(Figma 「Cocode Scaffold App v1.0」 페이지 49:7). 이 문서는 그 차이를 어떤 기기로 찍고, 무엇을 가리고, 어떻게 세고, 어디서 판정하는지를 정한다. 판정 규칙의 정본은 figma-fidelity.json 하나다 — 기준 · 세는 법을 바꾸는 것은 그 파일을 바꾸는 PR 이다.

구성 자리 하는 일
화면 대장 docs/product/figma-screens.json Figma 프레임 80개 — 노드 · 크기 · 기기 · 원본 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(웹 화면) 없음

가림

가리는 것은 띠 둘뿐이다 — 위 상태 바(insets.top)와 아래 홈 인디케이터(insets.bottom). 둘 다 기기(OS)가 그리고 앱은 그리지 않는다. 띠마다 사유가 기록된다(상태 바 띠(위 44px) — 기기(OS)가 그리고 앱은 그리지 않는다).

가림 예외

화면 영역(논리 px) 사유 이슈
(없음)

세는 법

분모는 띠 밖의 픽셀(comparable = 전체 − 띠)이다. 두 이미지를 8비트 RGBA 로 풀어 픽셀마다 견준다.

이름 「다르다」로 세는 픽셀 보고 키
exact RGBA 네 채널 중 하나라도 다르다 — 엄격(정확) 차이 exact
Δ≤N 무시 네 채널 차이의 최댓값이 N 을 넘는다(N 이하는 세지 않는다) — 기본 N = 16 delta

글자 밖 — 글자 잉크 상자를 빼고 세기

같은 글자를 같은 자리에 그려도 두 렌더러가 가장자리를 다르게 칠해, 글자 상자(+2 px) 픽셀의 26–32% 가 Δ>8 로 남는다 (아래 「글자 바닥값」). 그래서 글자는 구조로 엄격하게, 픽셀은 글자 밖에서 센다. 오너가 2026-11-02 에 판정에 쓰기로 했다 — figma-fidelity.json 의 "textMask": {"padding": 2}.

판정

상태 뜻
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: 'class');

void main() {
  figma.screen('CL-01@390', build: (_) => const HomePage());
  figma.screen('CL-03+CL-04@1440', build: (_) => const ServiceDetailPage(), interact: …);
}

로컬 실행

# 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 대조가 있는 변형은 원본과 캡처를 함께 보인다.

CI

golden.yml 의 골든 (compare) 가 PR(준비 완료) · dispatch 에서 확인용 capture 를 돌린 뒤 출력을 아티팩트 co-golden-capture 로 올리고, Figma 대조 (수락 보고) 잡이 그것을 받아 도구 시험 → 수락 보고를 돌려 잡 요약과 아티팩트 visual-acceptance(JSON · Markdown · 보정 지도)로 남긴다.

보정 — CL-01 첫 측정 (2026-11-02)

엄격 3% 를 무엇으로 셀지는 오너가 확정했다(2026-11-02) — 「글자 밖 Δ≤8 3% 미만」(count: delta · maxChannelDelta: 8 · textMask.padding: 2, co_golden 구조 오류 0건 지향). exact · Δ≤8 전체는 참고로 계속 보고한다. 아래는 CL-01(홈) 세 크기를 구현해 처음 잰 값이다 — 로컬 Darwin/arm64 capture(CO_GOLDEN_MODE=capture, --tags figma) · check_visual_acceptance.sh --count delta --delta N --text-mask. 글자 밖 수는 글자 노드의 잉크 상자(Figma absoluteRenderBounds)에 사방 2px 을 더해 뺀 값이다 — 글자 사양은 tool/design/figma/export_text_spec.figma.js 가 글자 노드만 읽어(컴포넌트 · 색 칠 · 레이아웃 사양은 없다) docs/design/figma/spec/CL-01@<크기>.design.json 에 있다(옮겨 적기는 digest 로 검산했다 — 390 efae2e31 · 1024 670232d4 · 1440 df9adbbd).

Δ≤N 표 (화면 대비 %, 띠 밖 · 오른쪽 짝이 글자 밖)

화면 exact exact 글자 밖 Δ≤4 글자 밖 Δ≤8 글자 밖 Δ≤16 글자 밖 Δ≤32 글자 밖
CL-01@390 29.559 28.994 27.484 27.017 5.313 3.136 4.090 2.259 2.543 1.345
CL-01@1024 29.894 29.613 27.666 27.346 3.348 1.416 2.514 0.773 1.600 0.297
CL-01@1440 32.536 32.609 31.003 31.107 2.115 0.648 1.395 0.105 0.956 0.038

Δ≤4 → Δ≤8 에서 27–31% → 2–5% 로 꺾이는 것은 Figma 의 사진 보정(온도 +0.11 · 채도 −0.02)이 사진 픽셀을 그 정도로 옮기기 때문이다 — 같은 이미지 칠 8곳을 읽는 전용 스크립트로 보정값을 확인했고, 사진 영역의 평균 채널 차이(구현 − 원본)는 R −3.3 · G −0.4 · B +5.4 로 세 화면 모두 같다(구현이 더 푸르다 — 원본 rawImages 에는 보정이 없다). 작업 기준(글자 밖 Δ≤8 3% 미만)으로는 1024 · 1440 이 수락이고 390 이 3.136% 로 넘는다 — 아래 원인에서 보듯 그 거의 전부(2.211%)가 CoUI NavigationBar 높이 차이다.

원인별 분해 (Δ≤8 · 글자 밖 · 글자 밖 분모 대비 %)

원인 390 1024 1440
CoUI ↔ Figma: NavigationBar — 막대 높이 115 ↔ 103(항목 12px 위 · 막대 위 12px 만큼 사진이 가려진다) · 선택 라벨 14/600 ↔ 12 2.211 0.512 —
사진 — Figma 보정(온도 · 채도) · 리샘플링 · 모서리 안티에일리어싱 0.766 0.595 0.547
그 밖 — 막대 아이콘 자리(아래) · 칩 · 링크 폭 · 둥근 상자 가장자리 · 입력칸 둘레 0.159 0.308 0.101
합계(글자 밖 Δ≤8) 3.136 1.416 0.648

NavigationBar 를 빼면 390 은 0.925% 다. 높이 차이는 디자인 판단이라(Figma 103 · CoUI 115) 앱에서 맞추지 않고 수치만 보고한다 — 같은 부품 차이를 scaffold-booking 은 전체 기준으로 390 1.6% · 1024 2.2% 로 적었다. 분해는 화면의 영역으로 센 값이다 (NavigationBar 영역 = Figma 막대 위 12px 부터 아래, 사진 = 사진 상자, 나머지 = 그 밖).

co_golden 구조 대조(글자 노드만)

화면 글자 노드(띠 밖) 맞춤 서체 차이 위치 차이
CL-01@390 20 20 1 — 선택 라벨 「홈」 14/600/lh 160% ↔ CoUI 12 4 — 하단 탭 라벨 −10.3…−11.8px(막대 높이)
CL-01@1024 26 26 1 — 〃 7 — 하단 탭 4 + 분류 칩 글자 −2.3…−4.4px 3(칩 폭)
CL-01@1440 31 31 0 3 — 상단 링크 −2.8…−5px(링크 폭)

글자 내용 · 크기 · 굵기 · 색 토큰은 모두 일치한다 — 그려진 글자의 서체 · 크기 · 행간 · 자간 · 색을 사양과 하나씩 견준 탐침(renderedTextStyle, 커밋하지 않았다)에서 하단 탭 선택 라벨 말고는 어긋남이 없었다(위 외 서체 차이 없음 · 없는 글자 없음 — 띠 밖 글자 77개 모두 맞춤). 남은 위치 차이는 두 가지다 — 하단 탭(CoUI 부품 높이)과, Figma 가 글자 상자 폭을 정수로 올려 줄지어 늘어선 칩 · 링크가 글자마다 1–2px 씩 밀리는 엔진 차이(글자 한 개씩은 맞고 합쳐진 간격만 다르다). 구조 오류 0건 기준은 이 두 CoUI · 엔진 차이 때문에 충족하지 못한다.

CoUI · Figma 가 다른 곳 (앱에서 덮지 않음)

  1. NavigationBar 높이 — Figma 103px(위 8 · 항목 66 · 아래 28, 홈 인디케이터 34 와 겹침) ↔ CoUI 115px. 막대가 12px 높고 아이콘 · 라벨이 12px 위. 차이 몫: 390 2.21% · 1024 0.51%.
  2. NavigationBar 선택 라벨 — Figma 14/600/lh 160%(스타일 미연결) ↔ CoUI 모두 Label/Small 12. 기본 labelMode 는 아이콘만(none)이라 Figma 와 같게 .all 로 줬다.
  3. Button 앞 아이콘 슬롯 — leading 을 쓰면 가득 찬 버튼 안에서 내용이 앞쪽에 붙는다(Figma 는 아이콘 · 글자 묶음이 가운데). 폰의 「이어서 보기」는 아이콘을 자식 줄에 함께 둔다 — 색 · 크기는 버튼의 아이콘 테마를 그대로 받는다 (CoUI 이슈 후보 #1459 — 앱의 우회라 지울 조건은 CoUI 가 가운데 정렬을 주는 것).
  4. Text 색 체인 — on-surface-subtle · primary-text · error 체인이 없다 → CourseTextTone(.color(CoreColor.token(…))). CoUI 가 체인을 더하면 지운다(CoUI 이슈 #1459).
  5. 상단 막대 행동(폰) 터치 대상 — Figma 의 검색 · 알림은 20px 아이콘이 4px 간격이라 두 행동 모두 터치 대상 48(× 배율)을 줄 수 없다. 누를 자리를 48×배율로 넓히고 아이콘은 그 자리의 오른쪽 끝에 둔다 — 알림은 Figma 자리 그대로, 검색은 24px 왼쪽(차이 몫 0.1–0.2%). 의도한 편차다.
  6. 막대 이름 줄 — CoUI AppBar 높이가 고정이라 글자 배율 2.0(실효 2.5) 에서 이름이 두 줄로 접히면 넘친다 → 한 줄에 들어가지 않을 때만 FittedBox(scaleDown) 으로 줄인다(들어가면 그대로).
  7. Figma 사진 보정 — 이미지 칠마다 온도 +0.11 · 채도 −0.02 가 걸려 있다. 원본(rawImages)은 보정 전이라 구현이 조금 푸르다. 끌지 · 시드 사진에 구울지는 오너 결정(scaffold-booking 과 같은 항목).

Figma 안의 불일치 (디자이너 확인 후보)

다음

글자 밖 Δ≤8 이 390 에서 3% 를 넘는 것은 구현 결함이 아니라 위 NavigationBar 차이 하나로 설명된다. 오너가 세는 법을 정하면(count · maxChannelDelta) figma-fidelity.json 의 calibration 을 바꾸는 PR 로 닫는다.

먼저 잰 앱 — scaffold-booking (2026-11-01)

단골 예약의 SH-01 세 크기를 같은 도구로 잰 결과(Δ≤N 표 · 원인별 분해 · 글자 바닥값 · 사진 보정 · CoUI NavigationBar 높이 차이)와 권고(exact 는 쓰지 않는다 · Δ≤8 을 권한다, 결정은 오너)는 coco-de/scaffold-booking docs/product/figma-fidelity.md 「보정」에 있다. 같은 Figma 파일 · 같은 CoUI 부품이라 이 앱에도 그대로 들어맞는 것(글자 가장자리 래스터 차이 · 사진 ±1 잡음 · 하단 탭 높이 차이)이 많지만, 숫자는 이 앱의 화면으로 다시 잰다.

파일럿 → 시작 화면

도구가 끝까지 도는지 보이려고 두었던 파일럿 자리표시(CL-S1 자리표시 화면과 그 픽스처 대장)는 시작 화면 구현이 CL-S1@390 을 실제 스플래시로 등록하면서 지웠다. 시작 화면 다섯 프레임(CL-S1@390 · CL-S2@390 · CL-S2@1440 · CL-S3@390 · CL-S3@1440)은 feature/phone_auth/app/test/golden/phone_auth_figma_golden_test.dart 가 정본 대장 키로 등록한다. 대장 읽기 · 기기 · 띠 시험(package/test_driver/test/figma/figma_ledger_test.dart)도 정본 대장의 CL-S1 로 본다.