빌드 안내 · 주석을 이 저장소 기준으로 정리

- 빌드 안내(BUILD.md)의 JDK 경로를 설치 위치와 무관한 지정 방식으로 변경
- 5탭 계약 파일 탐색 순서를 이 저장소 구조(app/design → 루트 design)로 정리
- README · 보안 규칙 · 소스 주석에서 이 저장소에 없는 문서 경로를 문서 이름으로 바꾸거나 삭제
- Flutter 버전 고정 파일(app/.fvmrc, 3.47.2) 추가, README 필수 환경을 같은 버전으로 갱신
- 동작 변경 없음
This commit is contained in:
Sn-Kinos
2026-10-01 01:05:35 +09:00
parent 4c3bc0304f
commit 9dd43ca405
14 changed files with 54 additions and 78 deletions
+3
View File
@@ -0,0 +1,3 @@
{
"flutter": "3.47.2"
}
+11 -22
View File
@@ -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 만 맞춘다.
```
## 빌드 ## 빌드
+7 -17
View File
@@ -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;
@@ -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)
+3
View File
@@ -2,3 +2,6 @@ analyzer:
exclude: exclude:
# Firebase 도구가 내려받는 서드파티 소스. 우리 분석 대상이 아니다. # Firebase 도구가 내려받는 서드파티 소스. 우리 분석 대상이 아니다.
- '**/node_modules/**' - '**/node_modules/**'
- build/**
- android/**
- ios/**
+2 -6
View File
@@ -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탭 네비게이션」으로 고정된 화면 이름이다.
+18 -18
View File
@@ -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` 인 새 행으로 남긴다.
* 동의 시점을 되짚을 수 없으면 분쟁에서 증명할 수단이 사라진다. * 동의 시점을 되짚을 수 없으면 분쟁에서 증명할 수단이 사라진다.
+1 -2
View File
@@ -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)이다.
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -344,7 +344,7 @@ const lineProvider = KeylessSocialProvider(AuthProviderKind.line, [
'LINE Developers 콘솔 채널 등록(사업자 정보 필요)', 'LINE Developers 콘솔 채널 등록(사업자 정보 필요)',
]); ]);
/// SDK 코드를 에러 카탈로그(`wireframe/planning/error-catalog.yaml`) ID 로 옮긴다. /// SDK 코드를 에러 카탈로그 ID 로 옮긴다.
/// ///
/// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호 /// **계정 존재 여부를 드러내지 않는다.** 「등록되지 않은 이메일」과 「비밀번호
/// 오류」를 갈라 주면 어느 이메일이 가입돼 있는지 훑을 수 있다. /// 오류」를 갈라 주면 어느 이메일이 가입돼 있는지 훑을 수 있다.
+2 -5
View File
@@ -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)
+1 -1
View File
@@ -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. 테마가 대비를 깎았는지 여기서 걸린다.
-1
View File
@@ -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탭 네비게이션」으로 고정된 화면 이름이다.
+2 -2
View File
@@ -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}