4 Commits
Author SHA1 Message Date
Sn-Kinos f483e8d94b 1차 제출 빌드 버전 0.1.0+9 (iOS TestFlight) 2026-10-02 22:01:35 +09:00
Sn-Kinos 79d4682e65 iOS 스토어 제출 구성 · Apple 로그인 · 푸시 · Firebase 설정 연결 2026-10-02 22:01:31 +09:00
Sn-Kinos 9dd43ca405 빌드 안내 · 주석을 이 저장소 기준으로 정리
- 빌드 안내(BUILD.md)의 JDK 경로를 설치 위치와 무관한 지정 방식으로 변경
- 5탭 계약 파일 탐색 순서를 이 저장소 구조(app/design → 루트 design)로 정리
- README · 보안 규칙 · 소스 주석에서 이 저장소에 없는 문서 경로를 문서 이름으로 바꾸거나 삭제
- Flutter 버전 고정 파일(app/.fvmrc, 3.47.2) 추가, README 필수 환경을 같은 버전으로 갱신
- 동작 변경 없음
2026-10-01 01:05:35 +09:00
Sn-Kinos 4c3bc0304f 정책 설계서 · 요구사항 정의서 · 기능 명세서 · Firestore 설계서 v1.3 · QA 체크리스트 v1.2
- 9/22 회의 합의와 9/21 회신서(구현 확정사항)를 반영해 개정
- 문장마다 정책 원장 근거 번호(C-44 ~ C-82)를 표기하고, 자료 사이에 해석이 갈리는 5건(S10 ~ S14)은 확인 전 유지 기준과 함께 별도 표기

정책 설계서 v1.3
- 출석 1일 1회 10 일반 POP, 발행 원천별 POP 유형, 보상 경로별 원장 분리
- Push 우선순위, 대표 팬덤 상태 · 변경 · 탈퇴 후 재가입 규칙, 로그인 제공자 계정 준비 현황

요구사항 정의서 v1.3
- Active Quest · Coach Mark · 일일 기여 요약 · Eclipse Auto Defense 반영, Sound Identity 보류 명시
- 공통 메시지 문법, 서버 확정 뒤 연출, 햅틱 근거(계약서 업무내역서) 추가
- 사전예약 조회를 전용 DB Role · 읽기 전용 View 방식으로 확정

기능 명세서 v1.3
- 명세 본문 변경 없음. 정책 우선 대조표 13건 추가(총 39건), 해당 화면 명세에 정책 우선 주석
- 상태 표기 규약에 M2 공식 판정 · 배포 후 상태 갱신 기준 추가

Firestore 설계서 v1.3
- 발행 원천별 POP 유형 대조표, 팬덤별 Passport 표, 팬덤 멤버 상태 필드 신설
- 출석 보상 운영값, Hero Gift 구매 · 사용 · 환불 정합성, 발행량 · 소모량 대조

QA 체크리스트 v1.2
- 시험 사례 15건 신설(출석 · POP 유형 · Hero Gift · 대표 팬덤 · Passport · 공통 메시지 · Push 등)
- 무료 / 유료 POP 판정 매핑을 일반 POP · 상품 혜택 기여 POP 기준으로 변경
- 로그인 제공자(Apple · Kakao · LINE) 시험 전제를 현재 계정 준비 상태로 갱신
2026-09-30 10:06:34 +09:00
29 changed files with 331 additions and 85 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"flutter": "3.47.2"
}
+2
View File
@@ -55,6 +55,8 @@ key.properties
*.mobileprovision
**/GoogleService-Info.plist
**/google-services.json
# GoogleService-Info.plist 에서 생성한다 — tool/ios/inject_url_scheme.py
ios/config/Google.xcconfig
# FVM Version Cache
.fvm/
+37 -24
View File
@@ -6,7 +6,7 @@ Gradle 이 조용히 실패하지 않고 명확히 죽으므로 아래를 그대
| 구성 | 값 | 비고 |
|---|---|---|
| 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` |
| AGP | 8.11.1 | `android/settings.gradle.kts` |
| 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 을 쓰는 쪽을 택했다.
```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
tar.gz 를 `~/.jdks/` 에 풀면 된다.
tar.gz 를 사용자 폴더에 풀고 그 경로를 지정하면 된다.
## 테스트
```bash
export ANDROID_HOME=/opt/homebrew/share/android-commandlinetools
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` 와 정확 일치를 검증한다.
**정본은 상위 저장소 `p-fancake/design/craft-allowlist.yaml`** 이고 이 저장소는 분리돼
있으므로, 같은 파일의 사본을 `design/craft-allowlist.yaml` 로 두었다. 탐색 순서는 아래와 같다.
스모크 테스트가 `craft-allowlist.yaml` 의 `APP_TABS` 를 직접 읽어 앱의 `appTabs` 와 정확 일치를
검증한다(5탭 계약 대조). 파일은 아래 순서로 찾는다.
1. `$FANUZU_DESIGN_DIR/craft-allowlist.yaml` — 정본을 직접 가리킬 때 쓴다.
2. `design/craft-allowlist.yaml` — 이 저장소의 사본.
3. `../design/craft-allowlist.yaml` · `../p-fancake/design/craft-allowlist.yaml` — p-fancake 와
형제로 둔 체크아웃.
1. `$FANUZU_DESIGN_DIR/craft-allowlist.yaml` — 다른 위치의 파일을 가리킬 때 쓴다.
2. `app/design/craft-allowlist.yaml` — 앱 폴더 안의 사본.
3. 저장소 루트 `design/craft-allowlist.yaml`.
사본이 있으므로 **git worktree 에서도 변수 없이 통과한다.** 그래도 못 찾으면 통과가 아니라
실패한다 — 계약 대조가 조용히 사라지면 안 된다.
사본은 정본과 갈리면 저장소마다 다른 답을 내므로, **정본을 고칠 때 사본에도 함께 복사한다.**
파일 머리에 같은 문장을 적어 두었다.
```bash
cp /path/to/p-fancake/design/craft-allowlist.yaml design/craft-allowlist.yaml
# 머리 주석 5줄은 사본에만 있으니 지우지 말고 APP_TABS·CAPS_OK 만 맞춘다.
```
하나도 못 찾으면 통과가 아니라 실패한다 — 계약 대조가 조용히 사라지면 안 된다.
루트 파일을 고칠 때는 `app/design/` 사본도 함께 맞춘다.
## 빌드
@@ -111,8 +100,32 @@ export DEVELOPER_DIR=~/Downloads/Xcode-beta.app/Contents/Developer
fvm flutter build ios --release --no-codesign
```
**서명은 Apple Developer 계정이 있어야 한다.** 계정 없이 여기까지는 확인되므로,
TestFlight 제출에 남은 것은 **서명과 업로드뿐**이다.
### 제출 빌드 — App Store Connect
서명 팀은 **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 이중 상태
+8 -18
View File
@@ -16,7 +16,7 @@
## 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
cd app
@@ -30,7 +30,7 @@ flutter build apk --debug # Android 디버그 APK
빌드 산출물은 `build/app/outputs/flutter-apk/app-debug.apk` 에 떨어진다.
> ⚠️ **`pubspec.yaml` 의 의존성을 추가·변경·업그레이드하면 최소 지원 OS 하한을 재계산할 것.**
> 절차: `주간보고/2주차/src/2주차_플러그인_선검증리포트.md` §3.5
> 절차: 2주차 플러그인 선검증 리포트 §3.5
>
> 최소 지원 OS 는 **별첨1 v1.0 에 계약값으로 박힌다**(견적서 1228행). 지킬 수 없는 하한을
> 계약에 적으면 그 아래 단말의 장애가 개발사 책임이 된다. 선검증 리포트의 값은
@@ -42,7 +42,7 @@ flutter build apk --debug # Android 디버그 APK
## 3. 디자인 토큰 — 손으로 옮기지 않는다
`lib/core/theme/tokens.dart` 는 **생성물**이다. 직접 고치면 시안(`시안/assets/ds.css`)과 어긋난다.
`lib/core/theme/tokens.dart` 는 **생성물**이다. 직접 고치면 토큰 문서(`design/design-tokens.md`)와 어긋난다.
```bash
cd app
@@ -79,7 +79,7 @@ Firebase 설정이 없으면 실 소셜 로그인이 물리적으로 불가능
* `FirebaseBoot.reason` 은 사용자에게 보이는 문자열이라 **내부 파일 경로를 넣지 않는다.**
경로와 원시 예외는 `FirebaseBoot.detail` 로 분리해 로그에만 남긴다.
**가드가 두 겹인 이유 (교차검증 A-01 반영)**
**가드가 두 겹인 이유**
```dart
bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;
@@ -103,7 +103,7 @@ bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;
|---|---|---|---|
| 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` | 목 모드로 기동. 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` 플러그인
> 추가가 함께 필요하다. 지금은 설정 파일이 없어 플러그인을 적용하지 않았다 —
@@ -191,7 +191,7 @@ Firebase가 live가 되면 보존된 `currentUser`를 앱 시작 시 복원하
**이메일 가입은 구현하지 않았다.** CD-18 미확정(Q2)이라 `SocialAuthProvider` 인터페이스에
구현체 하나가 더 붙을 자리만 비워 뒀다. **기능 플래그 상수는 두지 않았다** — 읽는 분기가 없는
플래그는 "false 라서 닫혀 있다" 는 착각만 만들고, 누가 true 로 바꿔도 아무 일이 일어나지 않는다
(교차검증 A-02). 지금 이메일을 닫고 있는 것은 `AuthRepository._defaultProviders()` 에
. 지금 이메일을 닫고 있는 것은 `AuthRepository._defaultProviders()` 에
구현체가 없다는 사실 하나다.
## 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` |
| 시안 하단 탭바 | `시안/screens/S-{H-HOME,I-LOUNGE,G-PLANET,S-HUB,M-HOME}.html` 의 `<nav class="tabbar">` |
**한글화·치환·순서 변경 금지.** 스모크 테스트가 `design/craft-allowlist.yaml` 을 직접 읽어
문자열과 순서를 정확 일치로 검증한다 — 앱 코드만 고치면 테스트가 깨진다.
@@ -288,16 +287,7 @@ $ python3 tool/gen_tokens.py
`contrast_checks` 25쌍에는 앱 테마가 조립하며 새로 만드는 3쌍(선택 탭 라벨 9.21: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)
+3
View File
@@ -2,3 +2,6 @@ analyzer:
exclude:
# Firebase 도구가 내려받는 서드파티 소스. 우리 분석 대상이 아니다.
- '**/node_modules/**'
- build/**
- android/**
- ios/**
+2 -6
View File
@@ -1,12 +1,8 @@
# ⚠️ 이 파일은 사본이다. 정본은 상위 저장소 p-fancake/design/craft-allowlist.yaml 이며,
# 정본을 갱신할 때 이 사본에도 함께 복사한다. 두 파일이 갈리면 계약 대조(APP_TABS)가
# 저장소마다 다른 답을 내므로, 사본만 고치는 일은 없어야 한다.
# 사본을 둔 이유는 git worktree 에서 형제 경로(../design/)가 없어 스모크 테스트가
# 계약 대조를 못 하고 실패하기 때문이다(BUILD.md 「테스트」).
# ⚠️ 이 파일은 저장소 루트 design/craft-allowlist.yaml 의 사본이다.
# 루트 파일을 고칠 때 이 사본도 함께 맞춘다(BUILD.md 「테스트」).
# craft-allowlist.yaml — 크래프트 체커의 "분류 결정"을 적는 곳.
# 경고를 끄는 곳이 아니다. 각 항목에 근거(reason)가 없으면 킷이 읽지 않는다.
# 계약: 하네스/디자인/resources/codex/check_craft_render.py C2
CAPS_OK:
# ── 5탭 라벨 — 견적서 v2.1 「5탭 네비게이션」으로 고정된 화면 이름이다.
+18 -18
View File
@@ -3,8 +3,8 @@ rules_version = '2';
// FANUZU — Firestore 보안 규칙 (2주차 설계본 v0.1)
//
// 근거
// - wireframe/handoff/rbac-summary.md 역할 3종 · 운영자 등급 3단계(D-18)
// - Docs/별첨1_기능별_완료기준표.html 「전 기능 공통 완료 기준 — 서버검증 원칙」
// - 요구사항 정의서 권한 요구 역할 3종 · 운영자 등급 3단계
// - 별첨 1 기능별 완료 기준표 「전 기능 공통 완료 기준 — 서버검증 원칙」
// - 1주차 정책 설계서 P-02 원장 무결성 · P-06 어뷰징 방어
//
// 3대 원칙
@@ -48,15 +48,15 @@ service cloud.firestore {
function myUser() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
}
// 작성자 표시명이 프로필 원본과 같은지 — 운영팀 사칭 차단 (교차검증 F-05)
// 작성자 표시명이 프로필 원본과 같은지 — 운영팀 사칭 차단
function authorNameMatchesProfile() {
return request.resource.data.authorName == myUser().displayName;
}
// 소속 팬덤이 본인 소속과 같은지 — 타 팬덤 피드 삽입 차단 (교차검증 F-05)
// 소속 팬덤이 본인 소속과 같은지 — 타 팬덤 피드 삽입 차단
function fandomIsMine() {
return request.resource.data.fandomId == myUser().fandomId;
}
// 무인증 열람을 허용하는 캠페인 상태 — draft·review 는 제외 (교차검증 F-08)
// 무인증 열람을 허용하는 캠페인 상태 — draft·review 는 제외
function campaignIsPublic() {
return resource.data.status in ['funding', 'success', 'fail', 'report'];
}
@@ -97,7 +97,7 @@ service cloud.firestore {
match /users/{uid} {
allow get: if isSelf(uid) || isViewer();
allow list: if isViewer(); // 전체 사용자 목록은 운영자만(S-AD-USER)
// 교차검증 F-06 — 클라이언트 생성 경로를 닫는다.
// 클라이언트 생성 경로를 닫는다.
// 기존에는 클라이언트 생성도 허용해 ① fn-auth-oncreate(§8.5)와 생성 경합이 생기고
// ② 클라이언트가 먼저 만들면 totalContributedPop 등 서버 산출 필드가 빈
// 사용자 문서가 남았다(별첨1 조건부적합 제외 ④ 로그인 불가와 닿는 지점).
@@ -150,7 +150,7 @@ service cloud.firestore {
// 보유 배지 이력. 부여는 서버 판정.
// 화면: S-M-BADGE(본인 도감) · S-M-IDCARD(본인 카드)
//
// 교차검증 G-04 — 예전 주석은 「타인 프로필의 배지 노출 허용」이라 적고 signedIn() 으로 열어 뒀는데,
// 예전 주석은 「타인 프로필의 배지 노출 허용」이라 적고 signedIn() 으로 열어 뒀는데,
// **1차 범위에 타인 프로필 화면이 없다**(화면 95개 전수 확인). 주석이 사실과 달랐다.
// 타인 배지가 실제로 보이는 곳은 S-M-HERO 히어로 랭킹 하나뿐이고,
// 그것은 /fandoms/{f}/members/{uid}.honorBadgeIds 비정규화 필드로 읽는다(설계서 §2.3·§4.4).
@@ -179,7 +179,7 @@ service cloud.firestore {
// 본인은 자기 동의 이력을 볼 수 있어야 한다(열람권). 화면: S-A-TERMS · S-M-SETTINGS
match /users/{uid}/consents/{consentId} {
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
//
// ⚠️ 목록 쿼리에는 status 필터가 필수다 (교차검증 G-03).
// ⚠️ 목록 쿼리에는 status 필터가 필수다.
// 아래 read 규칙이 공개 상태만 허용하므로, status 필터가 없는 목록 쿼리는
// draft·review 문서까지 매칭돼 **쿼리 전체가 거부**된다(Firestore 는 list 규칙을
// 반환 후보 전체에 적용한다). 인덱스 문제로 오인하기 쉬운 지점이다.
// 허용되는 목록 쿼리 형태는 설계서 §6.1 IDX-15·16·17·19 참조.
match /campaigns/{campaignId} {
// 교차검증 F-08 — 예전에는 read: if true 라 심사 중(review)·초안(draft) 캠페인 본문이
// 예전에는 read: if true 라 심사 중(review)·초안(draft) 캠페인 본문이
// URL만 알면 미로그인 상태로 읽혔다. FR-CMP-01「승인 대기 캠페인은 목록에 노출하지 않는다」(E-CMP-03)를
// 클라이언트 쿼리 필터가 아니라 규칙에서 강제한다. 공개 상태만 무인증 열람 대상이다.
allow read: if campaignIsPublic()
|| isOps()
|| (signedIn() && resource.data.creatorUid == request.auth.uid);
// 교차검증 F-02 — 화이트리스트 방식. 목록에 없는 필드가 하나라도 있으면 거부되므로
// 화이트리스트 방식. 목록에 없는 필드가 하나라도 있으면 거부되므로
// 나중에 집계 필드가 추가돼도 자동으로 막힌다. 집계·분류 필드는 초기값을 못 벗어난다.
allow create: if signedIn()
&& request.resource.data.keys().hasOnly([
@@ -367,7 +367,7 @@ service cloud.firestore {
// 화면: S-C-RESULT
match /campaigns/{campaignId}/reports/{reportId} {
allow read: if true;
// 교차검증 F-07 — §7.3 표는 「성공 캠페인 개설자」라고 적었는데 규칙은 creatorUid만 보고
// §7.3 표는 「성공 캠페인 개설자」라고 적었는데 규칙은 creatorUid만 보고
// 상태를 확인하지 않아 실패·초안 캠페인에도 결과보고를 올릴 수 있었다.
// 별첨1 ⑤ 판정 기준 「결과보고 게시 확인」의 대상은 성공한 캠페인이다.
allow create: if signedIn()
@@ -385,7 +385,7 @@ service cloud.firestore {
// 화면: S-I-LOUNGE · S-I-POST · S-I-POST-NEW
match /posts/{postId} {
allow read: if signedIn();
// 교차검증 F-05 — 예전에는 authorUid·likeCount·visibility 3개만 봐서
// 예전에는 authorUid·likeCount·visibility 3개만 봐서
// ① authorName 을 "FANUZU 운영팀"으로 적어 사칭 ② commentCount 위조
// ③ 소속하지 않은 팬덤 피드에 글 삽입이 가능했다. 화이트리스트 + 원본 대조로 닫는다.
allow create: if authorIsSelf()
@@ -398,7 +398,7 @@ service cloud.firestore {
&& request.resource.data.likeCount == 0
&& request.resource.data.commentCount == 0
&& 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
&& touchesOnly(['body','imageUrls','updatedAt']);
// 숨김·복원. rbac-summary 「콘텐츠 숨김·복원」 = 운영 이상
@@ -416,7 +416,7 @@ service cloud.firestore {
// 댓글. 화면: S-I-POST
match /posts/{postId}/comments/{commentId} {
allow read: if signedIn();
// 교차검증 F-05 — 댓글에도 같은 표시명 사칭 경로가 있었다.
// 댓글에도 같은 표시명 사칭 경로가 있었다.
allow create: if authorIsSelf()
&& request.resource.data.keys().hasOnly([
'authorUid','authorName','body','visibility','createdAt','updatedAt'
@@ -440,7 +440,7 @@ service cloud.firestore {
// 화면: S-I-QUESTION · S-I-ANSWER-NEW
match /questions/{questionId}/answers/{answerId} {
allow read: if signedIn();
// 교차검증 F-05 — 답변에도 같은 표시명 사칭 경로가 있었다.
// 답변에도 같은 표시명 사칭 경로가 있었다.
allow create: if authorIsSelf()
&& request.resource.data.keys().hasOnly([
'authorUid','authorName','body','sourceUrls','accepted',
@@ -505,7 +505,7 @@ service cloud.firestore {
}
// 푸시 발송 이력·도달률. 운영자만 보고, 발송 집계는 Functions(FCM)가 쓴다.
// 화면: S-AD-PUSH · rbac-summary 「푸시 발송 관리」 = 운영 이상 (교차검증 F-04 #2)
// 화면: S-AD-PUSH · rbac-summary 「푸시 발송 관리」 = 운영 이상
match /pushCampaigns/{pushId} {
allow read: if isViewer();
allow write: if false; // 발송·집계는 fn-push-send 경유
@@ -579,7 +579,7 @@ service cloud.firestore {
allow write: if false;
}
// ─────────────── 12. 컬렉션 그룹 쿼리 (교차검증 F-01) ───────────────
// ─────────────── 12. 컬렉션 그룹 쿼리 ───────────────
//
// Firestore에서 `match /users/{uid}/popTransactions/{txId}` 형태의 규칙은
// **단일 컬렉션 쿼리에만** 적용되고 collectionGroup('popTransactions') 에는 적용되지 않는다.
@@ -9,7 +9,7 @@ import { ERR, fail } from '../core/errors.js';
/**
* `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` 인 새 행으로 남긴다.
* 동의 시점을 되짚을 수 없으면 분쟁에서 증명할 수단이 사라진다.
+40
View File
@@ -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>&lt;thin-for-all-variants&gt;</string>
</dict>
</plist>
+4
View File
@@ -1,2 +1,6 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
// Google 로그인 콜백 스킴. tool/ios/inject_url_scheme.py 가 만든다.
// `#include?` 는 파일이 없어도 빌드를 멈추지 않는다 — 그때는 Google 로그인만 동작하지 않는다.
#include? "../config/Google.xcconfig"
+4
View File
@@ -1,2 +1,6 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
// Google 로그인 콜백 스킴. tool/ios/inject_url_scheme.py 가 만든다.
// `#include?` 는 파일이 없어도 빌드를 멈추지 않는다 — 그때는 Google 로그인만 동작하지 않는다.
#include? "../config/Google.xcconfig"
+10 -3
View File
@@ -15,6 +15,7 @@
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
87BFB00ABC23EA249E841B59 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CBE157220A7C382F845775F0 /* Pods_Runner.framework */; };
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 */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
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>"; };
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>"; };
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>"; };
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; };
@@ -149,6 +151,7 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
4F2B0E1A2C3D4E5F60718293 /* GoogleService-Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
@@ -275,6 +278,7 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
4F2B0E1A2C3D4E5F60718294 /* GoogleService-Info.plist in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -467,7 +471,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = P36BV95LP5;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
DEVELOPMENT_TEAM = V28G543VK5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -650,7 +655,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = P36BV95LP5;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
DEVELOPMENT_TEAM = V28G543VK5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -673,7 +679,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = P36BV95LP5;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
DEVELOPMENT_TEAM = V28G543VK5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
+24
View File
@@ -66,5 +66,29 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</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>
</plist>
+21
View File
@@ -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>
+1 -2
View File
@@ -37,10 +37,9 @@ class AppTab {
/// 계약 고정 5탭.
///
/// 근거 3개가 서로 일치한다:
/// 근거 2개가 서로 일치한다:
/// - 견적서 v2.1 「5탭 네비게이션」
/// - `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)이다.
+1 -1
View File
@@ -73,7 +73,7 @@ final class AuthCancelled extends AuthOutcome {
const AuthCancelled();
}
/// 실패. [code] 는 `wireframe/planning/error-catalog.yaml` 의 ID 다(소셜 실패 = E-AUTH-03).
/// 실패. [code] 는 에러 카탈로그 ID 다(소셜 실패 = E-AUTH-03).
final class AuthFailed extends AuthOutcome {
const AuthFailed(this.code, this.message);
+1 -1
View File
@@ -344,7 +344,7 @@ const lineProvider = KeylessSocialProvider(AuthProviderKind.line, [
'LINE Developers 콘솔 채널 등록(사업자 정보 필요)',
]);
/// SDK 코드를 에러 카탈로그(`wireframe/planning/error-catalog.yaml`) ID 로 옮긴다.
/// SDK 코드를 에러 카탈로그 ID 로 옮긴다.
///
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
/// 오류」를 갈라 주면 어느 이메일이 가입돼 있는지 훑을 수 있다.
+1 -1
View File
@@ -1,7 +1,7 @@
name: fancake
description: "FANUZU — 팬덤 플랫폼 앱 (골격). 발주사 (주)팬케이크 / 수행사 스위치 주식회사"
publish_to: 'none'
version: 0.1.0+8
version: 0.1.0+9
environment:
sdk: ^3.11.5
+2 -5
View File
@@ -314,10 +314,8 @@ Future<void> _enterShell(WidgetTester tester) async {
/// `design/craft-allowlist.yaml` 의 `APP_TABS` 를 읽어 온다.
/// 앱 코드가 계약 표기에서 벗어나면 여기서 깨진다.
///
/// 계약 정본은 상위 저장소(p-fancake)에 있고 이 저장소(p-fanuzu)는 분리돼 있어
/// 상대 경로가 체크아웃 위치를 탄다. 그래서 저장소 안에 사본을 두고
/// **FANUZU_DESIGN_DIR → 저장소 내 design/ → 형제 경로** 순으로 본다.
/// 사본이 있으므로 git worktree 에서도 환경 변수 없이 통과한다.
/// 계약 파일은 저장소 루트 `design/` 에 있고, `app/design/` 에 같은 사본을 둔다.
/// **FANUZU_DESIGN_DIR → app/design/ → 저장소 루트 design/** 순으로 본다.
///
/// 하나도 없으면 **통과가 아니라 실패**로 끝낸다 — 못 읽은 것을 「맞다」로 세면
/// 계약 대조가 조용히 사라진다.
@@ -327,7 +325,6 @@ List<String> _contractTabs() {
'$dir/craft-allowlist.yaml',
'design/craft-allowlist.yaml',
'../design/craft-allowlist.yaml',
'../p-fancake/design/craft-allowlist.yaml',
];
final file = candidates
.map(File.new)
+86
View File
@@ -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 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""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/ 에서)
- 부수 기능: design-tokens.md 의 contrast_checks 쌍을 WCAG 로 재계산해서 표로 찍고,
AA(4.5:1) 미달이 하나라도 있으면 exit 1. 테마가 대비를 깎았는지 여기서 걸린다.
+58
View File
@@ -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
View File
@@ -1,6 +1,5 @@
# craft-allowlist.yaml — 크래프트 체커의 "분류 결정"을 적는 곳.
# 경고를 끄는 곳이 아니다. 각 항목에 근거(reason)가 없으면 킷이 읽지 않는다.
# 계약: 하네스/디자인/resources/codex/check_craft_render.py C2
CAPS_OK:
# ── 5탭 라벨 — 견적서 v2.1 「5탭 네비게이션」으로 고정된 화면 이름이다.
+2 -2
View File
@@ -18,7 +18,7 @@
## 로고 결정
- 발주사 공식 자산 `고객사 준비 데모/LOGO/fanuzu logo only.png`를 수령했고, PNG의 알파 채널도 확인했다.
- 시안은 이 공식 심볼을 `시안/assets/logo.png`에 반영했다. 공식 앱 아이콘 원천도 같은 심볼이며
- 공식 앱 아이콘 원천도 같은 심볼이며
`app/assets/brand/app-icon.png`와 `app/assets/brand/app-icon-foreground.png`에 반영돼 있다.
- 따라서 현재 로고는 AI 생성 제안본이나 임시 Fancake 브랜드가 아니다. 별도 제안 로고 승인 대기도 없다.
- **서비스명은 `FANUZU`, 국문 병행 표기는 `팬우주`로 확정됐다.** 발주사 공식 검토 회신서의
@@ -173,7 +173,7 @@ font:
font-direction: "geometric"
font-source: "42dot Sans = Google Fonts OFL 가변 ttf · ABeeZee = Google Fonts OFL (app/assets/fonts/, 2026-09-02)"
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% 가 표준이다.
# 종전 값(Gmarket/포카 온 스테이지 재설계 — 44·32 Bold, 본문 400·행간 1.6)은 Figma 와 맞지 않아 교체한다.
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.