Files
fanuzu/app/BUILD.md
T

432 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 빌드 환경 — 실측 기록 (2026-08-31)
이 조합으로 **릴리스 APK·AAB 가 실제로 나오는 것을 확인**했다. 버전 조합이 어긋나면
Gradle 이 조용히 실패하지 않고 명확히 죽으므로 아래를 그대로 맞춘다.
| 구성 | 값 | 비고 |
|---|---|---|
| Flutter | **3.47.2** | `fvm` 으로 고정 — `app/.fvmrc` |
| JDK | **Temurin 17** | `~/.jdks/jdk-17.0.20.1+1/Contents/Home` |
| Gradle | 8.14 | `android/gradle/wrapper/gradle-wrapper.properties` |
| AGP | 8.11.1 | `android/settings.gradle.kts` |
| Android SDK | `/opt/homebrew/share/android-commandlinetools` | `brew install --cask android-commandlinetools` |
| build-tools | 35.0.1 | |
| platform | android-34 · android-36 | 빌드가 34 를 추가로 받는다 |
## JDK 를 17 로 못 박은 이유
Android Studio 번들 JBR 은 **JDK 25** 이고 **Gradle 8.14 와 비호환**이다. 그대로 빌드하면
`Gradle build failed due to Java/Gradle incompatibility` 로 죽는다. Gradle 을 9.1+ 로
올리는 선택지도 있으나 AGP 8.11.1 과의 조합을 다시 검증해야 하므로, Flutter 권장인
JDK 17 을 쓰는 쪽을 택했다.
```bash
fvm flutter config --jdk-dir ~/.jdks/jdk-17.0.20.1+1/Contents/Home
```
`brew install --cask temurin@17` 은 sudo 를 요구한다. sudo 없이 쓰려면 Adoptium
tar.gz 를 `~/.jdks/` 에 풀면 된다.
## 테스트
```bash
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
fvm flutter analyze
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` 와 정확 일치를
검증한다(5탭 계약 대조). 파일은 아래 순서로 찾는다.
1. `$FANUZU_DESIGN_DIR/craft-allowlist.yaml` — 다른 위치의 파일을 가리킬 때 쓴다.
2. `app/design/craft-allowlist.yaml` — 앱 폴더 안의 사본.
3. 저장소 루트 `design/craft-allowlist.yaml`.
하나도 못 찾으면 통과가 아니라 실패한다 — 계약 대조가 조용히 사라지면 안 된다.
루트 파일을 고칠 때는 `app/design/` 사본도 함께 맞춘다. 두 파일이 함께 보이면 스모크 테스트가
`APP_TABS` 줄을 서로 비교해 다르면 실패한다.
## 빌드
```bash
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
cd app
fvm flutter build apk --release # build/app/outputs/flutter-apk/app-release.apk
fvm flutter build appbundle --release # build/app/outputs/bundle/release/app-release.aab
```
### Android Apple 로그인 — 앱에 넣을 값 없음
Android 는 Apple SDK 가 없어 Firebase 웹 흐름(`apple.com` 제공자 · `signInWithProvider`)으로 로그인한다.
값은 모두 콘솔에 둔다 — 빌드 옵션은 없다.
1. Apple 개발자 계정 「Identifiers → Services IDs」에 서비스 ID 를 만들고 「Sign in with Apple」을 켠다.
- Domains: `<프로젝트 ID>.firebaseapp.com`
- Return URLs: `https://<프로젝트 ID>.firebaseapp.com/__/auth/handler`
2. 「Keys」에서 Sign in with Apple 키를 만들고 .p8 를 받는다(키 ID · 팀 ID 기록).
3. Firebase 콘솔 Authentication → 로그인 방법 → Apple 에 서비스 ID · 팀 ID · 키 ID · 비공개 키를 넣는다.
4. 확인: Android 첫 화면 Apple → 브라우저 탭에서 Apple 로그인 → 앱 HOME.
## 서명
`android/key.properties` 가 없으면 **릴리스 빌드가 의도적으로 실패**한다
(`android/app/build.gradle.kts`). 디버그 키로 조용히 서명된 산출물이 Play 에서
거부되는 것을 막기 위한 것이다.
키스토어는 `~/.fanuzu-signing/fanuzu-upload.p12` (PKCS12 · 별칭 `fanuzu-upload`).
**저장소에 넣지 않는다** — `android/.gitignore` 가 `key.properties` 를,
루트 `.gitignore` 가 `*.p12` `*.jks` 를 막는다.
서명 확인:
```bash
apksigner verify --print-certs build/app/outputs/flutter-apk/app-release.apk
# Signer #1 certificate DN: CN=FANUZU, OU=Development, O=Sweech Co Ltd, L=Daegu, C=KR
# Signer #1 certificate SHA-256 digest: 5e83f4c9cd35357b11561a5b82c47d300af8c31e33d80d94bc32a4f10f46dee7
```
## Firebase 앱 등록 — 식별자 `kr.co.fanuzu.app`
프로젝트는 `fanuzu-dev-b020d`(계정 `dev@sweech.io`). 2026-09-11 별첨 C 4번 확정으로
아래 두 앱을 새로 등록했다. 잠정 식별자(`kr.co.sweech.fancake`) 앱은 지우지 않고
남겨 두었다 — 그 식별자로 이미 설치된 테스트 빌드가 있고, Gradle 은
`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` 에 이미 들어 있다.
---
## iOS
**2026-08-31 실증** — `flutter build ios --release --no-codesign` 통과.
| 구성 | 값 |
|---|---|
| Xcode | **27.0** (`~/Downloads/Xcode-beta.app`) |
| CocoaPods | 1.17.0 (`brew install cocoapods`) |
| 산출물 | `build/ios/iphoneos/Runner.app` · 21.7MB · arm64 |
| bundle id | `kr.co.fanuzu.app` · 0.1.0(1) · label FANUZU |
| **MinimumOSVersion** | **15.0** — 문서(별첨 1 · 2주차 선검증)와 **일치** |
`xcode-select --switch` 는 sudo 를 요구하므로 **`DEVELOPER_DIR` 로 우회**한다.
```bash
export DEVELOPER_DIR=~/Downloads/Xcode-beta.app/Contents/Developer
fvm flutter build ios --release --no-codesign
```
**서명은 Apple Developer 계정이 있어야 한다.** 계정 없이 여기까지는 확인되므로,
TestFlight 제출에 남은 것은 **서명과 업로드뿐**이다.
### 정리해야 할 것 — CocoaPods / SwiftPM 이중 상태
빌드 중 Flutter 가 경고한다.
> All plugins found for ios are Swift Packages, but your project still has CocoaPods
> integration. … Removing CocoaPods integration will improve the project's build time.
iOS 플러그인이 전부 Swift Package 로 옮겨졌는데 프로젝트에 CocoaPods 통합이 남아 있다.
동작에는 지장이 없으나 빌드 시간이 늘고 구성이 이중이다. `pod deintegrate` 와
`Debug.xcconfig` · `Release.xcconfig` 의 Pods include 제거가 필요하다.
**11주차 최적화 구간에서 정리한다.**
## 서버 없이 로그인·원장을 돌려 보기
실 Firebase 프로젝트는 계정 명의가 확정되어야 열 수 있다(계정·권한 이전 별첨 A-3).
그 전까지는 **에뮬레이터를 물려 실제 왕복을 돈다.** 설정 파일 대신 에뮬레이터를 붙이는
것이라, 이 절차가 통과하면 설정 파일이 들어오는 날 코드 수정 없이 동작한다.
### 1. 에뮬레이터 기동
```
cd firebase
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 을
쓰므로 둘이 다르며, 에뮬레이터를 띄울 때만 `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. 앱을 에뮬레이터에 붙이기
```
flutter run --dart-define=FANUZU_EMULATOR_HOST=10.0.2.2 # Android 에뮬레이터
flutter run --dart-define=FANUZU_EMULATOR_HOST=127.0.0.1 # iOS 시뮬레이터
```
`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 로 뜨는데 Android 9 부터 평문이 기본 차단이다. 예외를
`android/app/src/{debug,profile}/` 에만 두었으므로 **릴리스 빌드에는 들어가지 않는다.**
확인:
```
unzip -p build/app/outputs/flutter-apk/app-release.apk AndroidManifest.xml \
| 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 는 세로 프레임만 있으나 계약 문서가 회전을 판정 항목으로
두었으므로 가로를 닫지 않는다.