Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ jobs:
- run: flutter pub get
- run: dart format --set-exit-if-changed lib test
- run: flutter analyze
# 디자인 토큰 소비 규율(DQ-13/14/15) — raw 색·fontSize·radius 리터럴 차단.
- run: dart run tool/check_design_tokens.dart
- run: flutter test --coverage --concurrency=1
4 changes: 2 additions & 2 deletions lib/screens/debt/debt_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,9 @@ class _DebtScreenState extends State<DebtScreen> {
t.warningFg,
Color.lerp(
t.warningFg,
const Color(0xFF000000),
const Color(0xFF000000), // token-exempt: warningFg 어둡게 섞는 색연산
0.22,
)!, // token-exempt: warningFg 어둡게 섞는 색연산
)!,
],
),
borderRadius: BorderRadius.circular(t.radiusLg),
Expand Down
2 changes: 1 addition & 1 deletion lib/screens/login.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class _LoginScreenState extends State<LoginScreen> {
SocialProvider.kakao,
'카카오로 시작하기',
SeedSource.kakao,
Color(0xFF191600),
Color(0xFF191600), // token-exempt: 카카오 브랜드 라벨색(가이드라인)
brand: 'kakao',
),
_SocialSpec(
Expand Down
1 change: 1 addition & 0 deletions lib/widgets/dk_icon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class DkIcon extends StatelessWidget {
this.name, {
super.key,
this.size = 22,
// token-exempt: const 기본값(라이트 fg) — 호출부가 대부분 토큰으로 덮어쓴다.
this.color = const Color(0xFF1A1B1E),
this.strokeWidth = 1.9,
this.fill,
Expand Down
101 changes: 101 additions & 0 deletions tool/check_design_tokens.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// 디자인 토큰 소비 규율 게이트(DQ-13/14/15, ADR-0032 소비 규율).
//
// lib/(theme/ 제외)에서 seed 토큰을 우회하는 리터럴을 찾으면 실패한다:
// 1. raw `Color(0x...)` — 투명(0x00000000) 제외. 예외는 해당 라인 또는 위아래
// 1줄의 `token-exempt: <사유>` 주석으로만 허용.
// 2. `fontSize: <숫자>` — 문서화된 예외 값(디스플레이 숫자 등)만 허용.
// 3. `BorderRadius.circular(<숫자>)` — 문서화된 예외 값만 허용.
//
// 예외 값 목록의 권위: docs/01-디자인분석/디자인시스템-토큰.md 1·4장.
// 실행: `dart run tool/check_design_tokens.dart` (CI ci.yml 이 호출).
import 'dart:io';

/// fontSize 허용 리터럴 — 스케일 밖 문서화 예외(디스플레이 숫자·목업 미니 라벨).
const Set<String> kFontSizeExempt = <String>{
'9',
'10.5',
'26',
'27',
'40',
'46',
'50',
'52',
'60',
'64',
'104',
};

/// radius 허용 리터럴 — 스펙 고정 예외(행 카드 18, 레일 카드 20 등).
const Set<String> kRadiusExempt = <String>{
'3',
'5',
'18',
'20',
'22',
'28',
'40',
};

final RegExp _color = RegExp(r'Color\(0x([0-9A-Fa-f]{8})\)');
final RegExp _fontSize = RegExp(r'fontSize:\s*(\d+(?:\.\d+)?)[,)\s]');
final RegExp _radius = RegExp(r'BorderRadius\.circular\((\d+(?:\.\d+)?)\)');

bool _exempt(List<String> lines, int i) {
for (int j = i - 1; j <= i + 1; j++) {
if (j >= 0 && j < lines.length && lines[j].contains('token-exempt')) {
return true;
}
}
return false;
}

void main() {
final List<String> violations = <String>[];
final Iterable<File> files = Directory('lib')
.listSync(recursive: true)
.whereType<File>()
.where((File f) => f.path.endsWith('.dart'))
.where((File f) => !f.path.startsWith('lib/theme/'));

for (final File f in files) {
final List<String> lines = f.readAsLinesSync();
// 파일 전체 예외 선언(브랜드 마크 등)은 파일을 통째로 건너뛴다.
if (lines.isNotEmpty && lines.first.contains('token-exempt(파일 전체)')) {
continue;
}
for (int i = 0; i < lines.length; i++) {
final String line = lines[i];
for (final RegExpMatch m in _color.allMatches(line)) {
if (m.group(1)!.toUpperCase() == '00000000') continue; // 투명
if (_exempt(lines, i)) continue;
violations.add(
'${f.path}:${i + 1}: raw ${m.group(0)} — 토큰 또는 token-exempt 주석 필요',
);
}
for (final RegExpMatch m in _fontSize.allMatches(line)) {
if (kFontSizeExempt.contains(m.group(1))) continue;
if (_exempt(lines, i)) continue;
violations.add(
'${f.path}:${i + 1}: fontSize ${m.group(1)} — SeedType 스케일 사용',
);
}
for (final RegExpMatch m in _radius.allMatches(line)) {
if (kRadiusExempt.contains(m.group(1))) continue;
if (_exempt(lines, i)) continue;
violations.add(
'${f.path}:${i + 1}: radius ${m.group(1)} — SeedRadius 스케일 사용',
);
}
}
}

if (violations.isNotEmpty) {
stderr.writeln('디자인 토큰 게이트 실패 (${violations.length}건):');
for (final String v in violations) {
stderr.writeln(' ✗ $v');
}
exitCode = 1;
return;
}
stdout.writeln('디자인 토큰 게이트 통과');
}
Loading