Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be995111ef | ||
|
|
f0a4bcf8a8 | ||
|
|
cc69c35049 | ||
|
|
40b2c0e6f6 | ||
|
|
a74edaef6b | ||
|
|
f7075f0e6f | ||
|
|
93150a0d5b | ||
|
|
0b95454b28 | ||
|
|
586039da0a | ||
|
|
62b9637db9 | ||
|
|
a04626b003 | ||
|
|
363c74736c | ||
|
|
e9e4260e2c | ||
|
|
14fef64999 | ||
|
|
4e8d989f29 | ||
|
|
5269b987e0 | ||
|
|
40756d5c86 | ||
|
|
976e569a56 | ||
|
|
99b9bb8665 | ||
|
|
65f73280aa | ||
|
|
55f3f503a3 | ||
|
|
6cf45284ca | ||
|
|
1539ae283e | ||
|
|
abe5f1ae50 | ||
|
|
ecf5998725 | ||
|
|
64825e72b0 | ||
|
|
82662b9b9f | ||
|
|
807e446b8b | ||
|
|
d441170dd7 | ||
|
|
83d9c1e8d4 | ||
|
|
cf3dd06774 | ||
|
|
dce2f64362 | ||
|
|
dc9861df38 | ||
|
|
c8c1a1ae67 | ||
|
|
55424bd4a2 | ||
|
|
e7010010bb | ||
|
|
cf608a1d99 | ||
|
|
fbf9a2c3d7 | ||
|
|
4bb92c2ee7 | ||
|
|
000c058097 | ||
|
|
cc0f2fb292 | ||
|
|
71ec17dc3c | ||
|
|
17c658b272 | ||
|
|
65669b969e | ||
|
|
576dcfbdfd | ||
|
|
be623d2e46 | ||
|
|
96a95a12d1 | ||
|
|
5238fc4ef3 | ||
|
|
3a3be4c3d3 | ||
|
|
8a1b824307 | ||
|
|
54d0b23260 | ||
|
|
f84d696c21 | ||
|
|
5ad229b971 | ||
|
|
8a0297eb92 | ||
|
|
c8d29d032b | ||
|
|
e255973a8d | ||
|
|
613798b863 | ||
|
|
4f6e1eac61 | ||
|
|
de44739e63 | ||
|
|
3aca678072 | ||
|
|
18afc9cb98 | ||
|
|
5de04ac281 | ||
|
|
c4e9a9430e | ||
|
|
cbfb72efe0 | ||
|
|
316821e7d5 |
@@ -55,15 +55,30 @@ key.properties
|
|||||||
*.mobileprovision
|
*.mobileprovision
|
||||||
**/GoogleService-Info.plist
|
**/GoogleService-Info.plist
|
||||||
**/google-services.json
|
**/google-services.json
|
||||||
# GoogleService-Info.plist 에서 생성한다 — tool/ios/inject_url_scheme.py
|
# GoogleService-Info.plist 에서 생성한다 — tools/ios/inject_url_scheme.py
|
||||||
ios/config/Google.xcconfig
|
ios/config/Google.xcconfig
|
||||||
|
# 구글 지도 API 키 — BUILD.md 「구글 지도 API 키」
|
||||||
|
ios/config/Maps.xcconfig
|
||||||
|
|
||||||
# FVM Version Cache
|
# FVM Version Cache
|
||||||
.fvm/
|
.fvm/
|
||||||
|
|
||||||
# Firebase 도구·에뮬레이터 산출물
|
# Node — Cloud Functions 의존성. 잠금 파일(package-lock.json)만 추적한다.
|
||||||
firebase/**/node_modules/
|
node_modules/
|
||||||
firebase/**/*-debug.log
|
|
||||||
|
# Cloud Functions 의 tsc 산출물. `npm --prefix firebase/functions run build` 가 여기에
|
||||||
|
# JS 를 떨군다. 소스는 src/ 이고 배포가 이 디렉터리를 다시 만든다 — 추적하면 빌드마다
|
||||||
|
# diff 가 수천 줄 뜨고 커밋에서 소스 변경이 보이지 않는다.
|
||||||
|
firebase/functions/lib/
|
||||||
|
|
||||||
|
# Gradle/Kotlin 빌드 캐시
|
||||||
|
/android/.kotlin/
|
||||||
|
|
||||||
|
# 확인용 캡처 산출물. 앱 저장소가 아니라 앱 저장소 밖에 둔다.
|
||||||
|
# test_driver/capture_driver.dart 가 여기에 PNG 를 떨군다.
|
||||||
|
docs/output/
|
||||||
|
|
||||||
|
# Cloud Functions 의 tsc 산출물. 소스는 firebase/functions/src 다.
|
||||||
firebase/functions/lib/
|
firebase/functions/lib/
|
||||||
|
|
||||||
# 카카오 네이티브 앱 키. 다른 설정 파일(google-services.json 등)과 같이 저장소 밖에서 넣는다.
|
# 카카오 네이티브 앱 키. 다른 설정 파일(google-services.json 등)과 같이 저장소 밖에서 넣는다.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Gradle 이 조용히 실패하지 않고 명확히 죽으므로 아래를 그대
|
|||||||
| 구성 | 값 | 비고 |
|
| 구성 | 값 | 비고 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Flutter | **3.47.2** | `fvm` 으로 고정 — `app/.fvmrc` |
|
| Flutter | **3.47.2** | `fvm` 으로 고정 — `app/.fvmrc` |
|
||||||
| JDK | **Temurin 17** | `fvm flutter config --jdk-dir` 로 지정 |
|
| JDK | **Temurin 17** | `~/.jdks/jdk-17.0.20.1+1/Contents/Home` |
|
||||||
| Gradle | 8.14 | `android/gradle/wrapper/gradle-wrapper.properties` |
|
| Gradle | 8.14 | `android/gradle/wrapper/gradle-wrapper.properties` |
|
||||||
| AGP | 8.11.1 | `android/settings.gradle.kts` |
|
| AGP | 8.11.1 | `android/settings.gradle.kts` |
|
||||||
| Android SDK | `/opt/homebrew/share/android-commandlinetools` | `brew install --cask android-commandlinetools` |
|
| Android SDK | `/opt/homebrew/share/android-commandlinetools` | `brew install --cask android-commandlinetools` |
|
||||||
@@ -21,11 +21,11 @@ Android Studio 번들 JBR 은 **JDK 25** 이고 **Gradle 8.14 와 비호환**이
|
|||||||
JDK 17 을 쓰는 쪽을 택했다.
|
JDK 17 을 쓰는 쪽을 택했다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
fvm flutter config --jdk-dir <JDK 17 설치 경로>
|
fvm flutter config --jdk-dir ~/.jdks/jdk-17.0.20.1+1/Contents/Home
|
||||||
```
|
```
|
||||||
|
|
||||||
`brew install --cask temurin@17` 은 sudo 를 요구한다. sudo 없이 쓰려면 Adoptium
|
`brew install --cask temurin@17` 은 sudo 를 요구한다. sudo 없이 쓰려면 Adoptium
|
||||||
tar.gz 를 사용자 폴더에 풀고 그 경로를 지정하면 된다.
|
tar.gz 를 `~/.jdks/` 에 풀면 된다.
|
||||||
|
|
||||||
## 테스트
|
## 테스트
|
||||||
|
|
||||||
@@ -33,8 +33,22 @@ tar.gz 를 사용자 폴더에 풀고 그 경로를 지정하면 된다.
|
|||||||
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
|
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
|
||||||
fvm flutter analyze
|
fvm flutter analyze
|
||||||
fvm flutter test
|
fvm flutter test
|
||||||
|
fvm flutter build apk --debug # 플러그인 네이티브 결합까지 본다
|
||||||
|
npm --prefix firebase/functions run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**`pubspec.lock` 의 `firebase_core` 는 4.13.0 이어야 한다.** 4.14.0 이 올라오면 Android
|
||||||
|
컴파일이 깨진다. `pub get` 이 올려 버렸는지 게이트에서 함께 본다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -A 6 '^ firebase_core:' pubspec.lock | grep version # → 4.13.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**`analyze` 와 `test` 는 Android 네이티브 컴파일을 하지 않는다.** 의존성을 추가하거나
|
||||||
|
올리면 Dart 쪽은 멀쩡한데 Gradle 이 죽는 조합이 생긴다(실례 — `firebase_core` 가
|
||||||
|
4.14.0 으로 올라가 `firebase_auth` 6.5.7 의 `customAuthDomain` 에서 깨졌다).
|
||||||
|
그래서 의존성을 건드린 회차는 **디버그 APK 빌드까지 게이트에 넣는다.**
|
||||||
|
|
||||||
스모크 테스트가 `craft-allowlist.yaml` 의 `APP_TABS` 를 직접 읽어 앱의 `appTabs` 와 정확 일치를
|
스모크 테스트가 `craft-allowlist.yaml` 의 `APP_TABS` 를 직접 읽어 앱의 `appTabs` 와 정확 일치를
|
||||||
검증한다(5탭 계약 대조). 파일은 아래 순서로 찾는다.
|
검증한다(5탭 계약 대조). 파일은 아래 순서로 찾는다.
|
||||||
|
|
||||||
@@ -43,7 +57,8 @@ fvm flutter test
|
|||||||
3. 저장소 루트 `design/craft-allowlist.yaml`.
|
3. 저장소 루트 `design/craft-allowlist.yaml`.
|
||||||
|
|
||||||
하나도 못 찾으면 통과가 아니라 실패한다 — 계약 대조가 조용히 사라지면 안 된다.
|
하나도 못 찾으면 통과가 아니라 실패한다 — 계약 대조가 조용히 사라지면 안 된다.
|
||||||
루트 파일을 고칠 때는 `app/design/` 사본도 함께 맞춘다.
|
루트 파일을 고칠 때는 `app/design/` 사본도 함께 맞춘다. 두 파일이 함께 보이면 스모크 테스트가
|
||||||
|
`APP_TABS` 줄을 서로 비교해 다르면 실패한다.
|
||||||
|
|
||||||
## 빌드
|
## 빌드
|
||||||
|
|
||||||
@@ -54,25 +69,7 @@ fvm flutter build apk --release # build/app/outputs/flutter-apk/app-relea
|
|||||||
fvm flutter build appbundle --release # build/app/outputs/bundle/release/app-release.aab
|
fvm flutter build appbundle --release # build/app/outputs/bundle/release/app-release.aab
|
||||||
```
|
```
|
||||||
|
|
||||||
## 카카오 · LINE 로그인 설정
|
### Android Apple 로그인 — 앱에 넣을 값 없음
|
||||||
|
|
||||||
**카카오 네이티브 앱 키** — `config/kakao.env`(저장소 밖, `config/kakao.env.example` 복사) 한 줄
|
|
||||||
`FANUZU_KAKAO_NATIVE_APP_KEY=<키>`. 주석을 넣지 않는다(iOS xcconfig 가 그대로 include 한다).
|
|
||||||
Android `build.gradle.kts`(리다이렉트 스킴) · iOS `Flutter/*.xcconfig`(URL 스킴) 가 이 파일을 읽고,
|
|
||||||
Dart 는 실행·빌드 때 `--dart-define-from-file=config/kakao.env` 로 받는다. 없으면 카카오 로그인만
|
|
||||||
「설정 전」으로 막힌다. 서버 `fnAuthKakao` 는 카카오 **앱 ID**(숫자)를 `firebase/functions/.env.<프로젝트>`
|
|
||||||
의 `KAKAO_APP_ID` 로 받는다(저장소 밖).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
fvm flutter build apk --release --dart-define-from-file=config/kakao.env
|
|
||||||
```
|
|
||||||
|
|
||||||
**LINE 채널 ID** — 같은 `config/kakao.env` 에 `FANUZU_LINE_CHANNEL_ID=<LINE Login 채널 ID(숫자)>`. Dart 만 읽는다
|
|
||||||
(iOS 복귀 스킴은 `line3rdp.<번들 ID>` 라 키가 필요 없다). 없으면 LINE 로그인만 「설정 전」으로 막힌다. 서버
|
|
||||||
`fnAuthLine` 은 같은 값을 `firebase/functions/.env.<프로젝트>` 의 `LINE_CHANNEL_ID` 로 받는다(저장소 밖).
|
|
||||||
LINE Developers 콘솔의 LINE Login 채널에 Android 패키지명 · iOS 번들 ID 를 등록한다.
|
|
||||||
|
|
||||||
## Android Apple 로그인 — 앱에 넣을 값 없음
|
|
||||||
|
|
||||||
Android 는 Apple SDK 가 없어 Firebase 웹 흐름(`apple.com` 제공자 · `signInWithProvider`)으로 로그인한다.
|
Android 는 Apple SDK 가 없어 Firebase 웹 흐름(`apple.com` 제공자 · `signInWithProvider`)으로 로그인한다.
|
||||||
값은 모두 콘솔에 둔다 — 빌드 옵션은 없다.
|
값은 모두 콘솔에 둔다 — 빌드 옵션은 없다.
|
||||||
@@ -80,12 +77,10 @@ Android 는 Apple SDK 가 없어 Firebase 웹 흐름(`apple.com` 제공자 · `s
|
|||||||
1. Apple 개발자 계정 「Identifiers → Services IDs」에 서비스 ID 를 만들고 「Sign in with Apple」을 켠다.
|
1. Apple 개발자 계정 「Identifiers → Services IDs」에 서비스 ID 를 만들고 「Sign in with Apple」을 켠다.
|
||||||
- Domains: `<프로젝트 ID>.firebaseapp.com`
|
- Domains: `<프로젝트 ID>.firebaseapp.com`
|
||||||
- Return URLs: `https://<프로젝트 ID>.firebaseapp.com/__/auth/handler`
|
- Return URLs: `https://<프로젝트 ID>.firebaseapp.com/__/auth/handler`
|
||||||
2. 「Keys」에서 Sign in with Apple 키를 만들고 .p8 를 받는다(키 ID · 팀 ID 기록). 키 파일은 저장소에 넣지 않는다.
|
2. 「Keys」에서 Sign in with Apple 키를 만들고 .p8 를 받는다(키 ID · 팀 ID 기록).
|
||||||
3. Firebase 콘솔 Authentication → 로그인 방법 → Apple 에 서비스 ID · 팀 ID · 키 ID · 비공개 키를 넣는다.
|
3. Firebase 콘솔 Authentication → 로그인 방법 → Apple 에 서비스 ID · 팀 ID · 키 ID · 비공개 키를 넣는다.
|
||||||
4. 확인: Android 첫 화면 Apple → 브라우저 탭에서 Apple 로그인 → 앱 HOME.
|
4. 확인: Android 첫 화면 Apple → 브라우저 탭에서 Apple 로그인 → 앱 HOME.
|
||||||
|
|
||||||
iOS 는 지금처럼 Apple SDK(`AppleSignInProvider`)로 로그인한다.
|
|
||||||
|
|
||||||
## 서명
|
## 서명
|
||||||
|
|
||||||
`android/key.properties` 가 없으면 **릴리스 빌드가 의도적으로 실패**한다
|
`android/key.properties` 가 없으면 **릴리스 빌드가 의도적으로 실패**한다
|
||||||
@@ -104,12 +99,104 @@ apksigner verify --print-certs build/app/outputs/flutter-apk/app-release.apk
|
|||||||
# Signer #1 certificate SHA-256 digest: 5e83f4c9cd35357b11561a5b82c47d300af8c31e33d80d94bc32a4f10f46dee7
|
# Signer #1 certificate SHA-256 digest: 5e83f4c9cd35357b11561a5b82c47d300af8c31e33d80d94bc32a4f10f46dee7
|
||||||
```
|
```
|
||||||
|
|
||||||
## 알려진 어긋남 — minSdk
|
## Firebase 앱 등록 — 식별자 `kr.co.fanuzu.app`
|
||||||
|
|
||||||
문서(`2주차_플러그인_선검증리포트` · 별첨 1)는 **Android 10(API 29)** 을 하한으로 적었으나
|
프로젝트는 `fanuzu-dev-b020d`(계정 `dev@sweech.io`). 2026-09-11 별첨 C 4번 확정으로
|
||||||
현재 빌드는 **API 24** 로 나간다. 하한을 29 로 올리는 것은 위치 퀘스트의
|
아래 두 앱을 새로 등록했다. 잠정 식별자(`kr.co.sweech.fancake`) 앱은 지우지 않고
|
||||||
`geofence_foreground_service` 인데 아직 넣지 않았다. **6주차 위치 퀘스트 구현 시점에
|
남겨 두었다 — 그 식별자로 이미 설치된 테스트 빌드가 있고, Gradle 은
|
||||||
29 로 올라간다.** 그때 기준 단말 2종 충족을 다시 확인해 빌드 노트에 적는다.
|
`google-services.json` 안에서 **패키지명으로 골라 쓰므로** 섞이지 않는다.
|
||||||
|
|
||||||
|
| 플랫폼 | App ID |
|
||||||
|
|---|---|
|
||||||
|
| Android | `1:959364197461:android:6f1b79847ba8c3ed08b8dc` |
|
||||||
|
| iOS | `1:959364197461:ios:716771ae8ece67ee08b8dc` |
|
||||||
|
|
||||||
|
설정 파일은 `firebase/setup-project.sh` 가 제자리에 내려받는다(둘 다 `.gitignore` 대상).
|
||||||
|
|
||||||
|
### SHA-1 지문은 firebase CLI 로 등록할 수 없다
|
||||||
|
|
||||||
|
firebase-tools 15 에 지문 명령이 없다. Firebase Management REST 로 넣었고, 등록된 값은
|
||||||
|
디버그 키스토어와 업로드 키스토어 둘이다.
|
||||||
|
|
||||||
|
지문을 다시 뽑으려면(JDK 17 의 keytool):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android | grep SHA
|
||||||
|
keytool -list -v -keystore ~/.fanuzu-signing/fanuzu-upload.p12 -alias fanuzu-upload -storetype PKCS12 | grep SHA
|
||||||
|
```
|
||||||
|
|
||||||
|
등록값 4건(디버그·릴리스 × SHA-1·SHA-256):
|
||||||
|
|
||||||
|
```
|
||||||
|
SHA-1 debug 85:2B:1B:A8:93:4D:CB:AF:D0:B6:D6:56:AF:98:99:5D:02:FF:07:C1
|
||||||
|
SHA-1 release 58:27:8E:F0:80:B8:DA:6E:0E:E8:C1:70:F4:90:74:AD:5A:54:94:E8
|
||||||
|
SHA-256 debug 28:F1:75:B8:…:88:46:5E:AF
|
||||||
|
SHA-256 release 5E:83:F4:C9:…:0F:46:DE:E7
|
||||||
|
```
|
||||||
|
|
||||||
|
### 설정 파일은 저장소에 없다 — 다시 받는 명령
|
||||||
|
|
||||||
|
`google-services.json` · `GoogleService-Info.plist` · `ios/config/Google.xcconfig` 셋 다
|
||||||
|
`.gitignore` 대상이다. 새 작업 폴더에서는 아래를 돌린다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
firebase login:use dev@sweech.io
|
||||||
|
firebase apps:sdkconfig ANDROID 1:959364197461:android:6f1b79847ba8c3ed08b8dc \
|
||||||
|
--project fanuzu-dev-b020d --out android/app/google-services.json
|
||||||
|
firebase apps:sdkconfig IOS 1:959364197461:ios:716771ae8ece67ee08b8dc \
|
||||||
|
--project fanuzu-dev-b020d --out ios/Runner/GoogleService-Info.plist
|
||||||
|
python3 tools/ios/inject_url_scheme.py # → ios/config/Google.xcconfig
|
||||||
|
```
|
||||||
|
|
||||||
|
> `apps:sdkconfig` 는 대상 파일이 이미 있으면 거부한다. 갱신하려면 먼저 지운다.
|
||||||
|
|
||||||
|
**카카오 네이티브 앱 키** — `config/kakao.env`(저장소 밖, `config/kakao.env.example` 복사) 한 줄
|
||||||
|
`FANUZU_KAKAO_NATIVE_APP_KEY=<키>`. 주석을 넣지 않는다(iOS xcconfig 가 그대로 include 한다).
|
||||||
|
Android `build.gradle.kts`(리다이렉트 스킴) · iOS `Flutter/*.xcconfig`(URL 스킴) 가 이 파일을 읽고,
|
||||||
|
Dart 는 실행·빌드 때 `--dart-define-from-file=config/kakao.env` 로 받는다. 없으면 카카오 로그인만
|
||||||
|
「설정 전」으로 막힌다. 서버 `fnAuthKakao` 는 카카오 **앱 ID**(숫자)를 `firebase/functions/.env.<프로젝트>`
|
||||||
|
의 `KAKAO_APP_ID` 로 받는다.
|
||||||
|
|
||||||
|
**구글 지도 API 키(위치 선택 지도)** — 키는 저장소에 없다. 없어도 빌드는 되고 지도만 빈다.
|
||||||
|
- Android: `android/local.properties` 에 `MAPS_API_KEY=<키>` → `build.gradle.kts` 가 `manifestPlaceholders` 로
|
||||||
|
AndroidManifest `com.google.android.geo.API_KEY` 에 넣는다.
|
||||||
|
- iOS: `ios/config/Maps.xcconfig`(`ios/config/Maps.xcconfig.example` 복사, gitignore) 에 `MAPS_API_KEY = <키>` →
|
||||||
|
`Flutter/*.xcconfig` include → Info.plist `GMSApiKey` → `AppDelegate` 의 `GMSServices.provideAPIKey`.
|
||||||
|
- 콘솔: Maps SDK for Android · iOS 사용 설정. 키 제한은 Android 앱 `kr.co.fanuzu.app` + SHA-1(디버그 · 릴리스),
|
||||||
|
iOS 번들 ID 로 건다.
|
||||||
|
|
||||||
|
Authentication 제공자는 이메일/비밀번호와 Google 둘 다 켜져 있다(2026-09-11). Google 을
|
||||||
|
켜야 `oauth_client` 와 `REVERSED_CLIENT_ID` 가 채워지며, 콘솔에서만 켤 수 있다.
|
||||||
|
|
||||||
|
|
||||||
|
> **테스터 안내(식별자 전환 이후):** 새 빌드(`kr.co.fanuzu.app`)는 기존 설치본(`kr.co.sweech.fancake`)과 **별개의 앱으로 나란히 설치**됩니다. 기존 앱의 SharedPreferences · iOS 키체인 · 캐시된 소셜 자격 증명은 넘어오지 않으므로 다시 로그인해야 합니다. Firebase Authentication 계정은 프로젝트 단위라 그대로 살아 있어 같은 계정으로 들어갑니다. "로그아웃됐다" 는 오인 신고를 막기 위해 빌드 노트에도 같은 문장을 넣습니다.
|
||||||
|
|
||||||
|
## minSdk 는 29 — 문서와 일치한다
|
||||||
|
|
||||||
|
`android/app/build.gradle.kts` 의 `minSdk = 29` 다. 문서(`2주차_플러그인_선검증리포트` ·
|
||||||
|
별첨 1)가 적은 **Android 10(API 29)** 과 같다. (이 절은 하한이 24 이던 때의 기록을
|
||||||
|
지운 것이다 — 값이 이미 29 이므로 「올려야 한다」는 서술이 사실과 달랐다.)
|
||||||
|
|
||||||
|
## 퀘스트 플러그인 3종 — 선검증리포트를 그대로 따른다
|
||||||
|
|
||||||
|
| 플러그인 | 버전 | 쓰는 곳 | 하한 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `pedometer` | 4.2.0 | 만보기 퀘스트(S-H-WALK) 걸음 수 | Android 16 · iOS 10 |
|
||||||
|
| `geolocator` | **13.x 고정** | 위치 퀘스트(S-H-PLACE) 좌표·mock location | iOS 11 |
|
||||||
|
| `permission_handler` | **12.x 고정** | 활동 인식 권한 | Android 24 · iOS 12 |
|
||||||
|
| `shared_preferences` | 2.5.x | 걸음 기준선을 하루 단위로 보관 | 하한 영향 없음 |
|
||||||
|
|
||||||
|
**두 버전을 올리지 않는 이유는 선검증리포트에 있다.** `geolocator 14.x` 는 `win32 6.x` 를
|
||||||
|
끌어와 `health 13.3.1` 과 공존하지 못하고(§2.2), `permission_handler 13.0.0` 은 Gradle
|
||||||
|
빌드가 깨진다(§2.3). 올리려면 그 두 항목을 다시 재고 리포트를 고쳐야 한다.
|
||||||
|
|
||||||
|
`geofence_foreground_service` 는 **넣지 않았다.** 지오펜스 판정은 서버가 하고
|
||||||
|
(`fn-quest-verify-place`), 앱은 인증 시점의 좌표 한 점만 보낸다 — 상시 추적을 하지 않는다는
|
||||||
|
v1.6.1 13장을 지키려면 상주 서비스가 필요 없다.
|
||||||
|
|
||||||
|
권한은 `android/app/src/main/AndroidManifest.xml` 에 `ACTIVITY_RECOGNITION` ·
|
||||||
|
`ACCESS_FINE_LOCATION` · `ACCESS_COARSE_LOCATION` 셋이다. **`ACCESS_BACKGROUND_LOCATION`
|
||||||
|
은 요청하지 않는다.** iOS 사용 문구는 `Info.plist` 에 이미 들어 있다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -132,32 +219,8 @@ export DEVELOPER_DIR=~/Downloads/Xcode-beta.app/Contents/Developer
|
|||||||
fvm flutter build ios --release --no-codesign
|
fvm flutter build ios --release --no-codesign
|
||||||
```
|
```
|
||||||
|
|
||||||
### 제출 빌드 — App Store Connect
|
**서명은 Apple Developer 계정이 있어야 한다.** 계정 없이 여기까지는 확인되므로,
|
||||||
|
TestFlight 제출에 남은 것은 **서명과 업로드뿐**이다.
|
||||||
서명 팀은 **Fancake Inc. `V28G543VK5`** 다(`Runner.xcodeproj` 의 `DEVELOPMENT_TEAM`,
|
|
||||||
`ios/ExportOptions.plist` 의 `teamID`). 번들 ID `kr.co.fanuzu.app`.
|
|
||||||
|
|
||||||
| 항목 | 파일 | 없으면 |
|
|
||||||
|---|---|---|
|
|
||||||
| Sign in with Apple · 푸시 환경 | `ios/Runner/Runner.entitlements` | Apple 로그인이 동작하지 않고 심사에서 반려된다 |
|
|
||||||
| 암호화 사용 신고 | `Info.plist` `ITSAppUsesNonExemptEncryption` | 업로드마다 수동 응답을 요구한다 |
|
|
||||||
| Google 콜백 스킴 | `Info.plist` + `ios/config/Google.xcconfig` | Google 로그인 콜백이 돌아오지 않는다 |
|
|
||||||
| Firebase 설정 | `ios/Runner/GoogleService-Info.plist` (번들 리소스) | 목 모드로 뜨고 Google 로그인 client ID 를 못 읽는다 |
|
|
||||||
| 내보내기 설정 | `ios/ExportOptions.plist` | `-exportArchive` 가 대화형으로 묻는다 |
|
|
||||||
|
|
||||||
`GoogleService-Info.plist` 와 `ios/config/Google.xcconfig` 는 저장소에 넣지 않는다.
|
|
||||||
Xcode 프로젝트가 `GoogleService-Info.plist` 를 리소스로 참조하므로 **파일이 없으면 iOS 빌드가 멈춘다.**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Firebase 콘솔에서 받은 iOS 설정 파일을 둔다
|
|
||||||
cp <받은 파일> ios/Runner/GoogleService-Info.plist
|
|
||||||
# 2. Google 콜백 스킴을 뽑는다 → ios/config/Google.xcconfig
|
|
||||||
python3 tool/ios/inject_url_scheme.py
|
|
||||||
# 3. 아카이브 · IPA
|
|
||||||
fvm flutter build ipa --release --export-options-plist=ios/ExportOptions.plist
|
|
||||||
```
|
|
||||||
|
|
||||||
배포 프로파일이 없으면 `xcodebuild` 로 나눠 `-allowProvisioningUpdates` 를 붙인다.
|
|
||||||
|
|
||||||
### 정리해야 할 것 — CocoaPods / SwiftPM 이중 상태
|
### 정리해야 할 것 — CocoaPods / SwiftPM 이중 상태
|
||||||
|
|
||||||
@@ -171,22 +234,64 @@ iOS 플러그인이 전부 Swift Package 로 옮겨졌는데 프로젝트에 Coc
|
|||||||
`Debug.xcconfig` · `Release.xcconfig` 의 Pods include 제거가 필요하다.
|
`Debug.xcconfig` · `Release.xcconfig` 의 Pods include 제거가 필요하다.
|
||||||
**11주차 최적화 구간에서 정리한다.**
|
**11주차 최적화 구간에서 정리한다.**
|
||||||
|
|
||||||
## 서버 없이 로그인을 돌려 보기
|
## 서버 없이 로그인·원장을 돌려 보기
|
||||||
|
|
||||||
실 Firebase 프로젝트는 계정 명의가 확정되어야 열 수 있다.
|
실 Firebase 프로젝트는 계정 명의가 확정되어야 열 수 있다(계정·권한 이전 별첨 A-3).
|
||||||
그 전까지는 **에뮬레이터를 물려 실제 왕복을 돈다.** 설정 파일 대신 에뮬레이터를 붙이는
|
그 전까지는 **에뮬레이터를 물려 실제 왕복을 돈다.** 설정 파일 대신 에뮬레이터를 붙이는
|
||||||
것이라, 이 절차가 통과하면 설정 파일이 들어오는 날 코드 수정 없이 동작한다.
|
것이라, 이 절차가 통과하면 설정 파일이 들어오는 날 코드 수정 없이 동작한다.
|
||||||
|
|
||||||
### 1. 에뮬레이터 기동
|
### 1. 에뮬레이터 기동
|
||||||
|
|
||||||
```
|
```
|
||||||
cd app/firebase
|
cd firebase
|
||||||
firebase emulators:start --only auth --project demo-fanuzu
|
npm install # 최초 1회
|
||||||
|
npm --prefix functions run build # Functions 를 JS 로 빌드
|
||||||
|
firebase emulators:start --only auth,firestore,functions --project demo-fanuzu
|
||||||
|
```
|
||||||
|
|
||||||
|
프로필 사진 업로드를 함께 돌려 보려면 `storage` 를 더한다(`firebase.json` 에
|
||||||
|
규칙과 포트 9199 가 들어 있다).
|
||||||
|
|
||||||
|
```
|
||||||
|
firebase emulators:start --only auth,firestore,functions,storage --project demo-fanuzu
|
||||||
```
|
```
|
||||||
|
|
||||||
**JDK 21 이상이 필요하다.** firebase-tools 15 가 그 아래를 거부한다. Gradle 은 Temurin 17 을
|
**JDK 21 이상이 필요하다.** firebase-tools 15 가 그 아래를 거부한다. Gradle 은 Temurin 17 을
|
||||||
쓰므로 둘이 다르며, 에뮬레이터를 띄울 때만 `JAVA_HOME` 을 21+ 로 바꾼다.
|
쓰므로 둘이 다르며, 에뮬레이터를 띄울 때만 `JAVA_HOME` 을 21+ 로 바꾼다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home
|
||||||
|
```
|
||||||
|
|
||||||
|
### 전역 `firebase` 가 조용히 죽으면 저장소 로컬 firebase-tools 를 쓴다
|
||||||
|
|
||||||
|
**증상** — `firebase emulators:exec` 가 출력을 한 줄도 남기지 않고 종료 코드 137(SIGKILL)로
|
||||||
|
끝난다. `firebase --version` 은 정상이고 `JAVA_HOME` 을 21 로 맞춰도 같다. 포트도 비어 있다.
|
||||||
|
에뮬레이터가 안 뜨는 것처럼 보이지만 원인은 CLI 래퍼다.
|
||||||
|
|
||||||
|
**원인과 대처** — 전역 Homebrew `firebase`(15.29.0)에서만 나며, 저장소 devDependency 인
|
||||||
|
firebase-tools 로는 정상 기동한다. `firebase/test` 는 자기 `node_modules/.bin` 이 PATH 에
|
||||||
|
올라 이미 로컬 바이너리를 쓰고, `firebase/functions` 는 firebase-tools 를 직접 의존하지
|
||||||
|
않으므로 스크립트가 형제 경로를 가리킨다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd firebase/functions && npm ci # 최초 1회
|
||||||
|
cd firebase/test && npm ci # firebase-tools 가 여기 들어 있다
|
||||||
|
npm --prefix firebase/functions run test:emulator # 로컬 바이너리로 돈다
|
||||||
|
cd firebase/test && npm run test:emulator
|
||||||
|
```
|
||||||
|
|
||||||
|
직접 부를 때도 같다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
../test/node_modules/.bin/firebase emulators:exec \
|
||||||
|
--config ../firebase.json --only firestore --project demo-fanuzu "npm test"
|
||||||
|
```
|
||||||
|
|
||||||
|
> 비상 대안 — 그마저 막히면 에뮬레이터 JAR 를 직접 띄우고
|
||||||
|
> `FIRESTORE_EMULATOR_HOST=127.0.0.1:8080` 을 실어 테스트를 돌린다(규칙 적재는 회귀 테스트가
|
||||||
|
> REST 로 따로 한다): `"$JAVA_HOME/bin/java" -jar ~/.cache/firebase/emulators/cloud-firestore-emulator-v1.22.0.jar --host=127.0.0.1 --port=8080`
|
||||||
|
|
||||||
### 2. 앱을 에뮬레이터에 붙이기
|
### 2. 앱을 에뮬레이터에 붙이기
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -196,6 +301,82 @@ flutter run --dart-define=FANUZU_EMULATOR_HOST=127.0.0.1 # iOS 시뮬레이터
|
|||||||
|
|
||||||
`10.0.2.2` 는 Android 에뮬레이터가 보는 호스트 PC 주소다.
|
`10.0.2.2` 는 Android 에뮬레이터가 보는 호스트 PC 주소다.
|
||||||
|
|
||||||
|
### 2-1. 에뮬레이터는 **저장소 안의 firebase-tools** 로 띄운다
|
||||||
|
|
||||||
|
전역 Homebrew `firebase`(15.29.0)는 일부 환경에서 시작 배너만 찍고 **SIGKILL(exit 137)**
|
||||||
|
로 죽는다(`emulators:*` · `deploy`. `--version` 과 `projects:list` 는 정상). 저장소의
|
||||||
|
devDependency 를 쓰면 그대로 뜬다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm --prefix firebase/test ci # 최초 1회 — firebase-tools 15.28.2
|
||||||
|
cd firebase
|
||||||
|
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
|
||||||
|
test/node_modules/.bin/firebase emulators:exec \
|
||||||
|
--only auth,firestore,functions --project fanuzu-dev-b020d "<명령>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**프로젝트 ID 를 앱과 맞춘다.** `firebase.json` 이 `singleProjectMode: true` 라 다른 ID 의
|
||||||
|
요청은 서지 않는다. 설정 파일이 들어간 Android 빌드는 네이티브가 만든 `[DEFAULT]` 앱을
|
||||||
|
그대로 쓰므로 **실 프로젝트 ID(`fanuzu-dev-b020d`)** 로 붙는다. 에뮬레이터도 같은 ID 로
|
||||||
|
띄워야 앱이 넣은 값과 시드가 같은 칸에 들어간다.
|
||||||
|
|
||||||
|
**시드는 `Authorization: Bearer owner` 로 넣는다.** 없으면 보안 규칙이 그대로 걸려
|
||||||
|
`quests` 쓰기가 마스터 전용으로 막히고, 시드가 조용히 사라진다.
|
||||||
|
|
||||||
|
### 2-2. Firestore 만 필요할 때 — CLI 없이
|
||||||
|
|
||||||
|
서버 단위 검증만 할 때는 받아 둔 jar 를 직접 띄워도 된다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -Djava.net.preferIPv4Stack=true \
|
||||||
|
-jar ~/.cache/firebase/emulators/cloud-firestore-emulator-v1.22.0.jar \
|
||||||
|
--host=0.0.0.0 --port=8080 &
|
||||||
|
CI=1 FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 GCLOUD_PROJECT=demo-fanuzu \
|
||||||
|
npm --prefix firebase/functions test
|
||||||
|
```
|
||||||
|
|
||||||
|
`CI=1` 이 필요하다. 없으면 vitest 가 대화형 모드로 남아 명령이 끝나지 않는다.
|
||||||
|
**`-Djava.net.preferIPv4Stack=true` 를 빼지 마라.** 없으면 JVM 이 8080 을 IPv6 로만 열고,
|
||||||
|
Android 에뮬레이터가 `10.0.2.2` 로 거는 연결이 **ECONNREFUSED** 로 떨어진다. 앱에서는
|
||||||
|
`[cloud_firestore/unavailable] client is offline` 으로만 보여 원인이 네트워크 설정처럼 읽힌다.
|
||||||
|
|
||||||
|
### 2-3. 앱을 붙여 한 바퀴 — 로그인 → 출석 → 원장 → 화면
|
||||||
|
|
||||||
|
Callable 까지 도는 확인이다. 기기 포트를 호스트로 넘기고 한 명령으로 돌린다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for p in 8080 9099 5001; do adb reverse tcp:$p tcp:$p; done
|
||||||
|
cd firebase
|
||||||
|
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home \
|
||||||
|
test/node_modules/.bin/firebase emulators:exec \
|
||||||
|
--only auth,firestore,functions --project fanuzu-dev-b020d \
|
||||||
|
"bash ../tool/emulator_quest_roundtrip.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
스크립트가 퀘스트 정의·운영값을 넣고 `integration_test/quest_callable_emulator_test.dart`
|
||||||
|
를 돌린다. 확인하는 것은 **가입 → `fn-auth-oncreate` 계정·지갑 생성 → 출석 참여 →
|
||||||
|
`fn-quest-attend` → 원장 1행 → 보유 POP → 목록 재조회 「보상완료」 → 2회차 E-POP-03 거부**다.
|
||||||
|
|
||||||
|
### 2-4. 서버가 쓴 값을 앱이 읽는지만 볼 때
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CI=1 FIRESTORE_EMULATOR_HOST=127.0.0.1:8080 GCLOUD_PROJECT=demo-fanuzu \
|
||||||
|
npm --prefix firebase/functions test # 서버가 상태를 만든다
|
||||||
|
fvm flutter test integration_test/pop_loop_emulator_test.dart \
|
||||||
|
-d <device> --dart-define=FANUZU_EMULATOR_HOST=10.0.2.2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 자동 검증
|
||||||
|
|
||||||
|
```
|
||||||
|
flutter drive --driver=test_driver/perf_driver.dart \
|
||||||
|
--target=integration_test/auth_emulator_test.dart \
|
||||||
|
--dart-define=FANUZU_EMULATOR_HOST=10.0.2.2 --profile --no-dds -d <device>
|
||||||
|
```
|
||||||
|
|
||||||
|
가입 → 로그인 → 재로그인 → Auth 트리거의 계정·지갑 문서 생성 → 클라이언트 지갑 쓰기
|
||||||
|
차단까지 6건을 확인한다.
|
||||||
|
|
||||||
### 평문 HTTP 예외에 대하여
|
### 평문 HTTP 예외에 대하여
|
||||||
|
|
||||||
에뮬레이터는 평문 HTTP 로 뜨는데 Android 9 부터 평문이 기본 차단이다. 예외를
|
에뮬레이터는 평문 HTTP 로 뜨는데 Android 9 부터 평문이 기본 차단이다. 예외를
|
||||||
@@ -206,3 +387,45 @@ flutter run --dart-define=FANUZU_EMULATOR_HOST=127.0.0.1 # iOS 시뮬레이터
|
|||||||
unzip -p build/app/outputs/flutter-apk/app-release.apk AndroidManifest.xml \
|
unzip -p build/app/outputs/flutter-apk/app-release.apk AndroidManifest.xml \
|
||||||
| strings | grep -c networkSecurityConfig # 0 이어야 한다
|
| strings | grep -c networkSecurityConfig # 0 이어야 한다
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## iOS 스토어 제출 준비 — 계정 전에 할 수 있는 것을 다 해 두었다
|
||||||
|
|
||||||
|
Apple Developer 계정은 발주사 명의 회신 대상이라(계정 이전 별첨 A-1) 서명과
|
||||||
|
업로드는 그날부터다. **그 외의 것을 미리 두지 않으면 계정이 열린 날 하루를
|
||||||
|
설정에 쓴다.** 아래는 이미 들어가 있다.
|
||||||
|
|
||||||
|
| 항목 | 파일 | 없으면 어떻게 되는가 |
|
||||||
|
|---|---|---|
|
||||||
|
| Sign in with Apple Capability | `ios/Runner/Runner.entitlements` | Apple 로그인 코드가 있어도 동작하지 않고 심사에서 반려된다 |
|
||||||
|
| 푸시 환경 선언 | 같은 파일 `aps-environment` | FCM 토큰이 발급되지 않는다 |
|
||||||
|
| 암호화 사용 신고 | `Info.plist` `ITSAppUsesNonExemptEncryption` | 업로드마다 수동 응답을 요구하고 그동안 배포가 멈춘다 |
|
||||||
|
| 권한 사용 문구 6종 | `Info.plist` `NS*UsageDescription` | **권한을 요청하는 순간 앱이 종료된다.** 크래시로 보이지만 원인은 이 파일이다 |
|
||||||
|
| Google 콜백 스킴 | `Info.plist` + `ios/config/Google.xcconfig` | Google 로그인 콜백이 조용히 돌아오지 않는다 |
|
||||||
|
| 내보내기 설정 | `ios/ExportOptions.plist` | `xcodebuild -exportArchive` 가 대화형으로 물어 자동 배포가 불가능하다 |
|
||||||
|
| 업로드 절차 | `tools/ios/upload_testflight.sh` | 계정이 열린 날 명령을 처음부터 찾게 된다 |
|
||||||
|
|
||||||
|
### 업로드 절차
|
||||||
|
|
||||||
|
서명 팀은 발주사 Fancake Inc.(`V28G543VK5`)다. `ios/Runner.xcodeproj` 의 `DEVELOPMENT_TEAM`
|
||||||
|
과 `ios/ExportOptions.plist` 의 `teamID` 가 모두 이 값이어야 하고, `store_readiness_test`
|
||||||
|
가 지킨다. 다른 팀으로 서명하면 업로드가 그 팀의 App Store Connect 로 가 버린다.
|
||||||
|
|
||||||
|
```
|
||||||
|
# 1. Firebase 설정 파일(ios/Runner/GoogleService-Info.plist)을 둔다 — 없으면 빌드가 멈춘다
|
||||||
|
# 2. Google 콜백 스킴을 뽑는다
|
||||||
|
python3 tools/ios/inject_url_scheme.py
|
||||||
|
|
||||||
|
# 3. 올린다 (Xcode 정식판 필요 — 베타 SDK 빌드는 App Store Connect 가 거부한다)
|
||||||
|
export ASC_KEY_ID=... ASC_ISSUER_ID=... ASC_KEY_PATH=...
|
||||||
|
bash tools/ios/upload_testflight.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
API 키가 없으면 Xcode 에 로그인된 계정으로 올린다:
|
||||||
|
`xcodebuild -exportArchive -archivePath build/ios/archive/Runner.xcarchive -exportOptionsPlist ios/ExportOptions.plist -exportPath build/ios/ipa -allowProvisioningUpdates`
|
||||||
|
(`ExportOptions.plist` 에 `destination` = `upload` 를 임시로 넣거나 생성된 IPA 를 Transporter 로 올린다.)
|
||||||
|
|
||||||
|
### 화면 회전을 막지 않은 이유
|
||||||
|
|
||||||
|
별첨 1 ① 예외 처리가 「백그라운드 복귀·**화면 회전** 시 렌더링 정상 재개」를
|
||||||
|
요구한다. Figma 는 세로 프레임만 있으나 계약 문서가 회전을 판정 항목으로
|
||||||
|
두었으므로 가로를 닫지 않는다.
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
|
|
||||||
발주사 (주)팬케이크 / 수행사 스위치 주식회사(Sweech)
|
발주사 (주)팬케이크 / 수행사 스위치 주식회사(Sweech)
|
||||||
|
|
||||||
이 저장소는 FANUZU 모바일 앱(Flutter)과 서버(Firebase — Cloud Functions · Firestore 보안 규칙 · 인덱스)의 전체 소스입니다.
|
이 저장소는 FANUZU 모바일 앱(Flutter)과 서버(Firebase — Cloud Functions · Firestore 보안 규칙 · 인덱스 · Storage 규칙)의 전체 소스입니다.
|
||||||
|
|
||||||
| 폴더 | 내용 |
|
| 폴더 | 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `lib/` | 앱 소스 (화면 · 데이터 저장소) |
|
| `lib/` | 앱 소스 (화면 · 데이터 저장소 · 다국어) |
|
||||||
|
| `lib/l10n/` | 다국어 리소스 5종(ko · en · ja · zh · es) |
|
||||||
| `test/` · `integration_test/` | 앱 테스트 |
|
| `test/` · `integration_test/` | 앱 테스트 |
|
||||||
| `firebase/functions/` | Cloud Functions (TypeScript, Node.js 22) · 서버 테스트 `firebase/functions/test/` |
|
| `firebase/functions/` | Cloud Functions (TypeScript, Node.js 22) · 서버 테스트 `firebase/functions/test/` |
|
||||||
| `firebase/firestore.rules` · `firebase/firestore.indexes.json` | 보안 규칙 · 인덱스 |
|
| `firebase/firestore.rules` · `firebase/storage.rules` · `firebase/firestore.indexes.json` | 보안 규칙 · 인덱스 |
|
||||||
| `firebase/test/` | 보안 규칙 테스트 |
|
| `firebase/test/` | 보안 규칙 테스트 |
|
||||||
| `android/` · `ios/` | 플랫폼 설정 |
|
| `android/` · `ios/` | 플랫폼 설정 |
|
||||||
|
|
||||||
@@ -36,17 +37,7 @@ JDK 17 을 Flutter 에 지정합니다.
|
|||||||
fvm flutter config --jdk-dir <JDK 17 경로>
|
fvm flutter config --jdk-dir <JDK 17 경로>
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. 이 브랜치 · 태그와 제출 빌드
|
## 2. 저장소에 포함하지 않은 파일
|
||||||
|
|
||||||
| 브랜치 · 태그 | 커밋 | 대응 제출 빌드 | 산출물 SHA-256 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `project-1` · 태그 `v0.1.1+14` | `0d2394c` | 1차 보완 빌드 0.1.1(14) — Android APK · iOS TestFlight 같은 소스 | APK `7d2ebadc624666c7992ac92ac3aa5d2c5e7b4bd0c602efeabc194ec73c99eb52` · IPA `d00870facdb27d38b458cdadc5723190b9fb18bfba06c64f578e0705a28627c8` |
|
|
||||||
| 태그 `v0.2.0+10` | `be99511` | 2차 제출 빌드 0.2.0(10) | APK `856e15781d002aff066a02da0d53f9d8eeada9e1fc6b6fc139c0705c77342c2c`(릴리스 「2차 제출 빌드 v0.2.0+10」 첨부) |
|
|
||||||
| `project-2` | `4236d75` | 제출 빌드 없음 — 태그 `v0.2.0+10` 에 1차 보완(0.1.1+14) 내용을 합친 최신 코드 | — |
|
|
||||||
|
|
||||||
**재빌드 확인.** 태그를 체크아웃한 뒤 아래 3절 명령으로 빌드하면 같은 버전 · 빌드 번호의 APK 가 나옵니다. 서명 키가 달라 서명 블록은 다르므로, 비교는 `unzip -l` 로 본 파일 목록과 `classes*.dex` · `lib/*/libapp.so` 의 SHA-256 으로 합니다. 제출본과 같은 서명으로 다시 만들어야 하면 서명 키를 별도 보안 경로로 요청해 주십시오.
|
|
||||||
|
|
||||||
## 3. 저장소에 포함하지 않은 파일
|
|
||||||
|
|
||||||
아래 파일은 보안상 저장소에 넣지 않았습니다. 빌드 전에 준비해 주십시오.
|
아래 파일은 보안상 저장소에 넣지 않았습니다. 빌드 전에 준비해 주십시오.
|
||||||
|
|
||||||
@@ -54,18 +45,16 @@ fvm flutter config --jdk-dir <JDK 17 경로>
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `android/app/google-services.json` | Firebase 콘솔 → 프로젝트 `fanuzu-dev-b020d` → 프로젝트 설정 → Android 앱(`kr.co.fanuzu.app`) |
|
| `android/app/google-services.json` | Firebase 콘솔 → 프로젝트 `fanuzu-dev-b020d` → 프로젝트 설정 → Android 앱(`kr.co.fanuzu.app`) |
|
||||||
| `ios/Runner/GoogleService-Info.plist` | 같은 화면의 iOS 앱 |
|
| `ios/Runner/GoogleService-Info.plist` | 같은 화면의 iOS 앱 |
|
||||||
| `config/kakao.env` | `config/kakao.env.example` 을 복사해 `FANUZU_KAKAO_NATIVE_APP_KEY`(카카오 네이티브 앱 키) · `FANUZU_LINE_CHANNEL_ID`(LINE Login 채널 ID)를 적습니다. 없으면 카카오 · LINE 로그인만 「설정 전」으로 막힙니다 |
|
|
||||||
| `firebase/functions/.env.<프로젝트>` | 서버 `fnAuthKakao` 의 `KAKAO_APP_ID`(카카오 앱 ID) · `fnAuthLine` 의 `LINE_CHANNEL_ID` 를 적습니다 |
|
|
||||||
| `android/key.properties` · 업로드 서명 키 | **릴리스(서명) 빌드에만** 필요합니다. 요청하시면 별도 보안 경로로 전달합니다. 디버그 빌드에는 필요 없습니다 |
|
| `android/key.properties` · 업로드 서명 키 | **릴리스(서명) 빌드에만** 필요합니다. 요청하시면 별도 보안 경로로 전달합니다. 디버그 빌드에는 필요 없습니다 |
|
||||||
|
|
||||||
설정 파일이 없으면 앱은 표본 데이터로 동작하는 목(mock) 모드로 뜹니다(디버그 빌드에서만 진입 가능).
|
설정 파일이 없으면 앱은 표본 데이터로 동작하는 목(mock) 모드로 뜹니다(디버그 빌드에서만 진입 가능).
|
||||||
|
|
||||||
## 4. 앱 빌드 · 실행
|
## 3. 앱 빌드 · 실행
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app
|
cd app
|
||||||
fvm flutter pub get
|
fvm flutter pub get
|
||||||
fvm flutter run --dart-define-from-file=config/kakao.env # 연결된 실기기 · 에뮬레이터에서 실행
|
fvm flutter run # 연결된 실기기 · 에뮬레이터에서 실행
|
||||||
fvm flutter build apk --debug # 디버그 APK → build/app/outputs/flutter-apk/app-debug.apk
|
fvm flutter build apk --debug # 디버그 APK → build/app/outputs/flutter-apk/app-debug.apk
|
||||||
fvm flutter build apk --release # 서명 릴리스 APK (key.properties 필요)
|
fvm flutter build apk --release # 서명 릴리스 APK (key.properties 필요)
|
||||||
fvm flutter build ios --no-codesign # iOS 빌드 확인
|
fvm flutter build ios --no-codesign # iOS 빌드 확인
|
||||||
@@ -78,7 +67,7 @@ fvm flutter build ios --no-codesign # iOS 빌드 확인
|
|||||||
| `FANUZU_SHARE_BASE_URL` | 공유 링크 주소(웹 브릿지) | 공유 시 링크 없이 카드 · 문구만 보냅니다 |
|
| `FANUZU_SHARE_BASE_URL` | 공유 링크 주소(웹 브릿지) | 공유 시 링크 없이 카드 · 문구만 보냅니다 |
|
||||||
| `FANUZU_EMULATOR_HOST` | 로컬 Firebase 에뮬레이터 연결(개발용) | 실서버에 연결합니다 |
|
| `FANUZU_EMULATOR_HOST` | 로컬 Firebase 에뮬레이터 연결(개발용) | 실서버에 연결합니다 |
|
||||||
|
|
||||||
## 5. 테스트
|
## 4. 테스트
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app
|
cd app
|
||||||
@@ -97,44 +86,32 @@ npm run test:emulator # 서버 테스트 (Firestore 에뮬레
|
|||||||
|
|
||||||
의존성을 바꾼 경우 `analyze` · `test` 만으로는 Android 네이티브 컴파일 오류를 잡지 못하므로 `fvm flutter build apk --debug` 까지 확인해 주십시오. `pubspec.lock` 의 `firebase_core` 는 4.13.0 이어야 합니다(4.14.0 에서 Android 컴파일이 실패합니다).
|
의존성을 바꾼 경우 `analyze` · `test` 만으로는 Android 네이티브 컴파일 오류를 잡지 못하므로 `fvm flutter build apk --debug` 까지 확인해 주십시오. `pubspec.lock` 의 `firebase_core` 는 4.13.0 이어야 합니다(4.14.0 에서 Android 컴파일이 실패합니다).
|
||||||
|
|
||||||
## 6. 서버 배포
|
## 5. 서버 배포
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app/firebase
|
cd app/firebase
|
||||||
npm --prefix functions ci
|
npm --prefix functions ci
|
||||||
npm --prefix functions run build
|
npm --prefix functions run build
|
||||||
firebase deploy --only firestore,functions --project fanuzu-dev-b020d
|
firebase deploy --only firestore,storage,functions --project fanuzu-dev-b020d
|
||||||
```
|
```
|
||||||
|
|
||||||
## 7. 로그인 구성
|
Storage 규칙 배포는 Firebase 콘솔에서 Storage 를 먼저 활성화해야 합니다.
|
||||||
|
|
||||||
1차 로그인은 Google · Apple · Kakao · LINE 4종입니다(별첨 B). 이메일 · 비밀번호 가입과 X 로그인은 없습니다.
|
## 6. 테스트 계정 · 권한
|
||||||
|
|
||||||
| 제공자 | 앱 쪽 | 서버 쪽 | Firebase 사용자 식별 |
|
- 릴리스 빌드의 로그인은 **Google 계정**으로 합니다. 각자의 Google 계정으로 가입하시면 됩니다. Apple 은 iOS(TestFlight) 준비 후, Kakao · LINE 은 제공자 앱 키 수령 후 연결합니다.
|
||||||
|---|---|---|---|
|
- 운영 기능(퀘스트 · 캠페인 검수, 타임어택 발동, 결과보고 등록 등)은 운영자 권한이 있는 계정만 호출할 수 있습니다. 권한을 드릴 Google 계정을 알려 주시면 운영자 권한을 부여합니다.
|
||||||
| Google | Google 로그인 SDK | Firebase Authentication | `google.com` |
|
|
||||||
| Apple | iOS 는 Apple 로그인 SDK, Android 는 Firebase 웹 흐름 | Firebase Authentication | `apple.com` |
|
|
||||||
| Kakao | 카카오 SDK 로그인 | `fnAuthKakao` 가 카카오 토큰을 확인하고 Firebase 커스텀 토큰 발급 | `oidc.kakao` 제공자 ID 로 기록 |
|
|
||||||
| LINE | LINE SDK 로그인(앱 설치 시 LINE 앱, 없으면 웹 로그인) | `fnAuthLine` 이 LINE 토큰을 확인하고 Firebase 커스텀 토큰 발급 | `oidc.line` 제공자 ID 로 기록 |
|
|
||||||
|
|
||||||
- Kakao · LINE 은 Firebase OIDC 웹 흐름이 아니라 각 SDK 와 서버 커스텀 토큰으로 로그인합니다. 제공자 ID `oidc.kakao` · `oidc.line` 은 이전 가입자의 UID 를 유지하려고 그대로 씁니다.
|
## 7. 실기기에서만 확인 가능한 기능
|
||||||
- Kakao · LINE 이메일은 선택 동의 항목입니다. 이메일이 없어도 제공자 고유 식별값으로 가입 · 로그인합니다.
|
|
||||||
- **SNS 연결(MY › 설정)** 은 서버가 현재 로그인과 새 제공자의 인증 자격을 확인해 붙입니다. 이메일이 같은지는 보지 않으며, 다른 FANUZU 계정에 이미 붙은 제공자는 거부합니다. 마지막 남은 제공자는 뗄 수 없습니다.
|
|
||||||
|
|
||||||
## 8. 폰트
|
| 기능 | 이유 |
|
||||||
|
|---|---|
|
||||||
|
| 만보기 퀘스트 | 걸음 수 센서 · 건강 데이터 권한 |
|
||||||
|
| 위치 퀘스트 | GPS 반경 인증 |
|
||||||
|
| 이클립스 방어 · Music Shake | 흔들기(가속도 센서) |
|
||||||
|
| 푸시 알림(이클립스 사전 알림 · 타임어택 · 브리핑) | FCM 수신 |
|
||||||
|
| 보상형 광고 | Google 테스트 광고 단위로 동작 |
|
||||||
|
| 인앱 결제 · Focus 구독 | 스토어 결제 창 — 스토어 상품 등록 전에는 「스토어 상품 준비 중」으로 표시 |
|
||||||
|
| 공유(카드 이미지) | 시스템 공유 시트 |
|
||||||
|
|
||||||
`pubspec.yaml` 에 등록되어 앱에 들어가 있습니다.
|
에뮬레이터에서는 위 기능의 화면은 뜨지만 센서 · 결제 · 푸시가 동작하지 않습니다.
|
||||||
|
|
||||||
| 패밀리 | 파일 | 쓰임 |
|
|
||||||
|---|---|---|
|
|
||||||
| 42dot Sans | `assets/fonts/42dotSans[wght].ttf` | 본문(디자인 토큰 `font-body`) |
|
|
||||||
| ABeeZee | `assets/fonts/ABeeZee-Regular.ttf` | 제목 · 수치(디자인 토큰 `font-display` · `font-mono`) |
|
|
||||||
|
|
||||||
## 9. 테스트 계정 · 권한
|
|
||||||
|
|
||||||
- 로그인은 위 4종 가운데 각자의 계정으로 가입하시면 됩니다.
|
|
||||||
- 운영 기능은 운영자 권한이 있는 계정만 호출할 수 있습니다. 권한을 드릴 Google 계정을 알려 주시면 운영자 권한을 부여합니다.
|
|
||||||
|
|
||||||
## 10. 실기기에서만 확인 가능한 기능
|
|
||||||
|
|
||||||
1차 범위의 Kakao · LINE 로그인은 각 제공자 앱이 설치된 실기기에서 확인해 주십시오. 센서 · 결제 · 푸시를 쓰는 기능은 2차 이후 범위이며 `project-2` README 에 적었습니다.
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
|
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
|
# SwiftPM 이 iOS 빌드 때 여기에 서드파티 소스를 받는다. 우리 코드가 아니라
|
||||||
|
# 분석 대상이 아니며, 그쪽 테스트 파일이 우리 lint 결과를 덮어 버린다.
|
||||||
|
- build/**
|
||||||
# Firebase 도구가 내려받는 서드파티 소스. 우리 분석 대상이 아니다.
|
# Firebase 도구가 내려받는 서드파티 소스. 우리 분석 대상이 아니다.
|
||||||
- '**/node_modules/**'
|
- '**/node_modules/**'
|
||||||
- build/**
|
|
||||||
- android/**
|
- android/**
|
||||||
- ios/**
|
- ios/**
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ val kakaoProperties = Properties().apply {
|
|||||||
if (f.exists()) f.inputStream().use { load(it) }
|
if (f.exists()) f.inputStream().use { load(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 구글 지도 API 키 — `android/local.properties` 의 `MAPS_API_KEY=<키>`(gitignore). 없으면 빈 값으로
|
||||||
|
// 빌드되고 지도만 비어 보인다(크래시 없음).
|
||||||
|
val localProperties = Properties().apply {
|
||||||
|
val f = rootProject.file("local.properties")
|
||||||
|
if (f.exists()) f.inputStream().use { load(it) }
|
||||||
|
}
|
||||||
|
|
||||||
/// 이번 실행이 릴리스 산출물(assembleRelease · bundleRelease 등)을 요구하는가.
|
/// 이번 실행이 릴리스 산출물(assembleRelease · bundleRelease 등)을 요구하는가.
|
||||||
val wantsRelease = gradle.startParameter.taskNames.any { it.contains("Release", ignoreCase = true) }
|
val wantsRelease = gradle.startParameter.taskNames.any { it.contains("Release", ignoreCase = true) }
|
||||||
|
|
||||||
@@ -64,6 +71,7 @@ android {
|
|||||||
// AndroidManifest 의 카카오 로그인 리다이렉트 스킴 `kakao{네이티브 앱 키}`.
|
// AndroidManifest 의 카카오 로그인 리다이렉트 스킴 `kakao{네이티브 앱 키}`.
|
||||||
manifestPlaceholders["kakaoNativeAppKey"] =
|
manifestPlaceholders["kakaoNativeAppKey"] =
|
||||||
kakaoProperties.getProperty("FANUZU_KAKAO_NATIVE_APP_KEY", "")
|
kakaoProperties.getProperty("FANUZU_KAKAO_NATIVE_APP_KEY", "")
|
||||||
|
manifestPlaceholders["mapsApiKey"] = localProperties.getProperty("MAPS_API_KEY", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
@@ -104,9 +112,3 @@ android {
|
|||||||
flutter {
|
flutter {
|
||||||
source = "../.."
|
source = "../.."
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
|
||||||
// MainActivity 의 「버려진 Firebase 웹 로그인 정리」(kr.co.fanuzu.app/auth) — firebase-auth 가 이미 쓰는
|
|
||||||
// 라이브러리라 새로 들어오는 것은 없다. 앱 모듈에서 직접 부르려고 컴파일 경로에 올린다.
|
|
||||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,17 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- 만보기 퀘스트(S-H-WALK). Android 10(API 29)부터 런타임 권한이고 minSdk 가 29 라
|
||||||
|
분기 없이 항상 요청한다. 거부하면 인증만 막히고 목록·상세는 그대로 열린다. -->
|
||||||
|
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
|
||||||
|
<!-- 만보기 걸음 수 — Health Connect(견적서 「만보기(Health API)」 · 대조표 B-02). 걸음 읽기 하나만
|
||||||
|
선언한다(Play 건강 앱 선언 양식 — 요청 권한마다 사유를 적는다). 거부하면 위 기기 센서로 센다(B-26). -->
|
||||||
|
<uses-permission android:name="android.permission.health.READ_STEPS" />
|
||||||
|
|
||||||
|
<!-- 위치 퀘스트(S-H-PLACE). 별첨 B **B-27** 「탐색은 가능하나 보상 인증은
|
||||||
|
GPS/Geofence 필수」 — 상시 추적이 아니라 인증 시점에만 쓴다(v1.6.1 13장).
|
||||||
|
그래서 ACCESS_BACKGROUND_LOCATION 은 요청하지 않는다. -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="FANUZU"
|
android:label="FANUZU"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
@@ -7,6 +20,7 @@
|
|||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
android:theme="@style/LaunchTheme"
|
android:theme="@style/LaunchTheme"
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
@@ -23,7 +37,22 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
<!-- Health Connect 권한 화면의 「개인정보처리방침」 링크가 여는 화면(health 플러그인 설정 안내). -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- Android 14+ 의 Health Connect 권한 사용 내역 → 앱. -->
|
||||||
|
<activity-alias
|
||||||
|
android:name="ViewPermissionUsageActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:targetActivity=".MainActivity"
|
||||||
|
android:permission="android.permission.START_VIEW_PERMISSION_USAGE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
||||||
|
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity-alias>
|
||||||
<!-- Apple Android 웹 인증 callback. Service ID/redirect URI가 투입되기 전에는
|
<!-- Apple Android 웹 인증 callback. Service ID/redirect URI가 투입되기 전에는
|
||||||
Dart 설정 게이트가 SDK 호출을 막는다. -->
|
Dart 설정 게이트가 SDK 호출을 막는다. -->
|
||||||
<activity
|
<activity
|
||||||
@@ -49,6 +78,27 @@
|
|||||||
<data android:scheme="kakao${kakaoNativeAppKey}" android:host="oauth" />
|
<data android:scheme="kakao${kakaoNativeAppKey}" android:host="oauth" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<!-- AdMob 앱 ID — Google 공식 **테스트** 앱 ID(9주차 샌드박스). 발주사 AdMob 계정의
|
||||||
|
실 앱 ID 로 바꿀 때 lib/core/monetization/rewarded_ad.dart 의 AdUnits 광고 단위도 함께 바꾼다. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.ads.APPLICATION_ID"
|
||||||
|
android:value="ca-app-pub-3940256099942544~3347511713" />
|
||||||
|
<!-- 구글 지도 API 키 — android/local.properties 의 MAPS_API_KEY(저장소 밖). 없으면 지도만 빈다. -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.geo.API_KEY"
|
||||||
|
android:value="${mapsApiKey}" />
|
||||||
|
<!-- FCM 알림 표시 기본값 — Android 가 유일한 실동작 플랫폼이다(iOS 는 APNs 키
|
||||||
|
대기). 둘 다 없으면 백그라운드 알림이 「Miscellaneous」 채널로 흘러가고
|
||||||
|
흰 사각형 아이콘으로 뜬다. 사용자가 채널을 끄면 5개 구분 설정과 무관하게
|
||||||
|
전부 막히므로, 정책 원장 B-77 의 구분 설정이 유일한 제어면이 되도록
|
||||||
|
앱이 채널을 직접 만든다(MainActivity.kt). -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="@string/default_notification_channel_id" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||||
|
android:resource="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -61,6 +111,11 @@
|
|||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
|
<!-- Health Connect 설치 여부 확인(health 플러그인). -->
|
||||||
|
<package android:name="com.google.android.apps.healthdata" />
|
||||||
|
<intent>
|
||||||
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
||||||
|
</intent>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
|
|||||||
@@ -1,29 +1,63 @@
|
|||||||
package kr.co.fanuzu.app
|
package kr.co.fanuzu.app
|
||||||
|
|
||||||
import android.content.Intent
|
import android.app.NotificationChannel
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import android.app.NotificationManager
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
import java.text.Collator
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
// Health Connect 권한 요청(health 플러그인)이 `registerForActivityResult` 를 쓰므로 FragmentActivity 여야 한다.
|
||||||
|
class MainActivity : FlutterFragmentActivity() {
|
||||||
|
/**
|
||||||
|
* FCM 기본 알림 채널을 만든다.
|
||||||
|
*
|
||||||
|
* AndroidManifest 의 `default_notification_channel_id` 는 **가리키기만 한다.**
|
||||||
|
* 채널을 만들지 않으면 Android 가 「Miscellaneous」로 대체하고, 사용자는 그
|
||||||
|
* 채널을 끄는 것 말고 조절할 수 없다. 정책 원장 B-77 의 5개 구분이 유일한
|
||||||
|
* 제어면이어야 하므로 채널을 하나만 두고 구분은 토픽 구독으로 가른다.
|
||||||
|
*
|
||||||
|
* 채널 생성은 멱등이다 — 같은 ID 로 다시 만들어도 사용자가 바꾼 설정을
|
||||||
|
* 덮지 않는다. minSdk 가 29 라 버전 분기가 필요 없다.
|
||||||
|
*/
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
manager.createNotificationChannel(
|
||||||
|
NotificationChannel(
|
||||||
|
getString(R.string.default_notification_channel_id),
|
||||||
|
getString(R.string.default_notification_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 국가 표시명 — 앱 언어로 OS 가 아는 이름(`Locale.getDisplayCountry`)을 OS 정렬(`Collator`) 순서로 준다.
|
||||||
|
* 국가 이름을 앱이 번역하지 않으려는 통로다(Dart `loadCountryNames`). 결과는 `[[코드, 이름], …]`.
|
||||||
|
*/
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
// 버려진 Firebase 웹 로그인(LINE · Apple OIDC) 정리 — Dart `clearStaleWebSignIn`.
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "kr.co.fanuzu.app/locale")
|
||||||
// 인가 중 다른 앱(LINE 본인 확인 등)으로 갔다 돌아오면 Firebase 의 대기 작업이 결과 없이 남아,
|
|
||||||
// 다음 시도가 `web-context-already-presented` 로 막힌다. Firebase 의 인가 화면이 「닫힘」을 알릴 때
|
|
||||||
// 보내는 것과 같은 취소 신호를 보내 대기 작업을 취소로 끝내고 다음 시도를 연다. 대기 작업이 없으면 아무 일도 없다.
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "kr.co.fanuzu.app/auth")
|
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result ->
|
||||||
if (call.method != "cancelPendingWebSignIn") {
|
if (call.method != "countryNames") {
|
||||||
result.notImplemented()
|
result.notImplemented()
|
||||||
return@setMethodCallHandler
|
return@setMethodCallHandler
|
||||||
}
|
}
|
||||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(
|
val locale = Locale.forLanguageTag(call.argument<String>("locale") ?: "ko")
|
||||||
Intent("com.google.firebase.auth.ACTION_RECEIVE_FIREBASE_AUTH_INTENT")
|
val collator = Collator.getInstance(locale)
|
||||||
.putExtra("com.google.firebase.auth.internal.EXTRA_CANCELED", true)
|
val pairs = Locale.getISOCountries()
|
||||||
)
|
.map { listOf(it, Locale("", it).getDisplayCountry(locale)) }
|
||||||
result.success(null)
|
.sortedWith(compareBy(collator) { it[1] })
|
||||||
|
result.success(pairs)
|
||||||
|
}
|
||||||
|
// API 레벨 — Android 13+ 는 클립보드 복사를 시스템이 알리므로 앱 토스트를 생략한다(Dart `copyToClipboard`).
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "kr.co.fanuzu.app/device")
|
||||||
|
.setMethodCallHandler { call, result ->
|
||||||
|
if (call.method == "sdkInt") result.success(Build.VERSION.SDK_INT) else result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- 런치 배경 = color-cosmic-void(#000). Flutter 첫 프레임 전 흰 화면이 번쩍이지 않게 한다. -->
|
<!-- 런치 배경 = color-cosmic-void(#141414, Figma 스플래시 `184:35466` 배경). Flutter 첫 프레임 전 흰 화면이 번쩍이지 않게 한다. -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@color/cosmic_void" />
|
<item android:drawable="@color/cosmic_void" />
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
<!-- 스플래시 로고 — Figma `510:19047` 「화면 중앙 정렬 · 앱 실행 시 노출」. 발주사 인수 자산
|
||||||
<!-- <item>
|
`고객사 준비 데모/LOGO/fanuzu logo only.png` 의 마크만 잘라 쓴다. -->
|
||||||
|
<item>
|
||||||
<bitmap
|
<bitmap
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:src="@mipmap/launch_image" />
|
android:src="@drawable/launch_logo" />
|
||||||
</item> -->
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 151 KiB |
@@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- 런치 배경 = color-cosmic-void(#000). Flutter 첫 프레임 전 흰 화면이 번쩍이지 않게 한다. -->
|
<!-- 런치 배경 = color-cosmic-void(#141414, Figma 스플래시 `184:35466` 배경). Flutter 첫 프레임 전 흰 화면이 번쩍이지 않게 한다. -->
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="@color/cosmic_void" />
|
<item android:drawable="@color/cosmic_void" />
|
||||||
|
|
||||||
<!-- You can insert your own image assets here -->
|
<!-- 스플래시 로고 — Figma `510:19047` 「화면 중앙 정렬 · 앱 실행 시 노출」. 발주사 인수 자산
|
||||||
<!-- <item>
|
`고객사 준비 데모/LOGO/fanuzu logo only.png` 의 마크만 잘라 쓴다. -->
|
||||||
|
<item>
|
||||||
<bitmap
|
<bitmap
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:src="@mipmap/launch_image" />
|
android:src="@drawable/launch_logo" />
|
||||||
</item> -->
|
</item>
|
||||||
</layer-list>
|
</layer-list>
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Android 12+ 시스템 스플래시 아이콘 — Figma `510:19047` 「화면 중앙 정렬 · 앱 실행 시 노출」.
|
||||||
|
launch_background 와 같은 FANUZU 마크(launch_logo, 120dp)를 쓴다. 시스템은 이 그림을
|
||||||
|
240dp 칸에 그리고 지름 160dp 원으로 자르므로, 마크를 원래 크기로 가운데 두어 잘리지 않게 한다. -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@drawable/launch_logo" />
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Android 12+ 는 windowBackground 대신 시스템 스플래시(배경색 + 가운데 아이콘)를 그린다.
|
||||||
|
지정하지 않으면 흰 둥근 사각 안 앱 아이콘이 뜬다(10/6 실기기 E-1).
|
||||||
|
배경 = Figma 스플래시 `184:35466` #141414(color-cosmic-void) · 아이콘 = FANUZU 마크. 앱은 다크 전용이라 night 도 같다. -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@color/cosmic_void</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||||
|
<item name="android:windowSplashScreenIconBackgroundColor">@color/cosmic_void</item>
|
||||||
|
</style>
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Android 12+ 는 windowBackground 대신 시스템 스플래시(배경색 + 가운데 아이콘)를 그린다.
|
||||||
|
지정하지 않으면 흰 둥근 사각 안 앱 아이콘이 뜬다(10/6 실기기 E-1).
|
||||||
|
배경 = Figma 스플래시 `184:35466` #141414(color-cosmic-void) · 아이콘 = FANUZU 마크. 앱은 다크 전용이라 night 도 같다. -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
<item name="android:windowSplashScreenBackground">@color/cosmic_void</item>
|
||||||
|
<item name="android:windowSplashScreenAnimatedIcon">@drawable/splash_icon</item>
|
||||||
|
<item name="android:windowSplashScreenIconBackgroundColor">@color/cosmic_void</item>
|
||||||
|
</style>
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
<!-- design/design-tokens.md 의 color-cosmic-void. 네이티브 런치 화면이 Flutter 첫 프레임과
|
<!-- design/design-tokens.md 의 color-cosmic-void. 네이티브 런치 화면이 Flutter 첫 프레임과
|
||||||
같은 캔버스에서 시작하도록 쓴다. 앱은 다크 전용이라 values-night 를 따로 두지 않는다. -->
|
같은 캔버스에서 시작하도록 쓴다. 앱은 다크 전용이라 values-night 를 따로 두지 않는다. -->
|
||||||
<resources>
|
<resources>
|
||||||
<color name="cosmic_void">#FF000000</color>
|
<color name="cosmic_void">#FF141414</color>
|
||||||
<color name="ic_launcher_background">#120E25</color>
|
<color name="ic_launcher_background">#120E25</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- FCM 기본 알림 채널. AndroidManifest 의 default_notification_channel_id 와
|
||||||
|
MainActivity 의 채널 생성이 같은 값을 봐야 한다. -->
|
||||||
|
<string name="default_notification_channel_id" translatable="false">fanuzu_default</string>
|
||||||
|
<!-- 기기 알림 설정에 보이는 채널 이름이라 사용자 노출 문자열이다. -->
|
||||||
|
<string name="default_notification_channel_name">FANUZU 알림</string>
|
||||||
|
</resources>
|
||||||
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 986 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 857 B |
|
After Width: | Height: | Size: 801 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 835 B |
|
After Width: | Height: | Size: 340 B |
|
After Width: | Height: | Size: 811 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 798 B |
|
After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 532 B |
|
After Width: | Height: | Size: 835 B |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 680 B |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 690 B |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 456 B |
|
After Width: | Height: | Size: 1006 B |
|
After Width: | Height: | Size: 944 B |
|
After Width: | Height: | Size: 237 B |
|
After Width: | Height: | Size: 760 B |
|
After Width: | Height: | Size: 823 B |
|
After Width: | Height: | Size: 510 B |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 472 B |
|
After Width: | Height: | Size: 696 B |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 845 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 863 B |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 370 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 768 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 355 KiB |
|
After Width: | Height: | Size: 423 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 591 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 629 B |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 100 KiB |