generated from ttipo/project-base
fix(flutter): 펫 명함 화면 시안 점검 반영
- 새 명함은 그 아이의 고유색 한 가지로 시작한다 (CardStyle.forPet — 직접 선택 칸이 켜진다). - 명함 이름이 길어도 윗줄(이름·성별·공유 버튼)이 깨지지 않게 하고, 공유 버튼에 누름 효과를 넣었다. - 꾸미기에서 저장하고 돌아오면 상세히 보기로 돌아온다. - 빈 명함 칸 누름 바탕, '기본' 칸은 골라도 그림자 없음, 명함 받기 안내 아이콘(QR) 등 상세·관리·목록 화면의 간격과 줄 높이를 시안 값으로 맞췄다. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -68,6 +68,12 @@ class _PetCardDetailPageState extends ConsumerState<PetCardDetailPage> {
|
||||
toast.show('대표 명함으로 설정했어요', icon: RupIconName.star);
|
||||
}
|
||||
|
||||
/// 꾸미기 — 저장하고 돌아오면 상세히로 보여 준다 (시안 onSave → setMode('detail'))
|
||||
Future<void> _edit(PetCard card) async {
|
||||
final saved = await context.push<bool>(AppRoutes.petCardEdit(card.id));
|
||||
if (saved == true && mounted) setState(() => _mode = CardViewMode.detail);
|
||||
}
|
||||
|
||||
Future<void> _delete(PetCard card) async {
|
||||
final ok = await showRupConfirm(
|
||||
context,
|
||||
@@ -111,6 +117,8 @@ class _PetCardDetailPageState extends ConsumerState<PetCardDetailPage> {
|
||||
title: pet.name,
|
||||
subtitle: '펫 명함',
|
||||
displayFont: true,
|
||||
// 시안 상세 머리 margin '4px 0 16px'
|
||||
bottomGap: 16,
|
||||
onBack: () => context.pop(),
|
||||
trailing: SegToggle<CardViewMode>(
|
||||
first: (CardViewMode.simple, CardViewMode.simple.label),
|
||||
@@ -146,7 +154,7 @@ class _PetCardDetailPageState extends ConsumerState<PetCardDetailPage> {
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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('펫 명함을 만들었어요');
|
||||
|
||||
@@ -58,7 +58,7 @@ class _PetCardManagePageState extends ConsumerState<PetCardManagePage> {
|
||||
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<PetCardManagePage> {
|
||||
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: [
|
||||
|
||||
@@ -67,7 +67,7 @@ class _PetCardsPageState extends ConsumerState<PetCardsPage> {
|
||||
),
|
||||
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),
|
||||
// 시안: 받은 명함이 한 장도 없으면 검색어와 상관없이 이 문구
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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<PetGender, Color> 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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<NameCardEmpty> createState() => _NameCardEmptyState();
|
||||
}
|
||||
|
||||
class _NameCardEmptyState extends State<NameCardEmpty> {
|
||||
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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -30,7 +30,7 @@ Future<String?> showPickPetSheet(BuildContext context, {required List<Pet> 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) ...[
|
||||
|
||||
Reference in New Issue
Block a user