diff --git a/.claude/skills/flutter/SKILL.md b/.claude/skills/flutter/SKILL.md index de3e43c..d4841cb 100644 --- a/.claude/skills/flutter/SKILL.md +++ b/.claude/skills/flutter/SKILL.md @@ -233,17 +233,18 @@ await storage.deleteAll(); ```bash # Android — 스토어 업로드용 (서버 주소는 빌드 때 넘긴다) -flutter build appbundle --release --dart-define=API_URL=https://<도메인>/api +flutter build appbundle --release --dart-define=API_URL=https://<도메인>/api --dart-define=DATA_MODE=api # 기기에서 릴리스 동작만 확인할 때 -flutter build apk --release --dart-define=API_URL=https://<도메인>/api +flutter build apk --release --dart-define=API_URL=https://<도메인>/api --dart-define=DATA_MODE=api # iOS — macOS 에서, Xcode 로 팀·서명을 고른 뒤 -flutter build ipa --release --dart-define=API_URL=https://<도메인>/api +flutter build ipa --release --dart-define=API_URL=https://<도메인>/api --dart-define=DATA_MODE=api ``` - **서버 주소**: 출시 빌드는 `.env` 대신 `--dart-define=API_URL` 로 넘긴다. https 가 아니거나 개발용 주소(`localhost`·`10.0.2.2` 등)면 **앱이 시작하지 않는다**(`releaseApiUrlProblem()`) — 사용자 기기에서 모든 요청이 조용히 실패하는 것보다 낫다. +- **데이터 모드**: 빌드에는 개발자 PC 의 `flutter/.env` 가 asset 으로 묶인다. `--dart-define=DATA_MODE=api` 로 정해 두어 그 `.env` 가 `local` 이어도 출시 빌드가 기기 저장으로 바뀌지 않게 한다. - **Android 서명**: `android/key.properties.example` → `android/key.properties` 복사 후 업로드 키 경로·비밀번호를 채운다. 파일이 없으면 디버그 키로 서명되어(빌드 로그에 경고) **스토어에 올릴 수 없다.** `key.properties`·키 파일은 커밋하지 않는다(`.gitignore`). 업로드 키를 잃어버리면 같은 앱으로 업데이트를 올릴 수 없으니 별도로 백업한다. @@ -254,6 +255,21 @@ flutter build ipa --release --dart-define=API_URL=https://<도메인>/api - **아이콘**: 원본은 `branding/app_icon.png`(전체)·`app_icon_foreground.png`(적응형 앞면, 안전 영역 안). 바꾸면 `dart run flutter_launcher_icons` 로 크기별 아이콘을 다시 만든다. 바탕색은 `pubspec.yaml` 의 `adaptive_icon_background`. - **시작 화면 색**: `#FFDE84` — Android `res/values/colors.xml` 의 `launch_background`(12+ 는 `values-v31` 스타일), iOS `LaunchScreen.storyboard`. 바꿀 때 함께 고친다. +### 운영 서버 전 휴대폰 테스트 (Android) + +출시 빌드는 https 만 받으므로, PC 개발 서버(http)에 붙이는 시험 빌드는 **profile** 로 만든다 (속도는 출시 빌드와 같고 디버그 키로 서명된다). + +```bash +# 휴대폰과 PC 가 같은 공유기에 있어야 한다. 는 PowerShell `Get-NetIPConfiguration` 의 IPv4 +flutter build apk --profile --target-platform android-arm64 --dart-define=API_URL=http://:3000/api --dart-define=DATA_MODE=api +adb install -r build/app/outputs/flutter-apk/app-profile.apk # USB 디버깅을 켜고 연결했을 때 +``` + +- 개발 서버는 개발용 로그인이 열려 있어 기기마다 새 회원이 된다. 공유 기능은 에뮬레이터(`adb reverse tcp:3000 tcp:3000`)를 두 번째 사람으로 쓴다. +- PC 의 IP 가 바뀌면(공유기 재시작 등) 다시 빌드한다. 공유기의 "무선 기기 격리(AP isolation)" 가 켜져 있으면 휴대폰이 PC 에 닿지 않는다. +- 시험 빌드(디버그 키)와 출시 빌드(업로드 키)는 서명이 달라 덮어 설치되지 않는다 — 앱을 지우고 설치한다(기기에 남은 로그인도 지워진다). +- iPhone 은 Mac(Xcode)이 있어야 한다 — Mac 에서 `flutter run --profile` 에 같은 `--dart-define` 을 넘긴다. 운영 서버가 생기면 TestFlight 로 나눠 준다. + - **앱 링크 scheme** `rup` 은 AndroidManifest 의 intent-filter 와 iOS `CFBundleURLTypes` 에 함께 등록되어 있다 — 하나만 바꾸면 한쪽 기기에서 QR 이 앱을 열지 못한다. --- diff --git a/flutter/README.md b/flutter/README.md index b47d556..0bfb510 100644 --- a/flutter/README.md +++ b/flutter/README.md @@ -11,3 +11,4 @@ flutter analyze && flutter test 출시 빌드는 서버 주소를 빌드 때 넘기고(`--dart-define=API_URL=https://<도메인>/api`), Android 는 `android/key.properties`(예시: `key.properties.example`)로 서명한다. +운영 서버 전에 휴대폰에서 시험하는 방법도 스킬의 "출시 빌드"에 있다. diff --git a/flutter/lib/core/config/app_config.dart b/flutter/lib/core/config/app_config.dart index a7e716a..9814e9d 100644 --- a/flutter/lib/core/config/app_config.dart +++ b/flutter/lib/core/config/app_config.dart @@ -9,8 +9,14 @@ enum DataMode { api, local } /// `.env` 에 API_URL 이 없을 때 — 호스트에서 띄운 개발 서버 (백엔드 글로벌 prefix /api 포함) const String kDefaultApiUrl = 'http://localhost:3000/api'; -/// `.env` 의 DATA_MODE (api | local). `.env` 를 읽지 않았으면(테스트) 기기 저장으로 둔다. +/// 빌드할 때 넘긴 데이터 모드 — `--dart-define=DATA_MODE=api|local`. +/// 빌드에는 개발자 PC 의 `.env` 가 그대로 묶이므로, 기기에 설치할 빌드는 이것으로 정해 둔다 +const String _kDefinedDataMode = String.fromEnvironment('DATA_MODE'); + +/// 데이터 모드 — 빌드 때 넘긴 값 → `.env` 의 DATA_MODE (api | local) 순. +/// 둘 다 없고 `.env` 도 읽지 않았으면(테스트) 기기 저장으로 둔다. DataMode readDataMode() { + if (_kDefinedDataMode.isNotEmpty) return _kDefinedDataMode == DataMode.local.name ? DataMode.local : DataMode.api; if (!dotenv.isInitialized) return DataMode.local; return dotenv.maybeGet('DATA_MODE') == DataMode.local.name ? DataMode.local : DataMode.api; }