22 KiB
빌드 환경 — 실측 기록 (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 을 쓰는 쪽을 택했다.
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/ 에 풀면 된다.
테스트
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 이 올려 버렸는지 게이트에서 함께 본다.
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탭 계약 대조). 파일은 아래 순서로 찾는다.
$FANUZU_DESIGN_DIR/craft-allowlist.yaml— 다른 위치의 파일을 가리킬 때 쓴다.app/design/craft-allowlist.yaml— 앱 폴더 안의 사본.- 저장소 루트
design/craft-allowlist.yaml.
하나도 못 찾으면 통과가 아니라 실패한다 — 계약 대조가 조용히 사라지면 안 된다.
루트 파일을 고칠 때는 app/design/ 사본도 함께 맞춘다. 두 파일이 함께 보이면 스모크 테스트가
APP_TABS 줄을 서로 비교해 다르면 실패한다.
빌드
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)으로 로그인한다.
값은 모두 콘솔에 둔다 — 빌드 옵션은 없다.
- Apple 개발자 계정 「Identifiers → Services IDs」에 서비스 ID 를 만들고 「Sign in with Apple」을 켠다.
- Domains:
<프로젝트 ID>.firebaseapp.com - Return URLs:
https://<프로젝트 ID>.firebaseapp.com/__/auth/handler
- Domains:
- 「Keys」에서 Sign in with Apple 키를 만들고 .p8 를 받는다(키 ID · 팀 ID 기록).
- Firebase 콘솔 Authentication → 로그인 방법 → Apple 에 서비스 ID · 팀 ID · 키 ID · 비공개 키를 넣는다.
- 확인: 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 를 막는다.
서명 확인:
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):
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 대상이다. 새 작업 폴더에서는 아래를 돌린다.
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 로 받는다.
LINE 채널 ID — 같은 config/kakao.env 에 FANUZU_LINE_CHANNEL_ID=<LINE Login 채널 ID(숫자)>. Dart 만 읽는다
(iOS 복귀 스킴은 line3rdp.<번들 ID> 라 키가 필요 없다). 없으면 LINE 로그인만 「설정 전」으로 막힌다. 서버
fnAuthLine 은 같은 값을 firebase/functions/.env.<프로젝트> 의 LINE_CHANNEL_ID 로 받는다.
구글 지도 API 키(위치 선택 지도) — 키는 저장소에 없다. 없어도 빌드는 되고 지도만 빈다.
- Android:
android/local.properties에MAPS_API_KEY=<키>→build.gradle.kts가manifestPlaceholders로 AndroidManifestcom.google.android.geo.API_KEY에 넣는다. - iOS:
ios/config/Maps.xcconfig(ios/config/Maps.xcconfig.example복사, gitignore) 에MAPS_API_KEY = <키>→Flutter/*.xcconfiginclude → Info.plistGMSApiKey→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 로 우회한다.
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+ 로 바꾼다.
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 를 직접 의존하지
않으므로 스크립트가 형제 경로를 가리킨다.
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
직접 부를 때도 같다.
../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 를 쓰면 그대로 뜬다.
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 를 직접 띄워도 된다.
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 까지 도는 확인이다. 기기 포트를 호스트로 넘기고 한 명령으로 돌린다.
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. 서버가 쓴 값을 앱이 읽는지만 볼 때
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 는 세로 프레임만 있으나 계약 문서가 회전을 판정 항목으로 두었으므로 가로를 닫지 않는다.