timsquad 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.ko.md +288 -0
  2. package/README.md +158 -151
  3. package/dist/commands/compile.d.ts +3 -0
  4. package/dist/commands/compile.d.ts.map +1 -0
  5. package/dist/commands/compile.js +170 -0
  6. package/dist/commands/compile.js.map +1 -0
  7. package/dist/commands/daemon.d.ts.map +1 -1
  8. package/dist/commands/daemon.js +94 -5
  9. package/dist/commands/daemon.js.map +1 -1
  10. package/dist/commands/init.d.ts.map +1 -1
  11. package/dist/commands/init.js +12 -3
  12. package/dist/commands/init.js.map +1 -1
  13. package/dist/commands/skills.d.ts +12 -0
  14. package/dist/commands/skills.d.ts.map +1 -0
  15. package/dist/commands/skills.js +231 -0
  16. package/dist/commands/skills.js.map +1 -0
  17. package/dist/commands/upgrade.js +5 -0
  18. package/dist/commands/upgrade.js.map +1 -1
  19. package/dist/daemon/entry.js +3 -3
  20. package/dist/daemon/entry.js.map +1 -1
  21. package/dist/daemon/index.d.ts +3 -2
  22. package/dist/daemon/index.d.ts.map +1 -1
  23. package/dist/daemon/index.js +137 -45
  24. package/dist/daemon/index.js.map +1 -1
  25. package/dist/daemon/meta-cache.d.ts +1 -0
  26. package/dist/daemon/meta-cache.d.ts.map +1 -1
  27. package/dist/daemon/meta-cache.js +9 -0
  28. package/dist/daemon/meta-cache.js.map +1 -1
  29. package/dist/daemon/session-state.d.ts +19 -0
  30. package/dist/daemon/session-state.d.ts.map +1 -0
  31. package/dist/daemon/session-state.js +132 -0
  32. package/dist/daemon/session-state.js.map +1 -0
  33. package/dist/daemon/shutdown.d.ts.map +1 -1
  34. package/dist/daemon/shutdown.js +7 -1
  35. package/dist/daemon/shutdown.js.map +1 -1
  36. package/dist/index.js +4 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/lib/compile-rules.d.ts +66 -0
  39. package/dist/lib/compile-rules.d.ts.map +1 -0
  40. package/dist/lib/compile-rules.js +114 -0
  41. package/dist/lib/compile-rules.js.map +1 -0
  42. package/dist/lib/compiler.d.ts +105 -0
  43. package/dist/lib/compiler.d.ts.map +1 -0
  44. package/dist/lib/compiler.js +368 -0
  45. package/dist/lib/compiler.js.map +1 -0
  46. package/dist/lib/config.d.ts +1 -0
  47. package/dist/lib/config.d.ts.map +1 -1
  48. package/dist/lib/config.js +8 -1
  49. package/dist/lib/config.js.map +1 -1
  50. package/dist/lib/template.d.ts.map +1 -1
  51. package/dist/lib/template.js +6 -0
  52. package/dist/lib/template.js.map +1 -1
  53. package/dist/types/config.d.ts +1 -0
  54. package/dist/types/config.d.ts.map +1 -1
  55. package/dist/types/config.js +12 -1
  56. package/dist/types/config.js.map +1 -1
  57. package/dist/types/project.d.ts +1 -1
  58. package/dist/types/project.d.ts.map +1 -1
  59. package/dist/types/project.js +2 -0
  60. package/dist/types/project.js.map +1 -1
  61. package/package.json +1 -1
  62. package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
  63. package/templates/base/skills/controller/SKILL.md +111 -0
  64. package/templates/base/skills/controller/references/README.md +35 -0
  65. package/templates/base/skills/controller/rules/README.md +18 -0
  66. package/templates/base/skills/mobile/dart/SKILL.md +69 -0
  67. package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
  68. package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
  69. package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
  70. package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
  71. package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
  72. package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
  73. package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
  74. package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
  75. package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
  76. package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
  77. package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
  78. package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
  79. package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
  80. package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
  81. package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
  82. package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
  83. package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
  84. package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
  85. package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
  86. package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
  87. package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
  88. package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
  89. package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
  90. package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
  91. package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
  92. package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
  93. package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
  94. package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
  95. package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
  96. package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
  97. package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
  98. package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
  99. package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
  100. package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
  101. package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
  102. package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
  103. package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
  104. package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
  105. package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
  106. package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
  107. package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
  108. package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
  109. package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
  110. package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
  111. package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
  112. package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
  113. package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
  114. package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
  115. package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
  116. package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
  117. package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
  118. package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
  119. package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
  120. package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
  121. package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
  122. package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
  123. package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
  124. package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
  125. package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
  126. package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
  127. package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
  128. package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
  129. package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
  130. package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
  131. package/templates/platforms/claude-code/scripts/completion-guard.sh +57 -0
  132. package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
  133. package/templates/platforms/claude-code/settings.json +75 -3
  134. package/templates/project-types/mobile-app/config.yaml +123 -0
  135. package/templates/project-types/mobile-app/process/workflow.xml +191 -0
@@ -0,0 +1,262 @@
1
+ ---
2
+ title: Local Notifications
3
+ impact: CRITICAL
4
+ impactDescription: "채널 미설정 → Android 8+ 알림 미표시, 초기화 누락 → 런타임 크래시"
5
+ tags: flutter-local-notifications, channel, schedule, timezone
6
+ ---
7
+
8
+ ## Local Notifications
9
+
10
+ **Impact: CRITICAL (채널 미설정 → Android 8+ 알림 미표시, 초기화 누락 → 런타임 크래시)**
11
+
12
+ flutter_local_notifications 설정, Android 채널, 스케줄 알림, FCM 포그라운드 연동.
13
+
14
+ ### 초기화
15
+
16
+ **Incorrect (플랫폼 설정 불완전):**
17
+ ```dart
18
+ final plugin = FlutterLocalNotificationsPlugin();
19
+ await plugin.initialize(const InitializationSettings());
20
+ // → Android: 아이콘 미지정 → 크래시
21
+ // → iOS: 권한 미요청 → 알림 미표시
22
+ ```
23
+
24
+ **Correct (완전한 초기화):**
25
+ ```dart
26
+ class NotificationService {
27
+ static final NotificationService instance = NotificationService._();
28
+ NotificationService._();
29
+
30
+ final FlutterLocalNotificationsPlugin _plugin =
31
+ FlutterLocalNotificationsPlugin();
32
+
33
+ Future<void> initialize() async {
34
+ // Android 설정
35
+ const androidSettings = AndroidInitializationSettings(
36
+ '@mipmap/ic_notification', // res/mipmap 또는 res/drawable
37
+ );
38
+
39
+ // iOS 설정
40
+ const iosSettings = DarwinInitializationSettings(
41
+ requestAlertPermission: false, // 별도 타이밍에 요청
42
+ requestBadgePermission: false,
43
+ requestSoundPermission: false,
44
+ // 포그라운드 알림 표시 콜백
45
+ notificationCategories: [
46
+ DarwinNotificationCategory(
47
+ 'match_invite',
48
+ actions: [
49
+ DarwinNotificationAction.plain('accept', 'Accept'),
50
+ DarwinNotificationAction.plain('decline', 'Decline'),
51
+ ],
52
+ ),
53
+ ],
54
+ );
55
+
56
+ await _plugin.initialize(
57
+ const InitializationSettings(
58
+ android: androidSettings,
59
+ iOS: iosSettings,
60
+ ),
61
+ // 알림 탭 콜백
62
+ onDidReceiveNotificationResponse: _onNotificationTap,
63
+ // 백그라운드 알림 탭 콜백
64
+ onDidReceiveBackgroundNotificationResponse: _onBackgroundNotificationTap,
65
+ );
66
+
67
+ // Android 알림 채널 생성
68
+ await _createNotificationChannels();
69
+ }
70
+
71
+ void _onNotificationTap(NotificationResponse response) {
72
+ final payload = response.payload;
73
+ if (payload != null) {
74
+ // 페이로드 파싱 → 딥링크 네비게이션
75
+ final data = jsonDecode(payload) as Map<String, dynamic>;
76
+ // NavigationService 또는 GoRouter로 네비게이션
77
+ NavigationService.instance.navigateFromPayload(data);
78
+ }
79
+ }
80
+
81
+ // 백그라운드 탭 핸들러 — top-level 또는 static 필수
82
+ @pragma('vm:entry-point')
83
+ static void _onBackgroundNotificationTap(NotificationResponse response) {
84
+ // 백그라운드에서 알림 탭 시 호출
85
+ // 앱이 다시 열리면 getInitialMessage 또는 onMessageOpenedApp에서 처리
86
+ }
87
+ }
88
+ ```
89
+
90
+ ### Android 알림 채널
91
+
92
+ ```dart
93
+ Future<void> _createNotificationChannels() async {
94
+ final androidPlugin =
95
+ _plugin.resolvePlatformSpecificImplementation<
96
+ AndroidFlutterLocalNotificationsPlugin>();
97
+
98
+ if (androidPlugin == null) return;
99
+
100
+ // 매치 알림 (높은 중요도 → 헤드업 알림)
101
+ await androidPlugin.createNotificationChannel(
102
+ const AndroidNotificationChannel(
103
+ 'matches', // channelId
104
+ 'Match Alerts', // channelName
105
+ description: 'Notifications for match invites and updates',
106
+ importance: Importance.high,
107
+ enableVibration: true,
108
+ playSound: true,
109
+ ),
110
+ );
111
+
112
+ // 채팅 메시지 (기본 중요도)
113
+ await androidPlugin.createNotificationChannel(
114
+ const AndroidNotificationChannel(
115
+ 'chat',
116
+ 'Chat Messages',
117
+ description: 'New chat messages',
118
+ importance: Importance.defaultImportance,
119
+ ),
120
+ );
121
+
122
+ // 시스템 공지 (낮은 중요도 → 소리 없음)
123
+ await androidPlugin.createNotificationChannel(
124
+ const AndroidNotificationChannel(
125
+ 'system',
126
+ 'System Notifications',
127
+ description: 'App updates and system alerts',
128
+ importance: Importance.low,
129
+ playSound: false,
130
+ ),
131
+ );
132
+
133
+ // 사일런트 (최소 중요도 → 상태바에만)
134
+ await androidPlugin.createNotificationChannel(
135
+ const AndroidNotificationChannel(
136
+ 'silent',
137
+ 'Background Sync',
138
+ description: 'Silent data synchronization',
139
+ importance: Importance.min,
140
+ playSound: false,
141
+ enableVibration: false,
142
+ ),
143
+ );
144
+ }
145
+ ```
146
+
147
+ ### 알림 표시
148
+
149
+ ```dart
150
+ /// FCM 포그라운드 메시지를 로컬 알림으로 표시
151
+ Future<void> showNotification({
152
+ required int id,
153
+ required String title,
154
+ required String body,
155
+ String? payload,
156
+ String? imageUrl,
157
+ String channelId = 'matches',
158
+ }) async {
159
+ // Android 상세 설정
160
+ final androidDetails = AndroidNotificationDetails(
161
+ channelId,
162
+ _getChannelName(channelId),
163
+ importance: Importance.high,
164
+ priority: Priority.high,
165
+ // 큰 이미지 (선택)
166
+ styleInformation: imageUrl != null
167
+ ? BigPictureStyleInformation(
168
+ FilePathAndroidBitmap(await _downloadImage(imageUrl)),
169
+ contentTitle: title,
170
+ summaryText: body,
171
+ )
172
+ : BigTextStyleInformation(body),
173
+ );
174
+
175
+ // iOS 상세 설정
176
+ const iosDetails = DarwinNotificationDetails(
177
+ presentAlert: true,
178
+ presentBadge: true,
179
+ presentSound: true,
180
+ );
181
+
182
+ await _plugin.show(
183
+ id,
184
+ title,
185
+ body,
186
+ NotificationDetails(
187
+ android: androidDetails,
188
+ iOS: iosDetails,
189
+ ),
190
+ payload: payload,
191
+ );
192
+ }
193
+ ```
194
+
195
+ ### 스케줄 알림
196
+
197
+ ```dart
198
+ import 'package:timezone/timezone.dart' as tz;
199
+ import 'package:timezone/data/latest_all.dart' as tz;
200
+
201
+ /// 초기화 시 timezone 설정
202
+ Future<void> initializeTimezone() async {
203
+ tz.initializeTimeZones();
204
+ // flutter_timezone 패키지로 디바이스 타임존 감지
205
+ final timeZoneName = await FlutterTimezone.getLocalTimezone();
206
+ tz.setLocalLocation(tz.getLocation(timeZoneName));
207
+ }
208
+
209
+ /// 매치 시작 30분 전 리마인더
210
+ Future<void> scheduleMatchReminder({
211
+ required String matchId,
212
+ required String matchTitle,
213
+ required DateTime matchTime,
214
+ }) async {
215
+ final scheduledTime = tz.TZDateTime.from(
216
+ matchTime.subtract(const Duration(minutes: 30)),
217
+ tz.local,
218
+ );
219
+
220
+ // 과거 시간이면 스킵
221
+ if (scheduledTime.isBefore(tz.TZDateTime.now(tz.local))) return;
222
+
223
+ await _plugin.zonedSchedule(
224
+ matchId.hashCode, // 알림 ID (취소용)
225
+ 'Match Reminder',
226
+ '$matchTitle starts in 30 minutes!',
227
+ scheduledTime,
228
+ const NotificationDetails(
229
+ android: AndroidNotificationDetails(
230
+ 'reminders',
231
+ 'Reminders',
232
+ importance: Importance.high,
233
+ ),
234
+ iOS: DarwinNotificationDetails(),
235
+ ),
236
+ androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
237
+ matchDateTimeComponents: null, // 1회성
238
+ payload: jsonEncode({'type': 'match', 'id': matchId}),
239
+ );
240
+ }
241
+
242
+ /// 특정 알림 취소
243
+ Future<void> cancelReminder(String matchId) async {
244
+ await _plugin.cancel(matchId.hashCode);
245
+ }
246
+
247
+ /// 모든 알림 취소
248
+ Future<void> cancelAll() async {
249
+ await _plugin.cancelAll();
250
+ }
251
+ ```
252
+
253
+ ### 규칙
254
+
255
+ - Android 알림 아이콘 → `res/mipmap` 또는 `res/drawable` 에 배치 (벡터 X, PNG 필수)
256
+ - Android 8+ → 채널 미생성 시 알림 미표시 (앱 시작 시 채널 생성 필수)
257
+ - 채널 중요도 → `high`(헤드업), `default`(소리), `low`(소리 X), `min`(사일런트)
258
+ - 스케줄 알림 → `timezone` + `flutter_timezone` 패키지 필수
259
+ - `zonedSchedule` → `exactAllowWhileIdle` (Doze 모드에서도 정확한 알림)
260
+ - 알림 ID → 고유값 사용 (같은 ID 시 덮어쓰기), 취소 시 동일 ID
261
+ - iOS `DarwinInitializationSettings` → 초기화 시 권한 요청 false, 별도 타이밍에 요청
262
+ - 백그라운드 탭 콜백 → `@pragma('vm:entry-point')` + static/top-level
@@ -0,0 +1,210 @@
1
+ ---
2
+ title: Notification Handling (All App States)
3
+ impact: CRITICAL
4
+ impactDescription: "상태별 미처리 → 알림 누락/중복, 사용자 혼란"
5
+ tags: fcm, foreground, background, terminated, onMessage
6
+ ---
7
+
8
+ ## Notification Handling (All App States)
9
+
10
+ **Impact: CRITICAL (상태별 미처리 → 알림 누락/중복, 사용자 혼란)**
11
+
12
+ FCM 메시지는 앱 상태(포그라운드/백그라운드/종료)에 따라 처리 경로가 다름.
13
+ 3가지 상태 모두 빠짐없이 처리해야 알림 누락 없음.
14
+
15
+ ### 앱 상태별 메시지 수신 경로
16
+
17
+ ```
18
+ ┌──────────────────────────────────────────────────────────┐
19
+ │ FCM Message 도착 │
20
+ ├──────────────┬──────────────────┬────────────────────────┤
21
+ │ Foreground │ Background │ Terminated │
22
+ │ (앱 활성) │ (앱 최소화) │ (앱 종료) │
23
+ ├──────────────┼──────────────────┼────────────────────────┤
24
+ │ onMessage │ onBackgroundMsg │ onBackgroundMsg │
25
+ │ 스트림 수신 │ top-level 함수 │ top-level 함수 │
26
+ │ │ │ │
27
+ │ 자동 표시 X │ 자동 표시 O │ 자동 표시 O │
28
+ │ → 로컬 알림 │ (notification │ (notification │
29
+ │ 직접 표시 │ payload 있으면) │ payload 있으면) │
30
+ ├──────────────┼──────────────────┼────────────────────────┤
31
+ │ 알림 탭 시 │
32
+ ├──────────────┬──────────────────┬────────────────────────┤
33
+ │ (이미 활성) │ onMessageOpened │ getInitialMessage │
34
+ │ │ App 스트림 │ (1회, cold start) │
35
+ └──────────────┴──────────────────┴────────────────────────┘
36
+ ```
37
+
38
+ ### 포그라운드 메시지 처리
39
+
40
+ **Incorrect (포그라운드에서 알림 미표시):**
41
+ ```dart
42
+ FirebaseMessaging.onMessage.listen((message) {
43
+ // 데이터만 처리하고 사용자에게 알림 미표시
44
+ print('Got message: ${message.notification?.title}');
45
+ // → 사용자는 알림이 왔는지 모름
46
+ });
47
+ ```
48
+
49
+ **Correct (로컬 알림으로 포그라운드 표시):**
50
+ ```dart
51
+ class NotificationHandler {
52
+ final FirebaseMessaging _messaging = FirebaseMessaging.instance;
53
+ final NotificationService _notificationService;
54
+ final GoRouter _router;
55
+
56
+ NotificationHandler({
57
+ required NotificationService notificationService,
58
+ required GoRouter router,
59
+ }) : _notificationService = notificationService,
60
+ _router = router;
61
+
62
+ void initialize() {
63
+ // 1. 포그라운드 메시지 → 로컬 알림 표시
64
+ FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
65
+
66
+ // 2. 백그라운드에서 알림 탭 → 네비게이션
67
+ FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
68
+
69
+ // 3. 종료 상태에서 알림 탭 → 네비게이션 (cold start)
70
+ _handleInitialMessage();
71
+ }
72
+
73
+ Future<void> _handleForegroundMessage(RemoteMessage message) async {
74
+ final notification = message.notification;
75
+ if (notification == null) {
76
+ // data-only 메시지 → 사일런트 처리 (데이터 동기화 등)
77
+ await _handleDataMessage(message.data);
78
+ return;
79
+ }
80
+
81
+ // 포그라운드: FCM이 자동 표시하지 않으므로 로컬 알림으로 직접 표시
82
+ await _notificationService.showNotification(
83
+ id: message.hashCode,
84
+ title: notification.title ?? '',
85
+ body: notification.body ?? '',
86
+ payload: jsonEncode(message.data),
87
+ imageUrl: notification.android?.imageUrl ?? notification.apple?.imageUrl,
88
+ );
89
+ }
90
+
91
+ Future<void> _handleNotificationTap(RemoteMessage message) async {
92
+ _navigateFromPayload(message.data);
93
+ }
94
+
95
+ Future<void> _handleInitialMessage() async {
96
+ // 종료 상태에서 알림 탭으로 앱 시작 시
97
+ // getInitialMessage()는 1회만 값을 반환 (이후 null)
98
+ final initialMessage = await _messaging.getInitialMessage();
99
+ if (initialMessage != null) {
100
+ // 약간의 지연 — 라우터 초기화 대기
101
+ await Future.delayed(const Duration(milliseconds: 500));
102
+ _navigateFromPayload(initialMessage.data);
103
+ }
104
+ }
105
+
106
+ void _navigateFromPayload(Map<String, dynamic> data) {
107
+ final type = data['type'] as String?;
108
+ final id = data['id'] as String?;
109
+
110
+ switch (type) {
111
+ case 'match':
112
+ if (id != null) _router.push('/match/$id');
113
+ case 'chat':
114
+ if (id != null) _router.push('/chat/$id');
115
+ case 'announcement':
116
+ _router.push('/announcements');
117
+ default:
118
+ _router.push('/notifications');
119
+ }
120
+ }
121
+
122
+ Future<void> _handleDataMessage(Map<String, dynamic> data) async {
123
+ // 사일런트 푸시: 데이터 동기화, 캐시 무효화 등
124
+ final action = data['action'] as String?;
125
+ switch (action) {
126
+ case 'sync_matches':
127
+ // 매치 데이터 새로고침 트리거
128
+ break;
129
+ case 'invalidate_cache':
130
+ // 특정 캐시 무효화
131
+ break;
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ ### 백그라운드 메시지 핸들러
138
+
139
+ ```dart
140
+ // main.dart (또는 별도 파일) — 반드시 top-level
141
+ @pragma('vm:entry-point')
142
+ Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
143
+ // 백그라운드에서는 별도 Isolate로 실행
144
+ // → 앱 상태, Provider, 싱글톤 인스턴스 접근 불가
145
+ await Firebase.initializeApp();
146
+
147
+ // 가벼운 처리만 수행
148
+ // 1. 로컬 DB 저장 (SQLite/Hive 직접 접근)
149
+ // 2. 알림 배지 카운트 업데이트
150
+ // 3. 로컬 알림 표시 (커스텀 알림 필요 시)
151
+
152
+ debugPrint('Background message: ${message.messageId}');
153
+ }
154
+ ```
155
+
156
+ ### iOS 포그라운드 표시 옵션
157
+
158
+ ```dart
159
+ // iOS에서 포그라운드 알림을 시스템 배너로 표시하려면:
160
+ await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
161
+ alert: true, // 배너 표시
162
+ badge: true, // 뱃지 업데이트
163
+ sound: true, // 사운드 재생
164
+ );
165
+ // 이 옵션 사용 시 flutter_local_notifications 포그라운드 표시 불필요 (iOS만)
166
+ // Android는 여전히 flutter_local_notifications 필요
167
+ ```
168
+
169
+ ### 메시지 타입별 처리 전략
170
+
171
+ ```dart
172
+ // FCM 메시지 구조:
173
+ // 1. notification + data → 알림 표시 + 데이터 전달
174
+ // 2. data-only → 사일런트 (앱이 직접 처리)
175
+ // 3. notification-only → 단순 알림
176
+
177
+ // 서버에서 보내는 JSON 예시 (notification + data):
178
+ // {
179
+ // "message": {
180
+ // "token": "device_token",
181
+ // "notification": {
182
+ // "title": "새 매치 초대",
183
+ // "body": "오후 3시 테니스 매치에 초대되었습니다"
184
+ // },
185
+ // "data": {
186
+ // "type": "match",
187
+ // "id": "match_123",
188
+ // "action": "invite"
189
+ // },
190
+ // "android": {
191
+ // "notification": { "channel_id": "matches" }
192
+ // },
193
+ // "apns": {
194
+ // "payload": {
195
+ // "aps": { "sound": "default", "badge": 1 }
196
+ // }
197
+ // }
198
+ // }
199
+ // }
200
+ ```
201
+
202
+ ### 규칙
203
+
204
+ - 포그라운드 → `onMessage` + 로컬 알림 직접 표시 (FCM은 포그라운드 자동 표시 안 함)
205
+ - 백그라운드 핸들러 → top-level 함수, `@pragma('vm:entry-point')` 필수
206
+ - 백그라운드 핸들러 → Provider/싱글톤 접근 불가, DB 직접 접근만
207
+ - `getInitialMessage()` → cold start 시 1회 체크, 라우터 준비 후 네비게이션
208
+ - `onMessageOpenedApp` → 백그라운드 탭 처리, `getInitialMessage` 와 중복 방지
209
+ - data-only 메시지 → 사일런트 데이터 동기화에 활용 (알림 미표시)
210
+ - iOS `setForegroundNotificationPresentationOptions` → iOS 전용 포그라운드 배너