Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f483e8d94b | ||
|
|
79d4682e65 | ||
|
|
9dd43ca405 | ||
|
|
4c3bc0304f |
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"flutter": "3.47.2"
|
||||||
|
}
|
||||||
@@ -55,6 +55,8 @@ 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
|
||||||
|
ios/config/Google.xcconfig
|
||||||
|
|
||||||
# FVM Version Cache
|
# FVM Version Cache
|
||||||
.fvm/
|
.fvm/
|
||||||
|
|||||||
+37
-24
@@ -6,7 +6,7 @@ Gradle 이 조용히 실패하지 않고 명확히 죽으므로 아래를 그대
|
|||||||
| 구성 | 값 | 비고 |
|
| 구성 | 값 | 비고 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Flutter | **3.47.2** | `fvm` 으로 고정 — `app/.fvmrc` |
|
| Flutter | **3.47.2** | `fvm` 으로 고정 — `app/.fvmrc` |
|
||||||
| JDK | **Temurin 17** | `~/.jdks/jdk-17.0.20.1+1/Contents/Home` |
|
| JDK | **Temurin 17** | `fvm flutter config --jdk-dir` 로 지정 |
|
||||||
| 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,40 +21,29 @@ Android Studio 번들 JBR 은 **JDK 25** 이고 **Gradle 8.14 와 비호환**이
|
|||||||
JDK 17 을 쓰는 쪽을 택했다.
|
JDK 17 을 쓰는 쪽을 택했다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
fvm flutter config --jdk-dir ~/.jdks/jdk-17.0.20.1+1/Contents/Home
|
fvm flutter config --jdk-dir <JDK 17 설치 경로>
|
||||||
```
|
```
|
||||||
|
|
||||||
`brew install --cask temurin@17` 은 sudo 를 요구한다. sudo 없이 쓰려면 Adoptium
|
`brew install --cask temurin@17` 은 sudo 를 요구한다. sudo 없이 쓰려면 Adoptium
|
||||||
tar.gz 를 `~/.jdks/` 에 풀면 된다.
|
tar.gz 를 사용자 폴더에 풀고 그 경로를 지정하면 된다.
|
||||||
|
|
||||||
## 테스트
|
## 테스트
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
|
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
|
||||||
fvm flutter analyze
|
fvm flutter analyze
|
||||||
FANUZU_DESIGN_DIR=/path/to/p-fancake/design fvm flutter test
|
fvm flutter test
|
||||||
```
|
```
|
||||||
|
|
||||||
`FANUZU_DESIGN_DIR` 은 5탭 계약 대조용이다. 스모크 테스트가
|
스모크 테스트가 `craft-allowlist.yaml` 의 `APP_TABS` 를 직접 읽어 앱의 `appTabs` 와 정확 일치를
|
||||||
`craft-allowlist.yaml` 의 `APP_TABS` 를 직접 읽어 앱의 `appTabs` 와 정확 일치를 검증한다.
|
검증한다(5탭 계약 대조). 파일은 아래 순서로 찾는다.
|
||||||
**정본은 상위 저장소 `p-fancake/design/craft-allowlist.yaml`** 이고 이 저장소는 분리돼
|
|
||||||
있으므로, 같은 파일의 사본을 `design/craft-allowlist.yaml` 로 두었다. 탐색 순서는 아래와 같다.
|
|
||||||
|
|
||||||
1. `$FANUZU_DESIGN_DIR/craft-allowlist.yaml` — 정본을 직접 가리킬 때 쓴다.
|
1. `$FANUZU_DESIGN_DIR/craft-allowlist.yaml` — 다른 위치의 파일을 가리킬 때 쓴다.
|
||||||
2. `design/craft-allowlist.yaml` — 이 저장소의 사본.
|
2. `app/design/craft-allowlist.yaml` — 앱 폴더 안의 사본.
|
||||||
3. `../design/craft-allowlist.yaml` · `../p-fancake/design/craft-allowlist.yaml` — p-fancake 와
|
3. 저장소 루트 `design/craft-allowlist.yaml`.
|
||||||
형제로 둔 체크아웃.
|
|
||||||
|
|
||||||
사본이 있으므로 **git worktree 에서도 변수 없이 통과한다.** 그래도 못 찾으면 통과가 아니라
|
하나도 못 찾으면 통과가 아니라 실패한다 — 계약 대조가 조용히 사라지면 안 된다.
|
||||||
실패한다 — 계약 대조가 조용히 사라지면 안 된다.
|
루트 파일을 고칠 때는 `app/design/` 사본도 함께 맞춘다.
|
||||||
|
|
||||||
사본은 정본과 갈리면 저장소마다 다른 답을 내므로, **정본을 고칠 때 사본에도 함께 복사한다.**
|
|
||||||
파일 머리에 같은 문장을 적어 두었다.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp /path/to/p-fancake/design/craft-allowlist.yaml design/craft-allowlist.yaml
|
|
||||||
# 머리 주석 5줄은 사본에만 있으니 지우지 말고 APP_TABS·CAPS_OK 만 맞춘다.
|
|
||||||
```
|
|
||||||
|
|
||||||
## 빌드
|
## 빌드
|
||||||
|
|
||||||
@@ -111,8 +100,32 @@ export DEVELOPER_DIR=~/Downloads/Xcode-beta.app/Contents/Developer
|
|||||||
fvm flutter build ios --release --no-codesign
|
fvm flutter build ios --release --no-codesign
|
||||||
```
|
```
|
||||||
|
|
||||||
**서명은 Apple Developer 계정이 있어야 한다.** 계정 없이 여기까지는 확인되므로,
|
### 제출 빌드 — App Store Connect
|
||||||
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 이중 상태
|
||||||
|
|
||||||
|
|||||||
+8
-18
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
## 2. 빌드·실행
|
## 2. 빌드·실행
|
||||||
|
|
||||||
전제: Flutter stable 3.41.7 / Dart 3.11.5 (검증에 쓴 버전), Android SDK 36, Xcode 26.6
|
전제: Flutter stable 3.47.2 / Dart 3.13.2 (`app/.fvmrc` 로 고정), Android SDK 36, Xcode 26.6
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app
|
cd app
|
||||||
@@ -30,7 +30,7 @@ flutter build apk --debug # Android 디버그 APK
|
|||||||
빌드 산출물은 `build/app/outputs/flutter-apk/app-debug.apk` 에 떨어진다.
|
빌드 산출물은 `build/app/outputs/flutter-apk/app-debug.apk` 에 떨어진다.
|
||||||
|
|
||||||
> ⚠️ **`pubspec.yaml` 의 의존성을 추가·변경·업그레이드하면 최소 지원 OS 하한을 재계산할 것.**
|
> ⚠️ **`pubspec.yaml` 의 의존성을 추가·변경·업그레이드하면 최소 지원 OS 하한을 재계산할 것.**
|
||||||
> 절차: `주간보고/2주차/src/2주차_플러그인_선검증리포트.md` §3.5
|
> 절차: 2주차 플러그인 선검증 리포트 §3.5
|
||||||
>
|
>
|
||||||
> 최소 지원 OS 는 **별첨1 v1.0 에 계약값으로 박힌다**(견적서 1228행). 지킬 수 없는 하한을
|
> 최소 지원 OS 는 **별첨1 v1.0 에 계약값으로 박힌다**(견적서 1228행). 지킬 수 없는 하한을
|
||||||
> 계약에 적으면 그 아래 단말의 장애가 개발사 책임이 된다. 선검증 리포트의 값은
|
> 계약에 적으면 그 아래 단말의 장애가 개발사 책임이 된다. 선검증 리포트의 값은
|
||||||
@@ -42,7 +42,7 @@ flutter build apk --debug # Android 디버그 APK
|
|||||||
|
|
||||||
## 3. 디자인 토큰 — 손으로 옮기지 않는다
|
## 3. 디자인 토큰 — 손으로 옮기지 않는다
|
||||||
|
|
||||||
`lib/core/theme/tokens.dart` 는 **생성물**이다. 직접 고치면 시안(`시안/assets/ds.css`)과 어긋난다.
|
`lib/core/theme/tokens.dart` 는 **생성물**이다. 직접 고치면 토큰 문서(`design/design-tokens.md`)와 어긋난다.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd app
|
cd app
|
||||||
@@ -79,7 +79,7 @@ Firebase 설정이 없으면 실 소셜 로그인이 물리적으로 불가능
|
|||||||
* `FirebaseBoot.reason` 은 사용자에게 보이는 문자열이라 **내부 파일 경로를 넣지 않는다.**
|
* `FirebaseBoot.reason` 은 사용자에게 보이는 문자열이라 **내부 파일 경로를 넣지 않는다.**
|
||||||
경로와 원시 예외는 `FirebaseBoot.detail` 로 분리해 로그에만 남긴다.
|
경로와 원시 예외는 `FirebaseBoot.detail` 로 분리해 로그에만 남긴다.
|
||||||
|
|
||||||
**가드가 두 겹인 이유 (교차검증 A-01 반영)**
|
**가드가 두 겹인 이유**
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;
|
bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;
|
||||||
@@ -103,7 +103,7 @@ bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| S1 | Firebase 프로젝트(dev/prod) 개설 | — | 아래 S2 를 만들 수 없다 |
|
| S1 | Firebase 프로젝트(dev/prod) 개설 | — | 아래 S2 를 만들 수 없다 |
|
||||||
| S2 | `google-services.json` | `app/android/app/google-services.json` | 목 모드로 기동. Google 로그인 차단 |
|
| S2 | `google-services.json` | `app/android/app/google-services.json` | 목 모드로 기동. Google 로그인 차단 |
|
||||||
| S2 | `GoogleService-Info.plist` | `app/ios/Runner/GoogleService-Info.plist` | 목 모드로 기동. Google·Apple 로그인 차단 |
|
| S2 | `GoogleService-Info.plist` | `app/ios/Runner/GoogleService-Info.plist` | iOS 빌드가 멈춘다(번들 리소스로 참조). 투입 절차는 `BUILD.md` 「제출 빌드」 |
|
||||||
|
|
||||||
> S2 투입 시 Android 는 `android/settings.gradle.kts` 에 `com.google.gms.google-services` 플러그인
|
> S2 투입 시 Android 는 `android/settings.gradle.kts` 에 `com.google.gms.google-services` 플러그인
|
||||||
> 추가가 함께 필요하다. 지금은 설정 파일이 없어 플러그인을 적용하지 않았다 —
|
> 추가가 함께 필요하다. 지금은 설정 파일이 없어 플러그인을 적용하지 않았다 —
|
||||||
@@ -191,7 +191,7 @@ Firebase가 live가 되면 보존된 `currentUser`를 앱 시작 시 복원하
|
|||||||
**이메일 가입은 구현하지 않았다.** CD-18 미확정(Q2)이라 `SocialAuthProvider` 인터페이스에
|
**이메일 가입은 구현하지 않았다.** CD-18 미확정(Q2)이라 `SocialAuthProvider` 인터페이스에
|
||||||
구현체 하나가 더 붙을 자리만 비워 뒀다. **기능 플래그 상수는 두지 않았다** — 읽는 분기가 없는
|
구현체 하나가 더 붙을 자리만 비워 뒀다. **기능 플래그 상수는 두지 않았다** — 읽는 분기가 없는
|
||||||
플래그는 "false 라서 닫혀 있다" 는 착각만 만들고, 누가 true 로 바꿔도 아무 일이 일어나지 않는다
|
플래그는 "false 라서 닫혀 있다" 는 착각만 만들고, 누가 true 로 바꿔도 아무 일이 일어나지 않는다
|
||||||
(교차검증 A-02). 지금 이메일을 닫고 있는 것은 `AuthRepository._defaultProviders()` 에
|
. 지금 이메일을 닫고 있는 것은 `AuthRepository._defaultProviders()` 에
|
||||||
구현체가 없다는 사실 하나다.
|
구현체가 없다는 사실 하나다.
|
||||||
|
|
||||||
## 7. 계약 고정 표기 — 5탭
|
## 7. 계약 고정 표기 — 5탭
|
||||||
@@ -204,9 +204,8 @@ HOME · INSIDE · GALAXY · STATION · MY
|
|||||||
|
|
||||||
| 근거 | 위치 |
|
| 근거 | 위치 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 견적서 v2.1 「5탭 네비게이션」 | `Docs/Fancake_팬덤플랫폼_견적서.html` |
|
| 견적서 v2.1 「5탭 네비게이션」 | 견적서 v2.1 |
|
||||||
| 크래프트 게이트 C15 | `design/craft-allowlist.yaml` → `APP_TABS` |
|
| 크래프트 게이트 C15 | `design/craft-allowlist.yaml` → `APP_TABS` |
|
||||||
| 시안 하단 탭바 | `시안/screens/S-{H-HOME,I-LOUNGE,G-PLANET,S-HUB,M-HOME}.html` 의 `<nav class="tabbar">` |
|
|
||||||
|
|
||||||
**한글화·치환·순서 변경 금지.** 스모크 테스트가 `design/craft-allowlist.yaml` 을 직접 읽어
|
**한글화·치환·순서 변경 금지.** 스모크 테스트가 `design/craft-allowlist.yaml` 을 직접 읽어
|
||||||
문자열과 순서를 정확 일치로 검증한다 — 앱 코드만 고치면 테스트가 깨진다.
|
문자열과 순서를 정확 일치로 검증한다 — 앱 코드만 고치면 테스트가 깨진다.
|
||||||
@@ -288,16 +287,7 @@ $ python3 tool/gen_tokens.py
|
|||||||
|
|
||||||
`contrast_checks` 25쌍에는 앱 테마가 조립하며 새로 만드는 3쌍(선택 탭 라벨 9.21:1 ·
|
`contrast_checks` 25쌍에는 앱 테마가 조립하며 새로 만드는 3쌍(선택 탭 라벨 9.21:1 ·
|
||||||
인디케이터 위 선택 라벨 7.93:1 · 인디케이터 위 비선택 라벨 8.05:1)이 포함돼 있다.
|
인디케이터 위 선택 라벨 7.93:1 · 인디케이터 위 비선택 라벨 8.05:1)이 포함돼 있다.
|
||||||
토큰 문서에만 있는 쌍을 검사하면 위젯 테마에서 대비가 무너져도 생성기가 통과를 찍는다(교차검증 A-04).
|
토큰 문서에만 있는 쌍을 검사하면 위젯 테마에서 대비가 무너져도 생성기가 통과를 찍는다.
|
||||||
|
|
||||||
### 토큰 대조 — 시안 CSS ↔ 생성된 Dart
|
|
||||||
|
|
||||||
`시안/assets/ds.css` 의 `:root` 변수와 `tokens.dart` 의 색 토큰을 값 비교했다.
|
|
||||||
|
|
||||||
```
|
|
||||||
ds.css 변수 119개 · tokens.dart 색 토큰 51개 · 공통 51개
|
|
||||||
값 불일치: 0개
|
|
||||||
```
|
|
||||||
|
|
||||||
### 실기기 기동 확인 (Android 에뮬레이터 · API 36)
|
### 실기기 기동 확인 (Android 에뮬레이터 · API 36)
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,6 @@ analyzer:
|
|||||||
exclude:
|
exclude:
|
||||||
# Firebase 도구가 내려받는 서드파티 소스. 우리 분석 대상이 아니다.
|
# Firebase 도구가 내려받는 서드파티 소스. 우리 분석 대상이 아니다.
|
||||||
- '**/node_modules/**'
|
- '**/node_modules/**'
|
||||||
|
- build/**
|
||||||
|
- android/**
|
||||||
|
- ios/**
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
# ⚠️ 이 파일은 사본이다. 정본은 상위 저장소 p-fancake/design/craft-allowlist.yaml 이며,
|
# ⚠️ 이 파일은 저장소 루트 design/craft-allowlist.yaml 의 사본이다.
|
||||||
# 정본을 갱신할 때 이 사본에도 함께 복사한다. 두 파일이 갈리면 계약 대조(APP_TABS)가
|
# 루트 파일을 고칠 때 이 사본도 함께 맞춘다(BUILD.md 「테스트」).
|
||||||
# 저장소마다 다른 답을 내므로, 사본만 고치는 일은 없어야 한다.
|
|
||||||
# 사본을 둔 이유는 git worktree 에서 형제 경로(../design/)가 없어 스모크 테스트가
|
|
||||||
# 계약 대조를 못 하고 실패하기 때문이다(BUILD.md 「테스트」).
|
|
||||||
|
|
||||||
# craft-allowlist.yaml — 크래프트 체커의 "분류 결정"을 적는 곳.
|
# craft-allowlist.yaml — 크래프트 체커의 "분류 결정"을 적는 곳.
|
||||||
# 경고를 끄는 곳이 아니다. 각 항목에 근거(reason)가 없으면 킷이 읽지 않는다.
|
# 경고를 끄는 곳이 아니다. 각 항목에 근거(reason)가 없으면 킷이 읽지 않는다.
|
||||||
# 계약: 하네스/디자인/resources/codex/check_craft_render.py C2
|
|
||||||
|
|
||||||
CAPS_OK:
|
CAPS_OK:
|
||||||
# ── 5탭 라벨 — 견적서 v2.1 「5탭 네비게이션」으로 고정된 화면 이름이다.
|
# ── 5탭 라벨 — 견적서 v2.1 「5탭 네비게이션」으로 고정된 화면 이름이다.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ rules_version = '2';
|
|||||||
// FANUZU — Firestore 보안 규칙 (2주차 설계본 v0.1)
|
// FANUZU — Firestore 보안 규칙 (2주차 설계본 v0.1)
|
||||||
//
|
//
|
||||||
// 근거
|
// 근거
|
||||||
// - wireframe/handoff/rbac-summary.md 역할 3종 · 운영자 등급 3단계(D-18)
|
// - 요구사항 정의서 권한 요구 역할 3종 · 운영자 등급 3단계
|
||||||
// - Docs/별첨1_기능별_완료기준표.html 「전 기능 공통 완료 기준 — 서버검증 원칙」
|
// - 별첨 1 기능별 완료 기준표 「전 기능 공통 완료 기준 — 서버검증 원칙」
|
||||||
// - 1주차 정책 설계서 P-02 원장 무결성 · P-06 어뷰징 방어
|
// - 1주차 정책 설계서 P-02 원장 무결성 · P-06 어뷰징 방어
|
||||||
//
|
//
|
||||||
// 3대 원칙
|
// 3대 원칙
|
||||||
@@ -48,15 +48,15 @@ service cloud.firestore {
|
|||||||
function myUser() {
|
function myUser() {
|
||||||
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
|
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
|
||||||
}
|
}
|
||||||
// 작성자 표시명이 프로필 원본과 같은지 — 운영팀 사칭 차단 (교차검증 F-05)
|
// 작성자 표시명이 프로필 원본과 같은지 — 운영팀 사칭 차단
|
||||||
function authorNameMatchesProfile() {
|
function authorNameMatchesProfile() {
|
||||||
return request.resource.data.authorName == myUser().displayName;
|
return request.resource.data.authorName == myUser().displayName;
|
||||||
}
|
}
|
||||||
// 소속 팬덤이 본인 소속과 같은지 — 타 팬덤 피드 삽입 차단 (교차검증 F-05)
|
// 소속 팬덤이 본인 소속과 같은지 — 타 팬덤 피드 삽입 차단
|
||||||
function fandomIsMine() {
|
function fandomIsMine() {
|
||||||
return request.resource.data.fandomId == myUser().fandomId;
|
return request.resource.data.fandomId == myUser().fandomId;
|
||||||
}
|
}
|
||||||
// 무인증 열람을 허용하는 캠페인 상태 — draft·review 는 제외 (교차검증 F-08)
|
// 무인증 열람을 허용하는 캠페인 상태 — draft·review 는 제외
|
||||||
function campaignIsPublic() {
|
function campaignIsPublic() {
|
||||||
return resource.data.status in ['funding', 'success', 'fail', 'report'];
|
return resource.data.status in ['funding', 'success', 'fail', 'report'];
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ service cloud.firestore {
|
|||||||
match /users/{uid} {
|
match /users/{uid} {
|
||||||
allow get: if isSelf(uid) || isViewer();
|
allow get: if isSelf(uid) || isViewer();
|
||||||
allow list: if isViewer(); // 전체 사용자 목록은 운영자만(S-AD-USER)
|
allow list: if isViewer(); // 전체 사용자 목록은 운영자만(S-AD-USER)
|
||||||
// 교차검증 F-06 — 클라이언트 생성 경로를 닫는다.
|
// 클라이언트 생성 경로를 닫는다.
|
||||||
// 기존에는 클라이언트 생성도 허용해 ① fn-auth-oncreate(§8.5)와 생성 경합이 생기고
|
// 기존에는 클라이언트 생성도 허용해 ① fn-auth-oncreate(§8.5)와 생성 경합이 생기고
|
||||||
// ② 클라이언트가 먼저 만들면 totalContributedPop 등 서버 산출 필드가 빈
|
// ② 클라이언트가 먼저 만들면 totalContributedPop 등 서버 산출 필드가 빈
|
||||||
// 사용자 문서가 남았다(별첨1 조건부적합 제외 ④ 로그인 불가와 닿는 지점).
|
// 사용자 문서가 남았다(별첨1 조건부적합 제외 ④ 로그인 불가와 닿는 지점).
|
||||||
@@ -150,7 +150,7 @@ service cloud.firestore {
|
|||||||
// 보유 배지 이력. 부여는 서버 판정.
|
// 보유 배지 이력. 부여는 서버 판정.
|
||||||
// 화면: S-M-BADGE(본인 도감) · S-M-IDCARD(본인 카드)
|
// 화면: S-M-BADGE(본인 도감) · S-M-IDCARD(본인 카드)
|
||||||
//
|
//
|
||||||
// 교차검증 G-04 — 예전 주석은 「타인 프로필의 배지 노출 허용」이라 적고 signedIn() 으로 열어 뒀는데,
|
// 예전 주석은 「타인 프로필의 배지 노출 허용」이라 적고 signedIn() 으로 열어 뒀는데,
|
||||||
// **1차 범위에 타인 프로필 화면이 없다**(화면 95개 전수 확인). 주석이 사실과 달랐다.
|
// **1차 범위에 타인 프로필 화면이 없다**(화면 95개 전수 확인). 주석이 사실과 달랐다.
|
||||||
// 타인 배지가 실제로 보이는 곳은 S-M-HERO 히어로 랭킹 하나뿐이고,
|
// 타인 배지가 실제로 보이는 곳은 S-M-HERO 히어로 랭킹 하나뿐이고,
|
||||||
// 그것은 /fandoms/{f}/members/{uid}.honorBadgeIds 비정규화 필드로 읽는다(설계서 §2.3·§4.4).
|
// 그것은 /fandoms/{f}/members/{uid}.honorBadgeIds 비정규화 필드로 읽는다(설계서 §2.3·§4.4).
|
||||||
@@ -179,7 +179,7 @@ service cloud.firestore {
|
|||||||
// 본인은 자기 동의 이력을 볼 수 있어야 한다(열람권). 화면: S-A-TERMS · S-M-SETTINGS
|
// 본인은 자기 동의 이력을 볼 수 있어야 한다(열람권). 화면: S-A-TERMS · S-M-SETTINGS
|
||||||
match /users/{uid}/consents/{consentId} {
|
match /users/{uid}/consents/{consentId} {
|
||||||
allow read: if isSelf(uid) || isViewer();
|
allow read: if isSelf(uid) || isViewer();
|
||||||
allow write: if false; // 기록은 fn-consent-record 경유 (교차검증 F-04)
|
allow write: if false; // 기록은 fn-consent-record 경유
|
||||||
}
|
}
|
||||||
|
|
||||||
// 차단 목록. 본인만 읽고 쓴다.
|
// 차단 목록. 본인만 읽고 쓴다.
|
||||||
@@ -315,19 +315,19 @@ service cloud.firestore {
|
|||||||
// 캠페인. 목표 달성률·후원 합계는 집계 필드라 클라이언트가 못 쓴다.
|
// 캠페인. 목표 달성률·후원 합계는 집계 필드라 클라이언트가 못 쓴다.
|
||||||
// 화면: S-C-LIST · S-C-DETAIL · S-C-NEW · S-AD-CAMPAIGN
|
// 화면: S-C-LIST · S-C-DETAIL · S-C-NEW · S-AD-CAMPAIGN
|
||||||
//
|
//
|
||||||
// ⚠️ 목록 쿼리에는 status 필터가 필수다 (교차검증 G-03).
|
// ⚠️ 목록 쿼리에는 status 필터가 필수다.
|
||||||
// 아래 read 규칙이 공개 상태만 허용하므로, status 필터가 없는 목록 쿼리는
|
// 아래 read 규칙이 공개 상태만 허용하므로, status 필터가 없는 목록 쿼리는
|
||||||
// draft·review 문서까지 매칭돼 **쿼리 전체가 거부**된다(Firestore 는 list 규칙을
|
// draft·review 문서까지 매칭돼 **쿼리 전체가 거부**된다(Firestore 는 list 규칙을
|
||||||
// 반환 후보 전체에 적용한다). 인덱스 문제로 오인하기 쉬운 지점이다.
|
// 반환 후보 전체에 적용한다). 인덱스 문제로 오인하기 쉬운 지점이다.
|
||||||
// 허용되는 목록 쿼리 형태는 설계서 §6.1 IDX-15·16·17·19 참조.
|
// 허용되는 목록 쿼리 형태는 설계서 §6.1 IDX-15·16·17·19 참조.
|
||||||
match /campaigns/{campaignId} {
|
match /campaigns/{campaignId} {
|
||||||
// 교차검증 F-08 — 예전에는 read: if true 라 심사 중(review)·초안(draft) 캠페인 본문이
|
// 예전에는 read: if true 라 심사 중(review)·초안(draft) 캠페인 본문이
|
||||||
// URL만 알면 미로그인 상태로 읽혔다. FR-CMP-01「승인 대기 캠페인은 목록에 노출하지 않는다」(E-CMP-03)를
|
// URL만 알면 미로그인 상태로 읽혔다. FR-CMP-01「승인 대기 캠페인은 목록에 노출하지 않는다」(E-CMP-03)를
|
||||||
// 클라이언트 쿼리 필터가 아니라 규칙에서 강제한다. 공개 상태만 무인증 열람 대상이다.
|
// 클라이언트 쿼리 필터가 아니라 규칙에서 강제한다. 공개 상태만 무인증 열람 대상이다.
|
||||||
allow read: if campaignIsPublic()
|
allow read: if campaignIsPublic()
|
||||||
|| isOps()
|
|| isOps()
|
||||||
|| (signedIn() && resource.data.creatorUid == request.auth.uid);
|
|| (signedIn() && resource.data.creatorUid == request.auth.uid);
|
||||||
// 교차검증 F-02 — 화이트리스트 방식. 목록에 없는 필드가 하나라도 있으면 거부되므로
|
// 화이트리스트 방식. 목록에 없는 필드가 하나라도 있으면 거부되므로
|
||||||
// 나중에 집계 필드가 추가돼도 자동으로 막힌다. 집계·분류 필드는 초기값을 못 벗어난다.
|
// 나중에 집계 필드가 추가돼도 자동으로 막힌다. 집계·분류 필드는 초기값을 못 벗어난다.
|
||||||
allow create: if signedIn()
|
allow create: if signedIn()
|
||||||
&& request.resource.data.keys().hasOnly([
|
&& request.resource.data.keys().hasOnly([
|
||||||
@@ -367,7 +367,7 @@ service cloud.firestore {
|
|||||||
// 화면: S-C-RESULT
|
// 화면: S-C-RESULT
|
||||||
match /campaigns/{campaignId}/reports/{reportId} {
|
match /campaigns/{campaignId}/reports/{reportId} {
|
||||||
allow read: if true;
|
allow read: if true;
|
||||||
// 교차검증 F-07 — §7.3 표는 「성공 캠페인 개설자」라고 적었는데 규칙은 creatorUid만 보고
|
// §7.3 표는 「성공 캠페인 개설자」라고 적었는데 규칙은 creatorUid만 보고
|
||||||
// 상태를 확인하지 않아 실패·초안 캠페인에도 결과보고를 올릴 수 있었다.
|
// 상태를 확인하지 않아 실패·초안 캠페인에도 결과보고를 올릴 수 있었다.
|
||||||
// 별첨1 ⑤ 판정 기준 「결과보고 게시 확인」의 대상은 성공한 캠페인이다.
|
// 별첨1 ⑤ 판정 기준 「결과보고 게시 확인」의 대상은 성공한 캠페인이다.
|
||||||
allow create: if signedIn()
|
allow create: if signedIn()
|
||||||
@@ -385,7 +385,7 @@ service cloud.firestore {
|
|||||||
// 화면: S-I-LOUNGE · S-I-POST · S-I-POST-NEW
|
// 화면: S-I-LOUNGE · S-I-POST · S-I-POST-NEW
|
||||||
match /posts/{postId} {
|
match /posts/{postId} {
|
||||||
allow read: if signedIn();
|
allow read: if signedIn();
|
||||||
// 교차검증 F-05 — 예전에는 authorUid·likeCount·visibility 3개만 봐서
|
// 예전에는 authorUid·likeCount·visibility 3개만 봐서
|
||||||
// ① authorName 을 "FANUZU 운영팀"으로 적어 사칭 ② commentCount 위조
|
// ① authorName 을 "FANUZU 운영팀"으로 적어 사칭 ② commentCount 위조
|
||||||
// ③ 소속하지 않은 팬덤 피드에 글 삽입이 가능했다. 화이트리스트 + 원본 대조로 닫는다.
|
// ③ 소속하지 않은 팬덤 피드에 글 삽입이 가능했다. 화이트리스트 + 원본 대조로 닫는다.
|
||||||
allow create: if authorIsSelf()
|
allow create: if authorIsSelf()
|
||||||
@@ -398,7 +398,7 @@ service cloud.firestore {
|
|||||||
&& request.resource.data.likeCount == 0
|
&& request.resource.data.likeCount == 0
|
||||||
&& request.resource.data.commentCount == 0
|
&& request.resource.data.commentCount == 0
|
||||||
&& request.resource.data.visibility == 'visible';
|
&& request.resource.data.visibility == 'visible';
|
||||||
// 교차검증 F-14 — 'title' 을 뺐다. 설계서 §2.6 posts 스키마에 title 필드가 없다.
|
// 'title' 을 뺐다. 설계서 §2.6 posts 스키마에 title 필드가 없다.
|
||||||
allow update: if signedIn() && resource.data.authorUid == request.auth.uid
|
allow update: if signedIn() && resource.data.authorUid == request.auth.uid
|
||||||
&& touchesOnly(['body','imageUrls','updatedAt']);
|
&& touchesOnly(['body','imageUrls','updatedAt']);
|
||||||
// 숨김·복원. rbac-summary 「콘텐츠 숨김·복원」 = 운영 이상
|
// 숨김·복원. rbac-summary 「콘텐츠 숨김·복원」 = 운영 이상
|
||||||
@@ -416,7 +416,7 @@ service cloud.firestore {
|
|||||||
// 댓글. 화면: S-I-POST
|
// 댓글. 화면: S-I-POST
|
||||||
match /posts/{postId}/comments/{commentId} {
|
match /posts/{postId}/comments/{commentId} {
|
||||||
allow read: if signedIn();
|
allow read: if signedIn();
|
||||||
// 교차검증 F-05 — 댓글에도 같은 표시명 사칭 경로가 있었다.
|
// 댓글에도 같은 표시명 사칭 경로가 있었다.
|
||||||
allow create: if authorIsSelf()
|
allow create: if authorIsSelf()
|
||||||
&& request.resource.data.keys().hasOnly([
|
&& request.resource.data.keys().hasOnly([
|
||||||
'authorUid','authorName','body','visibility','createdAt','updatedAt'
|
'authorUid','authorName','body','visibility','createdAt','updatedAt'
|
||||||
@@ -440,7 +440,7 @@ service cloud.firestore {
|
|||||||
// 화면: S-I-QUESTION · S-I-ANSWER-NEW
|
// 화면: S-I-QUESTION · S-I-ANSWER-NEW
|
||||||
match /questions/{questionId}/answers/{answerId} {
|
match /questions/{questionId}/answers/{answerId} {
|
||||||
allow read: if signedIn();
|
allow read: if signedIn();
|
||||||
// 교차검증 F-05 — 답변에도 같은 표시명 사칭 경로가 있었다.
|
// 답변에도 같은 표시명 사칭 경로가 있었다.
|
||||||
allow create: if authorIsSelf()
|
allow create: if authorIsSelf()
|
||||||
&& request.resource.data.keys().hasOnly([
|
&& request.resource.data.keys().hasOnly([
|
||||||
'authorUid','authorName','body','sourceUrls','accepted',
|
'authorUid','authorName','body','sourceUrls','accepted',
|
||||||
@@ -505,7 +505,7 @@ service cloud.firestore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 푸시 발송 이력·도달률. 운영자만 보고, 발송 집계는 Functions(FCM)가 쓴다.
|
// 푸시 발송 이력·도달률. 운영자만 보고, 발송 집계는 Functions(FCM)가 쓴다.
|
||||||
// 화면: S-AD-PUSH · rbac-summary 「푸시 발송 관리」 = 운영 이상 (교차검증 F-04 #2)
|
// 화면: S-AD-PUSH · rbac-summary 「푸시 발송 관리」 = 운영 이상
|
||||||
match /pushCampaigns/{pushId} {
|
match /pushCampaigns/{pushId} {
|
||||||
allow read: if isViewer();
|
allow read: if isViewer();
|
||||||
allow write: if false; // 발송·집계는 fn-push-send 경유
|
allow write: if false; // 발송·집계는 fn-push-send 경유
|
||||||
@@ -579,7 +579,7 @@ service cloud.firestore {
|
|||||||
allow write: if false;
|
allow write: if false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────── 12. 컬렉션 그룹 쿼리 (교차검증 F-01) ───────────────
|
// ─────────────── 12. 컬렉션 그룹 쿼리 ───────────────
|
||||||
//
|
//
|
||||||
// Firestore에서 `match /users/{uid}/popTransactions/{txId}` 형태의 규칙은
|
// Firestore에서 `match /users/{uid}/popTransactions/{txId}` 형태의 규칙은
|
||||||
// **단일 컬렉션 쿼리에만** 적용되고 collectionGroup('popTransactions') 에는 적용되지 않는다.
|
// **단일 컬렉션 쿼리에만** 적용되고 collectionGroup('popTransactions') 에는 적용되지 않는다.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { ERR, fail } from '../core/errors.js';
|
|||||||
/**
|
/**
|
||||||
* `fn-auth-oncreate` — 계정 문서의 유일한 생성 주체.
|
* `fn-auth-oncreate` — 계정 문서의 유일한 생성 주체.
|
||||||
*
|
*
|
||||||
* 보안 규칙이 `users` 의 클라이언트 `create` 를 닫아 두었으므로(교차검증 F-06)
|
* 보안 규칙이 `users` 의 클라이언트 `create` 를 닫아 두었으므로
|
||||||
* 이 트리거가 만들지 않으면 계정 문서가 존재하지 않는다. 생성 주체를 하나로
|
* 이 트리거가 만들지 않으면 계정 문서가 존재하지 않는다. 생성 주체를 하나로
|
||||||
* 고정한 이유는 경합 때문이다 — 클라이언트가 먼저 만들면 서버 산출 필드가 빈
|
* 고정한 이유는 경합 때문이다 — 클라이언트가 먼저 만들면 서버 산출 필드가 빈
|
||||||
* 사용자 문서가 남는다.
|
* 사용자 문서가 남는다.
|
||||||
@@ -49,7 +49,7 @@ export const fnAuthOnCreate = functionsV1.auth.user().onCreate(async (user) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `fn-consent-record` — 약관 동의 이력(교차검증 F-04).
|
* `fn-consent-record` — 약관 동의 이력.
|
||||||
*
|
*
|
||||||
* append-only 다. 철회도 기존 행을 고치지 않고 `granted: false` 인 새 행으로 남긴다.
|
* append-only 다. 철회도 기존 행을 고치지 않고 `granted: false` 인 새 행으로 남긴다.
|
||||||
* 동의 시점을 되짚을 수 없으면 분쟁에서 증명할 수단이 사라진다.
|
* 동의 시점을 되짚을 수 없으면 분쟁에서 증명할 수단이 사라진다.
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<!--
|
||||||
|
TestFlight · App Store 업로드용 내보내기 설정.
|
||||||
|
|
||||||
|
`xcodebuild -exportArchive` 가 이 파일을 읽는다. 없으면 대화형으로 물어보므로
|
||||||
|
자동 배포가 불가능하다 — 계정이 열린 뒤에 만들면 그날 하루를 여기서 쓴다.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>app-store-connect</string>
|
||||||
|
|
||||||
|
<!-- Fancake Inc. -->
|
||||||
|
<key>teamID</key>
|
||||||
|
<string>V28G543VK5</string>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Xcode 가 프로비저닝 프로파일을 알아서 만들고 갱신한다. 수동으로 두면
|
||||||
|
인증서가 만료될 때마다 배포가 멈춘다.
|
||||||
|
-->
|
||||||
|
<key>signingStyle</key>
|
||||||
|
<string>automatic</string>
|
||||||
|
|
||||||
|
<!-- 심벌을 함께 올려야 크래시 로그가 사람이 읽을 수 있는 형태로 온다. -->
|
||||||
|
<key>uploadSymbols</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<!-- 비트코드는 Xcode 14 부터 폐지되었다. -->
|
||||||
|
<key>uploadBitcode</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
앱 씬 크기 최적화. 기기별로 필요한 자산만 내려가 설치 용량이 줄어든다.
|
||||||
|
-->
|
||||||
|
<key>thinning</key>
|
||||||
|
<string><thin-for-all-variants></string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|
||||||
|
// Google 로그인 콜백 스킴. tool/ios/inject_url_scheme.py 가 만든다.
|
||||||
|
// `#include?` 는 파일이 없어도 빌드를 멈추지 않는다 — 그때는 Google 로그인만 동작하지 않는다.
|
||||||
|
#include? "../config/Google.xcconfig"
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||||
#include "Generated.xcconfig"
|
#include "Generated.xcconfig"
|
||||||
|
|
||||||
|
// Google 로그인 콜백 스킴. tool/ios/inject_url_scheme.py 가 만든다.
|
||||||
|
// `#include?` 는 파일이 없어도 빌드를 멈추지 않는다 — 그때는 Google 로그인만 동작하지 않는다.
|
||||||
|
#include? "../config/Google.xcconfig"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
87BFB00ABC23EA249E841B59 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CBE157220A7C382F845775F0 /* Pods_Runner.framework */; };
|
87BFB00ABC23EA249E841B59 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CBE157220A7C382F845775F0 /* Pods_Runner.framework */; };
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
|
4F2B0E1A2C3D4E5F60718294 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4F2B0E1A2C3D4E5F60718293 /* GoogleService-Info.plist */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
EC46D318C684524E6F9C925F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB2C33122C7A95220B242430 /* Pods_RunnerTests.framework */; };
|
EC46D318C684524E6F9C925F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB2C33122C7A95220B242430 /* Pods_RunnerTests.framework */; };
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
4F2B0E1A2C3D4E5F60718293 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
A68C3B80D45981965B89CA40 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
A68C3B80D45981965B89CA40 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
AB2C33122C7A95220B242430 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
AB2C33122C7A95220B242430 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -149,6 +151,7 @@
|
|||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
97C147021CF9000F007C117D /* Info.plist */,
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
4F2B0E1A2C3D4E5F60718293 /* GoogleService-Info.plist */,
|
||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
@@ -275,6 +278,7 @@
|
|||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
|
4F2B0E1A2C3D4E5F60718294 /* GoogleService-Info.plist in Resources */,
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -467,7 +471,8 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = P36BV95LP5;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
DEVELOPMENT_TEAM = V28G543VK5;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -650,7 +655,8 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = P36BV95LP5;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
DEVELOPMENT_TEAM = V28G543VK5;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -673,7 +679,8 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
DEVELOPMENT_TEAM = P36BV95LP5;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
|
DEVELOPMENT_TEAM = V28G543VK5;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
@@ -66,5 +66,29 @@
|
|||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
|
<!--
|
||||||
|
암호화 사용 신고. HTTPS 만 쓰므로 면제 대상이다.
|
||||||
|
이 키가 없으면 업로드할 때마다 App Store Connect 가 수동 응답을 요구한다.
|
||||||
|
-->
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Google 로그인 콜백 URL 스킴. 값은 GoogleService-Info.plist 의 REVERSED_CLIENT_ID 이며
|
||||||
|
tool/ios/inject_url_scheme.py 가 ios/config/Google.xcconfig 로 뽑아 둔다.
|
||||||
|
-->
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>google-sign-in</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>$(GOOGLE_REVERSED_CLIENT_ID)</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!--
|
||||||
|
Sign in with Apple. 소셜 로그인을 제공하면 Apple 심사 필수 조건이다.
|
||||||
|
이 Capability 가 없으면 Apple 로그인 코드가 있어도 동작하지 않는다.
|
||||||
|
-->
|
||||||
|
<key>com.apple.developer.applesignin</key>
|
||||||
|
<array>
|
||||||
|
<string>Default</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
푸시 환경 선언. Firebase Auth 가 APNs 등록 API 를 링크하므로 선언해 둔다.
|
||||||
|
App Store 내보내기 때 Xcode 가 production 으로 바꿔 서명한다.
|
||||||
|
-->
|
||||||
|
<key>aps-environment</key>
|
||||||
|
<string>development</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -37,10 +37,9 @@ class AppTab {
|
|||||||
|
|
||||||
/// 계약 고정 5탭.
|
/// 계약 고정 5탭.
|
||||||
///
|
///
|
||||||
/// 근거 3개가 서로 일치한다:
|
/// 근거 2개가 서로 일치한다:
|
||||||
/// - 견적서 v2.1 「5탭 네비게이션」
|
/// - 견적서 v2.1 「5탭 네비게이션」
|
||||||
/// - `design/craft-allowlist.yaml` → `APP_TABS: [HOME, INSIDE, GALAXY, STATION, MY]`
|
/// - `design/craft-allowlist.yaml` → `APP_TABS: [HOME, INSIDE, GALAXY, STATION, MY]`
|
||||||
/// - 시안 `시안/screens/S-*.html` 의 `<nav class="tabbar">` (5개 루트 화면 전부 동일)
|
|
||||||
///
|
///
|
||||||
/// 라벨 문자열과 순서는 스모크 테스트가 정확 일치로 검증한다.
|
/// 라벨 문자열과 순서는 스모크 테스트가 정확 일치로 검증한다.
|
||||||
/// 아이콘은 Figma Iconset(house · message-square · badge · circle-user-round)이다.
|
/// 아이콘은 Figma Iconset(house · message-square · badge · circle-user-round)이다.
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ final class AuthCancelled extends AuthOutcome {
|
|||||||
const AuthCancelled();
|
const AuthCancelled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 실패. [code] 는 `wireframe/planning/error-catalog.yaml` 의 ID 다(소셜 실패 = E-AUTH-03).
|
/// 실패. [code] 는 에러 카탈로그 ID 다(소셜 실패 = E-AUTH-03).
|
||||||
final class AuthFailed extends AuthOutcome {
|
final class AuthFailed extends AuthOutcome {
|
||||||
const AuthFailed(this.code, this.message);
|
const AuthFailed(this.code, this.message);
|
||||||
|
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ const lineProvider = KeylessSocialProvider(AuthProviderKind.line, [
|
|||||||
'LINE Developers 콘솔 채널 등록(사업자 정보 필요)',
|
'LINE Developers 콘솔 채널 등록(사업자 정보 필요)',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/// SDK 코드를 에러 카탈로그(`wireframe/planning/error-catalog.yaml`) ID 로 옮긴다.
|
/// SDK 코드를 에러 카탈로그 ID 로 옮긴다.
|
||||||
///
|
///
|
||||||
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
|
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
|
||||||
/// 오류」를 갈라 주면 어느 이메일이 가입돼 있는지 훑을 수 있다.
|
/// 오류」를 갈라 주면 어느 이메일이 가입돼 있는지 훑을 수 있다.
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: fancake
|
name: fancake
|
||||||
description: "FANUZU — 팬덤 플랫폼 앱 (골격). 발주사 (주)팬케이크 / 수행사 스위치 주식회사"
|
description: "FANUZU — 팬덤 플랫폼 앱 (골격). 발주사 (주)팬케이크 / 수행사 스위치 주식회사"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 0.1.0+8
|
version: 0.1.0+9
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.5
|
sdk: ^3.11.5
|
||||||
|
|||||||
@@ -314,10 +314,8 @@ Future<void> _enterShell(WidgetTester tester) async {
|
|||||||
/// `design/craft-allowlist.yaml` 의 `APP_TABS` 를 읽어 온다.
|
/// `design/craft-allowlist.yaml` 의 `APP_TABS` 를 읽어 온다.
|
||||||
/// 앱 코드가 계약 표기에서 벗어나면 여기서 깨진다.
|
/// 앱 코드가 계약 표기에서 벗어나면 여기서 깨진다.
|
||||||
///
|
///
|
||||||
/// 계약 정본은 상위 저장소(p-fancake)에 있고 이 저장소(p-fanuzu)는 분리돼 있어
|
/// 계약 파일은 저장소 루트 `design/` 에 있고, `app/design/` 에 같은 사본을 둔다.
|
||||||
/// 상대 경로가 체크아웃 위치를 탄다. 그래서 저장소 안에 사본을 두고
|
/// **FANUZU_DESIGN_DIR → app/design/ → 저장소 루트 design/** 순으로 본다.
|
||||||
/// **FANUZU_DESIGN_DIR → 저장소 내 design/ → 형제 경로** 순으로 본다.
|
|
||||||
/// 사본이 있으므로 git worktree 에서도 환경 변수 없이 통과한다.
|
|
||||||
///
|
///
|
||||||
/// 하나도 없으면 **통과가 아니라 실패**로 끝낸다 — 못 읽은 것을 「맞다」로 세면
|
/// 하나도 없으면 **통과가 아니라 실패**로 끝낸다 — 못 읽은 것을 「맞다」로 세면
|
||||||
/// 계약 대조가 조용히 사라진다.
|
/// 계약 대조가 조용히 사라진다.
|
||||||
@@ -327,7 +325,6 @@ List<String> _contractTabs() {
|
|||||||
'$dir/craft-allowlist.yaml',
|
'$dir/craft-allowlist.yaml',
|
||||||
'design/craft-allowlist.yaml',
|
'design/craft-allowlist.yaml',
|
||||||
'../design/craft-allowlist.yaml',
|
'../design/craft-allowlist.yaml',
|
||||||
'../p-fancake/design/craft-allowlist.yaml',
|
|
||||||
];
|
];
|
||||||
final file = candidates
|
final file = candidates
|
||||||
.map(File.new)
|
.map(File.new)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// iOS 스토어 제출 구성 검사.
|
||||||
|
///
|
||||||
|
/// 여기 있는 것들은 사라져도 앱이 멀쩡히 빌드된다. 문제는 업로드·심사·로그인
|
||||||
|
/// 시점에야 드러나고, 그때는 원인이 코드처럼 보인다. 파일을 문자열로 읽어
|
||||||
|
/// 키가 있는지만 본다 — 값의 해석은 빌드가 한다.
|
||||||
|
|
||||||
|
const _teamId = 'V28G543VK5'; // Fancake Inc.
|
||||||
|
|
||||||
|
String _read(String path) {
|
||||||
|
final file = File(path);
|
||||||
|
expect(file.existsSync(), isTrue, reason: '$path 가 없습니다');
|
||||||
|
return file.readAsStringSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('iOS — 서명', () {
|
||||||
|
test('Xcode 프로젝트와 내보내기 설정이 발주사 팀으로 서명한다', () {
|
||||||
|
final pbx = _read('ios/Runner.xcodeproj/project.pbxproj');
|
||||||
|
final teams = RegExp(r'DEVELOPMENT_TEAM = (\w+);')
|
||||||
|
.allMatches(pbx)
|
||||||
|
.map((m) => m.group(1))
|
||||||
|
.toSet();
|
||||||
|
expect(teams, {_teamId});
|
||||||
|
expect(_read('ios/ExportOptions.plist'), contains(_teamId));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Sign in with Apple Capability 가 서명에 연결되어 있다', () {
|
||||||
|
expect(
|
||||||
|
_read('ios/Runner/Runner.entitlements'),
|
||||||
|
contains('com.apple.developer.applesignin'),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
'CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;'
|
||||||
|
.allMatches(_read('ios/Runner.xcodeproj/project.pbxproj'))
|
||||||
|
.length,
|
||||||
|
3, // Debug · Release · Profile
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('iOS — 제출 선언과 Firebase 설정', () {
|
||||||
|
late final String plist = _read('ios/Runner/Info.plist');
|
||||||
|
|
||||||
|
test('암호화 사용 신고가 있어 업로드가 멈추지 않는다', () {
|
||||||
|
expect(plist, contains('ITSAppUsesNonExemptEncryption'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Google 로그인 콜백 스킴이 설정 파일 값으로 채워진다', () {
|
||||||
|
expect(plist, contains(r'$(GOOGLE_REVERSED_CLIENT_ID)'));
|
||||||
|
for (final c in const ['Debug', 'Release']) {
|
||||||
|
expect(
|
||||||
|
_read('ios/Flutter/$c.xcconfig'),
|
||||||
|
contains('#include? "../config/Google.xcconfig"'),
|
||||||
|
reason: c,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GoogleService-Info.plist 가 앱 번들 리소스로 들어간다', () {
|
||||||
|
// 번들에 없으면 Firebase 가 목 모드로 뜨고 Google 로그인 client ID 도 못 읽는다.
|
||||||
|
expect(
|
||||||
|
_read('ios/Runner.xcodeproj/project.pbxproj'),
|
||||||
|
contains('/* GoogleService-Info.plist in Resources */,'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('설정·서명 자산을 git 이 추적하지 않는다', () {
|
||||||
|
final result = Process.runSync('git', [
|
||||||
|
'ls-files',
|
||||||
|
'--',
|
||||||
|
'ios/Runner/GoogleService-Info.plist',
|
||||||
|
'ios/config/Google.xcconfig',
|
||||||
|
'android/app/google-services.json',
|
||||||
|
'*.p12',
|
||||||
|
'*.p8',
|
||||||
|
'*.mobileprovision',
|
||||||
|
]);
|
||||||
|
expect(result.exitCode, 0, reason: 'git 을 실행하지 못했습니다');
|
||||||
|
expect((result.stdout as String).trim(), isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""design/design-tokens.md 의 머신블록(<!-- TOKENS --> 아래 yaml) → lib/core/theme/tokens.dart 생성기.
|
"""design/design-tokens.md 의 머신블록(<!-- TOKENS --> 아래 yaml) → lib/core/theme/tokens.dart 생성기.
|
||||||
|
|
||||||
- SoT 는 design/design-tokens.md 하나다. tokens.dart 를 손으로 고치면 시안(시안/assets/ds.css)과 어긋난다.
|
- SoT 는 design/design-tokens.md 하나다. tokens.dart 를 손으로 고치면 토큰 문서와 어긋난다.
|
||||||
- 재실행 가능(멱등). 실행: python3 tool/gen_tokens.py (앱 루트 app/ 에서)
|
- 재실행 가능(멱등). 실행: python3 tool/gen_tokens.py (앱 루트 app/ 에서)
|
||||||
- 부수 기능: design-tokens.md 의 contrast_checks 쌍을 WCAG 로 재계산해서 표로 찍고,
|
- 부수 기능: design-tokens.md 의 contrast_checks 쌍을 WCAG 로 재계산해서 표로 찍고,
|
||||||
AA(4.5:1) 미달이 하나라도 있으면 exit 1. 테마가 대비를 깎았는지 여기서 걸린다.
|
AA(4.5:1) 미달이 하나라도 있으면 exit 1. 테마가 대비를 깎았는지 여기서 걸린다.
|
||||||
|
|||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""GoogleService-Info.plist 의 REVERSED_CLIENT_ID 를 Xcode 빌드 설정에 넣는다.
|
||||||
|
|
||||||
|
**왜 스크립트인가.** Google 로그인 콜백 스킴은 Firebase 프로젝트마다 다르고,
|
||||||
|
Info.plist 에 손으로 옮기면 오타 한 글자에 로그인이 **조용히** 실패한다.
|
||||||
|
오류도 뜨지 않고 콜백만 돌아오지 않아 원인을 찾는 데 한나절이 든다.
|
||||||
|
|
||||||
|
설정 파일이 투입될 때 한 번 돌리면 `ios/config/Google.xcconfig` 가 만들어지고,
|
||||||
|
Info.plist 의 `$(GOOGLE_REVERSED_CLIENT_ID)` 가 그 값으로 치환된다.
|
||||||
|
|
||||||
|
실행: python3 tool/ios/inject_url_scheme.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import plistlib
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SOURCE = ROOT / "ios/Runner/GoogleService-Info.plist"
|
||||||
|
OUT = ROOT / "ios/config/Google.xcconfig"
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
if not SOURCE.exists():
|
||||||
|
print(
|
||||||
|
f"설정 파일이 아직 없습니다: {SOURCE.relative_to(ROOT)}\n"
|
||||||
|
"Firebase 콘솔에서 iOS 앱 설정 파일을 받아 그 자리에 두십시오.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
with SOURCE.open("rb") as f:
|
||||||
|
data = plistlib.load(f)
|
||||||
|
|
||||||
|
reversed_id = data.get("REVERSED_CLIENT_ID")
|
||||||
|
if not reversed_id:
|
||||||
|
# Firebase 콘솔에서 Google 로그인을 켜지 않으면 이 키가 빠진 채로 나온다.
|
||||||
|
print(
|
||||||
|
"REVERSED_CLIENT_ID 가 없습니다. Firebase 콘솔에서 "
|
||||||
|
"Authentication → Google 제공자를 켠 뒤 설정 파일을 다시 받으십시오.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUT.write_text(
|
||||||
|
"// 자동 생성 — tool/ios/inject_url_scheme.py\n"
|
||||||
|
"// GoogleService-Info.plist 가 바뀌면 다시 돌리십시오.\n"
|
||||||
|
f"GOOGLE_REVERSED_CLIENT_ID = {reversed_id}\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
print(f"기록: {OUT.relative_to(ROOT)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
# craft-allowlist.yaml — 크래프트 체커의 "분류 결정"을 적는 곳.
|
# craft-allowlist.yaml — 크래프트 체커의 "분류 결정"을 적는 곳.
|
||||||
# 경고를 끄는 곳이 아니다. 각 항목에 근거(reason)가 없으면 킷이 읽지 않는다.
|
# 경고를 끄는 곳이 아니다. 각 항목에 근거(reason)가 없으면 킷이 읽지 않는다.
|
||||||
# 계약: 하네스/디자인/resources/codex/check_craft_render.py C2
|
|
||||||
|
|
||||||
CAPS_OK:
|
CAPS_OK:
|
||||||
# ── 5탭 라벨 — 견적서 v2.1 「5탭 네비게이션」으로 고정된 화면 이름이다.
|
# ── 5탭 라벨 — 견적서 v2.1 「5탭 네비게이션」으로 고정된 화면 이름이다.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
## 로고 결정
|
## 로고 결정
|
||||||
- 발주사 공식 자산 `고객사 준비 데모/LOGO/fanuzu logo only.png`를 수령했고, PNG의 알파 채널도 확인했다.
|
- 발주사 공식 자산 `고객사 준비 데모/LOGO/fanuzu logo only.png`를 수령했고, PNG의 알파 채널도 확인했다.
|
||||||
- 시안은 이 공식 심볼을 `시안/assets/logo.png`에 반영했다. 공식 앱 아이콘 원천도 같은 심볼이며
|
- 공식 앱 아이콘 원천도 같은 심볼이며
|
||||||
`app/assets/brand/app-icon.png`와 `app/assets/brand/app-icon-foreground.png`에 반영돼 있다.
|
`app/assets/brand/app-icon.png`와 `app/assets/brand/app-icon-foreground.png`에 반영돼 있다.
|
||||||
- 따라서 현재 로고는 AI 생성 제안본이나 임시 Fancake 브랜드가 아니다. 별도 제안 로고 승인 대기도 없다.
|
- 따라서 현재 로고는 AI 생성 제안본이나 임시 Fancake 브랜드가 아니다. 별도 제안 로고 승인 대기도 없다.
|
||||||
- **서비스명은 `FANUZU`, 국문 병행 표기는 `팬우주`로 확정됐다.** 발주사 공식 검토 회신서의
|
- **서비스명은 `FANUZU`, 국문 병행 표기는 `팬우주`로 확정됐다.** 발주사 공식 검토 회신서의
|
||||||
@@ -173,7 +173,7 @@ font:
|
|||||||
font-direction: "geometric"
|
font-direction: "geometric"
|
||||||
font-source: "42dot Sans = Google Fonts OFL 가변 ttf · ABeeZee = Google Fonts OFL (app/assets/fonts/, 2026-09-02)"
|
font-source: "42dot Sans = Google Fonts OFL 가변 ttf · ABeeZee = Google Fonts OFL (app/assets/fonts/, 2026-09-02)"
|
||||||
type:
|
type:
|
||||||
# --- 발주사 Figma 계약 시점 사본 실측 (2026-09-26, _작업/design-match/foundation.json · 앱 93화면 텍스트 1,868건 집계) ---
|
# --- 발주사 Figma 계약 시점 사본 실측 (2026-09-26, 앱 93화면 텍스트 1,868건 집계) ---
|
||||||
# 42dot Sans 가 81%, ABeeZee 가 14%(Regular 한 벌뿐이라 굵기는 400). 본문 자간은 -2.5% 가 표준이다.
|
# 42dot Sans 가 81%, ABeeZee 가 14%(Regular 한 벌뿐이라 굵기는 400). 본문 자간은 -2.5% 가 표준이다.
|
||||||
# 종전 값(Gmarket/포카 온 스테이지 재설계 — 44·32 Bold, 본문 400·행간 1.6)은 Figma 와 맞지 않아 교체한다.
|
# 종전 값(Gmarket/포카 온 스테이지 재설계 — 44·32 Bold, 본문 400·행간 1.6)은 Figma 와 맞지 않아 교체한다.
|
||||||
type-display-l: {family: font-display, size: 46, weight: 400, leading: 1.0}
|
type-display-l: {family: font-display, size: 46, weight: 400, leading: 1.0}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user