generated from ttipo/project-base
fix(flutter): 마이페이지·아이 정보·로그인 시안 점검 반영
- 반려동물 정보에서 뒤로 가면 마이페이지 첫 화면으로 간다 (시안 동작). 명함 칩은 명함이 있으면 펫 명함 탭으로, 없으면 명함 관리로 간다. - 사진이 없는 칸은 시안 바탕색으로 채우고, 아이 이름은 제목 서체 800 으로 맞췄다 (보관함 화면 포함). - 아이 정보를 수정하면 그 아이를 선택한 채로 돌아온다. 삭제하기는 시안처럼 글자 버튼으로. - 로그인: 인사말은 화면 위에서 96, 버튼 묶음은 아래에서 76 (상태바·홈 인디케이터가 크면 비켜 준다). 소셜 로그인 버튼은 누르면 크기는 그대로 두고 어두워진다. - 온보딩 그림, 프로필 수정·몸무게 단위 시트 간격을 시안 값으로 맞췄다. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user