Flutter 폴더 / 패키지 구조
Flutter 앱(mobile/)의 폴더 구조와 각 레이어 책임. 시스템 전체 흐름은 architecture.md, 도메인별 동작은 domain-*.md 참조.
디렉토리 트리
mobile/
├── lib/
│ ├── main_dev.dart # dev 진입점 — bootstrap(AppFlavor.dev)
│ ├── main_prod.dart # prod 진입점 — bootstrap(AppFlavor.prod)
│ ├── firebase_options.dart # Firebase config (dev / prod 분기)
│ │
│ ├── app/ # 앱 셸 — 진입 / 라우팅 / Flavor
│ │ ├── app.dart # DartBriefApp (MaterialApp.router)
│ │ ├── bootstrap.dart # Kakao + Firebase + App Check + AdMob 초기화
│ │ ├── app_flavor.dart # AppFlavor enum (dev / prod)
│ │ ├── flavor_provider.dart # appFlavorProvider (Riverpod)
│ │ ├── router.dart # GoRouter + StatefulShellRoute
│ │ └── nav_bar_tuning.dart # 하단 floating pill 네비
│ │
│ ├── features/ # 도메인 모듈 (presentation / application / data)
│ │ ├── auth/
│ │ ├── disclosure/
│ │ ├── notifications/
│ │ ├── quota/
│ │ ├── settings/
│ │ └── watchlist/
│ │
│ └── shared/ # 피처 공통 인프라
│ ├── api/ # dio 클라이언트 + 토큰 저장
│ ├── auth/ # 전역 AuthController + UserInfo
│ ├── browser/ # 인앱 브라우저 시트
│ ├── debug/ # 인앱 로그 패널 (DebugLog)
│ ├── format/ # 날짜 포맷 유틸
│ ├── splash/ # 스플래시 애니메이션
│ ├── theme/ # 디자인 토큰 (color / typography / spacing / radius)
│ └── toast/ # 토스트 서비스
│
├── android/ # 네이티브 — flavor 설정, app/build.gradle, AdMob/App Check 메타
├── ios/ # 네이티브 — Runner.xcodeproj, Info.plist (flavor별)
├── pubspec.yaml # 의존성
└── Makefile # `make run-dev` / `run-prod` (Tailscale IP 자동 주입)레이어 패턴
각 features/{X}/는 동일한 3-folder 구조:
features/{X}/
├── application/ # Riverpod 컨트롤러 (Notifier / AsyncNotifier)
├── data/ # Repository + Model
└── presentation/
├── pages/ # 라우트로 매핑되는 화면
└── widgets/ # 그 화면에서만 쓰는 위젯- presentation → application (Provider 의존)
- application → data (Repository 의존)
- data → shared/api/api_client.dart (HTTP)
- presentation → shared/theme (디자인 토큰)
reverse dependency는 금지 (data가 presentation을 알지 못함).
진입 / 부트스트랩
main_dev.dart / main_prod.dart
// main_prod.dart
void main() => bootstrap(AppFlavor.prod);진입점은 단 한 줄. flavor만 다름.
app/bootstrap.dart
bootstrap(flavor) 순서:
WidgetsFlutterBinding.ensureInitialized()KakaoSdk.init(nativeAppKey: ...)—KAKAO_NATIVE_APP_KEYenvFirebase.initializeApp(options: firebaseOptionsFor(flavor))FirebaseAppCheck.activate(...)— flavor에 따라 Play Integrity / AppAttest / DebugMobileAds.instance.initialize()(fire-and-forget)runApp(ProviderScope(overrides: [appFlavorProvider.overrideWithValue(flavor)], child: DartBriefApp()))
dev flavor에서만 DebugLog 버퍼를 ProviderContainer에 attach.
app/router.dart
GoRouter 정의:
| 경로 | 화면 | 비고 |
|---|---|---|
/login | LoginPage | 비로그인 시 redirect target |
/disclosures/:rceptNo | DisclosureDetailPage | 디테일 / lazy reveal |
/disclosures | DashboardPage (관심 + 최근) | StatefulShellRoute 탭 1 |
/notifications | NotificationsPage | 탭 2 |
/settings | SettingsPage | 탭 3 |
redirect 룰:
authControllerProvider.user == null→/login- 로그인된 채
/login진입 →/disclosures _AuthRefreshListenable이authController변화 감지해 router 재평가
상태 관리: Riverpod
전역
| Provider | 위치 | 역할 |
|---|---|---|
authControllerProvider | shared/auth/auth_controller.dart | 콜드 스타트 시 /auth/me 자동 fetch. 로그인/로그아웃/탈퇴 액션. user 상태 변화는 router redirect를 트리거 |
appFlavorProvider | app/flavor_provider.dart | bootstrap에서 override |
피처별
| Provider | 위치 | 역할 |
|---|---|---|
disclosureDetailControllerProvider.family | features/disclosure/application/disclosure_detail_controller.dart | rceptNo별 디테일 + 분석 메타 + reveal 상태 |
rewardedAdControllerProvider | features/disclosure/application/rewarded_ad_controller.dart | RewardedAd preload / show / 보상 처리 |
feedbackControllerProvider | features/disclosure/application/feedback_controller.dart | 분석 피드백 (👍/👎) |
quotaControllerProvider | features/quota/application/quota_controller.dart | quota 상태 (auth.user?.userId watch — stale 요청 방지) |
watchlistControllerProvider | features/watchlist/application/watchlist_controller.dart | 관심종목 (optimistic toggle) |
dashboardDisclosuresControllerProvider | features/watchlist/application/dashboard_disclosures_controller.dart | 관심종목 기준 공시 목록 |
recentDisclosuresControllerProvider | features/watchlist/application/recent_disclosures_controller.dart | 전체 최근 공시 |
notificationsControllerProvider | features/notifications/application/notifications_controller.dart | 알림 권한 상태 (FCM 토큰 등록은 미구현) |
규칙: Provider는 application/ 안에만, presentation 안에는 두지 않는다. presentation은 ref.watch(...)로 소비만.
데이터 레이어
Repository
| 파일 | 역할 |
|---|---|
features/disclosure/data/disclosure_repository.dart | GET /disclosures/{rceptNo}, GET /ai/analyze/{rceptNo} (메타), POST /ai/analyze/{rceptNo}/reveal |
features/quota/data/quota_repository.dart | GET /quota, POST /quota/ad-reward (legacy dev 전용) |
features/watchlist/data/watchlist_repository.dart | GET/POST/DELETE /watchlist, GET /watchlist/capacity |
Model
*_models.dart는 freezed/json_serializable 기반 sealed/data class. 상태 변형이 있는 응답은 sealed (AnalysisMetaState, RevealResult) 으로 분기.
Shared 인프라
shared/api/
| 파일 | 역할 |
|---|---|
api_client.dart | Dio 인스턴스 + 인터셉터 (JWT, App Check 헤더, 401 → 자동 logout). generic request<T> + requestRaw 두 가지 |
api_base_url.dart | flavor별 baseUrl (https://api.dartbrief.com vs Tailscale IP) |
api_error.dart | ApiError (status + code + message) |
token_storage.dart | FlutterSecureStorage 래퍼. key=dartbrief_auth_token |
shared/auth/
| 파일 | 역할 |
|---|---|
auth_controller.dart | AuthController extends Notifier<AuthState>. 로그인 / 로그아웃 / 탈퇴 트리거 |
user_info.dart | UserInfo 모델 |
shared/theme/
| 파일 | 역할 |
|---|---|
app_theme.dart | AppTheme.light() (Material 3, CJK fallback fonts) |
app_colors.dart, app_typography.dart, app_spacing.dart, app_radius.dart | 토큰. ThemeData.extensions로 주입 → Theme.of(context).extension<AppColors>() |
shared/debug/
dev flavor에서만 활성. bootstrap이 ProviderContainer에 DebugLog 인스턴스 attach. 디테일 페이지에서 길게 누르면 DebugPanel이 떠 인앱에서 로그 확인. 모바일 디버깅 시 추측 없이 로그 우선 보기 위함 (memory: feedback_dont_guess_use_logs).
shared/toast/, shared/splash/, shared/format/, shared/browser/
각각 토스트 / 스플래시 애니메이션 / 날짜 포맷 / 인앱 브라우저 시트.
Build Flavor
| 항목 | dev | prod |
|---|---|---|
| 진입점 | main_dev.dart | main_prod.dart |
| API base URL | Tailscale IP (Makefile이 자동 주입) | https://api.dartbrief.com |
| Firebase | dev project | prod project |
| App Check | Debug provider | Play Integrity (Android) / AppAttest (iOS) |
| AdMob 광고 ID | Google 테스트 ID | 실 unit ID |
| 광고 보상 경로 | POST /quota/ad-reward (legacy) | AdMob SSV 콜백만 |
| DebugLog | 활성 | 비활성 |
상세는 docs/dev/environment.md, 정책은 docs/policies/monetization.md.
컨벤션
- 위젯 라이브러리:
phosphor_flutter만. Material Icons / Lucide 금지 (CLAUDE.md) - dialog: 네이티브
AlertDialog금지.showModalBottomSheet/showDialog커스텀 - 보조 인터랙션: 인라인. 피드백 / 평가는 별도 큰 카드로 분리하지 않음
- strict mode:
analysis_options.yaml에strict-casts: true등.dynamic/ 무분별한as금지 - 이미지/asset: pubspec.yaml에 명시
- 새 패키지 추가 시
flutter pub add→pubspec.lock커밋. iOS는pod install필요할 수 있음
새 화면 추가 체크리스트
features/<X>/폴더 생성 (presentation / application / data)data/— repository + modelapplication/— controller (Notifier/AsyncNotifier) + providerpresentation/pages/<X>_page.dartpresentation/widgets/— 화면 전용 위젯app/router.dart에 라우트 등록- 디자인 토큰은
shared/theme/에서만 가져오기 (직접Color(0xFF...)금지) - PR 전 docs 업데이트:
- 새 화면 →
docs/spec/screen-<X>.md - 라우팅 / 부트스트랩 변경 → 이 문서 업데이트
- 새 API 호출 →
docs/api/<X>.md
- 새 화면 →