timsquad 3.3.0 → 3.5.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 (196) 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 +95 -5
  9. package/dist/commands/daemon.js.map +1 -1
  10. package/dist/commands/full.js +1 -0
  11. package/dist/commands/full.js.map +1 -1
  12. package/dist/commands/git/pr.js +6 -5
  13. package/dist/commands/git/pr.js.map +1 -1
  14. package/dist/commands/git/release.js +2 -7
  15. package/dist/commands/git/release.js.map +1 -1
  16. package/dist/commands/improve.js +2 -2
  17. package/dist/commands/improve.js.map +1 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +12 -3
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/log.d.ts.map +1 -1
  22. package/dist/commands/log.js +2 -2
  23. package/dist/commands/log.js.map +1 -1
  24. package/dist/commands/metrics.d.ts.map +1 -1
  25. package/dist/commands/metrics.js +6 -2
  26. package/dist/commands/metrics.js.map +1 -1
  27. package/dist/commands/retro.js +8 -8
  28. package/dist/commands/retro.js.map +1 -1
  29. package/dist/commands/session.js +3 -3
  30. package/dist/commands/session.js.map +1 -1
  31. package/dist/commands/skills.d.ts +12 -0
  32. package/dist/commands/skills.d.ts.map +1 -0
  33. package/dist/commands/skills.js +228 -0
  34. package/dist/commands/skills.js.map +1 -0
  35. package/dist/commands/status.js +1 -1
  36. package/dist/commands/status.js.map +1 -1
  37. package/dist/commands/upgrade.d.ts.map +1 -1
  38. package/dist/commands/upgrade.js +23 -1
  39. package/dist/commands/upgrade.js.map +1 -1
  40. package/dist/daemon/entry.js +3 -3
  41. package/dist/daemon/entry.js.map +1 -1
  42. package/dist/daemon/event-queue.d.ts.map +1 -1
  43. package/dist/daemon/event-queue.js +2 -2
  44. package/dist/daemon/event-queue.js.map +1 -1
  45. package/dist/daemon/index.d.ts +4 -2
  46. package/dist/daemon/index.d.ts.map +1 -1
  47. package/dist/daemon/index.js +214 -52
  48. package/dist/daemon/index.js.map +1 -1
  49. package/dist/daemon/jsonl-watcher.d.ts +1 -0
  50. package/dist/daemon/jsonl-watcher.d.ts.map +1 -1
  51. package/dist/daemon/jsonl-watcher.js.map +1 -1
  52. package/dist/daemon/meta-cache.d.ts +1 -0
  53. package/dist/daemon/meta-cache.d.ts.map +1 -1
  54. package/dist/daemon/meta-cache.js +9 -0
  55. package/dist/daemon/meta-cache.js.map +1 -1
  56. package/dist/daemon/session-notes.d.ts +33 -0
  57. package/dist/daemon/session-notes.d.ts.map +1 -0
  58. package/dist/daemon/session-notes.js +74 -0
  59. package/dist/daemon/session-notes.js.map +1 -0
  60. package/dist/daemon/session-state.d.ts +27 -0
  61. package/dist/daemon/session-state.d.ts.map +1 -0
  62. package/dist/daemon/session-state.js +165 -0
  63. package/dist/daemon/session-state.js.map +1 -0
  64. package/dist/daemon/shutdown.d.ts.map +1 -1
  65. package/dist/daemon/shutdown.js +9 -1
  66. package/dist/daemon/shutdown.js.map +1 -1
  67. package/dist/index.js +4 -0
  68. package/dist/index.js.map +1 -1
  69. package/dist/lib/agent-generator.d.ts +4 -0
  70. package/dist/lib/agent-generator.d.ts.map +1 -1
  71. package/dist/lib/agent-generator.js +52 -3
  72. package/dist/lib/agent-generator.js.map +1 -1
  73. package/dist/lib/compile-rules.d.ts +66 -0
  74. package/dist/lib/compile-rules.d.ts.map +1 -0
  75. package/dist/lib/compile-rules.js +114 -0
  76. package/dist/lib/compile-rules.js.map +1 -0
  77. package/dist/lib/compiler.d.ts +105 -0
  78. package/dist/lib/compiler.d.ts.map +1 -0
  79. package/dist/lib/compiler.js +368 -0
  80. package/dist/lib/compiler.js.map +1 -0
  81. package/dist/lib/config.d.ts +1 -0
  82. package/dist/lib/config.d.ts.map +1 -1
  83. package/dist/lib/config.js +8 -1
  84. package/dist/lib/config.js.map +1 -1
  85. package/dist/lib/project.d.ts.map +1 -1
  86. package/dist/lib/project.js +8 -3
  87. package/dist/lib/project.js.map +1 -1
  88. package/dist/lib/skill-generator.d.ts.map +1 -1
  89. package/dist/lib/skill-generator.js +22 -1
  90. package/dist/lib/skill-generator.js.map +1 -1
  91. package/dist/lib/template.d.ts.map +1 -1
  92. package/dist/lib/template.js +6 -0
  93. package/dist/lib/template.js.map +1 -1
  94. package/dist/types/config.d.ts +1 -0
  95. package/dist/types/config.d.ts.map +1 -1
  96. package/dist/types/config.js +12 -1
  97. package/dist/types/config.js.map +1 -1
  98. package/dist/types/project.d.ts +1 -1
  99. package/dist/types/project.d.ts.map +1 -1
  100. package/dist/types/project.js +2 -0
  101. package/dist/types/project.js.map +1 -1
  102. package/package.json +4 -4
  103. package/templates/base/agents/base/tsq-architect.md +2 -2
  104. package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
  105. package/templates/base/knowledge/checklists/plan-quality.md +31 -0
  106. package/templates/base/knowledge/checklists/stability-verification.md +14 -0
  107. package/templates/base/skills/controller/SKILL.md +111 -0
  108. package/templates/base/skills/controller/references/README.md +35 -0
  109. package/templates/base/skills/controller/rules/README.md +18 -0
  110. package/templates/base/skills/mobile/dart/SKILL.md +69 -0
  111. package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
  112. package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
  113. package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
  114. package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
  115. package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
  116. package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
  117. package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
  118. package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
  119. package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
  120. package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
  121. package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
  122. package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
  123. package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
  124. package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
  125. package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
  126. package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
  127. package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
  128. package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
  129. package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
  130. package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
  131. package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
  132. package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
  133. package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
  134. package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
  135. package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
  136. package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
  137. package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
  138. package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
  139. package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
  140. package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
  141. package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
  142. package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
  143. package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
  144. package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
  145. package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
  146. package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
  147. package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
  148. package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
  149. package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
  150. package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
  151. package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
  152. package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
  153. package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
  154. package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
  155. package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
  156. package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
  157. package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
  158. package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
  159. package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
  160. package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
  161. package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
  162. package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
  163. package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
  164. package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
  165. package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
  166. package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
  167. package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
  168. package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
  169. package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
  170. package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
  171. package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
  172. package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
  173. package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
  174. package/templates/base/skills/stability-verification/SKILL.md +64 -0
  175. package/templates/base/skills/stability-verification/references/release-checklist.md +34 -0
  176. package/templates/base/skills/stability-verification/references/security-fix-patterns.md +112 -0
  177. package/templates/base/skills/stability-verification/rules/verification-layers.md +67 -0
  178. package/templates/base/skills/stability-verification/rules/verification-workflow.md +69 -0
  179. package/templates/base/skills/stability-verification/scripts/verify.sh +294 -0
  180. package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
  181. package/templates/platforms/claude-code/rules/build-gate.md +28 -0
  182. package/templates/platforms/claude-code/rules/completion-verification.md +30 -0
  183. package/templates/platforms/claude-code/rules/context-monitor.md +23 -0
  184. package/templates/platforms/claude-code/rules/plan-review.md +45 -0
  185. package/templates/platforms/claude-code/rules/quality-guards.md +43 -0
  186. package/templates/platforms/claude-code/rules/session-notes.md +18 -0
  187. package/templates/platforms/claude-code/rules/skill-suggest.md +27 -0
  188. package/templates/platforms/claude-code/scripts/build-gate.sh +73 -0
  189. package/templates/platforms/claude-code/scripts/completion-guard.sh +93 -0
  190. package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
  191. package/templates/platforms/claude-code/scripts/safe-guard.sh +83 -0
  192. package/templates/platforms/claude-code/scripts/skill-rules.json +85 -0
  193. package/templates/platforms/claude-code/scripts/skill-suggest.sh +105 -0
  194. package/templates/platforms/claude-code/settings.json +111 -3
  195. package/templates/project-types/mobile-app/config.yaml +123 -0
  196. package/templates/project-types/mobile-app/process/workflow.xml +191 -0
@@ -0,0 +1,227 @@
1
+ ---
2
+ title: Analytics Event Taxonomy & Screen Tracking
3
+ impact: HIGH
4
+ impactDescription: "이벤트 미설계 → 무의미한 데이터 축적, 의사결정 근거 부재"
5
+ tags: firebase-analytics, event, screen-view, user-property, observer
6
+ ---
7
+
8
+ ## Analytics Event Taxonomy & Screen Tracking
9
+
10
+ **Impact: HIGH (이벤트 미설계 → 무의미한 데이터 축적, 의사결정 근거 부재)**
11
+
12
+ Firebase Analytics 초기화, 이벤트 택소노미 설계, 사용자 속성,
13
+ AnalyticsObserver를 통한 화면 추적, 디버그 모드.
14
+
15
+ ### 의존성
16
+
17
+ ```yaml
18
+ # pubspec.yaml
19
+ dependencies:
20
+ firebase_core: ^3.8.0
21
+ firebase_analytics: ^11.4.0
22
+ ```
23
+
24
+ ### 이벤트 택소노미 설계
25
+
26
+ **Incorrect (무분별한 이벤트 로깅):**
27
+ ```dart
28
+ // 이벤트 이름이 일관성 없고 파라미터 미정의
29
+ analytics.logEvent(name: 'clicked_button');
30
+ analytics.logEvent(name: 'user_did_something');
31
+ analytics.logEvent(name: 'pageView', parameters: {'p': 'home'});
32
+ // → 분석 불가능한 데이터 축적
33
+ ```
34
+
35
+ **Correct (구조화된 택소노미):**
36
+ ```dart
37
+ /// 이벤트 이름 상수 (snake_case, 40자 이하)
38
+ abstract class AnalyticsEvents {
39
+ // 화면 이벤트
40
+ static const screenView = 'screen_view';
41
+
42
+ // 사용자 액션
43
+ static const buttonClick = 'button_click';
44
+ static const featureUse = 'feature_use';
45
+ static const search = 'search';
46
+
47
+ // 비즈니스 이벤트
48
+ static const matchCreated = 'match_created';
49
+ static const matchJoined = 'match_joined';
50
+ static const matchCompleted = 'match_completed';
51
+
52
+ // 전환 이벤트
53
+ static const signUp = 'sign_up';
54
+ static const login = 'login';
55
+ static const subscriptionStarted = 'subscription_started';
56
+
57
+ // 에러 이벤트
58
+ static const errorOccurred = 'error_occurred';
59
+ }
60
+
61
+ /// 파라미터 이름 상수
62
+ abstract class AnalyticsParams {
63
+ static const screenName = 'screen_name';
64
+ static const screenClass = 'screen_class';
65
+ static const buttonName = 'button_name';
66
+ static const featureName = 'feature_name';
67
+ static const sportType = 'sport_type';
68
+ static const errorType = 'error_type';
69
+ static const errorMessage = 'error_message';
70
+ static const source = 'source';
71
+ }
72
+ ```
73
+
74
+ ### Analytics 서비스
75
+
76
+ ```dart
77
+ class AnalyticsService {
78
+ final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
79
+
80
+ /// 화면 조회
81
+ Future<void> logScreenView({
82
+ required String screenName,
83
+ String? screenClass,
84
+ }) async {
85
+ await _analytics.logScreenView(
86
+ screenName: screenName,
87
+ screenClass: screenClass ?? screenName,
88
+ );
89
+ }
90
+
91
+ /// 버튼 클릭
92
+ Future<void> logButtonClick({
93
+ required String buttonName,
94
+ required String screenName,
95
+ }) async {
96
+ await _analytics.logEvent(
97
+ name: AnalyticsEvents.buttonClick,
98
+ parameters: {
99
+ AnalyticsParams.buttonName: buttonName,
100
+ AnalyticsParams.screenName: screenName,
101
+ },
102
+ );
103
+ }
104
+
105
+ /// 기능 사용
106
+ Future<void> logFeatureUse({
107
+ required String featureName,
108
+ Map<String, Object>? extra,
109
+ }) async {
110
+ await _analytics.logEvent(
111
+ name: AnalyticsEvents.featureUse,
112
+ parameters: {
113
+ AnalyticsParams.featureName: featureName,
114
+ ...?extra,
115
+ },
116
+ );
117
+ }
118
+
119
+ /// 비즈니스 이벤트
120
+ Future<void> logMatchCreated({
121
+ required String sportType,
122
+ required int maxPlayers,
123
+ }) async {
124
+ await _analytics.logEvent(
125
+ name: AnalyticsEvents.matchCreated,
126
+ parameters: {
127
+ AnalyticsParams.sportType: sportType,
128
+ 'max_players': maxPlayers,
129
+ },
130
+ );
131
+ }
132
+ }
133
+
134
+ // Riverpod Provider
135
+ final analyticsServiceProvider = Provider<AnalyticsService>((ref) {
136
+ return AnalyticsService();
137
+ });
138
+ ```
139
+
140
+ ### 사용자 속성
141
+
142
+ ```dart
143
+ /// 사용자 세그먼트 설정 (최대 25개 속성)
144
+ Future<void> setUserProperties({
145
+ required String userId,
146
+ String? userType,
147
+ String? subscriptionTier,
148
+ String? preferredSport,
149
+ String? region,
150
+ }) async {
151
+ final analytics = FirebaseAnalytics.instance;
152
+
153
+ await analytics.setUserId(id: userId);
154
+
155
+ if (userType != null) {
156
+ await analytics.setUserProperty(name: 'user_type', value: userType);
157
+ }
158
+ if (subscriptionTier != null) {
159
+ await analytics.setUserProperty(
160
+ name: 'subscription_tier',
161
+ value: subscriptionTier,
162
+ );
163
+ }
164
+ if (preferredSport != null) {
165
+ await analytics.setUserProperty(
166
+ name: 'preferred_sport',
167
+ value: preferredSport,
168
+ );
169
+ }
170
+ if (region != null) {
171
+ await analytics.setUserProperty(name: 'region', value: region);
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### go_router 화면 추적 (AnalyticsObserver)
177
+
178
+ ```dart
179
+ // GoRouter 설정에 observer 추가
180
+ final goRouter = GoRouter(
181
+ routes: [...],
182
+ observers: [
183
+ FirebaseAnalyticsObserver(
184
+ analytics: FirebaseAnalytics.instance,
185
+ nameExtractor: (settings) {
186
+ // route 이름을 screen_name으로 매핑
187
+ return settings.name ?? settings.arguments?.toString() ?? 'unknown';
188
+ },
189
+ ),
190
+ ],
191
+ );
192
+
193
+ // GoRoute에 name 명시 (observer가 이름 추출)
194
+ GoRoute(
195
+ path: '/match/:id',
196
+ name: 'match_detail',
197
+ builder: (context, state) => MatchDetailScreen(
198
+ matchId: state.pathParameters['id']!,
199
+ ),
200
+ ),
201
+ ```
202
+
203
+ ### 디버그 모드
204
+
205
+ ```
206
+ # Android: DebugView 활성화
207
+ adb shell setprop debug.firebase.analytics.app <package_name>
208
+
209
+ # 이벤트가 실시간으로 Firebase Console > DebugView에 표시
210
+ # 비활성화:
211
+ adb shell setprop debug.firebase.analytics.app .none.
212
+
213
+ # iOS: Xcode Scheme > Arguments
214
+ -FIRDebugEnabled # 활성화
215
+ -FIRDebugDisabled # 비활성화
216
+ ```
217
+
218
+ ### 규칙
219
+
220
+ - 이벤트 택소노미 → 구현 전에 문서화 (이름, 파라미터, 트리거 조건)
221
+ - 이벤트 이름 → snake_case, 40자 이하, 자동 수집 이벤트와 충돌 금지
222
+ - 파라미터 → 최대 25개/이벤트, 값 100자 이하
223
+ - 사용자 속성 → 세그먼트 기준만 (최대 25개, 값 36자 이하)
224
+ - `AnalyticsObserver` → go_router 연동으로 화면 전환 자동 추적
225
+ - `setUserId` → 로그인 시 설정, 로그아웃 시 `null` 로 해제
226
+ - 디버그 → DebugView로 실시간 이벤트 검증 후 프로덕션 배포
227
+ - PII (개인식별정보) → 이벤트 파라미터에 이름/이메일/전화번호 금지
@@ -0,0 +1,195 @@
1
+ ---
2
+ title: Crashlytics Setup & Error Capture
3
+ impact: CRITICAL
4
+ impactDescription: "크래시 미수집 → 프로덕션 장애 감지 불가, 사용자 이탈 원인 파악 불가"
5
+ tags: crashlytics, firebase, crash, error, fatal
6
+ ---
7
+
8
+ ## Crashlytics Setup & Error Capture
9
+
10
+ **Impact: CRITICAL (크래시 미수집 → 프로덕션 장애 감지 불가, 사용자 이탈 원인 파악 불가)**
11
+
12
+ Firebase Crashlytics 초기화, Flutter/Dart 에러 캡처, 비치명 에러 기록,
13
+ 사용자 식별자 및 커스텀 키 설정.
14
+
15
+ ### 의존성
16
+
17
+ ```yaml
18
+ # pubspec.yaml
19
+ dependencies:
20
+ firebase_core: ^3.8.0
21
+ firebase_crashlytics: ^4.3.0
22
+ ```
23
+
24
+ ### 초기화
25
+
26
+ **Incorrect (에러 핸들러 미연결):**
27
+ ```dart
28
+ void main() async {
29
+ WidgetsFlutterBinding.ensureInitialized();
30
+ await Firebase.initializeApp();
31
+ // Crashlytics 에러 핸들러 미등록 → 크래시 미수집
32
+ runApp(const MyApp());
33
+ }
34
+ ```
35
+
36
+ **Correct (완전한 에러 캡처 파이프라인):**
37
+ ```dart
38
+ Future<void> main() async {
39
+ WidgetsFlutterBinding.ensureInitialized();
40
+ await Firebase.initializeApp();
41
+
42
+ // 1. Flutter 프레임워크 에러 (위젯 빌드 에러 등)
43
+ FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
44
+
45
+ // 2. 비동기 에러 (Future, Isolate 등)
46
+ PlatformDispatcher.instance.onError = (error, stack) {
47
+ FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
48
+ return true;
49
+ };
50
+
51
+ // 3. Dart Zone 에러 (runZonedGuarded)
52
+ runZonedGuarded<Future<void>>(
53
+ () async {
54
+ runApp(const ProviderScope(child: MyApp()));
55
+ },
56
+ (error, stack) {
57
+ FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
58
+ },
59
+ );
60
+ }
61
+ ```
62
+
63
+ ### 디버그 모드 제어
64
+
65
+ ```dart
66
+ // 개발 중 Crashlytics 비활성화 (크래시 대시보드 오염 방지)
67
+ await FirebaseCrashlytics.instance
68
+ .setCrashlyticsCollectionEnabled(!kDebugMode);
69
+
70
+ // 또는 환경별 분리
71
+ final isProduction = const String.fromEnvironment('ENV') == 'production';
72
+ await FirebaseCrashlytics.instance
73
+ .setCrashlyticsCollectionEnabled(isProduction);
74
+ ```
75
+
76
+ ### 비치명 에러 기록
77
+
78
+ **Incorrect (try-catch만 하고 기록 안 함):**
79
+ ```dart
80
+ try {
81
+ await apiService.fetchData();
82
+ } catch (e) {
83
+ debugPrint('Error: $e');
84
+ // → 프로덕션에서 에러 발생 사실을 알 수 없음
85
+ }
86
+ ```
87
+
88
+ **Correct (비치명 에러 Crashlytics 기록):**
89
+ ```dart
90
+ try {
91
+ await apiService.fetchData();
92
+ } catch (error, stack) {
93
+ // fatal: false → 비치명 에러 (앱 크래시 아님)
94
+ await FirebaseCrashlytics.instance.recordError(
95
+ error,
96
+ stack,
97
+ fatal: false,
98
+ reason: 'fetchData API call failed',
99
+ );
100
+ // 사용자에게 에러 UI 표시
101
+ rethrow; // 또는 적절한 에러 핸들링
102
+ }
103
+ ```
104
+
105
+ ### 사용자 식별자
106
+
107
+ ```dart
108
+ class CrashlyticsService {
109
+ final FirebaseCrashlytics _crashlytics = FirebaseCrashlytics.instance;
110
+
111
+ /// 로그인 시 사용자 식별자 설정
112
+ Future<void> setUser(String userId) async {
113
+ await _crashlytics.setUserIdentifier(userId);
114
+ }
115
+
116
+ /// 로그아웃 시 식별자 제거
117
+ Future<void> clearUser() async {
118
+ await _crashlytics.setUserIdentifier('');
119
+ }
120
+
121
+ /// 커스텀 키 설정 (크래시 컨텍스트)
122
+ Future<void> setContext({
123
+ required String appVersion,
124
+ required String buildNumber,
125
+ String? subscriptionTier,
126
+ bool? isOnboarded,
127
+ }) async {
128
+ await _crashlytics.setCustomKey('app_version', appVersion);
129
+ await _crashlytics.setCustomKey('build_number', buildNumber);
130
+ if (subscriptionTier != null) {
131
+ await _crashlytics.setCustomKey('subscription_tier', subscriptionTier);
132
+ }
133
+ if (isOnboarded != null) {
134
+ await _crashlytics.setCustomKey('is_onboarded', isOnboarded);
135
+ }
136
+ }
137
+
138
+ /// 커스텀 로그 (크래시 발생 직전 흐름 추적)
139
+ void log(String message) {
140
+ _crashlytics.log(message);
141
+ }
142
+
143
+ /// 비치명 에러 기록
144
+ Future<void> recordNonFatal(
145
+ dynamic error,
146
+ StackTrace stack, {
147
+ String? reason,
148
+ }) async {
149
+ await _crashlytics.recordError(
150
+ error,
151
+ stack,
152
+ fatal: false,
153
+ reason: reason,
154
+ );
155
+ }
156
+ }
157
+
158
+ // Riverpod Provider
159
+ final crashlyticsServiceProvider = Provider<CrashlyticsService>((ref) {
160
+ return CrashlyticsService();
161
+ });
162
+ ```
163
+
164
+ ### dSYM / ProGuard 설정
165
+
166
+ ```
167
+ iOS (dSYM 업로드):
168
+ Xcode > Build Settings > Debug Information Format = DWARF with dSYM File
169
+ firebase_crashlytics가 빌드 스크립트 자동 추가 (Run Script Phase)
170
+ 수동: firebase crashlytics:symbols:upload --app=<APP_ID> path/to/dSYMs
171
+
172
+ Android (ProGuard/R8 mapping):
173
+ android/app/build.gradle:
174
+ buildTypes {
175
+ release {
176
+ minifyEnabled true
177
+ shrinkResources true
178
+ // firebase_crashlytics Gradle 플러그인이 mapping 자동 업로드
179
+ }
180
+ }
181
+ android/build.gradle:
182
+ plugins { id 'com.google.firebase.crashlytics' }
183
+ ```
184
+
185
+ ### 규칙
186
+
187
+ - `FlutterError.onError` → Crashlytics 연결 필수 (Flutter 프레임워크 에러)
188
+ - `PlatformDispatcher.instance.onError` → 비동기 에러 캡처 필수
189
+ - `runZonedGuarded` → Dart Zone 에러까지 포괄 (선택이지만 강력 권장)
190
+ - 디버그 모드 → `setCrashlyticsCollectionEnabled(false)` (대시보드 오염 방지)
191
+ - 비치명 에러 → `recordError(fatal: false)` + reason 명시
192
+ - `setUserIdentifier` → 로그인/로그아웃 시 설정/해제
193
+ - `setCustomKey` → 구독 등급, 기능 플래그 등 비즈니스 컨텍스트
194
+ - `log()` → 크래시 직전 사용자 흐름 기록 (최대 64KB)
195
+ - dSYM (iOS) / ProGuard mapping (Android) → 릴리스 빌드 심볼 업로드 필수
@@ -0,0 +1,258 @@
1
+ ---
2
+ title: Structured Logging & Sensitive Data Masking
3
+ impact: MEDIUM
4
+ impactDescription: "로그 미관리 → 프로덕션 디버깅 불가, 민감 데이터 유출 위험"
5
+ tags: logger, structured-logging, masking, remote-logging, log-level
6
+ ---
7
+
8
+ ## Structured Logging & Sensitive Data Masking
9
+
10
+ **Impact: MEDIUM (로그 미관리 → 프로덕션 디버깅 불가, 민감 데이터 유출 위험)**
11
+
12
+ logger 패키지를 활용한 레벨별 로깅, JSON 구조화된 로그 출력,
13
+ 릴리스 빌드 레벨 필터, 원격 로깅 연동, 민감 데이터 마스킹.
14
+
15
+ ### 의존성
16
+
17
+ ```yaml
18
+ # pubspec.yaml
19
+ dependencies:
20
+ logger: ^2.5.0
21
+ ```
22
+
23
+ ### 기본 설정
24
+
25
+ **Incorrect (print/debugPrint 직접 사용):**
26
+ ```dart
27
+ print('User logged in: ${user.email}');
28
+ debugPrint('API response: ${response.data}');
29
+ // → 레벨 구분 없음, 릴리스에서 제거 불가, PII 노출
30
+ ```
31
+
32
+ **Correct (구조화된 로거):**
33
+ ```dart
34
+ class AppLogger {
35
+ static final AppLogger instance = AppLogger._();
36
+ AppLogger._();
37
+
38
+ late final Logger _logger;
39
+ final List<LogOutput> _outputs = [];
40
+
41
+ void initialize({bool isRelease = false}) {
42
+ _logger = Logger(
43
+ filter: isRelease ? ProductionFilter() : DevelopFilter(),
44
+ printer: isRelease
45
+ ? JsonPrinter() // 프로덕션: JSON 구조화
46
+ : PrettyPrinter( // 개발: 색상 + 포맷팅
47
+ methodCount: 2,
48
+ errorMethodCount: 8,
49
+ lineLength: 120,
50
+ colors: true,
51
+ printEmojis: false,
52
+ dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
53
+ ),
54
+ output: MultiOutput(_outputs),
55
+ );
56
+ }
57
+
58
+ void addOutput(LogOutput output) => _outputs.add(output);
59
+
60
+ void verbose(String message, [dynamic error, StackTrace? stack]) =>
61
+ _logger.t(message, error: error, stackTrace: stack);
62
+
63
+ void debug(String message, [dynamic error, StackTrace? stack]) =>
64
+ _logger.d(message, error: error, stackTrace: stack);
65
+
66
+ void info(String message, [dynamic error, StackTrace? stack]) =>
67
+ _logger.i(message, error: error, stackTrace: stack);
68
+
69
+ void warning(String message, [dynamic error, StackTrace? stack]) =>
70
+ _logger.w(message, error: error, stackTrace: stack);
71
+
72
+ void error(String message, [dynamic error, StackTrace? stack]) =>
73
+ _logger.e(message, error: error, stackTrace: stack);
74
+
75
+ void fatal(String message, [dynamic error, StackTrace? stack]) =>
76
+ _logger.f(message, error: error, stackTrace: stack);
77
+ }
78
+
79
+ // 사용
80
+ final log = AppLogger.instance;
81
+ log.info('User logged in', null, null);
82
+ log.error('API call failed', exception, stackTrace);
83
+ ```
84
+
85
+ ### 릴리스 빌드 필터
86
+
87
+ ```dart
88
+ /// 프로덕션: warning 이상만 출력
89
+ class ProductionFilter extends LogFilter {
90
+ @override
91
+ bool shouldLog(LogEvent event) {
92
+ return event.level.index >= Level.warning.index;
93
+ }
94
+ }
95
+
96
+ /// 개발: 모든 레벨 출력
97
+ class DevelopFilter extends LogFilter {
98
+ @override
99
+ bool shouldLog(LogEvent event) {
100
+ return true;
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### JSON 구조화 프린터
106
+
107
+ ```dart
108
+ /// 프로덕션용 JSON 출력 (원격 로깅 시스템과 호환)
109
+ class JsonPrinter extends LogPrinter {
110
+ @override
111
+ List<String> log(LogEvent event) {
112
+ final output = {
113
+ 'timestamp': DateTime.now().toUtc().toIso8601String(),
114
+ 'level': event.level.name,
115
+ 'message': event.message,
116
+ };
117
+
118
+ if (event.error != null) {
119
+ output['error'] = event.error.toString();
120
+ }
121
+ if (event.stackTrace != null) {
122
+ output['stackTrace'] = event.stackTrace.toString();
123
+ }
124
+
125
+ return [jsonEncode(output)];
126
+ }
127
+ }
128
+ ```
129
+
130
+ ### 원격 로깅
131
+
132
+ ```dart
133
+ /// Sentry breadcrumb로 원격 로깅
134
+ class SentryLogOutput extends LogOutput {
135
+ @override
136
+ void output(OutputEvent event) {
137
+ // warning 이상만 Sentry breadcrumb으로 전송
138
+ if (event.level.index < Level.warning.index) return;
139
+
140
+ final level = switch (event.level) {
141
+ Level.warning => SentryLevel.warning,
142
+ Level.error => SentryLevel.error,
143
+ Level.fatal => SentryLevel.fatal,
144
+ _ => SentryLevel.info,
145
+ };
146
+
147
+ Sentry.addBreadcrumb(Breadcrumb(
148
+ message: event.lines.join('\n'),
149
+ level: level,
150
+ category: 'app.log',
151
+ timestamp: DateTime.now().toUtc(),
152
+ ));
153
+ }
154
+ }
155
+
156
+ /// Crashlytics 커스텀 로그
157
+ class CrashlyticsLogOutput extends LogOutput {
158
+ @override
159
+ void output(OutputEvent event) {
160
+ if (event.level.index < Level.info.index) return;
161
+
162
+ // Crashlytics log → 크래시 발생 시 컨텍스트로 포함 (최대 64KB)
163
+ FirebaseCrashlytics.instance.log(
164
+ event.lines.join('\n'),
165
+ );
166
+ }
167
+ }
168
+
169
+ // 초기화 시 출력 추가
170
+ void initializeLogging() {
171
+ final log = AppLogger.instance;
172
+ log.initialize(isRelease: kReleaseMode);
173
+
174
+ if (kReleaseMode) {
175
+ log.addOutput(SentryLogOutput());
176
+ log.addOutput(CrashlyticsLogOutput());
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### 민감 데이터 마스킹
182
+
183
+ ```dart
184
+ /// 민감 데이터 마스킹 유틸리티
185
+ class LogSanitizer {
186
+ static final _patterns = <RegExp, String>{
187
+ // 이메일
188
+ RegExp(r'[\w.+-]+@[\w-]+\.[\w.]+'):
189
+ '***@***.***',
190
+ // 전화번호
191
+ RegExp(r'\+?\d{10,15}'):
192
+ '***-****-****',
193
+ // 토큰/키 (Bearer, API key 등)
194
+ RegExp(r'(Bearer\s+|token[=:]\s*|api[_-]?key[=:]\s*)[A-Za-z0-9\-._~+/]+=*',
195
+ caseSensitive: false):
196
+ r'$1[REDACTED]',
197
+ // 카드 번호 (16자리)
198
+ RegExp(r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b'):
199
+ '****-****-****-****',
200
+ };
201
+
202
+ /// 문자열 내 민감 데이터 마스킹
203
+ static String sanitize(String input) {
204
+ var result = input;
205
+ for (final entry in _patterns.entries) {
206
+ result = result.replaceAll(entry.key, entry.value);
207
+ }
208
+ return result;
209
+ }
210
+
211
+ /// Map 내 민감 키 마스킹
212
+ static Map<String, dynamic> sanitizeMap(Map<String, dynamic> data) {
213
+ const sensitiveKeys = {
214
+ 'password', 'token', 'secret', 'api_key', 'apiKey',
215
+ 'authorization', 'credit_card', 'ssn', 'email', 'phone',
216
+ };
217
+
218
+ return data.map((key, value) {
219
+ if (sensitiveKeys.contains(key.toLowerCase())) {
220
+ return MapEntry(key, '[REDACTED]');
221
+ }
222
+ if (value is String) {
223
+ return MapEntry(key, sanitize(value));
224
+ }
225
+ if (value is Map<String, dynamic>) {
226
+ return MapEntry(key, sanitizeMap(value));
227
+ }
228
+ return MapEntry(key, value);
229
+ });
230
+ }
231
+ }
232
+
233
+ // HTTP 로깅에 마스킹 적용
234
+ class SanitizedLogInterceptor extends Interceptor {
235
+ @override
236
+ void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
237
+ final sanitizedHeaders = LogSanitizer.sanitizeMap(
238
+ options.headers.cast<String, dynamic>(),
239
+ );
240
+ AppLogger.instance.debug(
241
+ 'HTTP ${options.method} ${options.uri}\n'
242
+ 'Headers: $sanitizedHeaders',
243
+ );
244
+ handler.next(options);
245
+ }
246
+ }
247
+ ```
248
+
249
+ ### 규칙
250
+
251
+ - `print`/`debugPrint` 직접 사용 금지 → `AppLogger` 래퍼 사용
252
+ - 릴리스 빌드 → verbose/debug 레벨 비활성화 (ProductionFilter)
253
+ - 프로덕션 → JSON 구조화 출력 (원격 로깅 시스템 호환)
254
+ - 원격 로깅 → Sentry breadcrumb + Crashlytics log 연동
255
+ - warning 이상 → 원격 전송, info → Crashlytics 컨텍스트만
256
+ - 민감 데이터 → `LogSanitizer`로 마스킹 (토큰, PII, 카드번호)
257
+ - HTTP 로깅 → 헤더/바디 마스킹 후 출력
258
+ - 로그 레벨 가이드: verbose(개발 추적), debug(디버깅), info(흐름), warning(주의), error(복구 가능), fatal(복구 불가)