diff --git a/flutter/assets/svg/common/art_card.svg b/flutter/assets/svg/common/art_card.svg
new file mode 100644
index 0000000..48b466f
--- /dev/null
+++ b/flutter/assets/svg/common/art_card.svg
@@ -0,0 +1,25 @@
+
\ No newline at end of file
diff --git a/flutter/assets/svg/common/art_rup.svg b/flutter/assets/svg/common/art_rup.svg
new file mode 100644
index 0000000..5d189ff
--- /dev/null
+++ b/flutter/assets/svg/common/art_rup.svg
@@ -0,0 +1,31 @@
+
\ No newline at end of file
diff --git a/flutter/lib/core/constants/app_assets.dart b/flutter/lib/core/constants/app_assets.dart
index c73a332..a0ad6a0 100644
--- a/flutter/lib/core/constants/app_assets.dart
+++ b/flutter/lib/core/constants/app_assets.dart
@@ -40,6 +40,10 @@ abstract final class AppAssets {
static const String addPhoto = 'assets/svg/common/add_photo.svg';
static const String addCard = 'assets/svg/common/add_card.svg';
+ /// 마이페이지 통계 줄의 그림 아이콘 — 반려동물 / 명함 관리
+ static const String artRup = 'assets/svg/common/art_rup.svg';
+ static const String artCard = 'assets/svg/common/art_card.svg';
+
// 브랜드 로고 (명함 헤더 등)
static const String logo = 'assets/images/brand/rup_logo.png';
static const String logoWhite = 'assets/images/brand/rup_logo_white.png';
diff --git a/flutter/lib/core/constants/app_info.dart b/flutter/lib/core/constants/app_info.dart
new file mode 100644
index 0000000..9fa1084
--- /dev/null
+++ b/flutter/lib/core/constants/app_info.dart
@@ -0,0 +1,3 @@
+/// 앱 정보 — 화면에 보이는 버전 표기.
+/// pubspec.yaml 의 version(빌드 번호 제외)과 맞춘다.
+const String kAppVersion = '1.0.0';
diff --git a/flutter/lib/core/router/app_router.dart b/flutter/lib/core/router/app_router.dart
index 35a513b..ba23ca3 100644
--- a/flutter/lib/core/router/app_router.dart
+++ b/flutter/lib/core/router/app_router.dart
@@ -7,7 +7,13 @@ import 'package:tma_app/features/auth/presentation/pages/login_page.dart';
import 'package:tma_app/features/entry/presentation/providers/entry_provider.dart';
import 'package:tma_app/features/home/presentation/pages/home_page.dart';
import 'package:tma_app/features/intro/presentation/pages/intro_page.dart';
+import 'package:tma_app/features/mypage/domain/policy_doc.dart';
+import 'package:tma_app/features/mypage/presentation/pages/my_page.dart';
+import 'package:tma_app/features/mypage/presentation/pages/my_pets_page.dart';
+import 'package:tma_app/features/mypage/presentation/pages/policy_doc_page.dart';
+import 'package:tma_app/features/mypage/presentation/pages/settings_page.dart';
import 'package:tma_app/features/onboarding/presentation/pages/onboarding_page.dart';
+import 'package:tma_app/features/pet/presentation/pages/pet_info_page.dart';
import 'package:tma_app/features/pet/presentation/pages/pet_register_page.dart';
import 'package:tma_app/features/pet_card/presentation/pages/pet_card_detail_page.dart';
import 'package:tma_app/features/pet_card/presentation/pages/pet_card_editor_page.dart';
@@ -131,8 +137,38 @@ final appRouterProvider = Provider((ref) {
routes: [
GoRoute(
path: AppRoutes.myPage,
- builder: (context, state) =>
- const TabPlaceholderPage(title: '마이페이지'),
+ builder: (context, state) => const MyPage(),
+ // 탭바를 둔 채 넘어가는 하위 화면
+ routes: [
+ GoRoute(
+ path: 'pets',
+ builder: (context, state) => const MyPetsPage(),
+ routes: [
+ GoRoute(
+ path: ':petId',
+ builder: (context, state) =>
+ PetInfoPage(petId: state.pathParameters['petId']!),
+ ),
+ ],
+ ),
+ GoRoute(
+ path: 'settings',
+ builder: (context, state) => const SettingsPage(),
+ routes: [
+ GoRoute(
+ path: 'policy/:doc',
+ // 모르는 문서 이름이면 설정으로 돌려보낸다
+ redirect: (context, state) =>
+ PolicyDoc.byName(state.pathParameters['doc']) == null
+ ? AppRoutes.settings
+ : null,
+ builder: (context, state) => PolicyDocPage(
+ doc: PolicyDoc.byName(state.pathParameters['doc'])!,
+ ),
+ ),
+ ],
+ ),
+ ],
),
],
),
diff --git a/flutter/lib/core/router/app_routes.dart b/flutter/lib/core/router/app_routes.dart
index d428df1..78c1a39 100644
--- a/flutter/lib/core/router/app_routes.dart
+++ b/flutter/lib/core/router/app_routes.dart
@@ -1,4 +1,5 @@
import 'package:tma_app/features/entry/domain/entry_state.dart';
+import 'package:tma_app/features/mypage/domain/policy_doc.dart';
/// 라우트 경로 상수
abstract final class AppRoutes {
@@ -13,6 +14,12 @@ abstract final class AppRoutes {
static const String petCards = '/pet-cards';
static const String myPage = '/mypage';
+ // 마이페이지 하위 화면 (탭바 유지)
+ static const String myPets = '/mypage/pets';
+ static String petInfo(String petId) => '/mypage/pets/$petId';
+ static const String settings = '/mypage/settings';
+ static String policy(PolicyDoc doc) => '/mypage/settings/policy/${doc.name}';
+
// 탭 위에 덮는 화면 (탭바 없음)
static const String petRegister = '/pets/new';
static const String petEditPattern = '/pets/:petId/edit';
diff --git a/flutter/lib/features/auth/data/repositories/auth_repository.dart b/flutter/lib/features/auth/data/repositories/auth_repository.dart
index 8dd1e2f..893a5ad 100644
--- a/flutter/lib/features/auth/data/repositories/auth_repository.dart
+++ b/flutter/lib/features/auth/data/repositories/auth_repository.dart
@@ -13,6 +13,9 @@ abstract interface class AuthRepository {
/// 인사말에 쓰는 이름 — 소셜 계정 프로필에서 받는다. 모르면 null
String? get nickname;
+ /// 로그인한 소셜 계정 종류 — 로그인 전이면 null
+ SocialProvider? get provider;
+
Future signIn(SocialProvider provider);
Future signOut();
@@ -34,6 +37,10 @@ class DemoAuthRepository implements AuthRepository {
@override
String? get nickname => null;
+ @override
+ SocialProvider? get provider =>
+ SocialProvider.values.asNameMap()[_prefs.getString(_kDemoSessionKey)];
+
@override
Future signIn(SocialProvider provider) async {
await _prefs.setString(_kDemoSessionKey, provider.name);
diff --git a/flutter/lib/features/auth/domain/social_provider.dart b/flutter/lib/features/auth/domain/social_provider.dart
index 8399ff8..4c021cc 100644
--- a/flutter/lib/features/auth/domain/social_provider.dart
+++ b/flutter/lib/features/auth/domain/social_provider.dart
@@ -1,2 +1,12 @@
/// 지원하는 소셜 로그인 제공자
-enum SocialProvider { naver, kakao, google, apple }
+enum SocialProvider {
+ naver('네이버'),
+ kakao('카카오'),
+ google('구글'),
+ apple('Apple');
+
+ const SocialProvider(this.label);
+
+ /// 화면 표기 — "카카오 계정으로 로그인했어요"
+ final String label;
+}
diff --git a/flutter/lib/features/auth/presentation/providers/user_provider.dart b/flutter/lib/features/auth/presentation/providers/user_provider.dart
index 082bf01..ab59779 100644
--- a/flutter/lib/features/auth/presentation/providers/user_provider.dart
+++ b/flutter/lib/features/auth/presentation/providers/user_provider.dart
@@ -1,12 +1,22 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:tma_app/features/auth/data/repositories/auth_repository.dart';
+import 'package:tma_app/features/auth/domain/social_provider.dart';
import 'package:tma_app/features/entry/presentation/providers/entry_provider.dart';
+import 'package:tma_app/features/mypage/data/profile_repository.dart';
/// 이름을 모를 때 부르는 호칭 — 로그인 화면의 "반가워요, 집사님!" 과 맞춘다
const String kDefaultNickname = '집사';
-/// 인사말에 쓰는 사용자 이름 (로그인·로그아웃하면 다시 읽는다)
+/// 인사말에 쓰는 사용자 이름 — 앱에서 바꾼 닉네임 > 소셜 계정 이름 > 기본 호칭
final userNicknameProvider = Provider((ref) {
ref.watch(entryProvider.select((s) => s.signedIn));
- return ref.watch(authRepositoryProvider).nickname ?? kDefaultNickname;
+ return ref.watch(profileProvider).nickname ??
+ ref.watch(authRepositoryProvider).nickname ??
+ kDefaultNickname;
+});
+
+/// 로그인한 소셜 계정 종류
+final signedInProviderProvider = Provider((ref) {
+ ref.watch(entryProvider.select((s) => s.signedIn));
+ return ref.watch(authRepositoryProvider).provider;
});
diff --git a/flutter/lib/features/home/presentation/pages/home_page.dart b/flutter/lib/features/home/presentation/pages/home_page.dart
index 676c365..e2b4425 100644
--- a/flutter/lib/features/home/presentation/pages/home_page.dart
+++ b/flutter/lib/features/home/presentation/pages/home_page.dart
@@ -47,7 +47,7 @@ class HomePage extends ConsumerWidget {
title: '우리 아이',
sub: true,
trailing: '아이 상세보기',
- onTrailingTap: () => context.go(AppRoutes.myPage),
+ onTrailingTap: () => context.go(AppRoutes.myPets),
),
),
),
diff --git a/flutter/lib/features/home/presentation/widgets/home_schedule_section.dart b/flutter/lib/features/home/presentation/widgets/home_schedule_section.dart
index f28c529..72c299e 100644
--- a/flutter/lib/features/home/presentation/widgets/home_schedule_section.dart
+++ b/flutter/lib/features/home/presentation/widgets/home_schedule_section.dart
@@ -19,11 +19,13 @@ class HomeScheduleSection extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
+ // 시안: 이 섹션 제목만 기본 위 여백(26)이 살아 있어 다른 섹션보다 조금 아래에 있다
+ const SizedBox(height: AppSpacing.xs),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
child: SectionTitle(title: '일정', trailing: '전체보기', onTrailingTap: onSeeAll),
),
- const SizedBox(height: 17),
+ const SizedBox(height: 21),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.xs),
child: SectionTitle(
diff --git a/flutter/lib/features/mypage/data/app_settings_repository.dart b/flutter/lib/features/mypage/data/app_settings_repository.dart
new file mode 100644
index 0000000..277e63b
--- /dev/null
+++ b/flutter/lib/features/mypage/data/app_settings_repository.dart
@@ -0,0 +1,37 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:tma_app/core/storage/preferences_provider.dart';
+
+/// 마이페이지 설정 스위치 — 앱 안의 선호값이다.
+/// (카메라·갤러리 권한 자체는 OS 가 관리하므로, 실제 권한 요청은 해당 기능을 쓸 때 한다)
+enum AppSetting {
+ camera('settings.camera', true),
+ gallery('settings.gallery', true),
+ scheduleNotification('settings.notiSchedule', true),
+ eventNotification('settings.notiEvent', false),
+ batteryOptimizationExcluded('settings.batteryOpt', false);
+
+ const AppSetting(this.key, this.defaultValue);
+
+ final String key;
+ final bool defaultValue;
+}
+
+class AppSettingsNotifier extends Notifier