fix(flutter): 마이페이지·아이 정보·로그인 시안 점검 반영

- 반려동물 정보에서 뒤로 가면 마이페이지 첫 화면으로 간다 (시안 동작).
  명함 칩은 명함이 있으면 펫 명함 탭으로, 없으면 명함 관리로 간다.
- 사진이 없는 칸은 시안 바탕색으로 채우고, 아이 이름은 제목 서체 800 으로 맞췄다 (보관함 화면 포함).
- 아이 정보를 수정하면 그 아이를 선택한 채로 돌아온다. 삭제하기는 시안처럼 글자 버튼으로.
- 로그인: 인사말은 화면 위에서 96, 버튼 묶음은 아래에서 76 (상태바·홈 인디케이터가 크면 비켜 준다).
  소셜 로그인 버튼은 누르면 크기는 그대로 두고 어두워진다.
- 온보딩 그림, 프로필 수정·몸무게 단위 시트 간격을 시안 값으로 맞췄다.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-29 01:24:25 +09:00
parent 0c16763943
commit 1f7693898c
9 changed files with 113 additions and 51 deletions
@@ -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(
@@ -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,
@@ -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<LoginPage> {
@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<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle.dark,
child: Scaffold(
@@ -66,12 +74,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
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<LoginPage> {
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,
),
],
),
],
),
),
],
@@ -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,
@@ -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())),
],
),
@@ -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<OnboardingPage> {
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)),
),
],
);
},
),
);
}
}
@@ -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(
@@ -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,
),
),
),
),
),
@@ -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<WeightUnit?> showWeightUnitPicker(BuildContext context, {required WeightU
_ChoiceRow(
label: unit.label,
selected: unit == current,
vertical: 14,
onTap: () => Navigator.of(context).pop(unit),
),
],