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.
- package/README.ko.md +288 -0
- package/README.md +158 -151
- package/dist/commands/compile.d.ts +3 -0
- package/dist/commands/compile.d.ts.map +1 -0
- package/dist/commands/compile.js +170 -0
- package/dist/commands/compile.js.map +1 -0
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/daemon.js +95 -5
- package/dist/commands/daemon.js.map +1 -1
- package/dist/commands/full.js +1 -0
- package/dist/commands/full.js.map +1 -1
- package/dist/commands/git/pr.js +6 -5
- package/dist/commands/git/pr.js.map +1 -1
- package/dist/commands/git/release.js +2 -7
- package/dist/commands/git/release.js.map +1 -1
- package/dist/commands/improve.js +2 -2
- package/dist/commands/improve.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +12 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/log.d.ts.map +1 -1
- package/dist/commands/log.js +2 -2
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/metrics.d.ts.map +1 -1
- package/dist/commands/metrics.js +6 -2
- package/dist/commands/metrics.js.map +1 -1
- package/dist/commands/retro.js +8 -8
- package/dist/commands/retro.js.map +1 -1
- package/dist/commands/session.js +3 -3
- package/dist/commands/session.js.map +1 -1
- package/dist/commands/skills.d.ts +12 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +228 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/status.js +1 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +23 -1
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/daemon/entry.js +3 -3
- package/dist/daemon/entry.js.map +1 -1
- package/dist/daemon/event-queue.d.ts.map +1 -1
- package/dist/daemon/event-queue.js +2 -2
- package/dist/daemon/event-queue.js.map +1 -1
- package/dist/daemon/index.d.ts +4 -2
- package/dist/daemon/index.d.ts.map +1 -1
- package/dist/daemon/index.js +214 -52
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jsonl-watcher.d.ts +1 -0
- package/dist/daemon/jsonl-watcher.d.ts.map +1 -1
- package/dist/daemon/jsonl-watcher.js.map +1 -1
- package/dist/daemon/meta-cache.d.ts +1 -0
- package/dist/daemon/meta-cache.d.ts.map +1 -1
- package/dist/daemon/meta-cache.js +9 -0
- package/dist/daemon/meta-cache.js.map +1 -1
- package/dist/daemon/session-notes.d.ts +33 -0
- package/dist/daemon/session-notes.d.ts.map +1 -0
- package/dist/daemon/session-notes.js +74 -0
- package/dist/daemon/session-notes.js.map +1 -0
- package/dist/daemon/session-state.d.ts +27 -0
- package/dist/daemon/session-state.d.ts.map +1 -0
- package/dist/daemon/session-state.js +165 -0
- package/dist/daemon/session-state.js.map +1 -0
- package/dist/daemon/shutdown.d.ts.map +1 -1
- package/dist/daemon/shutdown.js +9 -1
- package/dist/daemon/shutdown.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-generator.d.ts +4 -0
- package/dist/lib/agent-generator.d.ts.map +1 -1
- package/dist/lib/agent-generator.js +52 -3
- package/dist/lib/agent-generator.js.map +1 -1
- package/dist/lib/compile-rules.d.ts +66 -0
- package/dist/lib/compile-rules.d.ts.map +1 -0
- package/dist/lib/compile-rules.js +114 -0
- package/dist/lib/compile-rules.js.map +1 -0
- package/dist/lib/compiler.d.ts +105 -0
- package/dist/lib/compiler.d.ts.map +1 -0
- package/dist/lib/compiler.js +368 -0
- package/dist/lib/compiler.js.map +1 -0
- package/dist/lib/config.d.ts +1 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +8 -1
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/project.d.ts.map +1 -1
- package/dist/lib/project.js +8 -3
- package/dist/lib/project.js.map +1 -1
- package/dist/lib/skill-generator.d.ts.map +1 -1
- package/dist/lib/skill-generator.js +22 -1
- package/dist/lib/skill-generator.js.map +1 -1
- package/dist/lib/template.d.ts.map +1 -1
- package/dist/lib/template.js +6 -0
- package/dist/lib/template.js.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +12 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/project.d.ts +1 -1
- package/dist/types/project.d.ts.map +1 -1
- package/dist/types/project.js +2 -0
- package/dist/types/project.js.map +1 -1
- package/package.json +4 -4
- package/templates/base/agents/base/tsq-architect.md +2 -2
- package/templates/base/agents/overlays/domain/mobile/_common.md +13 -0
- package/templates/base/knowledge/checklists/plan-quality.md +31 -0
- package/templates/base/knowledge/checklists/stability-verification.md +14 -0
- package/templates/base/skills/controller/SKILL.md +111 -0
- package/templates/base/skills/controller/references/README.md +35 -0
- package/templates/base/skills/controller/rules/README.md +18 -0
- package/templates/base/skills/mobile/dart/SKILL.md +69 -0
- package/templates/base/skills/mobile/dart/rules/async-patterns.md +112 -0
- package/templates/base/skills/mobile/dart/rules/code-style.md +96 -0
- package/templates/base/skills/mobile/dart/rules/null-safety.md +84 -0
- package/templates/base/skills/mobile/dart/rules/type-system.md +111 -0
- package/templates/base/skills/mobile/flutter/SKILL.md +89 -0
- package/templates/base/skills/mobile/flutter/ci-cd/SKILL.md +82 -0
- package/templates/base/skills/mobile/flutter/ci-cd/references/ci-cd-pipeline.md +314 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/code-signing.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/codemagic-setup.md +116 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/fastlane-setup.md +105 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/github-actions.md +112 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/store-deployment.md +106 -0
- package/templates/base/skills/mobile/flutter/ci-cd/rules/versioning.md +107 -0
- package/templates/base/skills/mobile/flutter/i18n/SKILL.md +78 -0
- package/templates/base/skills/mobile/flutter/i18n/references/i18n-architecture.md +225 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/arb-files.md +182 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/locale-switching.md +226 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/localization-setup.md +137 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/plural-gender.md +159 -0
- package/templates/base/skills/mobile/flutter/i18n/rules/text-direction.md +199 -0
- package/templates/base/skills/mobile/flutter/monitoring/SKILL.md +81 -0
- package/templates/base/skills/mobile/flutter/monitoring/references/monitoring-architecture.md +269 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/analytics.md +227 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/crashlytics-setup.md +195 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/logging.md +258 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/performance-monitoring.md +248 -0
- package/templates/base/skills/mobile/flutter/monitoring/rules/sentry-integration.md +249 -0
- package/templates/base/skills/mobile/flutter/networking/SKILL.md +88 -0
- package/templates/base/skills/mobile/flutter/networking/references/api-client-architecture.md +305 -0
- package/templates/base/skills/mobile/flutter/networking/rules/caching.md +212 -0
- package/templates/base/skills/mobile/flutter/networking/rules/connectivity.md +213 -0
- package/templates/base/skills/mobile/flutter/networking/rules/dio-setup.md +159 -0
- package/templates/base/skills/mobile/flutter/networking/rules/error-handling.md +209 -0
- package/templates/base/skills/mobile/flutter/networking/rules/interceptors.md +205 -0
- package/templates/base/skills/mobile/flutter/networking/rules/retrofit-patterns.md +194 -0
- package/templates/base/skills/mobile/flutter/push-notifications/SKILL.md +87 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/notification-architecture.md +340 -0
- package/templates/base/skills/mobile/flutter/push-notifications/references/platform-setup.md +286 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/background-processing.md +308 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/deep-linking.md +217 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/fcm-setup.md +164 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/local-notifications.md +262 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-handling.md +210 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/notification-permissions.md +246 -0
- package/templates/base/skills/mobile/flutter/push-notifications/rules/rich-notifications.md +320 -0
- package/templates/base/skills/mobile/flutter/references/freezed-patterns.md +162 -0
- package/templates/base/skills/mobile/flutter/references/project-structure.md +170 -0
- package/templates/base/skills/mobile/flutter/rules/animations.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/architecture.md +121 -0
- package/templates/base/skills/mobile/flutter/rules/navigation-routing.md +117 -0
- package/templates/base/skills/mobile/flutter/rules/performance.md +112 -0
- package/templates/base/skills/mobile/flutter/rules/platform-adaptive.md +126 -0
- package/templates/base/skills/mobile/flutter/rules/state-management.md +110 -0
- package/templates/base/skills/mobile/flutter/rules/testing.md +131 -0
- package/templates/base/skills/mobile/flutter/rules/widget-conventions.md +122 -0
- package/templates/base/skills/mobile/flutter/security/SKILL.md +86 -0
- package/templates/base/skills/mobile/flutter/security/references/mobile-security-checklist.md +168 -0
- package/templates/base/skills/mobile/flutter/security/rules/api-key-protection.md +206 -0
- package/templates/base/skills/mobile/flutter/security/rules/authentication.md +248 -0
- package/templates/base/skills/mobile/flutter/security/rules/data-protection.md +271 -0
- package/templates/base/skills/mobile/flutter/security/rules/obfuscation.md +213 -0
- package/templates/base/skills/mobile/flutter/security/rules/secure-storage.md +171 -0
- package/templates/base/skills/mobile/flutter/security/rules/ssl-pinning.md +197 -0
- package/templates/base/skills/stability-verification/SKILL.md +64 -0
- package/templates/base/skills/stability-verification/references/release-checklist.md +34 -0
- package/templates/base/skills/stability-verification/references/security-fix-patterns.md +112 -0
- package/templates/base/skills/stability-verification/rules/verification-layers.md +67 -0
- package/templates/base/skills/stability-verification/rules/verification-workflow.md +69 -0
- package/templates/base/skills/stability-verification/scripts/verify.sh +294 -0
- package/templates/platforms/claude-code/CLAUDE.md.template +25 -0
- package/templates/platforms/claude-code/rules/build-gate.md +28 -0
- package/templates/platforms/claude-code/rules/completion-verification.md +30 -0
- package/templates/platforms/claude-code/rules/context-monitor.md +23 -0
- package/templates/platforms/claude-code/rules/plan-review.md +45 -0
- package/templates/platforms/claude-code/rules/quality-guards.md +43 -0
- package/templates/platforms/claude-code/rules/session-notes.md +18 -0
- package/templates/platforms/claude-code/rules/skill-suggest.md +27 -0
- package/templates/platforms/claude-code/scripts/build-gate.sh +73 -0
- package/templates/platforms/claude-code/scripts/completion-guard.sh +93 -0
- package/templates/platforms/claude-code/scripts/phase-guard.sh +79 -0
- package/templates/platforms/claude-code/scripts/safe-guard.sh +83 -0
- package/templates/platforms/claude-code/scripts/skill-rules.json +85 -0
- package/templates/platforms/claude-code/scripts/skill-suggest.sh +105 -0
- package/templates/platforms/claude-code/settings.json +111 -3
- package/templates/project-types/mobile-app/config.yaml +123 -0
- package/templates/project-types/mobile-app/process/workflow.xml +191 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Freezed + JSON Serializable Patterns
|
|
3
|
+
category: guide
|
|
4
|
+
tags: freezed, json-serializable, immutable, dto
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Freezed + JSON Serializable
|
|
8
|
+
|
|
9
|
+
불변 데이터 모델 + JSON 직렬화 패턴. Domain Entity와 DTO 분리.
|
|
10
|
+
|
|
11
|
+
### 기본 설정
|
|
12
|
+
|
|
13
|
+
```yaml
|
|
14
|
+
# pubspec.yaml
|
|
15
|
+
dependencies:
|
|
16
|
+
freezed_annotation: ^2.4.0
|
|
17
|
+
json_annotation: ^4.9.0
|
|
18
|
+
|
|
19
|
+
dev_dependencies:
|
|
20
|
+
freezed: ^2.5.0
|
|
21
|
+
json_serializable: ^6.8.0
|
|
22
|
+
build_runner: ^2.4.0
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 코드 생성
|
|
27
|
+
dart run build_runner build --delete-conflicting-outputs
|
|
28
|
+
|
|
29
|
+
# 감시 모드 (개발 중)
|
|
30
|
+
dart run build_runner watch --delete-conflicting-outputs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Domain Entity (freezed)
|
|
34
|
+
|
|
35
|
+
```dart
|
|
36
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
37
|
+
|
|
38
|
+
part 'match.freezed.dart';
|
|
39
|
+
|
|
40
|
+
@freezed
|
|
41
|
+
class Match with _$Match {
|
|
42
|
+
const factory Match({
|
|
43
|
+
required String id,
|
|
44
|
+
required Sport sport,
|
|
45
|
+
required String title,
|
|
46
|
+
required DateTime scheduledAt,
|
|
47
|
+
required MatchLocation location,
|
|
48
|
+
required int maxPlayers,
|
|
49
|
+
@Default([]) List<String> playerIds,
|
|
50
|
+
@Default(MatchStatus.open) MatchStatus status,
|
|
51
|
+
}) = _Match;
|
|
52
|
+
|
|
53
|
+
// 커스텀 getter (const factory 밑에 private 생성자 필요)
|
|
54
|
+
const Match._();
|
|
55
|
+
|
|
56
|
+
bool get isFull => playerIds.length >= maxPlayers;
|
|
57
|
+
bool get isUpcoming => scheduledAt.isAfter(DateTime.now());
|
|
58
|
+
int get availableSlots => maxPlayers - playerIds.length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
enum Sport { tennis, badminton, pickleball, paddle }
|
|
62
|
+
enum MatchStatus { open, full, inProgress, completed, cancelled }
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### DTO (freezed + json_serializable)
|
|
66
|
+
|
|
67
|
+
```dart
|
|
68
|
+
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
69
|
+
|
|
70
|
+
part 'match_dto.freezed.dart';
|
|
71
|
+
part 'match_dto.g.dart';
|
|
72
|
+
|
|
73
|
+
@freezed
|
|
74
|
+
class MatchDto with _$MatchDto {
|
|
75
|
+
const factory MatchDto({
|
|
76
|
+
required String id,
|
|
77
|
+
required String sport,
|
|
78
|
+
required String title,
|
|
79
|
+
@JsonKey(name: 'scheduled_at') required String scheduledAt,
|
|
80
|
+
required MatchLocationDto location,
|
|
81
|
+
@JsonKey(name: 'max_players') required int maxPlayers,
|
|
82
|
+
@JsonKey(name: 'player_ids') @Default([]) List<String> playerIds,
|
|
83
|
+
@Default('open') String status,
|
|
84
|
+
}) = _MatchDto;
|
|
85
|
+
|
|
86
|
+
factory MatchDto.fromJson(Map<String, dynamic> json) =>
|
|
87
|
+
_$MatchDtoFromJson(json);
|
|
88
|
+
|
|
89
|
+
const MatchDto._();
|
|
90
|
+
|
|
91
|
+
/// DTO → Domain 변환
|
|
92
|
+
Match toDomain() => Match(
|
|
93
|
+
id: id,
|
|
94
|
+
sport: Sport.values.byName(sport),
|
|
95
|
+
title: title,
|
|
96
|
+
scheduledAt: DateTime.parse(scheduledAt),
|
|
97
|
+
location: location.toDomain(),
|
|
98
|
+
maxPlayers: maxPlayers,
|
|
99
|
+
playerIds: playerIds,
|
|
100
|
+
status: MatchStatus.values.byName(status),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
/// Domain → DTO 변환
|
|
104
|
+
factory MatchDto.fromDomain(Match match) => MatchDto(
|
|
105
|
+
id: match.id,
|
|
106
|
+
sport: match.sport.name,
|
|
107
|
+
title: match.title,
|
|
108
|
+
scheduledAt: match.scheduledAt.toIso8601String(),
|
|
109
|
+
location: MatchLocationDto.fromDomain(match.location),
|
|
110
|
+
maxPlayers: match.maxPlayers,
|
|
111
|
+
playerIds: match.playerIds,
|
|
112
|
+
status: match.status.name,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Sealed Class + Freezed (상태)
|
|
118
|
+
|
|
119
|
+
```dart
|
|
120
|
+
@freezed
|
|
121
|
+
sealed class AuthState with _$AuthState {
|
|
122
|
+
const factory AuthState.initial() = AuthInitial;
|
|
123
|
+
const factory AuthState.loading() = AuthLoading;
|
|
124
|
+
const factory AuthState.authenticated(User user) = AuthAuthenticated;
|
|
125
|
+
const factory AuthState.unauthenticated() = AuthUnauthenticated;
|
|
126
|
+
const factory AuthState.error(String message) = AuthError;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 사용: 패턴 매칭
|
|
130
|
+
Widget build(BuildContext context) {
|
|
131
|
+
final authState = ref.watch(authProvider);
|
|
132
|
+
|
|
133
|
+
return authState.when(
|
|
134
|
+
initial: () => const SplashScreen(),
|
|
135
|
+
loading: () => const LoadingScreen(),
|
|
136
|
+
authenticated: (user) => const HomeScreen(),
|
|
137
|
+
unauthenticated: () => const LoginScreen(),
|
|
138
|
+
error: (msg) => ErrorScreen(message: msg),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### copyWith (불변 업데이트)
|
|
144
|
+
|
|
145
|
+
```dart
|
|
146
|
+
// freezed가 자동 생성하는 copyWith
|
|
147
|
+
final updatedMatch = match.copyWith(
|
|
148
|
+
status: MatchStatus.full,
|
|
149
|
+
playerIds: [...match.playerIds, newPlayerId],
|
|
150
|
+
);
|
|
151
|
+
// 원본 match는 변경 없음 (불변)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 규칙
|
|
155
|
+
|
|
156
|
+
- Domain Entity: `@freezed` (JSON 직렬화 없음, 순수 도메인)
|
|
157
|
+
- DTO: `@freezed` + `@JsonKey` + `fromJson`/`toJson`
|
|
158
|
+
- DTO → Domain: `.toDomain()` 메서드
|
|
159
|
+
- Domain → DTO: `DTO.fromDomain()` factory
|
|
160
|
+
- API 응답 key naming: `@JsonKey(name: 'snake_case')`
|
|
161
|
+
- 상태 모델: `@freezed sealed class` + `.when()` 패턴 매칭
|
|
162
|
+
- `build_runner watch` 개발 중 항상 실행
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Project Structure (Feature-First + Melos)
|
|
3
|
+
category: guide
|
|
4
|
+
tags: structure, monorepo, melos, feature-first
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Project Structure
|
|
8
|
+
|
|
9
|
+
Flutter Feature-first 디렉토리 구조 + Melos 모노레포 관리.
|
|
10
|
+
|
|
11
|
+
### 단일 앱 구조
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
my_app/
|
|
15
|
+
├── lib/
|
|
16
|
+
│ ├── main.dart # 엔트리포인트
|
|
17
|
+
│ ├── app.dart # MaterialApp + ProviderScope
|
|
18
|
+
│ ├── core/
|
|
19
|
+
│ │ ├── constants/
|
|
20
|
+
│ │ │ ├── app_colors.dart
|
|
21
|
+
│ │ │ ├── app_spacing.dart
|
|
22
|
+
│ │ │ └── api_endpoints.dart
|
|
23
|
+
│ │ ├── extensions/
|
|
24
|
+
│ │ │ ├── context_ext.dart # BuildContext 확장
|
|
25
|
+
│ │ │ └── string_ext.dart
|
|
26
|
+
│ │ ├── router/
|
|
27
|
+
│ │ │ ├── app_router.dart # GoRouter 정의
|
|
28
|
+
│ │ │ └── route_names.dart # 경로 상수
|
|
29
|
+
│ │ ├── theme/
|
|
30
|
+
│ │ │ ├── app_theme.dart # ThemeData
|
|
31
|
+
│ │ │ └── typography.dart
|
|
32
|
+
│ │ ├── network/
|
|
33
|
+
│ │ │ ├── api_client.dart # Dio/http 설정
|
|
34
|
+
│ │ │ └── api_interceptor.dart
|
|
35
|
+
│ │ └── providers/
|
|
36
|
+
│ │ ├── auth_provider.dart # 전역 인증 상태
|
|
37
|
+
│ │ └── locale_provider.dart
|
|
38
|
+
│ │
|
|
39
|
+
│ ├── features/
|
|
40
|
+
│ │ ├── auth/
|
|
41
|
+
│ │ │ ├── data/
|
|
42
|
+
│ │ │ │ ├── datasources/
|
|
43
|
+
│ │ │ │ │ └── auth_remote_datasource.dart
|
|
44
|
+
│ │ │ │ ├── dtos/
|
|
45
|
+
│ │ │ │ │ └── auth_response_dto.dart
|
|
46
|
+
│ │ │ │ │ └── auth_response_dto.g.dart # json_serializable
|
|
47
|
+
│ │ │ │ │ └── auth_response_dto.freezed.dart
|
|
48
|
+
│ │ │ │ └── repositories/
|
|
49
|
+
│ │ │ │ └── auth_repository_impl.dart
|
|
50
|
+
│ │ │ ├── domain/
|
|
51
|
+
│ │ │ │ ├── entities/
|
|
52
|
+
│ │ │ │ │ └── user.dart
|
|
53
|
+
│ │ │ │ │ └── user.freezed.dart
|
|
54
|
+
│ │ │ │ └── repositories/
|
|
55
|
+
│ │ │ │ └── auth_repository.dart # abstract
|
|
56
|
+
│ │ │ └── presentation/
|
|
57
|
+
│ │ │ ├── providers/
|
|
58
|
+
│ │ │ │ └── auth_provider.dart
|
|
59
|
+
│ │ │ ├── screens/
|
|
60
|
+
│ │ │ │ ├── login_screen.dart
|
|
61
|
+
│ │ │ │ └── signup_screen.dart
|
|
62
|
+
│ │ │ └── widgets/
|
|
63
|
+
│ │ │ └── social_login_buttons.dart
|
|
64
|
+
│ │ │
|
|
65
|
+
│ │ ├── match/
|
|
66
|
+
│ │ │ ├── data/...
|
|
67
|
+
│ │ │ ├── domain/...
|
|
68
|
+
│ │ │ └── presentation/...
|
|
69
|
+
│ │ │
|
|
70
|
+
│ │ └── community/
|
|
71
|
+
│ │ ├── data/...
|
|
72
|
+
│ │ ├── domain/...
|
|
73
|
+
│ │ └── presentation/...
|
|
74
|
+
│ │
|
|
75
|
+
│ └── shared/
|
|
76
|
+
│ ├── widgets/
|
|
77
|
+
│ │ ├── app_button.dart
|
|
78
|
+
│ │ ├── app_text_field.dart
|
|
79
|
+
│ │ └── loading_indicator.dart
|
|
80
|
+
│ └── utils/
|
|
81
|
+
│ ├── date_formatter.dart
|
|
82
|
+
│ └── validators.dart
|
|
83
|
+
│
|
|
84
|
+
├── test/
|
|
85
|
+
│ ├── features/
|
|
86
|
+
│ │ ├── auth/
|
|
87
|
+
│ │ │ ├── data/repositories/auth_repository_impl_test.dart
|
|
88
|
+
│ │ │ └── presentation/providers/auth_provider_test.dart
|
|
89
|
+
│ │ └── match/...
|
|
90
|
+
│ ├── fixtures/
|
|
91
|
+
│ │ └── test_data.dart # 공유 테스트 데이터
|
|
92
|
+
│ └── helpers/
|
|
93
|
+
│ └── pump_app.dart # testWidgets 헬퍼
|
|
94
|
+
│
|
|
95
|
+
├── integration_test/
|
|
96
|
+
│ └── match_flow_test.dart
|
|
97
|
+
│
|
|
98
|
+
├── assets/
|
|
99
|
+
│ ├── images/
|
|
100
|
+
│ ├── icons/
|
|
101
|
+
│ └── translations/ # ARB 파일 (i18n)
|
|
102
|
+
│
|
|
103
|
+
├── pubspec.yaml
|
|
104
|
+
├── analysis_options.yaml
|
|
105
|
+
└── l10n.yaml # 국제화 설정
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Melos 모노레포 구조
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
project_root/
|
|
112
|
+
├── melos.yaml
|
|
113
|
+
├── apps/
|
|
114
|
+
│ ├── mobile/ # Flutter 앱
|
|
115
|
+
│ │ ├── lib/
|
|
116
|
+
│ │ └── pubspec.yaml
|
|
117
|
+
│ └── admin/ # 관리자 웹 (선택)
|
|
118
|
+
│ ├── lib/
|
|
119
|
+
│ └── pubspec.yaml
|
|
120
|
+
├── packages/
|
|
121
|
+
│ ├── core/ # 공유 코어 (모델, 유틸)
|
|
122
|
+
│ │ ├── lib/
|
|
123
|
+
│ │ └── pubspec.yaml
|
|
124
|
+
│ ├── ui_kit/ # 공유 UI 컴포넌트
|
|
125
|
+
│ │ ├── lib/
|
|
126
|
+
│ │ └── pubspec.yaml
|
|
127
|
+
│ ├── api_client/ # API 클라이언트
|
|
128
|
+
│ │ ├── lib/
|
|
129
|
+
│ │ └── pubspec.yaml
|
|
130
|
+
│ └── auth/ # 인증 모듈
|
|
131
|
+
│ ├── lib/
|
|
132
|
+
│ └── pubspec.yaml
|
|
133
|
+
└── tools/
|
|
134
|
+
└── custom_lint_rules/ # 프로젝트 커스텀 린트
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### melos.yaml
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
name: my_project
|
|
141
|
+
packages:
|
|
142
|
+
- apps/*
|
|
143
|
+
- packages/*
|
|
144
|
+
|
|
145
|
+
scripts:
|
|
146
|
+
analyze:
|
|
147
|
+
exec: dart analyze --fatal-infos
|
|
148
|
+
test:
|
|
149
|
+
exec: flutter test --coverage
|
|
150
|
+
format:
|
|
151
|
+
exec: dart format --set-exit-if-changed .
|
|
152
|
+
build_runner:
|
|
153
|
+
exec: dart run build_runner build --delete-conflicting-outputs
|
|
154
|
+
packageFilters:
|
|
155
|
+
dependsOn: build_runner
|
|
156
|
+
clean:
|
|
157
|
+
exec: flutter clean
|
|
158
|
+
|
|
159
|
+
command:
|
|
160
|
+
bootstrap:
|
|
161
|
+
usePubspecOverrides: true
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 규칙
|
|
165
|
+
|
|
166
|
+
- feature 안에서 다른 feature 직접 import 금지
|
|
167
|
+
- 공유 로직은 `core/` 또는 `shared/`
|
|
168
|
+
- 모노레포: `melos bootstrap`으로 의존성 연결
|
|
169
|
+
- 테스트 디렉토리는 `lib/` 미러링 구조
|
|
170
|
+
- generated 파일 (`.g.dart`, `.freezed.dart`) → `.gitignore`에 추가하지 않음 (CI에서 재생성 비용)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Animations
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
tags: animation, implicit, explicit, hero
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Animations
|
|
8
|
+
|
|
9
|
+
Flutter 애니메이션 패턴. 암시적(간단) → 명시적(복잡) 선택 기준.
|
|
10
|
+
|
|
11
|
+
### 선택 기준
|
|
12
|
+
|
|
13
|
+
| 복잡도 | 사용 | 예시 |
|
|
14
|
+
|--------|------|------|
|
|
15
|
+
| 단순 속성 변경 | `AnimatedContainer`, `AnimatedOpacity` | 페이드, 크기 변경 |
|
|
16
|
+
| 여러 위젯 전환 | `AnimatedSwitcher` | 탭 전환, 상태 전환 |
|
|
17
|
+
| 화면 간 공유 요소 | `Hero` | 카드 → 상세 전환 |
|
|
18
|
+
| 커스텀 곡선/시퀀스 | `AnimationController` + `Tween` | 복잡한 모션 |
|
|
19
|
+
| 반복/물리 기반 | `AnimationController` + `SpringSimulation` | 풀투리프레시 |
|
|
20
|
+
|
|
21
|
+
### 암시적 애니메이션 (간단)
|
|
22
|
+
|
|
23
|
+
```dart
|
|
24
|
+
// 상태에 따라 자동 전환
|
|
25
|
+
AnimatedContainer(
|
|
26
|
+
duration: const Duration(milliseconds: 300),
|
|
27
|
+
curve: Curves.easeInOut,
|
|
28
|
+
width: isExpanded ? 200 : 100,
|
|
29
|
+
height: isExpanded ? 200 : 100,
|
|
30
|
+
decoration: BoxDecoration(
|
|
31
|
+
color: isSelected ? Colors.blue : Colors.grey,
|
|
32
|
+
borderRadius: BorderRadius.circular(isExpanded ? 16 : 8),
|
|
33
|
+
),
|
|
34
|
+
child: content,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// 위젯 교체 시 전환
|
|
38
|
+
AnimatedSwitcher(
|
|
39
|
+
duration: const Duration(milliseconds: 200),
|
|
40
|
+
child: isLoading
|
|
41
|
+
? const CircularProgressIndicator(key: ValueKey('loading'))
|
|
42
|
+
: ContentView(key: ValueKey('content'), data: data),
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Hero 전환
|
|
47
|
+
|
|
48
|
+
```dart
|
|
49
|
+
// 리스트 아이템
|
|
50
|
+
Hero(
|
|
51
|
+
tag: 'match-${match.id}',
|
|
52
|
+
child: MatchCard(match: match),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// 상세 화면
|
|
56
|
+
Hero(
|
|
57
|
+
tag: 'match-${match.id}',
|
|
58
|
+
child: MatchDetailHeader(match: match),
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 명시적 애니메이션 (복잡)
|
|
63
|
+
|
|
64
|
+
```dart
|
|
65
|
+
class PulseWidget extends StatefulWidget {
|
|
66
|
+
const PulseWidget({super.key, required this.child});
|
|
67
|
+
final Widget child;
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
State<PulseWidget> createState() => _PulseWidgetState();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class _PulseWidgetState extends State<PulseWidget>
|
|
74
|
+
with SingleTickerProviderStateMixin {
|
|
75
|
+
late final AnimationController _controller;
|
|
76
|
+
late final Animation<double> _scale;
|
|
77
|
+
|
|
78
|
+
@override
|
|
79
|
+
void initState() {
|
|
80
|
+
super.initState();
|
|
81
|
+
_controller = AnimationController(
|
|
82
|
+
duration: const Duration(milliseconds: 1000),
|
|
83
|
+
vsync: this,
|
|
84
|
+
)..repeat(reverse: true);
|
|
85
|
+
|
|
86
|
+
_scale = Tween(begin: 1.0, end: 1.1).animate(
|
|
87
|
+
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@override
|
|
92
|
+
void dispose() {
|
|
93
|
+
_controller.dispose();
|
|
94
|
+
super.dispose();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
Widget build(BuildContext context) {
|
|
99
|
+
return ScaleTransition(scale: _scale, child: widget.child);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### 규칙
|
|
105
|
+
|
|
106
|
+
- 단순 속성 변경 → 암시적 (`Animated*` 위젯) 우선
|
|
107
|
+
- 위젯 교체 → `AnimatedSwitcher` + `key` 필수
|
|
108
|
+
- 화면 전환 공유 요소 → `Hero` (동일 tag)
|
|
109
|
+
- 커스텀 애니메이션 → `AnimationController` + `dispose()` 필수
|
|
110
|
+
- duration: 200-300ms (UI), 300-500ms (페이지 전환)
|
|
111
|
+
- `vsync: this` + `TickerProviderStateMixin` (프레임 동기화)
|
|
112
|
+
- 복잡한 시퀀스는 `staggered_animations` 패키지 검토
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Architecture (Feature-First + MVVM)
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
tags: architecture, feature-first, mvvm, clean-architecture
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Architecture (Feature-First + MVVM)
|
|
8
|
+
|
|
9
|
+
Flutter 공식 아키텍처 가이드(MVVM) + VGV 4-layer + Feature-first 구조 종합.
|
|
10
|
+
|
|
11
|
+
### 3-Layer 아키텍처
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─ Presentation (UI) ─────────────────┐
|
|
15
|
+
│ Widget → ViewModel (Notifier) │ UI 렌더링 + 사용자 입력
|
|
16
|
+
│ ref.watch로 상태 구독 │
|
|
17
|
+
└─────────────────────────────────────┘
|
|
18
|
+
↕ (Domain Model)
|
|
19
|
+
┌─ Domain ────────────────────────────┐
|
|
20
|
+
│ Entity, ValueObject, UseCase │ 비즈니스 규칙 (선택 레이어)
|
|
21
|
+
│ 플랫폼/프레임워크 의존성 없음 │
|
|
22
|
+
└─────────────────────────────────────┘
|
|
23
|
+
↕ (Domain Model)
|
|
24
|
+
┌─ Data ──────────────────────────────┐
|
|
25
|
+
│ Repository → DataSource │ API/DB/캐시 접근
|
|
26
|
+
│ DTO ↔ Domain Model 변환 │
|
|
27
|
+
└─────────────────────────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Feature-First 디렉토리
|
|
31
|
+
|
|
32
|
+
**Incorrect:**
|
|
33
|
+
```
|
|
34
|
+
lib/
|
|
35
|
+
models/ # 모든 모델이 한 폴더
|
|
36
|
+
screens/ # 모든 화면이 한 폴더
|
|
37
|
+
services/ # 모든 서비스가 한 폴더
|
|
38
|
+
widgets/ # 모든 위젯이 한 폴더
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Correct:**
|
|
42
|
+
```
|
|
43
|
+
lib/
|
|
44
|
+
core/ # 공유 인프라
|
|
45
|
+
constants/
|
|
46
|
+
extensions/
|
|
47
|
+
router/
|
|
48
|
+
theme/
|
|
49
|
+
providers/ # 전역 provider (auth, locale 등)
|
|
50
|
+
features/
|
|
51
|
+
auth/
|
|
52
|
+
data/
|
|
53
|
+
datasources/ # AuthRemoteDataSource
|
|
54
|
+
dtos/ # AuthResponseDto (JSON 매핑)
|
|
55
|
+
repositories/ # AuthRepositoryImpl
|
|
56
|
+
domain/
|
|
57
|
+
entities/ # User
|
|
58
|
+
repositories/ # AuthRepository (abstract)
|
|
59
|
+
presentation/
|
|
60
|
+
providers/ # authProvider, loginNotifier
|
|
61
|
+
screens/ # LoginScreen, SignUpScreen
|
|
62
|
+
widgets/ # AuthForm, SocialLoginButtons
|
|
63
|
+
match/
|
|
64
|
+
data/...
|
|
65
|
+
domain/...
|
|
66
|
+
presentation/...
|
|
67
|
+
shared/
|
|
68
|
+
widgets/ # 재사용 UI 컴포넌트
|
|
69
|
+
utils/ # 순수 유틸리티 함수
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Repository 패턴
|
|
73
|
+
|
|
74
|
+
```dart
|
|
75
|
+
// Domain layer — 추상 인터페이스
|
|
76
|
+
abstract class MatchRepository {
|
|
77
|
+
Future<List<Match>> getMatches({required Sport sport});
|
|
78
|
+
Future<void> joinMatch(String matchId);
|
|
79
|
+
Stream<Match> watchMatch(String matchId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Data layer — 구현
|
|
83
|
+
class MatchRepositoryImpl implements MatchRepository {
|
|
84
|
+
const MatchRepositoryImpl({
|
|
85
|
+
required this.remoteDataSource,
|
|
86
|
+
required this.localDataSource,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
final MatchRemoteDataSource remoteDataSource;
|
|
90
|
+
final MatchLocalDataSource localDataSource;
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
Future<List<Match>> getMatches({required Sport sport}) async {
|
|
94
|
+
try {
|
|
95
|
+
final dtos = await remoteDataSource.fetchMatches(sport.name);
|
|
96
|
+
final matches = dtos.map((dto) => dto.toDomain()).toList();
|
|
97
|
+
await localDataSource.cacheMatches(matches);
|
|
98
|
+
return matches;
|
|
99
|
+
} on NetworkException {
|
|
100
|
+
return localDataSource.getCachedMatches(sport);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Provider 연결
|
|
106
|
+
final matchRepositoryProvider = Provider<MatchRepository>((ref) {
|
|
107
|
+
return MatchRepositoryImpl(
|
|
108
|
+
remoteDataSource: ref.watch(matchRemoteDataSourceProvider),
|
|
109
|
+
localDataSource: ref.watch(matchLocalDataSourceProvider),
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 규칙
|
|
115
|
+
|
|
116
|
+
- **Feature-first**: `lib/features/{name}/` 구조 필수
|
|
117
|
+
- **레이어 방향**: Presentation → Domain ← Data (의존성 역전)
|
|
118
|
+
- **Domain layer**: Flutter import 금지 (`dart:` 만 허용)
|
|
119
|
+
- **DTO ↔ Model 분리**: API 응답 구조와 도메인 모델 분리
|
|
120
|
+
- **Repository**: abstract (domain) + impl (data) 분리
|
|
121
|
+
- **1 feature = 1 독립 단위**: feature 간 직접 import 금지 → 공유 도메인은 `core/`
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Navigation & Routing
|
|
3
|
+
impact: HIGH
|
|
4
|
+
tags: go-router, navigation, deep-link, routing
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Navigation & Routing
|
|
8
|
+
|
|
9
|
+
go_router 기반 선언적 라우팅. 딥링크, 가드, 중첩 네비게이션.
|
|
10
|
+
|
|
11
|
+
### 라우터 설정
|
|
12
|
+
|
|
13
|
+
```dart
|
|
14
|
+
final routerProvider = Provider<GoRouter>((ref) {
|
|
15
|
+
final authState = ref.watch(authStateProvider);
|
|
16
|
+
|
|
17
|
+
return GoRouter(
|
|
18
|
+
initialLocation: '/',
|
|
19
|
+
redirect: (context, state) {
|
|
20
|
+
final isLoggedIn = authState.valueOrNull != null;
|
|
21
|
+
final isAuthRoute = state.matchedLocation.startsWith('/auth');
|
|
22
|
+
|
|
23
|
+
if (!isLoggedIn && !isAuthRoute) return '/auth/login';
|
|
24
|
+
if (isLoggedIn && isAuthRoute) return '/';
|
|
25
|
+
return null; // 리다이렉트 불필요
|
|
26
|
+
},
|
|
27
|
+
routes: [
|
|
28
|
+
// 인증 라우트 (바텀 네비 없음)
|
|
29
|
+
GoRoute(
|
|
30
|
+
path: '/auth/login',
|
|
31
|
+
builder: (context, state) => const LoginScreen(),
|
|
32
|
+
),
|
|
33
|
+
// 메인 앱 (바텀 네비 + 중첩 라우팅)
|
|
34
|
+
StatefulShellRoute.indexedStack(
|
|
35
|
+
builder: (context, state, child) => MainShell(child: child),
|
|
36
|
+
branches: [
|
|
37
|
+
StatefulShellBranch(routes: [
|
|
38
|
+
GoRoute(
|
|
39
|
+
path: '/',
|
|
40
|
+
builder: (_, __) => const HomeScreen(),
|
|
41
|
+
routes: [
|
|
42
|
+
GoRoute(
|
|
43
|
+
path: 'match/:id',
|
|
44
|
+
builder: (_, state) => MatchDetailScreen(
|
|
45
|
+
matchId: state.pathParameters['id']!,
|
|
46
|
+
),
|
|
47
|
+
),
|
|
48
|
+
],
|
|
49
|
+
),
|
|
50
|
+
]),
|
|
51
|
+
StatefulShellBranch(routes: [
|
|
52
|
+
GoRoute(
|
|
53
|
+
path: '/community',
|
|
54
|
+
builder: (_, __) => const CommunityScreen(),
|
|
55
|
+
),
|
|
56
|
+
]),
|
|
57
|
+
StatefulShellBranch(routes: [
|
|
58
|
+
GoRoute(
|
|
59
|
+
path: '/profile',
|
|
60
|
+
builder: (_, __) => const ProfileScreen(),
|
|
61
|
+
),
|
|
62
|
+
]),
|
|
63
|
+
],
|
|
64
|
+
),
|
|
65
|
+
],
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 네비게이션 호출
|
|
71
|
+
|
|
72
|
+
**Incorrect:**
|
|
73
|
+
```dart
|
|
74
|
+
// 명령형 — 히스토리 관리 어려움
|
|
75
|
+
Navigator.push(
|
|
76
|
+
context,
|
|
77
|
+
MaterialPageRoute(builder: (_) => MatchDetailScreen(id: matchId)),
|
|
78
|
+
);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Correct:**
|
|
82
|
+
```dart
|
|
83
|
+
// 선언적 — URL 기반
|
|
84
|
+
context.go('/match/$matchId'); // 교체
|
|
85
|
+
context.push('/match/$matchId'); // 스택 추가
|
|
86
|
+
context.pop(); // 뒤로가기
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 딥링크 설정
|
|
90
|
+
|
|
91
|
+
```dart
|
|
92
|
+
// go_router는 자동으로 URL 파싱
|
|
93
|
+
// Android: AndroidManifest.xml
|
|
94
|
+
// <intent-filter android:autoVerify="true">
|
|
95
|
+
// <data android:scheme="https" android:host="app.example.com" />
|
|
96
|
+
// </intent-filter>
|
|
97
|
+
|
|
98
|
+
// iOS: Info.plist
|
|
99
|
+
// Associated Domains: applinks:app.example.com
|
|
100
|
+
|
|
101
|
+
// Flutter 측: 라우트만 정의하면 딥링크 자동 처리
|
|
102
|
+
GoRoute(
|
|
103
|
+
path: '/match/:id',
|
|
104
|
+
builder: (_, state) => MatchDetailScreen(
|
|
105
|
+
matchId: state.pathParameters['id']!,
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### 규칙
|
|
111
|
+
|
|
112
|
+
- `go_router` 선언적 라우팅 사용 (Navigator.push 금지)
|
|
113
|
+
- 라우트 정의는 `core/router/` 에 중앙화
|
|
114
|
+
- `redirect` 로 인증 가드 구현 (미들웨어 패턴)
|
|
115
|
+
- `StatefulShellRoute` 로 바텀 네비 + 각 탭 스택 유지
|
|
116
|
+
- path parameter는 `/:id` 패턴, query는 `state.uri.queryParameters`
|
|
117
|
+
- 라우트 경로 상수화 (`RouteNames` 또는 `RoutePaths` 클래스)
|