From e880f121b065a6a01931c7ddc05c5053d4af5d60 Mon Sep 17 00:00:00 2001 From: TtiPo Date: Mon, 28 Sep 2026 19:35:23 +0900 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=EB=B2=84=ED=8A=BC=C2=B7?= =?UTF-8?q?=ED=83=AD=20=EB=9D=BC=EB=B2=A8=20=EA=B8=80=EA=BC=B4=EC=9D=84=20?= =?UTF-8?q?=EC=8B=9C=EC=95=88=20=EC=A0=84=EC=97=AD=20=EA=B7=9C=EC=B9=99?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 시안 styles.css 뒷부분의 전역 규칙이 .rup-btn · 선택 버튼 · 하단 탭 라벨을 제목 서체(충북대체) 800 으로 덮어쓴다는 것을 놓쳤다. - RupButton: 제목 서체 사용, thin(600)·danger 변형 추가 (인트로 CTA 는 본문 서체 유지) - 탭 라벨: 제목 서체 800, 가운뎃점만 본문 서체로 그려 "일정·기록" 이 벌어져 보이지 않게 함 (브라우저는 CJK 문장부호 간격을 자동으로 줄이지만 Flutter 는 글리프 폭을 그대로 쓴다) - 이후 화면에 필요한 색 토큰(오류·시트 배경·빈 명함 등) 추가 Co-Authored-By: Claude Opus 5.5 (1M context) --- flutter/lib/core/constants/app_colors.dart | 23 ++++++++++++ flutter/lib/core/theme/app_typography.dart | 13 +++++-- .../presentation/widgets/rup_tab_bar.dart | 18 +++++++++- flutter/lib/shared/widgets/rup_button.dart | 36 ++++++++++++++----- 4 files changed, 77 insertions(+), 13 deletions(-) diff --git a/flutter/lib/core/constants/app_colors.dart b/flutter/lib/core/constants/app_colors.dart index 5c82575..77fc7d5 100644 --- a/flutter/lib/core/constants/app_colors.dart +++ b/flutter/lib/core/constants/app_colors.dart @@ -37,6 +37,29 @@ abstract final class AppColors { // 하단 탭바 반투명 배경 — rgba(255,253,250,.86) static const Color tabBarBg = Color(0xDBFFFDFA); + // 시트·모달 뒤 어둡게 — rgba(40,28,18,.42) + static const Color scrim = Color(0x6B281C12); + + // 입력 오류 · 파괴적 동작 + static const Color error = Color(0xFFE0534F); + static const Color danger = Color(0xFFD9503F); + + // 옅은 베이지 면 — 구분 띠·사진 추가 칸·텍스트 다이어리 (#F6F4EF) + static const Color sand = Color(0xFFF6F4EF); + static const Color sandInk = Color(0xFF8C8170); // 사진 추가 칸 아이콘·문구 + + // 빈 명함 슬롯 + static const Color emptyCardBg = Color(0xFFFEFCFA); + static const Color emptyCardBorder = Color(0xFFCDC7C0); // mix(muted, #fff 42%) + static const Color emptyCardIcon = Color(0xFFB8AFA6); // mix(muted, #fff 18%) + static const Color emptyCardButtonBorder = Color(0xFFFFC594); // mix(primary, #fff 58%) + + // 그림자 — 시안 --shadow-sm / --shadow-md 의 색 + static const Color shadowWarm = Color(0x0D46321E); // rgba(70,50,30,.05) + static const Color shadowWarmSoft = Color(0x0A46321E); // rgba(70,50,30,.04) + static const Color shadowMd = Color(0x1450371E); // rgba(80,55,30,.08) + static const Color shadowMdSoft = Color(0x0D50371E); // rgba(80,55,30,.05) + // 진입 화면 전용 static const Color splashYellow = Color(0xFFFFDE84); static const Color splashInk = Color(0xFF2B2622); diff --git a/flutter/lib/core/theme/app_typography.dart b/flutter/lib/core/theme/app_typography.dart index 9691e0e..e20726a 100644 --- a/flutter/lib/core/theme/app_typography.dart +++ b/flutter/lib/core/theme/app_typography.dart @@ -59,11 +59,18 @@ abstract final class AppTypography { bodyMedium: _ui(13.5, FontWeight.w400, height: 1.6), // 12 — 메타 정보 bodySmall: _ui(12, FontWeight.w400), - // 15 — 버튼 + // 15 — 본문 서체 버튼 (로그인·인트로 CTA 처럼 .rup-btn 이 아닌 버튼) labelLarge: _ui(15, FontWeight.w700), // 13 — 작은 버튼·링크 labelMedium: _ui(13, FontWeight.w600), - // 12 — 탭 라벨 - labelSmall: _ui(12, FontWeight.w700), + // 12 — 하단 탭 라벨 (.rup-tab 은 제목 서체 800) + labelSmall: _display(12, FontWeight.w800), ); + + /// `.rup-btn` · 선택 버튼 — 시안은 모든 버튼을 제목 서체 800 으로 통일한다 + static TextStyle button(double size, {bool thin = false, Color? color}) { + // 제목 서체의 기본 줄 높이가 커서 버튼이 시안보다 높아지므로 줄 높이를 고정한다 + return _display(size, thin ? FontWeight.w600 : FontWeight.w800, height: 1.2) + .copyWith(color: color); + } } diff --git a/flutter/lib/features/shell/presentation/widgets/rup_tab_bar.dart b/flutter/lib/features/shell/presentation/widgets/rup_tab_bar.dart index 8c4af70..411b724 100644 --- a/flutter/lib/features/shell/presentation/widgets/rup_tab_bar.dart +++ b/flutter/lib/features/shell/presentation/widgets/rup_tab_bar.dart @@ -3,6 +3,7 @@ import 'dart:ui' show ImageFilter; import 'package:flutter/material.dart'; import 'package:tma_app/core/constants/app_colors.dart'; import 'package:tma_app/core/constants/app_sizes.dart'; +import 'package:tma_app/core/theme/app_typography.dart'; import 'package:tma_app/features/shell/domain/app_tab.dart'; import 'package:tma_app/shared/widgets/icons/rup_icon.dart'; @@ -63,6 +64,21 @@ class RupTabBar extends StatelessWidget { } } +/// 제목 서체의 가운뎃점(·)은 폭이 넓고 왼쪽에 붙어 있다. +/// 브라우저는 문장부호 간격을 자동으로 줄이지만 Flutter 는 그대로 그리므로 +/// 가운뎃점만 본문 서체로 바꿔 "일정·기록" 처럼 붙여 보이게 한다. +TextSpan _narrowDots(String label) { + final parts = label.split('·'); + return TextSpan( + children: [ + for (final (i, part) in parts.indexed) ...[ + if (i > 0) const TextSpan(text: '·', style: TextStyle(fontFamily: AppFonts.ui)), + TextSpan(text: part), + ], + ], + ); +} + class _TabItem extends StatelessWidget { const _TabItem({required this.tab, required this.active, required this.onTap}); @@ -108,7 +124,7 @@ class _TabItem extends StatelessWidget { color: color, letterSpacing: 0, ), - child: Text(tab.label, maxLines: 1, softWrap: false), + child: Text.rich(_narrowDots(tab.label), maxLines: 1, softWrap: false), ), ], ), diff --git a/flutter/lib/shared/widgets/rup_button.dart b/flutter/lib/shared/widgets/rup_button.dart index e8332ab..0d77ead 100644 --- a/flutter/lib/shared/widgets/rup_button.dart +++ b/flutter/lib/shared/widgets/rup_button.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:tma_app/core/constants/app_colors.dart'; import 'package:tma_app/core/constants/app_sizes.dart'; +import 'package:tma_app/core/theme/app_typography.dart'; import 'package:tma_app/shared/widgets/pressable.dart'; /// 시안 `.rup-btn` 변형 @@ -14,10 +15,19 @@ enum RupButtonVariant { /// 연주황 바탕 (.rup-btn--soft) soft, - /// 주황 화면 위의 흰 버튼 (인트로·온보딩 CTA) + /// 파괴적 동작 (삭제 확인) + danger, + + /// 주황 화면 위의 흰 버튼 (인트로·온보딩 CTA — .rup-btn 이 아니라 본문 서체) light, } +const double _kFontSize = 15; +const double _kFontSizeLarge = 16; +const double _kPadV = 13; +const double _kPadVLarge = 16; +const double _kPadH = 18; + /// 시안의 pill 버튼. 기본은 가로를 꽉 채운다(.rup-btn--block). class RupButton extends StatelessWidget { const RupButton({ @@ -26,7 +36,9 @@ class RupButton extends StatelessWidget { required this.onPressed, this.variant = RupButtonVariant.primary, this.large = false, + this.thin = false, this.expand = true, + this.dimmed = false, this.height, this.leading, }); @@ -38,9 +50,15 @@ class RupButton extends StatelessWidget { /// .rup-btn--lg (글자 16, 세로 여백 16) final bool large; + /// .rup-btn--thin (제목 서체 굵기 600) + final bool thin; + /// 가로 채움 여부 final bool expand; + /// 누를 수는 있지만 아직 조건이 안 된 상태를 흐리게 보여 준다 (등록 버튼 등) + final bool dimmed; + /// 높이를 시안 값으로 고정해야 하는 자리(인트로 CTA 46 등)에서만 지정 final double? height; final Widget? leading; @@ -59,13 +77,13 @@ class RupButton extends StatelessWidget { AppColors.primary600, AppColors.primary100, ), + RupButtonVariant.danger => (AppColors.danger, Colors.white, null), RupButtonVariant.light => (AppColors.offWhite, AppColors.ink, null), }; - final textStyle = Theme.of(context).textTheme.labelLarge!.copyWith( - color: fg, - fontSize: large ? 16 : null, - ); - final verticalPad = large ? AppSpacing.lg : 13.0; + final size = large ? _kFontSizeLarge : _kFontSize; + final textStyle = variant == RupButtonVariant.light + ? Theme.of(context).textTheme.labelLarge!.copyWith(color: fg, fontSize: size) + : AppTypography.button(size, thin: thin, color: fg); final content = Row( mainAxisSize: expand ? MainAxisSize.max : MainAxisSize.min, @@ -84,7 +102,7 @@ class RupButton extends StatelessWidget { ); return Opacity( - opacity: onPressed == null ? 0.5 : 1, + opacity: onPressed == null || dimmed ? 0.5 : 1, child: Pressable( onTap: onPressed, child: Container( @@ -92,8 +110,8 @@ class RupButton extends StatelessWidget { width: expand ? double.infinity : null, alignment: Alignment.center, padding: EdgeInsets.symmetric( - horizontal: 18, - vertical: height == null ? verticalPad : 0, + horizontal: _kPadH, + vertical: height == null ? (large ? _kPadVLarge : _kPadV) : 0, ), decoration: BoxDecoration( color: bg,