From 1f7693898c50dab62803899a1c22d7e1cee33c54 Mon Sep 17 00:00:00 2001 From: TtiPo Date: Tue, 29 Sep 2026 01:24:25 +0900 Subject: [PATCH] =?UTF-8?q?fix(flutter):=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=C2=B7=EC=95=84=EC=9D=B4=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=C2=B7=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=EC=95=88=20?= =?UTF-8?q?=EC=A0=90=EA=B2=80=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 반려동물 정보에서 뒤로 가면 마이페이지 첫 화면으로 간다 (시안 동작). 명함 칩은 명함이 있으면 펫 명함 탭으로, 없으면 명함 관리로 간다. - 사진이 없는 칸은 시안 바탕색으로 채우고, 아이 이름은 제목 서체 800 으로 맞췄다 (보관함 화면 포함). - 아이 정보를 수정하면 그 아이를 선택한 채로 돌아온다. 삭제하기는 시안처럼 글자 버튼으로. - 로그인: 인사말은 화면 위에서 96, 버튼 묶음은 아래에서 76 (상태바·홈 인디케이터가 크면 비켜 준다). 소셜 로그인 버튼은 누르면 크기는 그대로 두고 어두워진다. - 온보딩 그림, 프로필 수정·몸무게 단위 시트 간격을 시안 값으로 맞췄다. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../pages/archived_pet_info_page.dart | 8 ++- .../presentation/pages/archived_pet_page.dart | 1 + .../auth/presentation/pages/login_page.dart | 50 ++++++++----------- .../widgets/social_login_button.dart | 3 +- .../widgets/profile_edit_sheet.dart | 3 ++ .../presentation/pages/onboarding_page.dart | 44 +++++++++++++--- .../pet/presentation/pages/pet_info_page.dart | 20 ++++++-- .../presentation/pages/pet_register_page.dart | 27 +++++++--- .../presentation/widgets/pet_pick_sheets.dart | 8 ++- 9 files changed, 113 insertions(+), 51 deletions(-) diff --git a/flutter/lib/features/archive/presentation/pages/archived_pet_info_page.dart b/flutter/lib/features/archive/presentation/pages/archived_pet_info_page.dart index f6f5db5..d3553a7 100644 --- a/flutter/lib/features/archive/presentation/pages/archived_pet_info_page.dart +++ b/flutter/lib/features/archive/presentation/pages/archived_pet_info_page.dart @@ -39,7 +39,13 @@ class ArchivedPetInfoPage extends ConsumerWidget { children: [ Text( pet.name, - style: const TextStyle(fontFamily: AppFonts.display, fontSize: 22, height: 1, color: AppColors.ink), + style: const TextStyle( + fontFamily: AppFonts.display, + fontWeight: FontWeight.w800, + fontSize: 22, + height: 1, + color: AppColors.ink, + ), ), const SizedBox(height: 5), Text( diff --git a/flutter/lib/features/archive/presentation/pages/archived_pet_page.dart b/flutter/lib/features/archive/presentation/pages/archived_pet_page.dart index 0f1098a..32fc77b 100644 --- a/flutter/lib/features/archive/presentation/pages/archived_pet_page.dart +++ b/flutter/lib/features/archive/presentation/pages/archived_pet_page.dart @@ -171,6 +171,7 @@ class _MemorialHero extends StatelessWidget { pet.name, style: const TextStyle( fontFamily: AppFonts.display, + fontWeight: FontWeight.w800, fontSize: 25, height: 1.1, color: Colors.white, diff --git a/flutter/lib/features/auth/presentation/pages/login_page.dart b/flutter/lib/features/auth/presentation/pages/login_page.dart index a8bcb9f..1dcfc64 100644 --- a/flutter/lib/features/auth/presentation/pages/login_page.dart +++ b/flutter/lib/features/auth/presentation/pages/login_page.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -22,7 +24,9 @@ const Size _kArtSize = Size(327, 179); const Alignment _kArtAlignment = Alignment(.33, .68); /// 시안: 버튼 묶음은 화면 아래에서 76 (홈 인디케이터 34 포함) -const double _kButtonsBottomGap = 42; +/// 시안: 인사말은 화면 위에서 96, 버튼 묶음은 화면 아래에서 76 (.rup-login__head / __btns) +const double _kHeadTop = 96; +const double _kButtonsBottom = 76; const double _kButtonGap = 15; /// 로그인 — 소셜 계정으로 가입/로그인한다. @@ -55,6 +59,10 @@ class _LoginPageState extends ConsumerState { @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; + final insets = MediaQuery.paddingOf(context); + // 화면 가장자리 기준 위치를 따르되, 상태바·홈 인디케이터가 크면 그만큼 비켜 준다 + final headGap = math.max(_kHeadTop - insets.top - AppControlSize.navBarHeight, AppSpacing.xxl); + final buttonsBottom = math.max(_kButtonsBottom, insets.bottom + AppSpacing.xl); return AnnotatedRegion( value: SystemUiOverlayStyle.dark, child: Scaffold( @@ -66,12 +74,7 @@ class _LoginPageState extends ConsumerState { children: [ _LoginNavBar(onBack: () => context.go(AppRoutes.intro)), Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.xl, - AppSpacing.xxl, - AppSpacing.xl, - 0, - ), + padding: EdgeInsets.fromLTRB(AppSpacing.xl, headGap, AppSpacing.xl, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -93,28 +96,19 @@ class _LoginPageState extends ConsumerState { const Expanded( child: Align(alignment: _kArtAlignment, child: _LoginArt()), ), - SafeArea( - top: false, - minimum: const EdgeInsets.only(bottom: AppSpacing.xl), - child: Padding( - padding: const EdgeInsets.fromLTRB( - AppSpacing.xl, - AppSpacing.lg, - AppSpacing.xl, - _kButtonsBottomGap, - ), - child: Column( - children: [ - for (final provider in SocialProvider.values) ...[ - if (provider != SocialProvider.values.first) - const SizedBox(height: _kButtonGap), - SocialLoginButton( - provider: provider, - onPressed: _pending == null ? () => _signIn(provider) : null, - ), - ], + Padding( + padding: EdgeInsets.fromLTRB(AppSpacing.xl, AppSpacing.lg, AppSpacing.xl, buttonsBottom), + child: Column( + children: [ + for (final provider in SocialProvider.values) ...[ + if (provider != SocialProvider.values.first) + const SizedBox(height: _kButtonGap), + SocialLoginButton( + provider: provider, + onPressed: _pending == null ? () => _signIn(provider) : null, + ), ], - ), + ], ), ), ], diff --git a/flutter/lib/features/auth/presentation/widgets/social_login_button.dart b/flutter/lib/features/auth/presentation/widgets/social_login_button.dart index 5055abd..4f87634 100644 --- a/flutter/lib/features/auth/presentation/widgets/social_login_button.dart +++ b/flutter/lib/features/auth/presentation/widgets/social_login_button.dart @@ -86,7 +86,8 @@ class SocialLoginButton extends StatelessWidget { final style = _styleOf(provider); return Pressable( onTap: onPressed, - pressedScale: .98, + pressedScale: 1, + pressedBrightness: .95, semanticLabel: style.label, child: Container( height: AppControlSize.buttonHeight, diff --git a/flutter/lib/features/mypage/presentation/widgets/profile_edit_sheet.dart b/flutter/lib/features/mypage/presentation/widgets/profile_edit_sheet.dart index 3884545..c006f43 100644 --- a/flutter/lib/features/mypage/presentation/widgets/profile_edit_sheet.dart +++ b/flutter/lib/features/mypage/presentation/widgets/profile_edit_sheet.dart @@ -6,6 +6,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:tma_app/core/constants/app_assets.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/auth/presentation/providers/user_provider.dart'; import 'package:tma_app/features/mypage/data/profile_repository.dart'; @@ -125,6 +126,8 @@ class _ProfileEditSheetState extends ConsumerState<_ProfileEditSheet> { onSubmitted: (_) => unawaited(_submit()), ), ), + // 시안 .rup-field 아래 16 (바닥 버튼 위 18 과 겹치지 않는다) + const SizedBox(height: AppSpacing.lg), RupSheetBleedButton(label: '수정 완료', onPressed: () => unawaited(_submit())), ], ), diff --git a/flutter/lib/features/onboarding/presentation/pages/onboarding_page.dart b/flutter/lib/features/onboarding/presentation/pages/onboarding_page.dart index a63b126..819fc94 100644 --- a/flutter/lib/features/onboarding/presentation/pages/onboarding_page.dart +++ b/flutter/lib/features/onboarding/presentation/pages/onboarding_page.dart @@ -7,7 +7,6 @@ import 'package:tma_app/core/constants/app_colors.dart'; import 'package:tma_app/core/constants/app_sizes.dart'; import 'package:tma_app/features/entry/presentation/providers/entry_provider.dart'; import 'package:tma_app/features/onboarding/presentation/widgets/onboarding_scene.dart'; -import 'package:tma_app/shared/widgets/design_art_box.dart'; import 'package:tma_app/shared/widgets/rup_button.dart'; /// 온보딩 CTA 높이 — 시안 onb-4 의 버튼(372×42, rx 21) @@ -59,11 +58,8 @@ class _OnboardingPageState extends ConsumerState { child: Stack( fit: StackFit.expand, children: [ - DesignCover( - designSize: kOnboardingCanvas, - // 장이 바뀌면 등장 애니메이션을 처음부터 다시 재생한다 - child: OnboardingScene(key: ValueKey(slide), slide: slide), - ), + // 장이 바뀌면 등장 애니메이션을 처음부터 다시 재생한다 + _OnboardingArt(child: OnboardingScene(key: ValueKey(slide), slide: slide)), Positioned( left: 0, right: 0, @@ -119,3 +115,39 @@ class _Hint extends StatelessWidget { ); } } + +/// 시안 .rup-onb__art — 402 폭 기기에서 414×897 그림을 (-6, -11) 에 놓는다. +/// 폭에 맞춰 같은 비율로 키우고, 키가 큰 화면에서 아래가 비면 그만큼 더 키워 가운데 둔다 +class _OnboardingArt extends StatelessWidget { + const _OnboardingArt({required this.child}); + + final Widget child; + + static const double _deviceWidth = 402; + static const Rect _art = Rect.fromLTWH(-6, -11, 414, 897); + + @override + Widget build(BuildContext context) { + return ClipRect( + child: LayoutBuilder( + builder: (context, box) { + final unit = box.maxWidth / _deviceWidth; + var rect = Rect.fromLTWH(_art.left * unit, _art.top * unit, _art.width * unit, _art.height * unit); + if (rect.bottom < box.maxHeight) { + final grow = (box.maxHeight - rect.top) / rect.height; + final width = rect.width * grow; + rect = Rect.fromLTWH((box.maxWidth - width) / 2, rect.top, width, rect.height * grow); + } + return Stack( + children: [ + Positioned.fromRect( + rect: rect, + child: FittedBox(fit: BoxFit.fill, child: SizedBox.fromSize(size: kOnboardingCanvas, child: child)), + ), + ], + ); + }, + ), + ); + } +} diff --git a/flutter/lib/features/pet/presentation/pages/pet_info_page.dart b/flutter/lib/features/pet/presentation/pages/pet_info_page.dart index 415de51..40ae0db 100644 --- a/flutter/lib/features/pet/presentation/pages/pet_info_page.dart +++ b/flutter/lib/features/pet/presentation/pages/pet_info_page.dart @@ -27,6 +27,9 @@ import 'package:tma_app/shared/widgets/tab_sub_page.dart'; const double _kPhotoSize = 76; const double _kPhotoRadius = 20; const Color _kPhotoBorder = Color(0xFFC8C8C8); + +/// 사진이 없을 때 칸 바탕 (시안 image-slot rgba(0,0,0,.04)) +const Color _kEmptyPhotoFill = Color(0x0A000000); const double _kNormal = AppTypography.normalHeight; /// 반려동물 정보 — 기본·건강 정보를 읽기 전용으로 보여 주고, 연필을 누르면 수정 @@ -58,7 +61,7 @@ class PetInfoPage extends ConsumerWidget { child: SubPageHeader( title: '반려동물 정보', subtitle: '${pet.name}의 상세 정보', - onBack: () => context.pop(), + onBack: () => context.go(AppRoutes.myPage), trailing: Pressable( onTap: () => unawaited(context.push(AppRoutes.petEdit(pet.id))), pressedScale: .9, @@ -74,9 +77,9 @@ class PetInfoPage extends ConsumerWidget { child: _IdentityCard( pet: pet, cardCount: cardCount, - onCardTap: () => unawaited( - context.push(cardCount == 0 ? AppRoutes.petCardCreate(pet.id) : AppRoutes.petCardManage), - ), + onCardTap: () => cardCount == 0 + ? unawaited(context.push(AppRoutes.petCardManage)) + : context.go(AppRoutes.petCards), ), ), ...petInfoSections(pet), @@ -161,6 +164,7 @@ class _IdentityCard extends StatelessWidget { width: _kPhotoSize, height: _kPhotoSize, decoration: BoxDecoration( + color: photo == null ? _kEmptyPhotoFill : null, borderRadius: BorderRadius.circular(_kPhotoRadius), border: Border.all(color: _kPhotoBorder, width: 1.5), ), @@ -178,7 +182,13 @@ class _IdentityCard extends StatelessWidget { pet.name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle(fontFamily: AppFonts.display, fontSize: 22, height: 1, color: AppColors.ink), + style: const TextStyle( + fontFamily: AppFonts.display, + fontWeight: FontWeight.w800, + fontSize: 22, + height: 1, + color: AppColors.ink, + ), ), const SizedBox(height: 5), Text( diff --git a/flutter/lib/features/pet/presentation/pages/pet_register_page.dart b/flutter/lib/features/pet/presentation/pages/pet_register_page.dart index b13d169..55492fd 100644 --- a/flutter/lib/features/pet/presentation/pages/pet_register_page.dart +++ b/flutter/lib/features/pet/presentation/pages/pet_register_page.dart @@ -7,6 +7,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:tma_app/core/constants/app_colors.dart'; import 'package:tma_app/core/constants/app_sizes.dart'; import 'package:tma_app/core/router/app_routes.dart'; +import 'package:tma_app/core/theme/app_typography.dart'; import 'package:tma_app/core/utils/date_format.dart'; import 'package:tma_app/core/utils/korean.dart'; import 'package:tma_app/features/diary/presentation/providers/diary_providers.dart'; @@ -244,6 +245,7 @@ class _PetRegisterFormState extends ConsumerState<_PetRegisterForm> { if (mounted) context.go(AppRoutes.myPets); } else { await notifier.updatePet(editing.id, _buildInput()); + ref.read(selectedPetIdProvider.notifier).select(editing.id); toast.show('반려동물 정보를 저장했어요'); // 수정은 반려동물 정보에서 열었으므로 그 화면으로 돌아간다 if (mounted) context.pop(); @@ -300,8 +302,8 @@ class _PetRegisterFormState extends ConsumerState<_PetRegisterForm> { onBack: () => context.pop(), ), const SizedBox(height: 18), + // 시안: 사진 아래 4 는 "기본 정보" 위 30 과 겹쳐 사라진다 PetPhotoSlot(photoPath: _photoPath, onPick: _pickPhoto), - const SizedBox(height: AppSpacing.xs), const PetFormSectionLabel(text: '기본 정보', required: true), RupField( @@ -468,13 +470,22 @@ class _PetRegisterFormState extends ConsumerState<_PetRegisterForm> { if (editing != null) Padding( padding: const EdgeInsets.only(top: 22), - child: TextButton( - onPressed: _delete, - child: Text( - '삭제하기', - style: textTheme.bodyMedium!.copyWith( - fontWeight: FontWeight.w700, - color: AppColors.muted, + child: Semantics( + button: true, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _delete, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: AppSpacing.sm), + child: Text( + '삭제하기', + textAlign: TextAlign.center, + style: textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w700, + color: AppColors.muted, + height: AppTypography.normalHeight, + ), + ), ), ), ), diff --git a/flutter/lib/features/pet/presentation/widgets/pet_pick_sheets.dart b/flutter/lib/features/pet/presentation/widgets/pet_pick_sheets.dart index 11c0b32..4f824f0 100644 --- a/flutter/lib/features/pet/presentation/widgets/pet_pick_sheets.dart +++ b/flutter/lib/features/pet/presentation/widgets/pet_pick_sheets.dart @@ -219,12 +219,15 @@ class _BleedList extends StatelessWidget { /// 선택지 한 줄 — 고른 항목은 연주황 바탕 + 체크 class _ChoiceRow extends StatelessWidget { - const _ChoiceRow({required this.label, required this.selected, required this.onTap}); + const _ChoiceRow({required this.label, required this.selected, required this.onTap, this.vertical = 13}); final String label; final bool selected; final VoidCallback onTap; + /// 세로 여백 — 종 선택 13, 몸무게 단위 14 + final double vertical; + @override Widget build(BuildContext context) { return Semantics( @@ -236,7 +239,7 @@ class _ChoiceRow extends StatelessWidget { behavior: HitTestBehavior.opaque, onTap: onTap, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), + padding: EdgeInsets.symmetric(horizontal: 14, vertical: vertical), decoration: BoxDecoration( color: selected ? AppColors.primary50 : Colors.transparent, borderRadius: BorderRadius.circular(12), @@ -280,6 +283,7 @@ Future showWeightUnitPicker(BuildContext context, {required WeightU _ChoiceRow( label: unit.label, selected: unit == current, + vertical: 14, onTap: () => Navigator.of(context).pop(unit), ), ],