From 4173c64af8130504f384d6368bdca653d39145ea Mon Sep 17 00:00:00 2001 From: TtiPo Date: Tue, 29 Sep 2026 01:24:24 +0900 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=ED=8E=AB=20=EB=AA=85=ED=95=A8?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EC=8B=9C=EC=95=88=20=EC=A0=90=EA=B2=80?= =?UTF-8?q?=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 명함은 그 아이의 고유색 한 가지로 시작한다 (CardStyle.forPet — 직접 선택 칸이 켜진다). - 명함 이름이 길어도 윗줄(이름·성별·공유 버튼)이 깨지지 않게 하고, 공유 버튼에 누름 효과를 넣었다. - 꾸미기에서 저장하고 돌아오면 상세히 보기로 돌아온다. - 빈 명함 칸 누름 바탕, '기본' 칸은 골라도 그림자 없음, 명함 받기 안내 아이콘(QR) 등 상세·관리·목록 화면의 간격과 줄 높이를 시안 값으로 맞췄다. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../features/pet_card/domain/card_style.dart | 5 +- .../pages/pet_card_detail_page.dart | 16 ++++- .../pages/pet_card_editor_page.dart | 10 ++- .../pages/pet_card_manage_page.dart | 8 ++- .../presentation/pages/pet_cards_page.dart | 10 ++- .../widgets/card_style_controls.dart | 25 +++++-- .../presentation/widgets/name_card.dart | 71 ++++++++++--------- .../presentation/widgets/name_card_empty.dart | 23 +++++- .../presentation/widgets/pet_id_card.dart | 2 + .../presentation/widgets/pick_pet_sheet.dart | 2 +- 10 files changed, 119 insertions(+), 53 deletions(-) diff --git a/flutter/lib/features/pet_card/domain/card_style.dart b/flutter/lib/features/pet_card/domain/card_style.dart index a9a4e00..0333af5 100644 --- a/flutter/lib/features/pet_card/domain/card_style.dart +++ b/flutter/lib/features/pet_card/domain/card_style.dart @@ -52,9 +52,12 @@ class CardStyle { this.scrimTone = ScrimTone.light, }); - /// 새 명함의 기본값 — 첫 번째 헤더색(코랄), 흰 배경 + /// 기본값 — 첫 번째 헤더색(코랄), 흰 배경 factory CardStyle.initial() => CardStyle(tint: kCardTints.first.a, tint2: kCardTints.first.b); + /// 새 명함 — 그 아이의 고유색 한 가지로 시작한다 (시안: tint = tint2 = pet.tint → 직접 선택 칸이 켜진다) + factory CardStyle.forPet(Color petTint) => CardStyle(tint: petTint, tint2: petTint); + /// 카드 상단 그라데이션 두 색 final Color tint; final Color tint2; diff --git a/flutter/lib/features/pet_card/presentation/pages/pet_card_detail_page.dart b/flutter/lib/features/pet_card/presentation/pages/pet_card_detail_page.dart index 1cd3944..9780bc9 100644 --- a/flutter/lib/features/pet_card/presentation/pages/pet_card_detail_page.dart +++ b/flutter/lib/features/pet_card/presentation/pages/pet_card_detail_page.dart @@ -68,6 +68,12 @@ class _PetCardDetailPageState extends ConsumerState { toast.show('대표 명함으로 설정했어요', icon: RupIconName.star); } + /// 꾸미기 — 저장하고 돌아오면 상세히로 보여 준다 (시안 onSave → setMode('detail')) + Future _edit(PetCard card) async { + final saved = await context.push(AppRoutes.petCardEdit(card.id)); + if (saved == true && mounted) setState(() => _mode = CardViewMode.detail); + } + Future _delete(PetCard card) async { final ok = await showRupConfirm( context, @@ -111,6 +117,8 @@ class _PetCardDetailPageState extends ConsumerState { title: pet.name, subtitle: '펫 명함', displayFont: true, + // 시안 상세 머리 margin '4px 0 16px' + bottomGap: 16, onBack: () => context.pop(), trailing: SegToggle( first: (CardViewMode.simple, CardViewMode.simple.label), @@ -146,7 +154,7 @@ class _PetCardDetailPageState extends ConsumerState { child: _TextAction( label: '카드 수정', color: AppColors.ink2, - onTap: () => unawaited(context.push(AppRoutes.petCardEdit(card.id))), + onTap: () => unawaited(_edit(card)), ), ), const SizedBox(width: 1, height: 13, child: ColoredBox(color: AppColors.line)), @@ -236,7 +244,11 @@ class _TextAction extends StatelessWidget { child: Text( label, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontWeight: FontWeight.w700, color: color), + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w700, + color: color, + height: AppTypography.normalHeight, + ), ), ), ), diff --git a/flutter/lib/features/pet_card/presentation/pages/pet_card_editor_page.dart b/flutter/lib/features/pet_card/presentation/pages/pet_card_editor_page.dart index 4b89f06..84a4163 100644 --- a/flutter/lib/features/pet_card/presentation/pages/pet_card_editor_page.dart +++ b/flutter/lib/features/pet_card/presentation/pages/pet_card_editor_page.dart @@ -74,7 +74,12 @@ class _CardEditor extends ConsumerStatefulWidget { } class _CardEditorState extends ConsumerState<_CardEditor> { - late CardStyle _style = widget.card?.style ?? CardStyle.initial(); + late CardStyle _style = widget.card?.style ?? _newStyle(); + + CardStyle _newStyle() { + final tint = ref.read(petByIdProvider(widget.petId))?.tint; + return tint == null ? CardStyle.initial() : CardStyle.forPet(tint); + } bool _saving = false; bool get _isEdit => widget.card != null; @@ -138,7 +143,8 @@ class _CardEditorState extends ConsumerState<_CardEditor> { if (_isEdit) { await notifier.updateStyle(widget.card!.id, _style); toast.show('펫 명함을 수정했어요'); - if (mounted) context.pop(); + // 저장했음을 알려 상세 화면이 "상세히" 로 돌아가게 한다 + if (mounted) context.pop(true); } else { final card = await notifier.create(petId, _style); toast.show('펫 명함을 만들었어요'); diff --git a/flutter/lib/features/pet_card/presentation/pages/pet_card_manage_page.dart b/flutter/lib/features/pet_card/presentation/pages/pet_card_manage_page.dart index 8021a40..a4110d4 100644 --- a/flutter/lib/features/pet_card/presentation/pages/pet_card_manage_page.dart +++ b/flutter/lib/features/pet_card/presentation/pages/pet_card_manage_page.dart @@ -58,7 +58,7 @@ class _PetCardManagePageState extends ConsumerState { child: ListView( padding: EdgeInsets.fromLTRB( AppSpacing.xl, - AppSpacing.sm, + AppSpacing.xs, AppSpacing.xl, MediaQuery.paddingOf(context).bottom + 40, ), @@ -73,8 +73,10 @@ class _PetCardManagePageState extends ConsumerState { onPressed: () => unawaited(_create(pets, cards.length)), ), ), - SizedBox( - height: _kChipHeight, + // 시안 .rup-petbar { padding: 2px 0 6px } + Container( + height: _kChipHeight + 8, + padding: const EdgeInsets.only(top: 2, bottom: 6), child: ListView( scrollDirection: Axis.horizontal, children: [ diff --git a/flutter/lib/features/pet_card/presentation/pages/pet_cards_page.dart b/flutter/lib/features/pet_card/presentation/pages/pet_cards_page.dart index 6b5371d..74a7d79 100644 --- a/flutter/lib/features/pet_card/presentation/pages/pet_cards_page.dart +++ b/flutter/lib/features/pet_card/presentation/pages/pet_cards_page.dart @@ -67,7 +67,7 @@ class _PetCardsPageState extends ConsumerState { ), const SizedBox(height: AppSpacing.sm), if (pet == null) - const EmptyMessage('등록된 반려동물이 없어요', padding: 40) + const EmptyMessage('등록된 반려동물이 없어요') else ...[ padded(PetPicker(pets: pets, selectedId: pet.id, onSelect: (id) => setState(() => _selectedPetId = id))), const SizedBox(height: 14), @@ -195,6 +195,8 @@ class _ReceivedSectionState extends ConsumerState<_ReceivedSection> { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // 시안 .rup-section-title — 구분 띠 아래 22 와 제목 위 26 이 겹쳐 26 (좌우 4 는 줄 안에서) + const SizedBox(height: 4), Row( children: [ Expanded( @@ -234,7 +236,7 @@ class _ReceivedSectionState extends ConsumerState<_ReceivedSection> { const SizedBox(width: 5), Pressable( pressedScale: .96, - onTap: () => ref.read(toastProvider.notifier).show('명함 받기는 곧 열릴 예정이에요', icon: RupIconName.bell), + onTap: () => ref.read(toastProvider.notifier).show('명함 받기는 곧 열릴 예정이에요', icon: RupIconName.qrcode), child: Container( padding: const EdgeInsets.symmetric(horizontal: 13, vertical: AppSpacing.sm), decoration: BoxDecoration( @@ -252,9 +254,11 @@ class _ReceivedSectionState extends ConsumerState<_ReceivedSection> { ), ), ), + const SizedBox(width: AppSpacing.xs), ], ), - const SizedBox(height: 2), + // 시안: 제목 줄 아래 12 + 목록 위 2 + const SizedBox(height: 14), Padding( padding: const EdgeInsets.fromLTRB(28, 64, 28, 48), // 시안: 받은 명함이 한 장도 없으면 검색어와 상관없이 이 문구 diff --git a/flutter/lib/features/pet_card/presentation/widgets/card_style_controls.dart b/flutter/lib/features/pet_card/presentation/widgets/card_style_controls.dart index 0959549..f15d2a7 100644 --- a/flutter/lib/features/pet_card/presentation/widgets/card_style_controls.dart +++ b/flutter/lib/features/pet_card/presentation/widgets/card_style_controls.dart @@ -153,8 +153,9 @@ class BackgroundColorRow extends StatelessWidget { _Swatch( label: '기본', onTap: () => onChanged(style.withBackgroundColor(null)), - decoration: _swatchDecoration(selected: isDefault, color: Colors.white), - child: _DefaultLabel(selected: isDefault), + // 시안: '기본' 칸은 골라도 그림자가 없다 + decoration: _swatchDecoration(selected: isDefault, color: Colors.white).copyWith(boxShadow: []), + child: _DefaultLabel(selected: isDefault, fontSize: 11), ), for (final b in kCardBackgrounds) _Swatch( @@ -176,16 +177,19 @@ class BackgroundColorRow extends StatelessWidget { } class _DefaultLabel extends StatelessWidget { - const _DefaultLabel({required this.selected}); + const _DefaultLabel({required this.selected, required this.fontSize}); final bool selected; + /// 시안: 색 줄 11, 이미지 줄 11.5 + final double fontSize; + @override Widget build(BuildContext context) { return Text( '기본', style: Theme.of(context).textTheme.bodySmall!.copyWith( - fontSize: 11.5, + fontSize: fontSize, fontWeight: FontWeight.w700, color: selected ? AppColors.primary : AppColors.muted, ), @@ -217,8 +221,13 @@ class BackgroundImageGrid extends StatelessWidget { _ImageTile( label: '기본', selected: isDefault, + shadow: false, onTap: () => onChanged(style.withBackgroundImage(null)), - child: Container(color: Colors.white, alignment: Alignment.center, child: _DefaultLabel(selected: isDefault)), + child: Container( + color: Colors.white, + alignment: Alignment.center, + child: _DefaultLabel(selected: isDefault, fontSize: 11.5), + ), ), for (final (i, path) in AppAssets.cardBackgrounds.indexed) _ImageTile( @@ -275,6 +284,7 @@ class _ImageTile extends StatelessWidget { required this.onTap, required this.child, this.onRemove, + this.shadow = true, }); final String label; @@ -283,6 +293,9 @@ class _ImageTile extends StatelessWidget { final Widget child; final VoidCallback? onRemove; + /// 골랐을 때 주황 그림자 — '기본' 칸은 없다 + final bool shadow; + @override Widget build(BuildContext context) { return Stack( @@ -299,7 +312,7 @@ class _ImageTile extends StatelessWidget { foregroundDecoration: _swatchDecoration(selected: selected).copyWith(boxShadow: []), decoration: BoxDecoration( borderRadius: BorderRadius.circular(_kSwatchRadius), - boxShadow: selected + boxShadow: selected && shadow ? [BoxShadow(color: AppColors.primary.withValues(alpha: .3), offset: const Offset(0, 2), blurRadius: 8)] : null, ), diff --git a/flutter/lib/features/pet_card/presentation/widgets/name_card.dart b/flutter/lib/features/pet_card/presentation/widgets/name_card.dart index 811743f..51d41b1 100644 --- a/flutter/lib/features/pet_card/presentation/widgets/name_card.dart +++ b/flutter/lib/features/pet_card/presentation/widgets/name_card.dart @@ -10,6 +10,7 @@ import 'package:tma_app/features/pet_card/domain/card_style.dart'; import 'package:tma_app/features/pet_card/domain/pet_card_facts.dart'; import 'package:tma_app/features/pet_card/presentation/widgets/card_background_image.dart'; import 'package:tma_app/shared/widgets/icons/rup_icon.dart'; +import 'package:tma_app/shared/widgets/pressable.dart'; /// 성별 표시 색 — 시안 GENDER const Map kGenderColors = { @@ -237,42 +238,46 @@ class _Body extends StatelessWidget { children: [ Row( children: [ - Flexible( - child: Text( - pet.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - // 시안: 충북대체 일반 굵기 + 0.7px 외곽선 → 굵게 보인다 - fontFamily: AppFonts.displayRegular, - fontWeight: FontWeight.w700, - fontSize: 22, - height: 1, - color: ink.strong, - ), + // 이름·성별 묶음이 남은 폭을 모두 차지해 공유 버튼을 오른쪽 끝으로 민다 (시안 flex:1 빈칸) + Expanded( + child: Row( + children: [ + Flexible( + child: Text( + pet.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + // 시안: 충북대체 일반 굵기 + 0.7px 외곽선 → 굵게 보인다 + fontFamily: AppFonts.displayRegular, + fontWeight: FontWeight.w700, + fontSize: 22, + height: 1, + color: ink.strong, + ), + ), + ), + const SizedBox(width: 7), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: gender.withValues(alpha: .12), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + pet.gender.label, + style: small.copyWith(fontSize: 11, fontWeight: FontWeight.w700, color: gender), + ), + ), + ], ), ), - const SizedBox(width: 7), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - color: gender.withValues(alpha: .12), - borderRadius: BorderRadius.circular(999), - ), - child: Text( - pet.gender.label, - style: small.copyWith(fontSize: 11, fontWeight: FontWeight.w700, color: gender), - ), - ), - const Spacer(), if (onShare != null) - Semantics( - button: true, - label: '${pet.name} 명함 공유', - child: GestureDetector( - onTap: onShare, - child: SvgPicture.string(_kShareGlyph, width: 32, height: 32), - ), + Pressable( + onTap: onShare, + pressedScale: .92, + semanticLabel: '${pet.name} 명함 공유', + child: SvgPicture.string(_kShareGlyph, width: 32, height: 32), ), ], ), diff --git a/flutter/lib/features/pet_card/presentation/widgets/name_card_empty.dart b/flutter/lib/features/pet_card/presentation/widgets/name_card_empty.dart index 1505f8b..26578ef 100644 --- a/flutter/lib/features/pet_card/presentation/widgets/name_card_empty.dart +++ b/flutter/lib/features/pet_card/presentation/widgets/name_card_empty.dart @@ -12,15 +12,31 @@ import 'package:tma_app/shared/widgets/pressable.dart'; const double _kMinHeight = 193; const double _kRadius = 18; +/// 누르는 동안 바탕 (시안 .rup-namecard-empty:active) +const Color _kPressedBg = Color(0xFFFCF7F2); + /// 아직 펫 명함이 없는 아이 — 이름 + 안내 + "명함 만들기" -class NameCardEmpty extends StatelessWidget { +class NameCardEmpty extends StatefulWidget { const NameCardEmpty({super.key, required this.petName, required this.onCreate}); final String petName; final VoidCallback onCreate; + @override + State createState() => _NameCardEmptyState(); +} + +class _NameCardEmptyState extends State { + bool _pressed = false; + + void _press(bool v) { + if (_pressed != v) setState(() => _pressed = v); + } + @override Widget build(BuildContext context) { + final petName = widget.petName; + final onCreate = widget.onCreate; final textTheme = Theme.of(context).textTheme; return Semantics( button: true, @@ -29,10 +45,13 @@ class NameCardEmpty extends StatelessWidget { cursor: SystemMouseCursors.click, child: GestureDetector( onTap: onCreate, + onTapDown: (_) => _press(true), + onTapUp: (_) => _press(false), + onTapCancel: () => _press(false), child: DashedBorderBox( color: AppColors.emptyCardBorder, radius: _kRadius, - backgroundColor: AppColors.emptyCardBg, + backgroundColor: _pressed ? _kPressedBg : AppColors.emptyCardBg, child: ConstrainedBox( constraints: const BoxConstraints(minHeight: _kMinHeight, minWidth: double.infinity), child: Padding( diff --git a/flutter/lib/features/pet_card/presentation/widgets/pet_id_card.dart b/flutter/lib/features/pet_card/presentation/widgets/pet_id_card.dart index 007701d..fd85c22 100644 --- a/flutter/lib/features/pet_card/presentation/widgets/pet_id_card.dart +++ b/flutter/lib/features/pet_card/presentation/widgets/pet_id_card.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.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/pet/domain/pet.dart'; import 'package:tma_app/features/pet_card/domain/card_style.dart'; import 'package:tma_app/features/pet_card/presentation/widgets/name_card.dart'; @@ -83,6 +84,7 @@ class PetIdCard extends StatelessWidget { style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontSize: 13, fontWeight: FontWeight.w800, + height: AppTypography.normalHeight, ), ), ], diff --git a/flutter/lib/features/pet_card/presentation/widgets/pick_pet_sheet.dart b/flutter/lib/features/pet_card/presentation/widgets/pick_pet_sheet.dart index d2ce675..015d939 100644 --- a/flutter/lib/features/pet_card/presentation/widgets/pick_pet_sheet.dart +++ b/flutter/lib/features/pet_card/presentation/widgets/pick_pet_sheet.dart @@ -30,7 +30,7 @@ Future showPickPetSheet(BuildContext context, {required List pets} Text( '어떤 반려동물의 카드를 만들까요?\n등록된 정보로 카드가 자동 채워져요.', textAlign: TextAlign.center, - style: textTheme.bodySmall!.copyWith(fontSize: 12.5, color: AppColors.muted, height: 1.5), + style: textTheme.bodySmall!.copyWith(fontSize: 12.5, color: AppColors.muted, height: AppTypography.normalHeight), ), const SizedBox(height: 18), for (final (i, p) in pets.indexed) ...[