visual-verify
구현한 Flutter·Jaspr 화면이 Figma 시안과 일치하는지 판정할 때 사용합니다. 상태·크기·테마·글꼴을 맞춰 픽셀을 비교하고 통과·실패·판정 불가와 차이 원인을 보고합니다. 수정 반복은 pixel-loop-guide에서 다룹니다.
전 화면 개선, 공유 co_faker 데이터, 실제 앱 실행, CoUI 확장을 함께 수행할 때는 전 화면 일치 작업의 상태 목록·원본 보존·중간 커밋 절차도 적용한다.
한마디로#
상태·크기·테마·글꼴을 맞춰 픽셀을 비교하고 통과·실패·판정 불가와 차이 원인을 보고합니다. 수정 반복은 pixel-loop-guide에서 다룹니다.
무엇을·언제#
-
무엇을 해 주나요
- 시안의 상태별 화면(기본 · 로딩 · 빈 화면 · 오류)을 밝은 테마와 어두운 테마로 각각 찍습니다.
- 같은 조건으로 앱 화면을 그려 두 사진을 점 단위로 비교하고, 다른 곳을 칠한 비교 이미지와 보고서를 남깁니다.
- "비교할 수 없었다"는 결과를 통과로 바꾸지 않습니다 — 왜 비교하지 못했는지가 보고서에 남습니다.
-
언제 쓰면 좋나요
- "이 화면 구현이 끝났다"고 말하기 전 — 화면 개발의 완료 기준입니다(
/cc-blueprint:build,/cc-dev:runStep 7.3). - PR 을 올리기 전 디자인 확인, 여러 화면을 한꺼번에 점검할 때.
- "이 화면 구현이 끝났다"고 말하기 전 — 화면 개발의 완료 기준입니다(
- 언제 쓰면 안 되나요
- 다른 곳을 직접 고쳐 주길 원할 때는
cc-pixel-loop:pixel-loop(이건 검사만 합니다).
- 다른 곳을 직접 고쳐 주길 원할 때는
핵심 용어#
| 용어 | 쉬운 설명 |
|---|---|
| 상태 프레임 | 시안에서 한 화면의 상태별 그림. Mobile-Default · Mobile-Empty 처럼 「플랫폼-상태」 이름을 씁니다 |
| 렌더 | 앱 코드가 실제로 그린 화면 사진 |
| 배율 | 화면의 한 점을 실제 화소 몇 개로 그리는지. 2 면 가로세로 두 배로 촘촘합니다 |
| 가림 | 비교에서 빼는 띠. 상태 바처럼 앱이 그리지 않는 곳만, 이유와 함께 뺍니다 |
| 영역 | 서로 다른 점들이 모인 덩어리. 보고서의 한 줄입니다 |
| 판정 불가 | 비교하지 못했다는 결과. 통과가 아닙니다 |
| 알려진 차이 | CoUI 가 이미 추적하는, 시안과 앱이 다르게 그리는 자리. 앱을 고치지 않습니다 |
| 차이 항목 | co_golden 디자인 분석이 남기는 한 줄. 어느 글자 · 컴포넌트가 어떤 값으로 다른지와 고칠 소스 줄이 들어 있습니다 |
판정은 스크립트가, 설명은 모델이#
비교의 통과 · 실패는 scripts/figma_compare.py(표준 라이브러리만 쓴다)가 정한다. 모델이 두 이미지를 보고 "±1px 차이"를 주장하는 방식은 쓰지 않는다 — 같은 입력이면 같은 결과가 나와야 게이트다. 모델이 하는 일은 남은
영역을 설명하고, 고치기 전에 그 영역을 고칠 자리로 나누는 것(아래 5절)뿐이다.
FC="${CLAUDE_SKILL_DIR}/scripts/figma_compare.py"
순서#
1. 입력 compare-frames 레시피(cc-blueprint)로 섹션의 상태 프레임을 읽는다 → frames-<섹션>.json
사양(선택) — design-spec 레시피로 비교할 프레임마다 → design/<label>.design.json (co_golden 구조 대조)
2. 계획 python3 "$FC" plan → plan.json (프레임 × 밝기, 배율 · 가림 · Figma 원천)
3. 확인 python3 "$FC" fonts → 시안의 서체가 앱 · 의존 패키지에 있고 테스트가 로드하는가
python3 "$FC" coui → 앱이 해석한 CoUI 릴리스 · 라이브러리와 달라진 컴포넌트
4. Figma 쪽 download_assets(png · defaultScale = 배율) → figma/<label>.png
다른 밝기는 dark-frames 레시피의 복제본을 찍고 지운다
5. 렌더 쪽 use_case 상태를 plan 의 장치로 캡처(Flutter) · 브라우저 캡처(Jaspr) → render/<label>.png
co_golden 캡처는 python3 "$FC" ingest 가 label 로 옮긴다
디자인 참조(figma/<label>.png + 사양)를 붙이면 co_golden 이 analysis/ 보고서도 남긴다(3절)
6. 비교 python3 "$FC" diff --run <run> --label <label> (비교마다)
7. 판정 남은 영역마다 고칠 자리를 judgement.json 에 — 고치기 전에(5절). co_golden 차이 항목이 있으면 그것이 근거다
8. 덧대기 python3 "$FC" literals → 변경분의 리터럴 덧대기
9. 합계 python3 "$FC" summary → summary.json · summary.md (PR 본문에 그대로)
종료 코드는 모든 명령이 같다: 0 통과 · 1 실패 · 2 사용 오류 · 3 판정 불가.
판정 불가는 통과가 아니다 — 부른 쪽은 그것을 실패와 따로, 정지(BLOCKED)나 사람이 확인한 스킵으로 다룬다(cc-dev:run
Step 7.3 · /cc-blueprint:build Step 5). 실패는 고칠 곳이 있다는 뜻이고, 판정 불가는 대조할 수단이나 시안이 없다는 뜻이라 처방이 다르다.
1. 맞추는 것 — 정규화#
| 축 | Figma 쪽 | 렌더 쪽 | 어긋나면 |
|---|---|---|---|
| 크기 | 상태 프레임의 크기(논리 px) | 장치의 논리 크기 = 프레임 크기 | undetermined:size-mismatch |
| 배율 | download_assets 의 defaultScale |
devicePixelRatio = 같은 배율. 기본 Mobile 2 · Web 1 |
같다 |
| 상태 바 · 홈 인디케이터 | 그 노드의 높이(레시피 insets) |
safeArea 를 같은 값으로. 그 띠는 이유를 단 가림 |
— |
| 글자 배율 | — | textScaler 1.0 (textScales: [1]) |
— |
| 서체 | 프레임이 쓰는 패밀리(가림 띠 밖) | 앱 · 의존 패키지 pubspec.yaml 에 선언되고, 테스트가 로드 |
fonts 실패 → 판정 불가 |
| 밝기 | 프레임에 고정한 모드. 다른 밝기는 복제본 | 같은 밝기의 앱 테마 | 리터럴 색 → figma-unthemable |
| 상태 | 프레임 이름의 상태 | use_case 의 같은 상태 시나리오(위젯북 Knob 과 같은 mock) | 상태를 못 읽으면 비교하지 않고 경고 |
| 앱 인자 | frame-modes 의 브랜드 · 서페이스 스타일 · 밀도 | 같은 CoUIApp 인자 |
/cc-blueprint:build Step 0 |
| 움직임 | — | MediaQuery(disableAnimations: true) — 스켈레톤 맥동이 멈춘 상태 |
— |
-
Figma 이미지는
download_assets로 받는다.get_screenshot의maxDimension은 크기를 줄이기만 하고 키우지 않아 늘 1배 이하다(실측) — 배율을 맞출 수 없다.download_assets(fileKey, nodeId, defaultFormat: "png", defaultScale: <배율>)의exportURL 을 curl 로figma/<label>.png에 받는다. - 프레임에는 불투명한 채움이 있어야 한다. 내보내기는 투명한 곳을 캔버스 색으로 칠한다(실측) — 레시피가 경고한다.
- 배율은 정수다. 렌더가 Figma 의 정수배면(기기 3배 ↔ Figma 1배) 상자 평균으로 줄인다. 그 밖의 비율은 보간하지 않고 판정 불가다.
2. Figma 쪽#
어느 섹션을 대조하나 — 화면의 디자인 소스는 아래 셋 중 하나라도 있으면 있다. 셋 다 같은 섹션을 가리키면 한 번만 대조한다.
| 출처 | 읽는 곳 | 대조 단위 |
|---|---|---|
| 화면 레지스트리 |
저장소의 화면 레지스트리(
docs/figma-screen-registry.yaml
, 없으면 이름이
figma*screen*registry*
인 파일) 중 그 화면(
dart_class
)의 행
figma_node
|
섹션 |
designLink |
그 화면 use_case 의 @UseCase(designLink:) |
그 프레임이 든 섹션(compare-frames 가 넓힌다) |
figma_analysis |
.claude/docs/<scope>/figma_analysis.md 의 Figma URL |
그 노드 — 프레임이면 그 섹션 |
출처가 하나도 없으면 대조 대상이 없다(해당 없음). 출처를 읽지 못한 것(파일 오류 · 깨진 URL)은 해당 없음이 아니라 판정 불가다.
-
cc-blueprint의references/recipes/compare-frames.figma.js를 위 표가 고른 섹션마다 돌려(읽기 전용,figma:figma-use먼저) 반환값을 그대로frames-<섹션>.json에 저장한다. 「플랫폼-상태」 이름이 아닌 옛 파일은 레시피의NAMES로 프레임마다 상태를 준다(보고에 남는다). -
python3 "$FC" plan --frames frames-*.json --run <run>— 비교마다label(<프레임 id>__<밝기>) · 배율 · 가림 · Figma 원천(frame·clone:light·clone:dark·unthemable)과 캡처 장치(device) · 시나리오 이름(captureScenario)이 정해진다. 밝기는 늘 라이트 · 다크 둘이다 — 하나를 빼려면--exclude-theme '<밝기>:<이유>'(6절).- 「플랫폼-상태」 이름이 아닌 프레임은 비교 목록에 들지 않고
unplanned에 남아 요약을 판정 불가로 만든다(undetermined:unnamed-frame) — 등록된 상태가 조용히 빠지지 않게. 섹션 안의 화면이 아닌 프레임(메모 류)은--ignore-frame '<프레임 id>=<이유>'로 뺀다. 그 이유는summary.md에 남는다. - 계속 바뀌는 노드(스피너 · 시계 류)는
--mask-node '<노드 id>=<이유>'로 그 범위를 가린다 — 노드 행(레시피)에 있는 노드만, 이유와 함께.devices는 렌더 쪽 장치 목록이다 — 크기 · 배율 · 상태 바 · 홈 인디케이터가 같은 프레임은 장치 하나를 같이 쓴다.
- 「플랫폼-상태」 이름이 아닌 프레임은 비교 목록에 들지 않고
-
원천이
frame인 비교는 그 프레임을,clone:<밝기>인 비교는dark-frames.figma.jscreate(MODE= 그 비교의 밝기 —Dark또는Light)가 돌려준 복제본을download_assets로 받는다. 프레임이 Dark 로 고정돼 있으면 라이트가 복제본이다. 받은 뒤에는 실패해도remove를 돌린다 — 다음create도 남은 복제본을 먼저 지운다.- 이 레시피만 쓰기다. 원본은 건드리지 않고, 이름이
⟂ compare로 시작하는 잠근 복제본을 캔버스 오른쪽에 잠깐 둔다. 디자이너의 파일이므로 편집 권한이 없으면 다른 밝기는 판정 불가로 남는다.
- 이 레시피만 쓰기다. 원본은 건드리지 않고, 이름이
-
원천이
unthemable이면 받지 않는다 — 시안의 색이 변수에 묶이지 않아 Figma 가 그 밝기를 그릴 수 없다. 그것은 디자인 쪽 결함(cc-blueprint:design-self-check3번 「토큰 단일 출처」 L0)이고, 그 비교는 판정 불가로 남아 디자이너에게 간다. 복제본을 만들지 못한 비교(권한 · 모드 없음)도 그 비교만 판정 불가다 — Figma 를 읽지 못한 것으로 세지 않는다.
3. 렌더 쪽#
Flutter — use_case 상태를 프레임 크기 장치로 (기본)#
기기도 실행 중인 앱도 필요 없다 — flutter test 가 그린다. 그래서 Flutter Web 앱도 같은 경로다. 렌더는 위젯북 use_case 의 상태 Knob 과
같은 시나리오 enum 의 같은 bloc() 을 쓴다(데이터는 build 의 mock/). enum 의 값마다 시안 프레임의 상태 이름이 들어 있어, 상태 프레임과 캡처가 그 이름으로 짝지어진다(cc-flutter:figma-widgetbook-alignment
§2-1). 그래서 캡처 테스트는 그 enum 을 import 할 수 있는 패키지 — use_case 의 fixture 를 가진 패키지 — 의 test/ 에 둔다.
새 앱 표준(cc-flutter:golden-test-guide 의 co_golden)이면 capture 모드로 찍는다 — 기준 이미지가 아니라 대조용 사진이다. 장치는
plan.json 의 devices 를 그대로 옮기고, 시나리오 이름은 비교의 captureScenario(상태의 소문자 — co_golden 의 이름 규칙)다:
// test/src/golden/<feature>_figma_frames_test.dart — Figma 대조용 캡처. 기준 이미지를 만들지 않는다
final figmaFrames = GoldenMatrix<ThemeData>(
suite: '<feature>_figma',
coverage: GoldenCoverage(
devices: const [
// plan.json 의 devices 그대로 — 이름 · 논리 크기 · 배율 · 상태 바 / 홈 인디케이터
GoldenDevice(
name: 'mobile-402x874',
logicalSize: Size(402, 874),
devicePixelRatio: 2,
platform: TargetPlatform.iOS,
safeArea: EdgeInsets.only(top: 62, bottom: 34),
),
],
themes: [
GoldenTheme(name: 'light', data: AppTheme.light),
GoldenTheme(name: 'dark', data: AppTheme.dark, brightness: Brightness.dark),
],
locales: const [Locale('ko')],
textScales: const [1],
sampling: GoldenSampling.full,
),
// 앱 루트와 같은 CoUIApp 인자(브랜드 · 서페이스 스타일 · 밀도)로 감싸고 동작 줄이기를 켠다.
// co_golden 은 ticker 도 멈춘다(freezeAnimations 기본 true)
app: (variant, child) => FigmaFrameHost(theme: variant.theme.data, child: child),
);
void main() {
for (final s in XxxScenario.values) {
// 시나리오 이름 = 비교의 captureScenario(default · loading · empty · error) — 위젯북 Knob 과 같은 enum · 같은 bloc()
figmaFrames.scenario(s.captureScenario, build: (_) => XxxPage(bloc: s.bloc()));
}
}
CO_GOLDEN_MODE=capture flutter test --tags co_golden test/src/golden/<feature>_figma_frames_test.dart
python3 "$FC" ingest --run "$RUN" --captures build/co_golden/images/<feature>_figma
# <captureScenario>/<device>__<theme>__<locale>.png → <run>/render/<label>.png — 없는 캡처는 판정 불가로 남는다
-
서체는
flutter_test_config.dart의loadGoldenFonts()가 로드한다. 앱이 번들하지 않은 서체는 상자로 그려진다 — 그래서fonts가 먼저다. -
co_golden이 없는 저장소는 같은 일을testWidgets로 한다 —tester.view.physicalSize = 프레임 크기 × 배율·devicePixelRatio = 배율, 프레임 크기의SizedBox를 감싼RepaintBoundary를toImage(pixelRatio: 배율)로 PNG 에 쓴다. Alchemist 의 CI 골든은 글자를 상자로 가리므로 대조에 쓰지 않는다. -
맥동 · 스피너가 있는 상태(Loading)는
disableAnimations: true로 멈춘 프레임을 찍는다 — CoUISkeleton은 동작 줄이기에서 맥동을 멈추고 쉼 끝의 면으로 그린다. 스피너처럼 계속 도는 표시는plan --mask-node '<노드 id>=<이유>'로 가린다(가림 상한 15%).
co_golden 디자인 분석 — 판정의 근거
같은 캡처 테스트에 디자인 참조를 붙이면 co_golden 이 캡처마다 analysis/<suite>/<scenario>/<variant>.json(co_golden.design_report
v1)을 남긴다 — 어느 글자의 서체 · 굵기 · 크기 · 행간이, 어느 컴포넌트의 변형 속성이, 어느 요소의 위치 · 크기 · 토큰이 다른지를 Figma 노드(레이어 경로 · 텍스트 스타일)와 위젯의 소스 줄(file:line:column)까지 붙여서. 이 스킬이 받은 PNG(figma/<label>.png)와, 비교마다
design-spec.figma.js
로 읽은 사양(design/<label>.design.json)을 그대로 넘긴다:
figmaFrames.scenario(
s.captureScenario,
build: (_) => XxxPage(bloc: s.bloc()),
designReference: (variant) {
// plan.json 의 비교(captureScenario · theme)로 label 을 찾는다 — 없으면 대조하지 않는다
final label = labelFor(s.captureScenario, variant.theme.name);
return label == null
? null
: GoldenDesignReference(
imagePath: '<run>/figma/$label.png',
specPath: '<run>/design/$label.design.json',
);
},
);
-
판정은 바뀌지 않는다. 통과 · 실패 · 판정 불가는 여전히
diff·summary가 정한다. co_golden 의 픽셀 규칙 · 기본값은 이 스크립트를 옮긴 것이라(CIEDE2000 · 안티앨리어싱 · 위치 반경 · 글자 상자 완화 · 4 논리 px 칸 · 예산 0.2% / 32 px²) 같은 기준으로 센다. co_golden 의 판정과 차이 항목은 5절에서 영역을 판정할 때의 근거다. -
상태 바 · 홈 인디케이터 띠는 co_golden 도 기본으로 가린다(
maskSafeArea).plan --mask-node로 가린 노드는 같은 이유로GoldenDesignMask.figmaNode('<노드 id>', reason: …)를 준다 — 두 도구의 가림이 같아야 영역이 맞는다. - 사용법 정본은
cc-flutter:golden-test-guide6절과 co_golden README 「Design analysis」다.
Jaspr — 브라우저 캡처#
cc-jaspr-web 의 Playwright 로 페이지를 띄워 browser_resize(프레임 너비, 프레임 높이) 뒤 browser_take_screenshot
을 render/<label>.png 로 저장한다. 다크는 browser_emulate_media(colorScheme: dark)
로 바꾼다. window.devicePixelRatio 를 읽어 plan 의 배율과 다르면 --scale 로 plan 을 다시 만든다.
서체는 pubspec 이 아니라 CSS 로 오므로, 페이지가 실제로 불러온 서체로 판정한다 — browser_evaluate 로 [...document.fonts].filter(f => f.status === "loaded").map(f => f.family)
를 읽어 fonts --browser-fonts <쉼표 목록> 에 넘긴다(--app 대신).
기기 — 실행 중인 앱 (게이트 밖)#
Marionette 스크린샷은 실제 기기의 동작까지 보는 데 쓴다(/cc-pixel-loop:loop --source=device). 배율은 기기의 것이고 상태 바는 가림으로 뺀다. 애니메이션 · 시계 · 네트워크가 사진을 바꾸므로
완료 판정(게이트)의 근거로는 쓰지 않는다 — 게이트는 위의 테스트 렌더다.
4. 비교 — 규칙과 기본값#
픽셀 p 는 다음이 모두 참일 때 차이다:
- 두 색의 CIEDE2000 이
color_delta_e를 넘는다. - 반경(
pixel× 배율) 안 상대 이미지 어디에도 그 색이 없다 — 양쪽으로 본다(시안에만 있는 것 · 렌더에만 있는 것). - 안티앨리어싱 가장자리가 아니다(두 색 사이에 낀 화소 — pixelmatch 와 같은 판정).
글자 상자(레시피가 준 TEXT 노드의 글리프 범위) 안에서는 가로 반경만 text_pixel 로, 색 허용치를 text_delta_e
로 넓힌다 — Figma 와 Flutter 의 글자 엔진은 같은 줄에서 글자 폭이 조금씩 쌓여 어긋나고 획의 진하기가 다르다. 세로 반경은 그대로라 줄 간격 · 여백이 어긋난 글자는 센다. 프레임 가장자리의 허용 반경 띠는 밖을 볼 수 없어 세지 않는다.
차이 픽셀은 4 논리 px 칸으로 묶여 영역이 되고, 영역마다 범위 · 가장 깊은 노드(레시피의 노드 행) · Figma 색과 렌더 색 · 평균 ΔE 가 보고된다. 영역 id 는
node:<노드 id> 라서 반복해도 같은 이름이다.
| 기본값 | 값 | 뜻 |
|---|---|---|
pixel | 1 | 위치 허용(논리 px) |
color_delta_e | 3 | 색 허용(CIEDE2000) |
text_pixel | 2 | 글자 상자 안 가로 위치 허용(논리 px) |
text_delta_e | 10 | 글자 상자 안 색 허용 |
예산 ratio | 0.2% | 비교한 픽셀 중 차이 비율 상한 |
예산 region | 32 | 가장 큰 영역의 차이 넓이 상한(논리 px²) |
| 가림 상한 | 15% | 넘으면 undetermined:over-masked — 가리지 말고 비교한다 |
통과는 ratio 와 region 을 둘 다 지킬 때다. 바꾸려면 --tolerance
· --budget 로 주고, 바꾼 값은 보고서에 남는다.
기본값은 실측으로 정했다 — CoUI 라이브러리 멤버의 Figma 내보내기와, 같은 위젯을 Pretendard 로 배율 2 에서 그린 Flutter 렌더:
| 쌍 | 결과 | 차이 비율 | 가장 큰 영역(논리 px²) |
|---|---|---|---|
| 같은 시안 — Button primary · secondary md | 통과 | 0.017% | 0.5 |
| 같은 시안 — Card outlined(머리 · 본문 · 푸터) | 통과 | 0.032% | 4.25 |
| 라벨 2px 이동 | 실패 | 3.18% | 45.5 |
| 푸터 글자색 다름 | 실패 | 0.32% | 108.25 |
| 본문 2px 아래 | 실패 | 0.54% | 133.25 |
| 다른 변형(primary → secondary) | 실패 | 86.5% | 2596 |
| 라벨 굵기 한 단계(600 → 500) | 통과 — 놓친다 | 0.042% | 0.5 |
같은 시안의 잡음(≤ 4.25)과 가장 작은 결함(45.5) 사이에 region 32 가 있다. 같은 실측에서 Card 는 라이브러리가 본문 · 푸터 간격을 코드보다 16px 넓게 그려 실패했다 — 라이브러리 결함이라
CoUI 이슈 — Card 본문 · 푸터 간격으로 올렸고, 그 차이는 앱이 고치지 않는다(5절
known).
5. 판정 — 고치기 전에#
실패한 비교의 영역마다, 패치를 쓰기 전에 고칠 자리를 judgement.json 에 적는다. 질문은 이 순서다:
| 순서 | 자리 | 언제 | 하는 일 | 면제 |
|---|---|---|---|---|
| 1 | known |
cc-flutter:figma-to-coui 의 「알려진 차이」(F7)에 해당하거나, 라이브러리 인스턴스가 코드와 다르게 그린 것 |
앱을 고치지 않는다. F7 행의 이슈, 없으면 CoUI 에 올린 이슈를 ref 에 |
이슈 URL 이 있고, --check-issues 면 그 이슈가 열려 있을 때 |
| 2 | version |
영역이 라이브러리 인스턴스 안이고, 그 컴포넌트가 앱의 CoUI 릴리스와 라이브러리 사이에서 달라졌다(
coui.json
의
drift
·
unknownInstances
)
|
앱을 고치지 않는다 — 앱의 CoUI 를 올리거나(
cc-coui-upgrade
) 라이브러리를 맞춘다(
cc-blueprint:coui-library-update
).
ref
=
coui:<컴포넌트>
|
그 컴포넌트가 coui.json 에 있고 영역의 노드가 그 인스턴스 안일 때 |
| 3 | style-slot |
라이브러리 인스턴스인데 캔버스의 덮어쓰기(색 · 패딩 · 반경 다시 묶기, 효과 스타일)가 코드에 없다 | 그 인스턴스의 withStyle(...) · 토큰 getter, 제품 전체 값이면 CoreComponentTheme(F3 · F6) |
없음 — 고친다 |
| 4 | app |
제품이 그린 레이아웃 · 고른 컴포넌트 · 상태 대응 · 내용 | 앱 코드를 CoUI 부품 · 토큰으로 고친다 | 없음 — 고친다 |
{"schema": 1, "regions": [
{"label": "12-345__light", "id": "node:12:359", "place": "version", "ref": "coui:card",
"evidence": "Card 의 bodyFooterGapStyle 이 앱 릴리스와 라이브러리 ref 에서 다르다", "fix": null},
{"label": "12-345__light", "id": "node:12:353", "place": "app", "ref": null,
"evidence": "제목 위 여백 16 ↔ 렌더 24", "fix": "Gap 을 CoreSpace.space16 으로"}
]}
-
리터럴 덧대기는 고침으로 세지 않는다.
literals가 변경분에서 색 · 여백 · 반경 · 글자 크기 · 손 장식(BoxDecoration류) · 위치 밀기 리터럴을 찾으면summary가 실패한다(fail:literal-patch). 꼭 필요한 리터럴은 그 줄에// literal-ok: <이유>를 달고, 그 이유가 보고서에 남는다. -
version·known만 면제된다. 면제는summary가 근거를 다시 확인한 뒤 예산을 남은 영역으로 다시 잰다 — 근거가 맞지 않으면 그 영역은 남는다. 보고서 상한(50) 밖으로 잘린 영역은 면제할 수 없다. - 판정은 모델의 몫이지만, 면제의 근거는 기계가 확인한다. 면제된 영역은 전부 PR 본문(
summary.md)에 이슈 · 컴포넌트와 함께 남아 리뷰어가 본다.
co_golden 차이 항목 — 영역의 근거#
캡처에 디자인 참조(사양 포함)를 붙였으면, 영역을 판정하기 전에 같은 자리의 차이 항목을 읽는다. 영역 id 의 노드(node:<노드 id>)와 차이 항목의
design.id 는 같은 Figma 노드다 — 두 레시피가 같은 파일의 노드 id 를 쓴다. 차이 항목은 무엇이(서체 · 굵기 · 크기 · 역할, 컴포넌트와 변형, 위치 · 간격, 토큰) 얼마나 다르고 어느 소스 줄에서 고치는지를 말한다 — 그 메시지와 소스 줄을
evidence 에, 힌트를 fix 에 옮긴다.
| 차이 항목 | 판정 자리 |
|---|---|
known 이 붙은 항목(GoldenKnownDifference 의 issue) |
known — ref 는 그 이슈 |
owner: designSystem(CoUI 컴포넌트가 안에서 그린 값 — 앱은 그 컴포넌트를 만들기만 했다) |
앱을 고치지 않는다 — 그 컴포넌트가
coui.json
의
drift
면
version
, 추적 이슈가 있으면
known
, 둘 다 아니면 CoUI 에 이슈를 올리고
known
|
component 변형 속성 차이(Variant secondary → primary) |
그 컴포넌트가
coui.json
의
drift
면
version
, 아니면
app
(F3 의 생성자 인자)
|
component 손으로 만든 위젯(시안은 CoUI 컴포넌트) | app — CoUI 부품으로 바꾼다 |
component catalog(앱의 CoUI 카탈로그에 없는 컴포넌트) |
cc-flutter:figma-to-coui
F8 의 판정을 따른다 — 추적 이슈가 있으면
known
, 없으면
app
(CoUI 부품 조립 + 이슈 · 만료 조건)
|
typography 역할 · 굵기 · 크기 · 행간 · 자간 |
app — 텍스트 역할 체인(F1). 휴대폰 · 태블릿 제목 크기면 known(F7) |
layout · shape · color 가 라이브러리 인스턴스 안 |
캔버스의 덮어쓰기면 style-slot, 아니면 F7 · version 부터 본다 |
layout · shape · color 가 제품이 그린 곳 |
app — 토큰 · Gap · 컴포넌트 기하로 |
font · frame | 판정 자리가 아니다 — 서체 로드 · 장치 크기를 먼저 맞추고 다시 찍는다 |
visual(차이 항목이 설명하지 못한 영역) | 지금처럼 영역을 보고 판정한다 |
-
차이 항목은 근거이지 면제가 아니다 — 면제(
known·version)의 근거는 지금처럼summary가 다시 확인한다. -
error차이 항목이 남아 있는데 이 스크립트가pass이면, 그 항목을 결과 보고에 함께 싣는다 — 굵기 한 단계처럼 픽셀이 놓치는 차이다(8절). 고치는 일은/cc-pixel-loop:loop가 한다.
6. 결과#
summary 의 상태 | 뜻 |
|---|---|
pass | 계획한 비교가 전부 있고, 남은 영역이 예산 안이며(면제 포함), 서체 · 덧대기 검사도 통과 |
fail |
예산을 넘는 영역이 남았거나(fail:diff), 리터럴 덧대기가 있다(fail:literal-patch) |
undetermined | 비교하지 못한 것이 있다 |
summary.json 의 status 는 이 셋 중 하나이고, 이유는 reasons[] 에 따로 있다 — fail:diff
· fail:literal-patch · undetermined:comparison(비교 하나 이상이 판정 불가) · undetermined:font-missing
· undetermined:fonts-not-checked · undetermined:literals-not-checked · undetermined:unnamed-frame
· undetermined:no-comparisons, 그리고 막지 않는 warn:defaults-changed · warn:judgement-for-unknown-region. 비교마다의 사유는
rows[].status 다 — undetermined:not-compared(보고서 없음) · missing-image
· size-mismatch · over-masked · unexplained-mask · unreadable-png
· figma-unthemable:<리터럴 수> · no-status.
- 서체 · 덧대기 검사를 돌리지 않았으면 통과가 아니다(
*-not-checked) — 검사하지 않은 것을 통과로 읽지 않는다. -
밝기 하나를 빼는 것은 이유와 함께만 된다 —
plan --exclude-theme 'dark:<이유>'(예: 제품 결정으로 라이트만 내는 앱 — 그 결정 기록). 밝기를 목록으로 고르는 옵션은 없고, 제외는summary.md에 남는다. -
허용치 · 예산 · 가림 상한을 기본값과 다르게 판정한 비교는
summary.md에 그 값이 남는다(warn:defaults-changed) — 리뷰어가 본다.
7. 산출물#
.claude/docs/visual-verify/<run_id>/
├── frames-<섹션>.json # compare-frames 반환값 그대로
├── design/<label>.design.json # design-spec 반환값 그대로(선택) — co_golden 디자인 참조의 사양
├── plan.json · nodes/ # 비교 목록 · 프레임별 노드 행
├── fonts.json · coui.json · literals.json · judgement.json
├── figma/<label>.png · render/<label>.png
├── reports/<label>.report.json · <label>.diff.png # Figma | 렌더 | 차이(빨강 차이 · 노랑 안티앨리어싱 · 파랑 가림)
└── summary.json · summary.md # summary.md 는 PR 본문에 그대로
python3 "$FC" plan --frames frames-*.json --run "$RUN" [--exclude-theme 'dark:<이유>'] \
[--ignore-frame '<프레임 id>=<이유>'] [--mask-node '<노드 id>=<이유>'] [--scale 2]
python3 "$FC" fonts --frames frames-*.json --app <앱 루트> --test-package <렌더 테스트 패키지> --run "$RUN"
# Jaspr: python3 "$FC" fonts --frames frames-*.json --browser-fonts "<불러온 서체, 쉼표>" --run "$RUN"
gh api -H 'Accept: application/vnd.github.raw' \
'repos/coco-de/coui/contents/tools/figma-token-sync/src/data/components.json?ref=main' > "$RUN/library-components.json"
python3 "$FC" coui --app <앱 루트> --library-components "$RUN/library-components.json" --instances frames-*.json --run "$RUN"
python3 "$FC" ingest --run "$RUN" --captures build/co_golden/images/<suite> # co_golden 캡처 → render/
python3 "$FC" diff --run "$RUN" --label <label> # plan 의 비교마다
python3 "$FC" literals --app <앱 루트> --base origin/main --run "$RUN"
python3 "$FC" summary --run "$RUN" --check-issues
-
coui는 앱의.dart_tool/package_config.json·pubspec.lock이 해석한 CoUI 를 읽는다 — 스킬 저장소의 기준(coui_skills_basis)이 아니다. 버전 차이를 판정하는 기준이 앱의 릴리스여야, 그 차이를 앱 코드에서 "고치지" 않는다. -
라이브러리 ref 는 라이브러리를 마지막으로 그린 CoUI 의
components.json이다. 디자이너가 게시한 라이브러리는 CoUI main 에서 그리므로 기본은 main 이다. -
같은 실행 폴더로 다시 돌릴 때(
--run-id) Figma 쪽(frames-*.json·plan.json·figma/)은 재사용하고 렌더 · 보고서 · 요약은 늘 다시 만든다. 시안을 고쳤으면(색을 변수에 묶음 · 프레임 이름) 새 실행이다 — 옛 plan 과 내보내기는 고치기 전의 시안이다.
8. 도달 범위 — 여기까지만 참이다#
-
굵기 한 단계(600 ↔ 500)는 픽셀 비교가 놓친다(실측 0.042%) — 이 스크립트만으로 대조하면(PNG 만) 지금도 그렇다. 사양(
design-spec.figma.js)을 붙인 co_golden 캡처면 구조 비교가 굵기를 값으로 견주어typography차이 항목(fontWeight600 → 500)과 그 글자의 소스 줄로 잡는다(co_golden 테스트에서 700 → 600 을 소스 줄과 함께 확인했다). 이 스크립트의 판정은 그대로 픽셀이다 — 그 차이 항목은 5절의 근거다. 글자의 굵기 · 크기는 텍스트 스타일 이름의 역할 체인이 정한다(cc-flutter:figma-to-couiF1). - 글자 상자 안에서는 가로 2 논리 px · 색 ΔE 10 까지 같은 것으로 본다. 다른 색의 글자는 잡지만(실측 108 px²), 같은 계열의 아주 가까운 색으로 바뀐 글자는 놓칠 수 있다. 사양이 있으면 co_golden 이 글자 색을 값으로(ΔE 3) 견준다.
- 프레임 가장자리의 허용 반경 띠(1 논리 px)는 위치 차이를 세지 않는다.
- 모양만 본다 — 동작(누름 · 스크롤 · 전환)은 보지 않는다(
cc-marionette·cc-mcp-toolkit). - 시안의 색이 변수에 묶이지 않았으면 다른 밝기는 판정 불가다. 실측한 제품 파일 두 벌은 화면 페인트의 대부분이 리터럴이라 다크를 그릴 수 없었다 — 그 비교는 디자이너의 일로 돌아간다.
다른 검증과의 경계#
| 검증 | 담당 | 이 스킬과의 관계 |
|---|---|---|
| 시안 ↔ 렌더 픽셀 대조(게이트) | 이 스킬 | 등록된 상태 프레임 × 밝기 |
| 고치며 수렴 | cc-pixel-loop:pixel-loop (/cc-pixel-loop:loop) |
같은 비교를 반복마다 돌린다 |
| 정적 회귀(골든) | cc-flutter:golden-test-guide |
코드가 스스로 승인한 기준 — 시안과 같다는 증거가 아니다 |
| 글자 · 컴포넌트 · 위치의 값 대조(차이 항목) | cc-flutter:golden-test-guide 6절 — co_golden 디자인 분석 |
같은 캡처에서 영역의 근거를 낸다 — 판정은 이 스킬 |
| Figma ↔ 위젯북 집합 대조 | /cc-blueprint:sync | 어느 화면이 어디에만 있는가 |
| 위젯 트리 · 앱 내부 상태 | cc-marionette · cc-mcp-toolkit |
실행 중인 앱 |
| Jaspr DOM · 웹 회귀 | cc-jaspr-web | 브라우저 캡처를 이 스킬이 빌려 쓴다 |
Related#
-
cc-blueprint의references/recipes/compare-frames.figma.js·dark-frames.figma.js— Figma 쪽 입력 -
cc-blueprint의references/recipes/design-spec.figma.js— co_golden 구조 대조의 사양 cc-flutter:figma-to-coui— F1(텍스트 역할) · F3(인스턴스 덮어쓰기) · F7(알려진 차이)-
cc-flutter:golden-test-guide—co_golden캡처 · 서체 로드, 6절 디자인 참조 · 분석 · 갤러리--design-report cc-pixel-loop:pixel-loop— 고치며 수렴cc-dev:runStep 7.3 ·/cc-blueprint:buildStep 5 — 이 스킬을 완료 기준으로 부르는 곳