Files
fanuzu/app
..

FANUZU 앱 골격 (Flutter)

2주차 산출물 · 발주사 (주)팬케이크 / 수행사 스위치 주식회사(Sweech) 별첨2 2주차 「첫 빌드(앱 골격)」의 빌드 포함 범위 = Flutter 앱 골격 · 5탭 네비게이션 · 소셜 로그인 · 디자인시스템

1. 이 골격의 범위

내용
포함 앱 기동 · 코스믹 다크 테마(design-tokens.md 생성) · 탭 셸 네비게이션 · 로그인 화면(S-A-LOGIN) 소셜 4종 버튼 · 인증 인터페이스와 Google·Apple·카카오·LINE 실 코드 경로 · 계정 연결(linkWithCredential) · Firebase 초기화(미설정 시 목 모드)
미포함 각 탭의 실제 화면(3주차 이후) · 온보딩 나머지 6화면(S-A-INTRO · S-A-TERMS · S-A-PROFILE · S-A-ARTIST · S-A-PLANET-NEW · S-A-PERMS) · 도메인 로직(POP·FPI·퀘스트·이클립스) · 상태관리 라이브러리

미포함 항목을 지금 만들지 않은 이유: RN/Flutter 승인이 아직 없다(2주차_확인요청항목 Q1). 미승인 시 폐기할 수 있도록 골격과 도메인 로직을 섞지 않았다. 5탭 라벨·화면 ID·토큰처럼 언어와 무관한 계약 정보만 코드에 들어가 있다.

2. 빌드·실행

전제: Flutter stable 3.47.2 / Dart 3.13.2 (app/.fvmrc 로 고정), Android SDK 36, Xcode 26.6

cd app
flutter pub get
flutter run                  # 실기기·시뮬레이터
flutter analyze              # 정적 분석
flutter test                 # 스모크·인증 상태 전이·회귀 테스트 19건
flutter build apk --debug    # Android 디버그 APK

빌드 산출물은 build/app/outputs/flutter-apk/app-debug.apk 에 떨어진다.

⚠️ pubspec.yaml 의 의존성을 추가·변경·업그레이드하면 최소 지원 OS 하한을 재계산할 것. 절차: 2주차 플러그인 선검증 리포트 §3.5

최소 지원 OS 는 별첨1 v1.0 에 계약값으로 박힌다(견적서 1228행). 지킬 수 없는 하한을 계약에 적으면 그 아래 단말의 장애가 개발사 책임이 된다. 선검증 리포트의 값은 2026-08-08 시점 측정치이므로, 이 앱의 의존성이 바뀌면 그 값은 그 순간 거짓이 된다.

2026-08-08 기준 합집합: Android API 29 · iOS 15.0 (이 프로젝트 단독값은 Android API 24 · iOS 15.0 — Android 29 는 geofence_foreground_service, iOS 15.0 은 firebase_auth 가 올린 값이다. 퀘스트 플러그인이 들어오는 6주차 전에 minSdk 를 29 로 올려야 한다.)

3. 디자인 토큰 — 손으로 옮기지 않는다

lib/core/theme/tokens.dart 는 생성물이다. 직접 고치면 토큰 문서(design/design-tokens.md)와 어긋난다.

cd app
python3 tool/gen_tokens.py   # PyYAML 필요: pip install pyyaml
  • 입력: design/design-tokens.md 의 <!-- TOKENS --> 아래 yaml 블록 (SoT)
  • 출력: lib/core/theme/tokens.dart
  • 부수 검증: design-tokens.md 의 contrast_checks 22쌍을 WCAG 로 재계산해 표로 출력하고, AA(4.5:1) 미달이 하나라도 있으면 exit 1. 테마가 대비를 깎았는지 여기서 걸린다.
  • lib/core/theme/app_theme.dart 가 이 토큰으로 ThemeData 를 조립한다. 색·크기를 여기서 새로 만들지 마라.

생성하지 않는 토큰 — CSS 전용 표현이라 Flutter 등가물이 없다: cosmic_fx.glow-* · grad-* · nebula · starfield · elevation.*. 필요해지는 주차에 tool/gen_tokens.py 헤더의 목록에서 하나씩 내려서 추가한다.

4. Firebase — 지금은 목(mock) 모드

google-services.json / GoogleService-Info.plist 가 아직 없다. 가짜 파일을 만들어 두지 않았다.

lib/core/firebase/firebase_boot.dart 가 Firebase.initializeApp() 을 시도하고, 실패하면 FirebaseMode.mock 으로 내려간 뒤 사유를 로그([FirebaseBoot] mode=mock :: ...)로 남긴다. 앱은 정상적으로 뜬다.

파일이 투입되면 코드 수정 없이 FirebaseMode.live 로 올라가고, 목 모드 전용 UI는 자동으로 사라진다.

목 모드에서의 로그인

Firebase 설정이 없으면 실 소셜 로그인이 물리적으로 불가능하다. 그런데 별첨2 2주차 발주사 확인 항목에 「앱 설치·로그인·탭 이동 확인」이 있다. 그래서 로그인 화면 하단에 「목 세션으로 탭 둘러보기」 입구를 목 모드에서만 노출한다.

  • 화면에 목 모드임과 그 사유가 함께 표시된다 — 실 로그인으로 오인할 여지가 없다.
  • FirebaseBoot.reason 은 사용자에게 보이는 문자열이라 내부 파일 경로를 넣지 않는다. 경로와 원시 예외는 FirebaseBoot.detail 로 분리해 로그에만 남긴다.

가드가 두 겹인 이유

bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;

처음에는 !FirebaseBoot.isLive 한 겹만 두고 "설정 파일이 들어오면 자동으로 막힌다" 고 적었는데, 그 설명은 틀렸다. FirebaseBoot.init() 은 모든 예외를 삼키고 목 모드로 내려가므로, 설정 파일이 정상 투입된 릴리스 빌드라도 초기화가 어떤 이유로든 실패하면(플랫폼 초기화 실패, 리소스 패키징 오류, 기기 환경) 목 모드가 되고 우회 입구가 배포 앱에 노출된다.

그래서 빌드 모드로 한 겹 더 막았다. 릴리스 빌드에서는 어떤 경우에도 열리지 않는다. kReleaseMode 는 컴파일 상수라 테스트에서 뒤집을 수 없어, AuthRepository({bool? releaseMode}) 로 주입 지점을 열어 두고 릴리스 동작을 테스트로 고정했다(회귀 가드 4종). 가드를 지우면 테스트 3개가 깨진다 — 확인했다.

5. 미투입 항목

5-1. 스위치(개발사) 내부 조치 — 발주사 요청 사항 아님

ID 항목 투입 위치 없으면
S1 Firebase 프로젝트(dev/prod) 개설 — 아래 S2 를 만들 수 없다
S2 google-services.json app/android/app/google-services.json 목 모드로 기동. Google 로그인 차단
S2 GoogleService-Info.plist app/ios/Runner/GoogleService-Info.plist iOS 빌드가 멈춘다(번들 리소스로 참조). 투입 절차는 BUILD.md 「제출 빌드」

S2 투입 시 Android 는 android/settings.gradle.kts 에 com.google.gms.google-services 플러그인 추가가 함께 필요하다. 지금은 설정 파일이 없어 플러그인을 적용하지 않았다 — 적용한 채로 파일이 없으면 Gradle 빌드 자체가 실패한다.

5-2. 발주사 확인 필요

ID 항목 투입 위치 없으면
Q3 카카오 네이티브 앱 키 config/kakao.env(저장소 밖) 카카오 버튼이 「설정 전」 안내 다이얼로그를 띄운다
Q3 LINE Login 채널 ID / 시크릿 Firebase 콘솔 oidc.line 제공자 LINE 로그인이 E-AUTH-11 로 끝난다
Q5 스토어 등록 명의 — 3주차 TestFlight·Play 내부트랙 제출 불가
Q14 앱 식별자 확정 android/app/build.gradle.kts · ios/Runner.xcodeproj 2026-09-11 확정 — 별첨 C 4번 kr.co.fanuzu.app. Android applicationId·namespace·Kotlin 패키지, iOS bundle id 를 모두 옮겼다. 스토어 등록 후에는 변경 불가
Q6 공식 FANUZU 심볼 원본 assets/brand/ 2026-08-13 반영 완료. 제공본이 심볼 전용이라 앱 아이콘은 공식 자산으로 생성했고 로그인 워드마크는 FANUZU 텍스트를 유지
Q15 하단 탭 아이콘 자산 assets/icons/ Material 기본 아이콘 5종으로 잠정

5-3. 폰트

폰트 파일이 리포에 없다. pubspec.yaml 의 fonts: 블록이 주석 처리된 채로 투입 위치만 표시돼 있다.

용도 패밀리 투입 위치 출처
제목 Gmarket Sans assets/fonts/GmarketSansTTF{Light,Medium,Bold}.ttf webfontworld/gmarket
본문 Pretendard Variable assets/fonts/PretendardVariable.ttf orioncactus/pretendard
수치 JetBrains Mono assets/fonts/JetBrainsMono-Regular.ttf JetBrains

미투입 상태에서는 시스템 폰트로 폴백된다. 크기·굵기·행간·자간은 토큰 값 그대로 적용되므로 투입 전후 레이아웃 차이는 자소 폭 수준에 그친다.

6. 로그인 — 별첨 B v1.4 확정 4종

1차 로그인은 Google / Apple / Kakao / LINE 4종입니다. 이메일/비밀번호 가입 및 X 로그인은 제외합니다. 하나의 FANUZU UID 에 복수 소셜 Provider 연결은 허용합니다. — 별첨B_정책설계서_요구사항정의서_반영_회신표_v1.4

버튼 순서는 이 회신 문구 순서다(AuthRepository._defaultProviders()).

제공자 상태 구현 막고 있는 것
Google 실 코드 경로 GoogleSignInProvider Firebase 콘솔에서 Google 제공자 미활성 + Android OAuth 클라이언트(SHA-1) 미등록
Apple 실 코드 경로 AppleSignInProvider Apple 개발자 키 + Xcode Capability + Android Service ID/redirect URI
카카오 실 코드 경로 KakaoSignInProvider — 카카오 SDK 로그인 + 서버 fnAuthKakao 커스텀 토큰 카카오 네이티브 앱 키(config/kakao.env) · 서버 KAKAO_APP_ID
LINE 실 코드 경로 OidcSignInProvider — Firebase OIDC oidc.line Firebase 콘솔 LINE 제공자 등록

키가 없는 제공자의 버튼에는 「키 대기」 배지가 붙고, 누르면 무엇이 없어서 못 쓰는지 안내 다이얼로그가 뜬다. 버튼을 회색으로 잠그지 않는 것은 회색이 사유를 말해 주지 않기 때문이다.

이메일 가입 — 사용자 노출 없음

별첨 B v1.4 가 이메일/비밀번호 가입을 1차 범위에서 뺐다. 로그인 화면에 이메일 버튼도, 「회원가입」도, 「비밀번호 재설정」도 없다. 닫은 방법은 기능 플래그가 아니라 목록에서 빼는 것이다 — AuthRepository.providers 에 없으면 화면이 버튼을 그리지 않고, 버튼이 없으면 닿는 경로가 없다.

발주사 QA 계정(qa-test@fanuzu.co.kr)용 이메일 로그인 코드는 남아 있으나 입구가 하나뿐이다:

  • 디버그 빌드에서 로그인 화면의 FANUZU 로고를 길게 누른다.
  • 릴리스 빌드에서는 AuthRepository.qaEmailProvider 가 null 이라 폼이 열리지 않고 signInWithEmail 도 실패로 닫힌다. 회귀 가드 4건이 이걸 고정한다.
  • 보이는 「QA 로그인」 버튼을 두지 않은 이유: 디버그 APK 도 발주사가 설치해 보는 산출물이라 그 버튼 자체가 「이메일 가입이 살아 있다」로 읽힌다.

계정 연결 — 하나의 UID 에 복수 Provider

FirebaseAccountLinker 가 User.linkWithCredential / User.unlink 를 쓴다. signInWithCredential 이 아니라 링크 API 만 쓰는 것이 「UID 는 하나」를 지키는 지점이다 — 전자는 해당 계정이 없으면 새로 만들어 버린다.

  • 로그인과 연결은 자격증명을 얻는 데까지 완전히 같아서 그 공통 부분을 CredentialProvider 하나로 뺐다. 두 벌로 적으면 Apple nonce 같은 보안 절차가 한쪽에서만 고쳐진다.
  • 명세 554:22797(동일 이메일만 연동 가능)은 붙이기 전에 판정한다. 붙인 뒤 되돌리는 방식은 unlink 가 실패하면 남의 계정이 붙은 채로 남는다. 단 Apple 은 최초 인가 때만 이메일을 주므로 재연동 시에는 판정할 값이 없어 통과한다 — 서버에서 다시 보려면 Functions 층(fn-account-link)에 같은 판정을 둔다.
  • 마지막 하나 남은 Provider 는 뗄 수 없다. 떼는 순간 로그인 수단이 0개가 되어 계정에 다시 들어올 방법이 사라지는데, Firebase 는 이걸 막아 주지 않는다.

설계 원칙: 「키 미투입」은 예외가 아니라 상태다. 예외를 던지지 않고 AuthNotConfigured(provider, missing) 를 반환해 "무엇이 없어서 못 쓰는지"를 화면에 그대로 보여 준다.

Apple은 로그인 시도마다 암호학적 nonce를 만들고 SHA-256 해시를 Apple에, 원문 nonce를 Firebase에 전달한다. Android에서는 Service ID와 redirect URI가 없으면 SDK를 호출하지 않는다. Firebase가 live가 되면 보존된 currentUser를 앱 시작 시 복원하고 authStateChanges를 계속 들어 만료·원격 로그아웃도 라우터에 반영한다. 이 내부 계약은 fake 세션·provider로 자동 검증한다.

이메일 가입은 구현하지 않았다. CD-18 미확정(Q2)이라 SocialAuthProvider 인터페이스에 구현체 하나가 더 붙을 자리만 비워 뒀다. 기능 플래그 상수는 두지 않았다 — 읽는 분기가 없는 플래그는 "false 라서 닫혀 있다" 는 착각만 만들고, 누가 true 로 바꿔도 아무 일이 일어나지 않는다 . 지금 이메일을 닫고 있는 것은 AuthRepository._defaultProviders() 에 구현체가 없다는 사실 하나다.

7. 계약 고정 표기 — 5탭

HOME · INSIDE · GALAXY · STATION · MY

근거 3개가 서로 일치한다:

근거 위치
견적서 v2.1 「5탭 네비게이션」 견적서 v2.1
크래프트 게이트 C15 design/craft-allowlist.yaml → APP_TABS

한글화·치환·순서 변경 금지. 스모크 테스트가 design/craft-allowlist.yaml 을 직접 읽어 문자열과 순서를 정확 일치로 검증한다 — 앱 코드만 고치면 테스트가 깨진다.

탭별 루트 화면:

탭 화면 ID 화면명
HOME S-H-HOME 오늘의 퀘스트
INSIDE S-I-LOUNGE 팬덤 라운지
GALAXY S-G-PLANET 내 행성
STATION S-S-HUB STATION 허브
MY S-M-HOME 마이 페이지

8. 폴더 구조

app/
├── pubspec.yaml
├── analysis_options.yaml           flutter_lints + strict-casts/raw-types/inference
├── tool/gen_tokens.py              design-tokens.md → tokens.dart 생성기
├── firebase/                       T1(Firestore 설계) 영역 — 규칙·인덱스
├── test/app_smoke_test.dart        앱 기동·5탭·목 세션 회귀 10건
├── test/auth_repository_test.dart  provider 결과·세션 복원·로그아웃 9건
└── lib/
    ├── main.dart                   FirebaseBoot.init() → runApp
    ├── app.dart                    MaterialApp.router + 테마 주입
    ├── core/
    │   ├── theme/tokens.dart       ← 생성물. 손대지 마라
    │   ├── theme/app_theme.dart    ThemeData 조립 (MD3 · ColorScheme.dark)
    │   ├── router/app_router.dart  5탭 셸 + 로그인 라우트 + AppShell
    │   ├── firebase/firebase_boot.dart
    │   └── ui/tab_placeholder.dart
    └── features/
        ├── auth/                   인터페이스 · 제공자 4종 · 저장소 · 로그인 화면
        └── home/ inside/ galaxy/ station/ my/    탭 루트 플레이스홀더

9. 검증 결과

2026-08-14 재실행. macOS 27.0 · Flutter 3.41.7 (stable) · Dart 3.11.5 · Android SDK 36 · Xcode 26.6

$ flutter analyze
Analyzing app...
No issues found! (ran in 3.2s)

$ flutter test
app_smoke_test.dart: 앱 기동·세션 복원·5탭·릴리스 목 우회 차단 10건 통과
auth_repository_test.dart: 제공자 4종 성공/취소/실패/미설정·예외 안전성·
                           Android Apple 설정 게이트·세션 복원/만료·로그아웃/재로그인 9건 통과
00:01 +19: All tests passed!

$ flutter build apk --debug
Running Gradle task 'assembleDebug'...    219.5s     (첫 빌드 — NDK 28.2 자동 설치 포함)
✓ Built build/app/outputs/flutter-apk/app-debug.apk        (164,473,984 bytes · 약 156.9 MiB · debug)
                                          10.6s     (증분 재빌드)

$ flutter build ios --debug --no-codesign
Running pod install...       72.1s
Running Xcode build...       63.1s
✓ Built build/ios/iphoneos/Runner.app

$ python3 tool/gen_tokens.py
생성: lib/core/theme/tokens.dart  (169줄)
… contrast_checks 25쌍 재계산 …
전 쌍 AA 통과.        (최저 color-on-neon-violet / color-neon-violet = 5.09:1)

테스트 19종은 Firebase 설정 파일이 없는 상태(목 모드)에서 통과한 결과다.

음성 검증 2회 — 테스트가 실제로 게이트 역할을 하는지 직접 깨뜨려 확인했다.

변조 결과
appTabs 의 STATION → 스테이션 2·3번 실패. 되돌린 뒤 재통과
isMockSessionAvailable 에서 && !_releaseMode 제거 릴리스 가드 3개 실패. 되돌린 뒤 재통과

contrast_checks 25쌍에는 앱 테마가 조립하며 새로 만드는 3쌍(선택 탭 라벨 9.21:1 · 인디케이터 위 선택 라벨 7.93:1 · 인디케이터 위 비선택 라벨 8.05:1)이 포함돼 있다. 토큰 문서에만 있는 쌍을 검사하면 위젯 테마에서 대비가 무너져도 생성기가 통과를 찍는다.

실기기 기동 확인 (Android 에뮬레이터 · API 36)

APK 를 설치·실행해 목 모드 기동과 탭 이동을 확인했다. FATAL EXCEPTION 0건.

$ adb install -r build/app/outputs/flutter-apk/app-debug.apk
Success

$ adb logcat | grep FirebaseBoot
I flutter : [FirebaseBoot] mode=mock :: Firebase 설정 파일이 없어 목(mock) 모드로 기동했습니다.
            투입 대상: app/android/app/google-services.json
                       app/ios/Runner/GoogleService-Info.plist
I flutter : [FirebaseBoot] detail :: PlatformException(java.lang.Exception: Failed to load
            FirebaseOptions from resource. Check that you have defined values.xml correctly. …)

확인한 동선: 로그인 화면 렌더(소셜 4버튼 라벨 일치) → 「목 세션으로 탭 둘러보기」 → 5탭 셸 진입 → STATION 탭 전환 시 S-S-HUB 로 화면 전환.

10. 알려진 제약

# 제약 영향 해소 시점
L1 Firebase 설정 파일 미투입 → 목 모드 실 소셜 로그인 불가. 로그인 버튼은 「키 미투입」 안내를 반환 S2 (3주차 빌드 전)
L2 com.google.gms.google-services Gradle 플러그인 미적용 지금은 이게 정상이다. 설정 파일 없이 플러그인만 적용하면 Gradle 빌드가 실패한다. 파일 투입과 동시에 android/settings.gradle.kts · android/app/build.gradle.kts 에 추가해야 한다 S2
L3 카카오 · LINE 로그인 연결 카카오는 SDK + 서버 커스텀 토큰(fnAuthKakao, 사용자 제공자 정보는 oidc.kakao), LINE 은 Firebase OIDC(oidc.line)다. 설정 방법은 BUILD.md 「카카오 · LINE 로그인 설정」 반영 완료
L3-1 「동일 이메일만 연동」이 Apple 재연동에서는 판정되지 않는다 Apple 이 최초 인가 때만 이메일을 준다. 클라이언트에서 볼 값이 없어 통과시킨다 — 서버(fn-account-link)에 같은 판정을 두어야 완결된다 Firestore 설계서 v1.1
L4 폰트 파일 미투입 시스템 폰트로 폴백. 크기·굵기·행간·자간은 토큰 값대로 적용됨 라이선스 원본 확보 시
L6 탭 아이콘이 Material 기본(Q15) 시안 탭바에는 아이콘이 없어 대조 대상이 없다 3주차
L7 iOS 최소 지원 버전을 13.0 → 15.0 으로 올렸다 firebase_auth 6.x 가 15.0 이상을 요구해 13.0 으로는 pod install 이 실패했다. 기준 단말 iPhone 11(S4)은 iOS 18까지 지원하므로 범위 영향 없음. ios/Podfile · Runner.xcodeproj 3곳 수정 반영 완료
L8 릴리스 서명 키 없음 → flutter build apk --release 는 의도적으로 실패한다 서명 키는 스토어 등록 명의(Q5·S3)가 정해져야 만들 수 있다. Flutter 기본 템플릿은 이 상태에서 debug 키로 조용히 서명해 Play 가 거부하는 산출물을 "성공"으로 내놓는다 — 그 경로를 막고 사유를 띄우도록 android/app/build.gradle.kts 를 고쳤다. 키가 생기면 android/key.properties(storeFile·storePassword·keyAlias·keyPassword)만 놓으면 된다. 해당 파일과 *.jks/*.keystore 는 app/android/.gitignore 가 이미 막고 있다(확인함) Q5·S3
L8-1 debug APK 164,473,984 bytes(약 156.9 MiB) debug 빌드 특성(미난독화·전 ABI 포함). release + --split-per-abi 로 크게 줄어들지만 L8 때문에 아직 실측하지 못했다 Q5·S3
L9 첫 Gradle 빌드에서 Kotlin 데몬 경고 1회 e: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0 — 자동 재시도로 성공했고 산출물에 영향 없음. 증분 빌드에서는 재현되지 않음 —
L9-1 iOS 런치 화면 색 변경은 컴파일 검증만 했다 LaunchScreen.storyboard 배경을 흰색 → color-cosmic-void(#000)로 바꿨다. Android 는 에뮬레이터에서 눈으로 확인했지만 iOS 는 시뮬레이터 실행을 하지 않아 빌드 성공까지만 확인했다 3주차 실기기 빌드
L10 온보딩 6화면 라우트 없음 S-A-INTRO · S-A-TERMS · S-A-PROFILE · S-A-ARTIST · S-A-PLANET-NEW · S-A-PERMS 3주차
L11 상태관리 라이브러리 없음 골격 범위라 ChangeNotifier 하나로 충분하다. 도메인 로직이 들어오는 3주차에 도입 여부를 판단 3주차
L12 app/ios/Podfile.lock 이 리포 .gitignore 에 잡혀 있다 Pod 버전이 고정되지 않아 빌드 재현성이 떨어진다. 커밋 대상으로 되돌릴지 리드 판단 필요 리드 확인

시연 시 유의 — 실 소셜 로그인은 L1 때문에 동작하지 않는다. 「목 세션으로 탭 둘러보기」로 탭 이동을 확인하시고, 로그인 자체는 3주차 빌드에서 확인 부탁드립니다.