Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2835485b91 | ||
|
|
ec84a9da43 | ||
|
|
0d2394c002 | ||
|
|
de65316205 | ||
|
|
59a8f9ead8 | ||
|
|
3b2bb5435e | ||
|
|
3511ba2de9 |
+114
-312
@@ -1,338 +1,140 @@
|
|||||||
# FANUZU 앱 골격 (Flutter)
|
# FANUZU 앱 · 서버 소스
|
||||||
|
|
||||||
> 2주차 산출물 · 발주사 (주)팬케이크 / 수행사 스위치 주식회사(Sweech)
|
발주사 (주)팬케이크 / 수행사 스위치 주식회사(Sweech)
|
||||||
> 별첨2 2주차 「첫 빌드(앱 골격)」의 빌드 포함 범위 = **Flutter 앱 골격 · 5탭 네비게이션 · 소셜 로그인 · 디자인시스템**
|
|
||||||
|
|
||||||
## 1. 이 골격의 범위
|
이 저장소는 FANUZU 모바일 앱(Flutter)과 서버(Firebase — Cloud Functions · Firestore 보안 규칙 · 인덱스)의 전체 소스입니다.
|
||||||
|
|
||||||
| | 내용 |
|
| 폴더 | 내용 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **포함** | 앱 기동 · 코스믹 다크 테마(design-tokens.md 생성) · 탭 셸 네비게이션 · 로그인 화면(S-A-LOGIN) 소셜 4종 버튼 · 인증 인터페이스와 Google·Apple·카카오·LINE 실 코드 경로 · 계정 연결(linkWithCredential) · Firebase 초기화(미설정 시 목 모드) |
|
| `lib/` | 앱 소스 (화면 · 데이터 저장소) |
|
||||||
| **미포함** | 각 탭의 실제 화면(3주차 이후) · 온보딩 나머지 6화면(S-A-INTRO · S-A-TERMS · S-A-PROFILE · S-A-ARTIST · S-A-PLANET-NEW · S-A-PERMS) · 도메인 로직(POP·FPI·퀘스트·이클립스) · 상태관리 라이브러리 |
|
| `test/` · `integration_test/` | 앱 테스트 |
|
||||||
|
| `firebase/functions/` | Cloud Functions (TypeScript, Node.js 22) · 서버 테스트 `firebase/functions/test/` |
|
||||||
|
| `firebase/firestore.rules` · `firebase/firestore.indexes.json` | 보안 규칙 · 인덱스 |
|
||||||
|
| `firebase/test/` | 보안 규칙 테스트 |
|
||||||
|
| `android/` · `ios/` | 플랫폼 설정 |
|
||||||
|
|
||||||
미포함 항목을 지금 만들지 않은 이유: **RN/Flutter 승인이 아직 없다(2주차_확인요청항목 Q1).**
|
---
|
||||||
미승인 시 폐기할 수 있도록 골격과 도메인 로직을 섞지 않았다. 5탭 라벨·화면 ID·토큰처럼
|
|
||||||
언어와 무관한 계약 정보만 코드에 들어가 있다.
|
|
||||||
|
|
||||||
## 2. 빌드·실행
|
## 1. 개발 환경
|
||||||
|
|
||||||
전제: Flutter stable 3.47.2 / Dart 3.13.2 (`app/.fvmrc` 로 고정), Android SDK 36, Xcode 26.6
|
| 항목 | 버전 |
|
||||||
|
|---|---|
|
||||||
|
| Flutter | **3.47.2** (`.fvmrc` 로 고정 — [fvm](https://fvm.app) 사용 권장) |
|
||||||
|
| Dart | 3.11.5 이상 |
|
||||||
|
| JDK (Android 빌드) | **17** (Temurin 17 권장. Android Studio 번들 JDK 25 는 현재 Gradle 8.14 와 호환되지 않아 빌드가 실패합니다) |
|
||||||
|
| JDK (Firebase 에뮬레이터) | **21 이상** (firebase-tools 요구) |
|
||||||
|
| Android | minSdk 29 (Android 10) · Android SDK build-tools 35 |
|
||||||
|
| iOS | 15.0 이상 · Xcode 26 |
|
||||||
|
| Node.js | 22 (Cloud Functions 런타임과 동일) |
|
||||||
|
| Firebase CLI | 최신 (`npm i -g firebase-tools`) |
|
||||||
|
|
||||||
|
`fvm` 을 쓰지 않는 경우 아래 명령의 `fvm flutter` 를 `flutter` 로 바꾸고, Flutter 3.47.2 를 직접 설치해 주십시오.
|
||||||
|
|
||||||
|
JDK 17 을 Flutter 에 지정합니다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fvm flutter config --jdk-dir <JDK 17 경로>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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. 저장소에 포함하지 않은 파일
|
||||||
|
|
||||||
|
아래 파일은 보안상 저장소에 넣지 않았습니다. 빌드 전에 준비해 주십시오.
|
||||||
|
|
||||||
|
| 파일 | 받는 곳 |
|
||||||
|
|---|---|
|
||||||
|
| `android/app/google-services.json` | Firebase 콘솔 → 프로젝트 `fanuzu-dev-b020d` → 프로젝트 설정 → Android 앱(`kr.co.fanuzu.app`) |
|
||||||
|
| `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` · 업로드 서명 키 | **릴리스(서명) 빌드에만** 필요합니다. 요청하시면 별도 보안 경로로 전달합니다. 디버그 빌드에는 필요 없습니다 |
|
||||||
|
|
||||||
|
설정 파일이 없으면 앱은 표본 데이터로 동작하는 목(mock) 모드로 뜹니다(디버그 빌드에서만 진입 가능).
|
||||||
|
|
||||||
|
## 4. 앱 빌드 · 실행
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app
|
cd app
|
||||||
flutter pub get
|
fvm flutter pub get
|
||||||
flutter run # 실기기·시뮬레이터
|
fvm flutter run --dart-define-from-file=config/kakao.env # 연결된 실기기 · 에뮬레이터에서 실행
|
||||||
flutter analyze # 정적 분석
|
fvm flutter build apk --debug # 디버그 APK → build/app/outputs/flutter-apk/app-debug.apk
|
||||||
flutter test # 스모크·인증 상태 전이·회귀 테스트 19건
|
fvm flutter build apk --release # 서명 릴리스 APK (key.properties 필요)
|
||||||
flutter build apk --debug # Android 디버그 APK
|
fvm flutter build ios --no-codesign # iOS 빌드 확인
|
||||||
```
|
```
|
||||||
|
|
||||||
빌드 산출물은 `build/app/outputs/flutter-apk/app-debug.apk` 에 떨어진다.
|
선택 빌드 설정(`--dart-define`):
|
||||||
|
|
||||||
> ⚠️ **`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`)와 어긋난다.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
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` 로 분리해 로그에만 남긴다.
|
|
||||||
|
|
||||||
**가드가 두 겹인 이유**
|
|
||||||
|
|
||||||
```dart
|
|
||||||
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 | 실 코드 경로 | iOS `AppleSignInProvider`(Apple SDK) · Android `OidcSignInProvider`(Firebase 웹 흐름 `apple.com`) | iOS Xcode Capability · Android 는 Firebase 콘솔 Apple 제공자에 서비스 ID · 키 등록(`BUILD.md` 「Android Apple 로그인」) |
|
|
||||||
| 카카오 | 실 코드 경로 | `KakaoSignInProvider` — 카카오 SDK 로그인 + 서버 `fnAuthKakao` 커스텀 토큰 | 카카오 네이티브 앱 키(`config/kakao.env`) · 서버 `KAKAO_APP_ID` |
|
|
||||||
| LINE | 실 코드 경로 | `OidcSignInProvider` — Firebase OIDC `oidc.line` | Firebase 콘솔 LINE 제공자 등록 |
|
|
||||||
|
|
||||||
카카오 · LINE 의 이메일은 **선택 동의 항목**이다. 이메일을 받지 못하거나 사용자가 동의하지 않아도
|
|
||||||
제공자 고유 식별값(카카오 회원번호 · LINE 사용자 ID)으로 가입 · 로그인되고, 같은 계정으로 다시 들어오면
|
|
||||||
같은 UID 로 로그인된다. 동의한 이메일만 사용자 문서(`users/{uid}.email`)에 남는다.
|
|
||||||
|
|
||||||
키가 없는 제공자의 버튼에는 **「키 대기」 배지**가 붙고, 누르면 무엇이 없어서 못 쓰는지
|
|
||||||
**안내 다이얼로그**가 뜬다. 버튼을 회색으로 잠그지 않는 것은 회색이 사유를 말해 주지 않기 때문이다.
|
|
||||||
|
|
||||||
### 이메일 가입 — 사용자 노출 없음
|
|
||||||
|
|
||||||
별첨 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 | 오늘의 퀘스트 |
|
| `FANUZU_SHARE_BASE_URL` | 공유 링크 주소(웹 브릿지) | 공유 시 링크 없이 카드 · 문구만 보냅니다 |
|
||||||
| INSIDE | S-I-LOUNGE | 팬덤 라운지 |
|
| `FANUZU_EMULATOR_HOST` | 로컬 Firebase 에뮬레이터 연결(개발용) | 실서버에 연결합니다 |
|
||||||
| GALAXY | S-G-PLANET | 내 행성 |
|
|
||||||
| STATION | S-S-HUB | STATION 허브 |
|
|
||||||
| MY | S-M-HOME | 마이 페이지 |
|
|
||||||
|
|
||||||
## 8. 폴더 구조
|
## 5. 테스트
|
||||||
|
|
||||||
```
|
```bash
|
||||||
app/
|
cd app
|
||||||
├── pubspec.yaml
|
fvm flutter analyze # 정적 분석 — 0건이어야 합니다
|
||||||
├── analysis_options.yaml flutter_lints + strict-casts/raw-types/inference
|
fvm flutter test # 앱 테스트
|
||||||
├── tool/gen_tokens.py design-tokens.md → tokens.dart 생성기
|
|
||||||
├── firebase/ T1(Firestore 설계) 영역 — 규칙·인덱스
|
cd firebase/test
|
||||||
├── test/app_smoke_test.dart 앱 기동·5탭·목 세션 회귀 10건
|
npm ci # 에뮬레이터용 Firebase CLI 포함 — 서버 테스트도 이것을 씁니다
|
||||||
├── test/auth_repository_test.dart provider 결과·세션 복원·로그아웃 9건
|
npm run test:emulator # 보안 규칙 테스트 (JDK 21 필요)
|
||||||
└── lib/
|
|
||||||
├── main.dart FirebaseBoot.init() → runApp
|
cd ../functions
|
||||||
├── app.dart MaterialApp.router + 테마 주입
|
npm ci
|
||||||
├── core/
|
npm run typecheck # 서버 타입 검사
|
||||||
│ ├── theme/tokens.dart ← 생성물. 손대지 마라
|
npm run test:emulator # 서버 테스트 (Firestore 에뮬레이터 · JDK 21 필요)
|
||||||
│ ├── 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. 검증 결과
|
의존성을 바꾼 경우 `analyze` · `test` 만으로는 Android 네이티브 컴파일 오류를 잡지 못하므로 `fvm flutter build apk --debug` 까지 확인해 주십시오. `pubspec.lock` 의 `firebase_core` 는 4.13.0 이어야 합니다(4.14.0 에서 Android 컴파일이 실패합니다).
|
||||||
|
|
||||||
<!-- VERIFY:BEGIN 실행 출력 그대로. 손으로 고치지 마라. -->
|
## 6. 서버 배포
|
||||||
2026-08-14 재실행. macOS 27.0 · Flutter 3.41.7 (stable) · Dart 3.11.5 · Android SDK 36 · Xcode 26.6
|
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ flutter analyze
|
cd app/firebase
|
||||||
Analyzing app...
|
npm --prefix functions ci
|
||||||
No issues found! (ran in 3.2s)
|
npm --prefix functions run build
|
||||||
|
firebase deploy --only firestore,functions --project fanuzu-dev-b020d
|
||||||
$ 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 설정 파일이 없는 상태(목 모드)에서 통과한 결과다.**
|
## 7. 로그인 구성
|
||||||
|
|
||||||
음성 검증 2회 — 테스트가 실제로 게이트 역할을 하는지 직접 깨뜨려 확인했다.
|
1차 로그인은 Google · Apple · Kakao · LINE 4종입니다(별첨 B). 이메일 · 비밀번호 가입과 X 로그인은 없습니다.
|
||||||
|
|
||||||
| 변조 | 결과 |
|
| 제공자 | 앱 쪽 | 서버 쪽 | Firebase 사용자 식별 |
|
||||||
|---|---|
|
|
||||||
| `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` 로 화면 전환.
|
|
||||||
<!-- VERIFY:END -->
|
|
||||||
|
|
||||||
## 10. 알려진 제약
|
|
||||||
|
|
||||||
<!-- LIMITS:BEGIN -->
|
|
||||||
| # | 제약 | 영향 | 해소 시점 |
|
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| L1 | Firebase 설정 파일 미투입 → 목 모드 | **실 소셜 로그인 불가.** 로그인 버튼은 「키 미투입」 안내를 반환 | S2 (3주차 빌드 전) |
|
| Google | Google 로그인 SDK | Firebase Authentication | `google.com` |
|
||||||
| L2 | `com.google.gms.google-services` Gradle 플러그인 **미적용** | 지금은 이게 정상이다. 설정 파일 없이 플러그인만 적용하면 Gradle 빌드가 실패한다. 파일 투입과 **동시에** `android/settings.gradle.kts` · `android/app/build.gradle.kts` 에 추가해야 한다 | S2 |
|
| Apple | iOS 는 Apple 로그인 SDK, Android 는 Firebase 웹 흐름 | Firebase Authentication | `apple.com` |
|
||||||
| L3 | 카카오 · LINE 로그인 연결 | 카카오는 SDK + 서버 커스텀 토큰(`fnAuthKakao`, 사용자 제공자 정보는 `oidc.kakao`), LINE 은 Firebase OIDC(`oidc.line`)다. 설정 방법은 `BUILD.md` 「카카오 · LINE 로그인 설정」 | 반영 완료 |
|
| Kakao | 카카오 SDK 로그인 | `fnAuthKakao` 가 카카오 토큰을 확인하고 Firebase 커스텀 토큰 발급 | `oidc.kakao` 제공자 ID 로 기록 |
|
||||||
| L3-1 | 「동일 이메일만 연동」이 Apple 재연동에서는 판정되지 않는다 | Apple 이 **최초 인가 때만** 이메일을 준다. 클라이언트에서 볼 값이 없어 통과시킨다 — 서버(fn-account-link)에 같은 판정을 두어야 완결된다 | Firestore 설계서 v1.1 |
|
| LINE | LINE SDK 로그인(앱 설치 시 LINE 앱, 없으면 웹 로그인) | `fnAuthLine` 이 LINE 토큰을 확인하고 Firebase 커스텀 토큰 발급 | `oidc.line` 제공자 ID 로 기록 |
|
||||||
| 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 때문에 동작하지 않는다. 「목 세션으로 탭 둘러보기」로
|
- Kakao · LINE 은 Firebase OIDC 웹 흐름이 아니라 각 SDK 와 서버 커스텀 토큰으로 로그인합니다. 제공자 ID `oidc.kakao` · `oidc.line` 은 이전 가입자의 UID 를 유지하려고 그대로 씁니다.
|
||||||
탭 이동을 확인하시고, 로그인 자체는 3주차 빌드에서 확인 부탁드립니다.
|
- Kakao · LINE 이메일은 선택 동의 항목입니다. 이메일이 없어도 제공자 고유 식별값으로 가입 · 로그인합니다.
|
||||||
<!-- LIMITS:END -->
|
- **SNS 연결(MY › 설정)** 은 서버가 현재 로그인과 새 제공자의 인증 자격을 확인해 붙입니다. 이메일이 같은지는 보지 않으며, 다른 FANUZU 계정에 이미 붙은 제공자는 거부합니다. 마지막 남은 제공자는 뗄 수 없습니다.
|
||||||
|
|
||||||
|
## 8. 폰트
|
||||||
|
|
||||||
|
`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 에 적었습니다.
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
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"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import 'auth_repository.dart';
|
|||||||
/// - 개인정보처리방침은 동의 항목이 아니라 고지다 — 체크 칸 없이 하단 버튼 위에 안내와 보기 링크만 둔다.
|
/// - 개인정보처리방침은 동의 항목이 아니라 고지다 — 체크 칸 없이 하단 버튼 위에 안내와 보기 링크만 둔다.
|
||||||
///
|
///
|
||||||
/// 약관 본문은 확정 문구를 받기 전까지 시안의 자리 표시 문구다 — 받으면
|
/// 약관 본문은 확정 문구를 받기 전까지 시안의 자리 표시 문구다 — 받으면
|
||||||
/// [termsPlaceholderBody] 와 [signupTermsVersion] 을 함께 바꾼다(동의 기록에 버전이 남는다).
|
/// 항목별 본문 · [privacyPolicyPlaceholderBody] 와 [signupTermsVersion] 을 함께 바꾼다(동의 기록에 버전이 남는다).
|
||||||
class TermsScreen extends StatefulWidget {
|
class TermsScreen extends StatefulWidget {
|
||||||
const TermsScreen({super.key, this.onAgreed, this.onBack, this.busy = false});
|
const TermsScreen({super.key, this.onAgreed, this.onBack, this.busy = false});
|
||||||
|
|
||||||
@@ -38,22 +38,53 @@ const signupTermsVersion = 'draft-1';
|
|||||||
List<String> get signupTermKeys => [for (final t in _Term.values) t.name];
|
List<String> get signupTermKeys => [for (final t in _Term.values) t.name];
|
||||||
|
|
||||||
enum _Term {
|
enum _Term {
|
||||||
service('서비스 이용약관 (필수)', required: true, body: true),
|
service('서비스 이용약관 (필수)', required: true, body: _serviceBody),
|
||||||
privacy('개인정보 수집·이용 동의 (필수)', required: true, body: true),
|
privacy('개인정보 수집·이용 동의 (필수)', required: true, body: _privacyBody),
|
||||||
age('만 14세 이상입니다. (필수)', required: true, body: false),
|
age('만 14세 이상입니다. (필수)', required: true),
|
||||||
marketing('마케팅 정보 및 이벤트 알림 수신 동의 (선택)', required: false, body: true);
|
marketing(
|
||||||
|
'마케팅 정보 및 이벤트 알림 수신 동의 (선택)',
|
||||||
|
required: false,
|
||||||
|
body: _marketingBody,
|
||||||
|
);
|
||||||
|
|
||||||
const _Term(this.label, {required this.required, required this.body});
|
const _Term(this.label, {required this.required, this.body});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final bool required;
|
final bool required;
|
||||||
final bool body;
|
|
||||||
|
/// 항목 아래에 보이는 본문. 없으면 체크 줄만 그린다.
|
||||||
|
final String? body;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 약관 본문 자리 표시 문구.
|
// 약관·방침 본문 자리 표시 문구 — 항목마다 제목이 다르다(한 문구를 돌려 쓰면 방침 창에 약관 제목이 뜬다).
|
||||||
const termsPlaceholderBody =
|
// 정식 문안을 받으면 이 문구와 [signupTermsVersion] 을 함께 바꾼다.
|
||||||
|
const _serviceBody =
|
||||||
'FANUZU 서비스 이용약관\n\n제1조 (적용범위)\n서비스 이용약관에 대한 더미 텍스트 입니다.\n'
|
'FANUZU 서비스 이용약관\n\n제1조 (적용범위)\n서비스 이용약관에 대한 더미 텍스트 입니다.\n'
|
||||||
'추후 작성해주시기 바랍니다. 서비스 이용약관에 대한 더미 텍스트 입니다.';
|
'추후 작성해주시기 바랍니다. 서비스 이용약관에 대한 더미 텍스트 입니다.';
|
||||||
|
const _privacyBody =
|
||||||
|
'FANUZU 개인정보 수집·이용 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n'
|
||||||
|
'수집 항목\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'이용 목적\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'동의를 거부할 권리와 거부 시 불이익\n정식 문안을 받으면 기재합니다.';
|
||||||
|
const _marketingBody =
|
||||||
|
'FANUZU 마케팅 정보 수신 동의\n\n정식 문안을 받기 전 임시 문구입니다.\n\n'
|
||||||
|
'수신 내용\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'수신 방법\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'동의 철회 방법\n정식 문안을 받으면 기재합니다.';
|
||||||
|
|
||||||
|
/// 개인정보처리방침 본문 자리 표시 문구 — 동의 항목이 아니라 하단 고지의 「보기」로 연다.
|
||||||
|
const privacyPolicyPlaceholderBody =
|
||||||
|
'FANUZU 개인정보처리방침\n\n정식 문안을 받기 전 임시 문구입니다.\n\n'
|
||||||
|
'1. 수집하는 개인정보 항목\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'2. 개인정보의 이용 목적\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'3. 개인정보의 보유 및 이용 기간\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'4. 개인정보 처리 위탁 및 국외 이전\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'5. 개인정보의 파기\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'6. 이용자의 권리와 행사 방법\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'7. 개인정보 보호책임자 및 문의처\n정식 문안을 받으면 기재합니다.\n\n'
|
||||||
|
'8. 시행일\n정식 문안을 받으면 기재합니다.';
|
||||||
|
|
||||||
const _stroke = Color(0xFF545454);
|
const _stroke = Color(0xFF545454);
|
||||||
|
|
||||||
@@ -79,7 +110,9 @@ class _TermsScreenState extends State<TermsScreen> {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('개인정보처리방침'),
|
title: const Text('개인정보처리방침'),
|
||||||
content: const SingleChildScrollView(child: Text(termsPlaceholderBody)),
|
content: const SingleChildScrollView(
|
||||||
|
child: Text(privacyPolicyPlaceholderBody),
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
@@ -194,7 +227,7 @@ class _TermsScreenState extends State<TermsScreen> {
|
|||||||
for (final t in _Term.values) ...[
|
for (final t in _Term.values) ...[
|
||||||
const SizedBox(height: AppSpace.s3),
|
const SizedBox(height: AppSpace.s3),
|
||||||
_Box(
|
_Box(
|
||||||
child: t.body
|
child: t.body != null
|
||||||
? Column(
|
? Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@@ -209,15 +242,17 @@ class _TermsScreenState extends State<TermsScreen> {
|
|||||||
onTap: () => _toggle(t),
|
onTap: () => _toggle(t),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: AppSpace.s4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: AppSpace.s4,
|
||||||
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 80,
|
height: 80,
|
||||||
child: ClipRect(
|
child: ClipRect(
|
||||||
child: Text(
|
child: Text(
|
||||||
termsPlaceholderBody,
|
t.body!,
|
||||||
overflow: TextOverflow.clip,
|
overflow: TextOverflow.clip,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: AppFonts.display,
|
fontFamily: AppFonts.display,
|
||||||
fontFamilyFallback: [AppFonts.body],
|
fontFamilyFallback: [AppFonts.body],
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: fancake
|
name: fancake
|
||||||
description: "FANUZU — 팬덤 플랫폼 앱 (골격). 발주사 (주)팬케이크 / 수행사 스위치 주식회사"
|
description: "FANUZU — 팬덤 플랫폼 앱 (골격). 발주사 (주)팬케이크 / 수행사 스위치 주식회사"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.1.1+11
|
version: 0.1.1+14
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.5
|
sdk: ^3.11.5
|
||||||
|
|||||||
@@ -117,6 +117,24 @@ void main() {
|
|||||||
expect(find.text('개인정보처리방침'), findsOneWidget);
|
expect(find.text('개인정보처리방침'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('개인정보처리방침 보기는 약관 문구가 아닌 처리방침 문구를 보여 준다', (tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(home: TermsScreen()));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const ValueKey('privacy-policy-link')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final body = tester
|
||||||
|
.widget<Text>(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byType(AlertDialog),
|
||||||
|
matching: find.textContaining('임시 문구'),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.data!;
|
||||||
|
expect(body, contains('개인정보처리방침'));
|
||||||
|
expect(body, isNot(contains('서비스 이용약관')));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('이미 동의한 계정은 약관 없이 HOME', (tester) async {
|
testWidgets('이미 동의한 계정은 약관 없이 HOME', (tester) async {
|
||||||
final auth = AuthRepository(
|
final auth = AuthRepository(
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -69,6 +69,18 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Android — MainActivity 가 패키지 기본 task 에 있어 LINE 로그인 후 앱으로 돌아온다', () {
|
||||||
|
// LINE SDK 는 lineauth:// 복귀 시 결과 화면을 NEW_TASK|CLEAR_TOP 으로 띄워 패키지
|
||||||
|
// affinity 의 task 를 찾는다. MainActivity 의 taskAffinity 가 비어 있으면 새 task 가
|
||||||
|
// 생겨 Chrome 맞춤 탭이 앱 위에 남는다(사용자가 X 를 눌러야 로그인이 끝난다).
|
||||||
|
final manifest = _read('android/app/src/main/AndroidManifest.xml');
|
||||||
|
final mainActivity = RegExp(
|
||||||
|
r'<activity\s[^>]*android:name="\.MainActivity"[^>]*>',
|
||||||
|
).firstMatch(manifest)?.group(0);
|
||||||
|
expect(mainActivity, isNotNull);
|
||||||
|
expect(mainActivity, isNot(contains('taskAffinity')));
|
||||||
|
});
|
||||||
|
|
||||||
test('설정·서명 자산을 git 이 추적하지 않는다', () {
|
test('설정·서명 자산을 git 이 추적하지 않는다', () {
|
||||||
final result = Process.runSync('git', [
|
final result = Process.runSync('git', [
|
||||||
'ls-files',
|
'ls-files',
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# FANUZU 기준문서 추가 확정
|
||||||
|
|
||||||
|
- 확정일: 2026-10-13 (KST)
|
||||||
|
- 기준: 통합검토회신 v2.3, 1장 10번·11장 성운 월 제한 및 Q-A23
|
||||||
|
- PDF 반영 위치: 통합검토회신 v2.3의 추가 확정 26쪽
|
||||||
|
|
||||||
|
## 1. 성운 생성 월 1회
|
||||||
|
|
||||||
|
사용자당 성운 생성은 한국 표준시(KST)의 달력상 월을 기준으로 월 1회입니다. 매월 1일 00:00 KST에 해당 월 생성 횟수를 초기화합니다. 마지막 생성 시점부터 30일을 계산하는 방식은 적용하지 않습니다.
|
||||||
|
|
||||||
|
예: 10월 31일 생성 후 11월 1일 00:00 KST부터 새 월의 1회 생성이 가능합니다.
|
||||||
|
|
||||||
|
## 2. 1인당 화력 분모
|
||||||
|
|
||||||
|
Q-A23의 “현재 유효 멤버수”는 동일 기준시각에 해당 행성(팬덤)에 가입되어 있는 멤버 수로 정의하며, 해당 기준시각 이전에 탈퇴한 멤버는 제외합니다. 최근 30일 활동 여부로 분모를 제한하지 않습니다.
|
||||||
|
|
||||||
|
Q-A03의 Active(최근 30일 해당 팬덤에서 서버 인정 활동 1건 이상 사용자)는 별도 지표이며, Q-A23의 1인당 화력 분모에 사용하지 않습니다.
|
||||||
|
|
||||||
|
**1인당 화력 = 해당 집계기간의 유효 Raw FPI 점수 / 동일 기준시각의 가입 멤버 수**
|
||||||
|
|
||||||
|
- 분자는 백분위 변환 전 점수를 사용합니다.
|
||||||
|
- 분모가 0이면 산정불가로 표시합니다.
|
||||||
|
- 표시용 백분위 0~100을 인원수로 나누거나 POP 획득·소비 금액을 합산하지 않습니다.
|
||||||
|
- 예: 유효 Raw FPI 1,000점, 가입 멤버 100명, 최근 30일 활동자 40명이면 1인당 화력은 10점입니다. 탈퇴자는 가입 멤버 100명에 포함하지 않습니다.
|
||||||
|
|
||||||
|
## 적용 및 변경 이력
|
||||||
|
|
||||||
|
이번 확정은 기존 “월 1회”의 기간 경계와 “현재 유효 멤버수”의 정의를 명확히 합니다. 두 항목의 해석에는 본 추가 확정 내용을 우선 적용합니다. 관련 명세·화면 설명·검수 기준에 동일하게 반영합니다.
|
||||||
|
|
||||||
|
## 문서 관리 원칙
|
||||||
|
|
||||||
|
발주자 요청(2026-10-13)에 따라 이후 기준문서 변경 시 GitHub에도 같은 확정 내용을 함께 반영하고 변경 이력을 남깁니다.
|
||||||
Reference in New Issue
Block a user