공식 로고 앱 아이콘 적용 · 확정 정책에 맞춰 등급·POP 소멸 필드를 규칙·인덱스에서 제외
@@ -23,7 +23,7 @@ cd app
|
||||
flutter pub get
|
||||
flutter run # 실기기·시뮬레이터
|
||||
flutter analyze # 정적 분석
|
||||
flutter test # 스모크 테스트 3종
|
||||
flutter test # 스모크·회귀 테스트 9건
|
||||
flutter build apk --debug # Android 디버그 APK
|
||||
```
|
||||
|
||||
@@ -117,7 +117,7 @@ bool get isMockSessionAvailable => !FirebaseBoot.isLive && !_releaseMode;
|
||||
| Q3 | 네이버 client ID / secret | 미정 | 네이버 버튼이 「키 미투입」 안내를 반환 |
|
||||
| Q5 | 스토어 등록 명의 | — | 3주차 TestFlight·Play 내부트랙 제출 불가 |
|
||||
| Q14 | 앱 식별자 확정 | `android/app/build.gradle.kts` · `ios/Runner.xcodeproj` | 현재 `kr.co.sweech.fancake` 로 잠정. **스토어 등록 후에는 변경 불가** |
|
||||
| Q6 | 확정 로고 원본(벡터·알파 PNG) | `assets/brand/` | 로그인 화면 워드마크를 텍스트로 대체 중 |
|
||||
| ~~Q6~~ | 공식 FANUZU 심볼 원본 | `assets/brand/` | **2026-08-13 반영 완료.** 제공본이 심볼 전용이라 앱 아이콘은 공식 자산으로 생성했고 로그인 워드마크는 `FANUZU` 텍스트를 유지 |
|
||||
| Q15 | 하단 탭 아이콘 자산 | `assets/icons/` | Material 기본 아이콘 5종으로 잠정 |
|
||||
|
||||
### 5-3. 폰트
|
||||
@@ -206,12 +206,12 @@ app/
|
||||
## 9. 검증 결과
|
||||
|
||||
<!-- VERIFY:BEGIN 실행 출력 그대로. 손으로 고치지 마라. -->
|
||||
2026-08-08 실행. macOS 27.0 · Flutter 3.41.7 (stable) · Dart 3.11.5 · Android SDK 36 · Xcode 26.6
|
||||
2026-08-14 재실행. macOS 27.0 · Flutter 3.41.7 (stable) · Dart 3.11.5 · Android SDK 36 · Xcode 26.6
|
||||
|
||||
```
|
||||
$ flutter analyze
|
||||
Analyzing app...
|
||||
No issues found! (ran in 1.3s)
|
||||
No issues found! (ran in 3.2s)
|
||||
|
||||
$ flutter test
|
||||
00:00 +0: loading /Users/Kinos/Projects/p-fancake/app/test/app_smoke_test.dart
|
||||
@@ -221,13 +221,14 @@ $ flutter test
|
||||
00:00 +3: 릴리스 빌드에서는 목 우회 입구가 열리지 않는다 릴리스에서 signInWithMockSession 을 불러도 로그인되지 않는다
|
||||
00:00 +4: 릴리스 빌드에서는 목 우회 입구가 열리지 않는다 릴리스 빌드 로그인 화면에 목 세션 버튼이 없다
|
||||
00:00 +5: 릴리스 빌드에서는 목 우회 입구가 열리지 않는다 디버그 빌드에서는 목 세션 버튼이 있다 (가드가 과하게 잠그지 않았는지)
|
||||
00:00 +6: 화면에 띄우는 사유에 내부 파일 경로가 들어가지 않는다
|
||||
00:00 +7: 3. 탭을 전환하면 각 탭 루트 화면이 바뀐다
|
||||
00:00 +8: All tests passed!
|
||||
00:00 +6: 제공자 4종의 미투입 안내에 내부 파일 경로가 들어가지 않는다
|
||||
00:00 +7: 화면에 띄우는 사유에 내부 파일 경로가 들어가지 않는다
|
||||
00:00 +8: 3. 탭을 전환하면 각 탭 루트 화면이 바뀐다
|
||||
00:00 +9: All tests passed!
|
||||
|
||||
$ flutter build apk --debug
|
||||
Running Gradle task 'assembleDebug'... 219.5s (첫 빌드 — NDK 28.2 자동 설치 포함)
|
||||
✓ Built build/app/outputs/flutter-apk/app-debug.apk (142.4 MB · debug)
|
||||
✓ Built build/app/outputs/flutter-apk/app-debug.apk (164,432,730 bytes · 약 156.8 MiB · debug)
|
||||
10.6s (증분 재빌드)
|
||||
|
||||
$ flutter build ios --debug --no-codesign
|
||||
@@ -241,7 +242,7 @@ $ python3 tool/gen_tokens.py
|
||||
전 쌍 AA 통과. (최저 color-on-neon-violet / color-neon-violet = 5.09:1)
|
||||
```
|
||||
|
||||
**테스트 8종은 Firebase 설정 파일이 없는 상태(목 모드)에서 통과한 결과다.**
|
||||
**테스트 9종은 Firebase 설정 파일이 없는 상태(목 모드)에서 통과한 결과다.**
|
||||
|
||||
음성 검증 2회 — 테스트가 실제로 게이트 역할을 하는지 직접 깨뜨려 확인했다.
|
||||
|
||||
@@ -292,11 +293,10 @@ I flutter : [FirebaseBoot] detail :: PlatformException(java.lang.Exception: Fail
|
||||
| L2 | `com.google.gms.google-services` Gradle 플러그인 **미적용** | 지금은 이게 정상이다. 설정 파일 없이 플러그인만 적용하면 Gradle 빌드가 실패한다. 파일 투입과 **동시에** `android/settings.gradle.kts` · `android/app/build.gradle.kts` 에 추가해야 한다 | S2 |
|
||||
| L3 | 카카오·네이버 SDK 미도입 | 스텁만 있다. 실 연동 시 `kakao_flutter_sdk_user` · 네이버 SDK 추가와 함께 `KeylessSocialProvider` 를 실 구현체로 교체 | Q3 회신 후 |
|
||||
| L4 | 폰트 파일 미투입 | 시스템 폰트로 폴백. 크기·굵기·행간·자간은 토큰 값대로 적용됨 | 라이선스 원본 확보 시 |
|
||||
| L5 | 로고 원본 미확정(Q6) | 로그인 화면 워드마크가 텍스트다 | 2주차 M1 |
|
||||
| L6 | 탭 아이콘이 Material 기본(Q15) | 시안 탭바에는 아이콘이 없어 대조 대상이 없다 | 3주차 |
|
||||
| L7 | **iOS 최소 지원 버전을 13.0 → 15.0 으로 올렸다** | `firebase_auth` 6.x 가 15.0 이상을 요구해 13.0 으로는 `pod install` 이 실패했다. 기준 단말 iPhone 11(S4)은 iOS 18까지 지원하므로 범위 영향 없음. `ios/Podfile` · `Runner.xcodeproj` 3곳 수정 | 반영 완료 |
|
||||
| L8 | **릴리스 서명 키 없음 → `flutter build apk --release` 는 의도적으로 실패한다** | 서명 키는 스토어 등록 명의(Q5·S3)가 정해져야 만들 수 있다. Flutter 기본 템플릿은 이 상태에서 **debug 키로 조용히 서명**해 Play 가 거부하는 산출물을 "성공"으로 내놓는다 — 그 경로를 막고 사유를 띄우도록 `android/app/build.gradle.kts` 를 고쳤다. 키가 생기면 `android/key.properties`(storeFile·storePassword·keyAlias·keyPassword)만 놓으면 된다. 해당 파일과 `*.jks`/`*.keystore` 는 `app/android/.gitignore` 가 이미 막고 있다(확인함) | Q5·S3 |
|
||||
| L8-1 | debug APK 142.4 MB | debug 빌드 특성(미난독화·전 ABI 포함). release + `--split-per-abi` 로 크게 줄어들지만 **L8 때문에 아직 실측하지 못했다** | Q5·S3 |
|
||||
| L8-1 | debug APK 164,432,730 bytes(약 156.8 MiB) | debug 빌드 특성(미난독화·전 ABI 포함). release + `--split-per-abi` 로 크게 줄어들지만 **L8 때문에 아직 실측하지 못했다** | Q5·S3 |
|
||||
| L9 | 첫 Gradle 빌드에서 Kotlin 데몬 경고 1회 | `e: The daemon has terminated unexpectedly on startup attempt #1 with error code: 0` — 자동 재시도로 성공했고 산출물에 영향 없음. 증분 빌드에서는 재현되지 않음 | — |
|
||||
| L9-1 | iOS 런치 화면 색 변경은 **컴파일 검증만** 했다 | `LaunchScreen.storyboard` 배경을 흰색 → `color-cosmic-void`(#000)로 바꿨다. Android 는 에뮬레이터에서 눈으로 확인했지만 iOS 는 시뮬레이터 실행을 하지 않아 빌드 성공까지만 확인했다 | 3주차 실기기 빌드 |
|
||||
| L10 | 온보딩 6화면 라우트 없음 | S-A-INTRO · S-A-TERMS · S-A-PROFILE · S-A-ARTIST · S-A-PLANET-NEW · S-A-PERMS | 3주차 |
|
||||
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 526 KiB |
|
Before Width: | Height: | Size: 706 KiB After Width: | Height: | Size: 526 KiB |
@@ -74,23 +74,6 @@
|
||||
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "popLots",
|
||||
"queryScope": "COLLECTION",
|
||||
"fields": [
|
||||
{ "fieldPath": "popType", "order": "ASCENDING" },
|
||||
{ "fieldPath": "status", "order": "ASCENDING" },
|
||||
{ "fieldPath": "grantedAt", "order": "ASCENDING" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "popLots",
|
||||
"queryScope": "COLLECTION_GROUP",
|
||||
"fields": [
|
||||
{ "fieldPath": "status", "order": "ASCENDING" },
|
||||
{ "fieldPath": "expiresAt", "order": "ASCENDING" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"collectionGroup": "activityLogs",
|
||||
"queryScope": "COLLECTION",
|
||||
|
||||
@@ -8,7 +8,7 @@ rules_version = '2';
|
||||
// - 주간보고/1주차/src/1주차_정책설계서_초안.md P-02 원장 무결성 · P-06 어뷰징 방어
|
||||
//
|
||||
// 3대 원칙
|
||||
// 1) POP · FPI · 등급 · 원장 필드는 클라이언트가 쓸 수 없다.
|
||||
// 1) POP · FPI 집계 · 원장 필드는 클라이언트가 쓸 수 없다.
|
||||
// 전부 Cloud Functions(Admin SDK)가 쓰며, Admin SDK는 본 규칙을 우회한다.
|
||||
// → 별첨1 ② 판정 기준 「클라이언트 단독 지급 0건」을 규칙 층에서 물리적으로 보장.
|
||||
// 2) 개인 리소스는 본인만 읽고, 쓰기는 화이트리스트 필드로만 허용한다.
|
||||
@@ -58,13 +58,13 @@ service cloud.firestore {
|
||||
}
|
||||
// 무인증 열람을 허용하는 캠페인 상태 — draft·review 는 제외 (교차검증 F-08)
|
||||
function campaignIsPublic() {
|
||||
return resource.data.status in ['voting', 'funding', 'success', 'fail', 'report'];
|
||||
return resource.data.status in ['funding', 'success', 'fail', 'report'];
|
||||
}
|
||||
|
||||
// ───────────────────────── 1. 운영 파라미터 ─────────────────────────
|
||||
|
||||
// 미확정 수치 원격 설정. 하드코딩 금지 대상 전부가 여기 들어온다.
|
||||
// 근거: 결정 D-07·D-13·D-14·D-21·D-22·D-23 · FPI 가중치(P-03) · 이클립스 수치(CD-91)
|
||||
// 근거: 결정 D-07·D-13·D-14 · FPI 가중치(P-03) · 이클립스 수치(CD-91)
|
||||
// 화면: S-AD-POP · S-AD-FPI · S-AD-QUEST · S-AD-ECLIPSE
|
||||
match /config/{configId} {
|
||||
allow read: if true; // 스플래시·로그인 전 화면도 읽어야 해 공개 읽기
|
||||
@@ -92,14 +92,14 @@ service cloud.firestore {
|
||||
|
||||
// ───────────────────────── 2. 사용자 ─────────────────────────
|
||||
|
||||
// 계정·프로필. 등급(grade)·누적 기여(totalContributedPop)는 서버 산출값이라 쓰기 목록에서 뺀다.
|
||||
// 계정·프로필. 누적 기여(totalContributedPop)는 서버 산출값이라 쓰기 목록에서 뺀다.
|
||||
// 화면: S-A-PROFILE · S-M-HOME · S-M-SETTINGS · S-AD-USER
|
||||
match /users/{uid} {
|
||||
allow get: if isSelf(uid) || isViewer();
|
||||
allow list: if isViewer(); // 전체 사용자 목록은 운영자만(S-AD-USER)
|
||||
// 교차검증 F-06 — 클라이언트 생성 경로를 닫는다.
|
||||
// 기존에는 클라이언트 생성도 허용해 ① fn-auth-oncreate(§8.5)와 생성 경합이 생기고
|
||||
// ② 클라이언트가 먼저 만들면 grade·totalContributedPop 이 허용 목록에 없어 필드가 빈
|
||||
// ② 클라이언트가 먼저 만들면 totalContributedPop 등 서버 산출 필드가 빈
|
||||
// 사용자 문서가 남았다(별첨1 조건부적합 제외 ④ 로그인 불가와 닿는 지점).
|
||||
// 생성 주체를 Auth 트리거 하나로 고정한다.
|
||||
allow create: if false;
|
||||
@@ -119,14 +119,14 @@ service cloud.firestore {
|
||||
allow write: if false; // 별첨1 ② 「클라이언트 단독 지급 0건」
|
||||
}
|
||||
|
||||
// POP Lot — 획득 단위별 잔량·만료일. FIFO 차감의 계산 단위.
|
||||
// 화면: S-M-WALLET(소멸 예정 POP)
|
||||
// POP Lot — 획득 단위별 잔량. 원장 대사와 차감의 계산 단위.
|
||||
// 화면: S-M-WALLET
|
||||
match /users/{uid}/popLots/{lotId} {
|
||||
allow read: if isSelf(uid) || isViewer();
|
||||
allow write: if false; // 잔량·만료 조작 차단
|
||||
allow write: if false; // 잔량 조작 차단
|
||||
}
|
||||
|
||||
// POP 원장. 지급·차감·환불·소멸 전건. 수정·삭제 불가(append-only)는 Functions에서도 지킨다.
|
||||
// POP 원장. 지급·차감·환불·보류 무효 전건. 수정·삭제 불가(append-only)는 Functions에서도 지킨다.
|
||||
// 화면: S-M-WALLET(거래 내역) · S-AD-LEDGER
|
||||
match /users/{uid}/popTransactions/{txId} {
|
||||
allow read: if isSelf(uid) || isViewer();
|
||||
@@ -147,7 +147,7 @@ service cloud.firestore {
|
||||
allow write: if false;
|
||||
}
|
||||
|
||||
// 보유 배지·등급 이력. 부여는 서버 판정.
|
||||
// 보유 배지 이력. 부여는 서버 판정.
|
||||
// 화면: S-M-BADGE(본인 도감) · S-M-IDCARD(본인 카드)
|
||||
//
|
||||
// 교차검증 G-04 — 예전 주석은 「타인 프로필의 배지 노출 허용」이라 적고 signedIn() 으로 열어 뒀는데,
|
||||
@@ -326,7 +326,7 @@ service cloud.firestore {
|
||||
&& request.resource.data.keys().hasOnly([
|
||||
'fandomId','creatorUid','creatorName','title','summary','body',
|
||||
'coverImageUrl','campaignType','targetPop','endsAt','status',
|
||||
'raisedPop','achievedRate','donorCount','votedPop','isAiDraft',
|
||||
'raisedPop','achievedRate','donorCount',
|
||||
'refundState','createdAt','updatedAt'
|
||||
])
|
||||
&& request.resource.data.creatorUid == request.auth.uid
|
||||
@@ -336,8 +336,6 @@ service cloud.firestore {
|
||||
&& request.resource.data.raisedPop == 0 // 개설 시 모금액 0 고정
|
||||
&& request.resource.data.achievedRate == 0 // 달성률 위조 차단(웹 브릿지 노출 대상)
|
||||
&& request.resource.data.donorCount == 0 // 후원자 수 위조 차단
|
||||
&& request.resource.data.votedPop == 0 // D-22 승격 임계치 판정 입력
|
||||
&& request.resource.data.isAiDraft == false // 「AI 초안」 배지 사칭 차단
|
||||
&& request.resource.data.refundState == 'none';
|
||||
// 개설자는 심사 전 초안만 고칠 수 있다.
|
||||
allow update: if signedIn()
|
||||
@@ -618,7 +616,7 @@ service cloud.firestore {
|
||||
allow read: if signedIn();
|
||||
}
|
||||
|
||||
// popLots(IDX-11)·환불 배치 donations(IDX-21)는 스케줄 Function 전용이다.
|
||||
// 환불 배치 donations(IDX-21)는 스케줄 Function 전용이다.
|
||||
// Admin SDK는 규칙을 우회하므로 컬렉션 그룹 블록을 두지 않는다 — 열면 권한만 넓어진다.
|
||||
|
||||
// ───────────────────────── 13. 기본 거부 ─────────────────────────
|
||||
|
||||
@@ -49,11 +49,9 @@ export function userDocument(uid, overrides = {}) {
|
||||
uid,
|
||||
displayName: uid,
|
||||
fandomId: 'fandom-a',
|
||||
grade: 'SILVER',
|
||||
totalContributedPop: 0,
|
||||
status: 'active',
|
||||
updatedAt: '2026-08-08T00:00:00Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ afterAll(async () => {
|
||||
await testEnvironment.cleanup();
|
||||
});
|
||||
|
||||
describe('원칙 1 — POP·FPI·등급·원장은 클라이언트 쓰기 불가', () => {
|
||||
describe('원칙 1 — POP·FPI·원장은 클라이언트 쓰기 불가', () => {
|
||||
test('본인 지갑 잔액 직접 증액을 거부한다', async () => {
|
||||
await seedDocuments(testEnvironment, {
|
||||
'users/alice/wallet/main': { balance: 100 },
|
||||
@@ -61,14 +61,14 @@ describe('원칙 1 — POP·FPI·등급·원장은 클라이언트 쓰기 불가
|
||||
);
|
||||
});
|
||||
|
||||
test('POP Lot 만료일 연장을 거부한다', async () => {
|
||||
test('POP Lot 잔량 조작을 거부한다', async () => {
|
||||
await seedDocuments(testEnvironment, {
|
||||
'users/alice/popLots/lot-1': { remaining: 100, expiresAt: '2026-08-09T00:00:00Z' },
|
||||
'users/alice/popLots/lot-1': { remaining: 100 },
|
||||
});
|
||||
|
||||
await assertFails(
|
||||
updateDoc(doc(dbFor('alice'), 'users/alice/popLots/lot-1'), {
|
||||
expiresAt: '2099-12-31T00:00:00Z',
|
||||
remaining: 999_999,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -82,12 +82,14 @@ describe('원칙 1 — POP·FPI·등급·원장은 클라이언트 쓰기 불가
|
||||
);
|
||||
});
|
||||
|
||||
test('사용자 등급 직접 승격을 거부한다', async () => {
|
||||
test('누적 기여 통계 직접 조작을 거부한다', async () => {
|
||||
await seedDocuments(testEnvironment, {
|
||||
'users/alice': userDocument('alice'),
|
||||
});
|
||||
|
||||
await assertFails(updateDoc(doc(dbFor('alice'), 'users/alice'), { grade: 'DIAMOND' }));
|
||||
await assertFails(
|
||||
updateDoc(doc(dbFor('alice'), 'users/alice'), { totalContributedPop: 999_999 }),
|
||||
);
|
||||
});
|
||||
|
||||
test('다른 사용자의 지갑 쓰기를 거부한다', async () => {
|
||||
@@ -125,11 +127,11 @@ describe('원칙 2 — 개인 리소스와 쓰기 필드 제한', () => {
|
||||
await assertFails(updateDoc(doc(dbFor('alice'), 'users/bob'), { displayName: 'Forged' }));
|
||||
});
|
||||
|
||||
test('허용 필드와 등급을 섞은 본인 업데이트를 전부 거부한다', async () => {
|
||||
test('허용 필드와 서버 통계를 섞은 본인 업데이트를 전부 거부한다', async () => {
|
||||
await assertFails(
|
||||
updateDoc(doc(dbFor('alice'), 'users/alice'), {
|
||||
displayName: 'New Alice',
|
||||
grade: 'DIAMOND',
|
||||
totalContributedPop: 999_999,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -251,15 +253,13 @@ describe('F-02 — 캠페인 집계 필드 위조 회귀', () => {
|
||||
summary: 'Summary',
|
||||
body: 'Body',
|
||||
coverImageUrl: 'https://example.test/cover.png',
|
||||
campaignType: 'support',
|
||||
campaignType: 'other',
|
||||
targetPop: 1000,
|
||||
endsAt: '2026-09-01T00:00:00Z',
|
||||
status: 'draft',
|
||||
raisedPop: 0,
|
||||
achievedRate: 0,
|
||||
donorCount: 0,
|
||||
votedPop: 0,
|
||||
isAiDraft: false,
|
||||
refundState: 'none',
|
||||
createdAt: '2026-08-08T00:00:00Z',
|
||||
updatedAt: '2026-08-08T00:00:00Z',
|
||||
@@ -275,14 +275,12 @@ describe('F-02 — 캠페인 집계 필드 위조 회귀', () => {
|
||||
await assertSucceeds(setDoc(doc(dbFor('alice'), 'campaigns/valid'), validCampaign));
|
||||
});
|
||||
|
||||
test('집계·투표·AI 초안 필드 위조를 거부한다', async () => {
|
||||
test('집계 필드 위조를 거부한다', async () => {
|
||||
await assertFails(
|
||||
setDoc(doc(dbFor('alice'), 'campaigns/forged'), {
|
||||
...validCampaign,
|
||||
achievedRate: 1,
|
||||
donorCount: 99_999,
|
||||
votedPop: 999_999,
|
||||
isAiDraft: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -389,7 +387,10 @@ describe('F-14 — posts 허용 필드 회귀', () => {
|
||||
describe('F-06 — users 클라이언트 생성 경로 회귀', () => {
|
||||
test('일반 사용자의 users 문서 생성을 거부한다', async () => {
|
||||
await assertFails(
|
||||
setDoc(doc(dbFor('alice'), 'users/alice'), userDocument('alice', { grade: 'DIAMOND' })),
|
||||
setDoc(
|
||||
doc(dbFor('alice'), 'users/alice'),
|
||||
userDocument('alice', { totalContributedPop: 999_999 }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 697 KiB After Width: | Height: | Size: 330 KiB |
|
Before Width: | Height: | Size: 591 B After Width: | Height: | Size: 486 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 1011 B After Width: | Height: | Size: 778 B |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 9.9 KiB |
@@ -33,14 +33,14 @@ dev_dependencies:
|
||||
# 재생성: dart run flutter_launcher_icons
|
||||
flutter_launcher_icons: ^0.14.4
|
||||
|
||||
# 앱 아이콘 — 원본은 시안/assets/app-icon.png (제안 자산).
|
||||
# 발주사 확정 로고(Q6)가 오면 assets/brand/ 의 두 파일만 교체하고 위 명령을 다시 돌린다.
|
||||
# 앱 아이콘 — 2026-08-13 수령한 공식 `fanuzu logo only.png`의 중앙 1:1 영역을
|
||||
# 픽셀 변경 없이 사용한다. `assets/brand/` 두 원본은 같은 공식 마크다.
|
||||
flutter_launcher_icons:
|
||||
android: true
|
||||
ios: true
|
||||
image_path: "assets/brand/app-icon.png"
|
||||
# Android 8+ 적응형 아이콘 — 마스크가 바깥을 잘라내므로 마크만 담은 전경을 따로 쓴다.
|
||||
adaptive_icon_background: "#120E25" # 원본 배경 실측값
|
||||
adaptive_icon_background: "#120E25" # 투명 공식 마크 바깥의 브랜드 다크 배경
|
||||
adaptive_icon_foreground: "assets/brand/app-icon-foreground.png"
|
||||
# iOS 는 알파 채널을 허용하지 않는다(App Store 심사 거부 사유).
|
||||
remove_alpha_ios: true
|
||||
|
||||
@@ -8,10 +8,6 @@ CAPS_OK:
|
||||
- {term: STATION, reason: "견적서 v2.1 5탭 라벨 · 한글 대체 불가(계약 표기)"}
|
||||
- {term: INSIDE, reason: "견적서 v2.1 5탭 라벨 · 한글 대체 불가(계약 표기)"}
|
||||
- {term: GALAXY, reason: "견적서 v2.1 5탭 라벨 · 한글 대체 불가(계약 표기)"}
|
||||
# ── 등급명 — 인수 데모가 확정한 제품 어휘(FANUZU_prototype.tsx:627 'DIAMOND TIER').
|
||||
- {term: SILVER, reason: "등급 체계 고유명 · 인수 데모 확정 어휘(결정 D-21)"}
|
||||
- {term: GOLD, reason: "등급 체계 고유명 · 인수 데모 확정 어휘(결정 D-21)"}
|
||||
- {term: DIAMOND, reason: "등급 체계 고유명 · 인수 데모 확정 어휘(결정 D-21)"}
|
||||
# ── 지표·재화 약어 — 화면 전반에서 단위처럼 쓰인다.
|
||||
- {term: FPI, reason: "Fandom Planet Index 약어 · PRD v9.0 PRD-03 공식 명칭"}
|
||||
|
||||
|
||||
@@ -17,16 +17,12 @@
|
||||
한 곳에만 쓰고 그 외에는 쓰지 않는다.
|
||||
|
||||
## 로고 결정
|
||||
- 발주사 인수 자산 `고객사 준비 데모/LOGO/*.png` 4종은 모두 **회색 배경 위 네온 글로우 목업**이다.
|
||||
알파 채널이 없어 헤더에 회색 사각형이 박히고, 글로우·발광은 계약 §5.1 이 금지한 신호다.
|
||||
또한 워드마크가 **FANUZU(팬우주)** 인데 견적서·별첨의 서비스명은 **FANUZU** 다.
|
||||
- 그래서 시안에는 **FANUZU 플랫 워드마크를 제안본으로 만들어** 썼다(첫 `a` 의 카운터를 고리 행성으로 치환,
|
||||
단색 플럼, 알파 PNG). 인수 원본은 `sources/logo-발주사원본-fanuzu.png` 로 보존했다.
|
||||
- **서비스명은 `FANUZU` 로 확정됐다 (CD-01).** 데모 최신 소스 3종이 전부 FANUZU 표기이고
|
||||
견적서·별첨의 서비스명과도 일치한다. **FANUZU(팬우주)는 인수 원본에만 남은 옛 표기이므로 신규 산출물에 쓰지 않는다.**
|
||||
인수 자산의 FANUZU 워드마크는 이력 보존용으로만 둔다.
|
||||
- **남은 확인 항목은 로고 원본 하나뿐**: 이 제안 워드마크를 쓸지, 발주사가 별도 로고 원본(벡터·알파)을
|
||||
제공할지 (확인요청항목 **Q6**).
|
||||
- 발주사 공식 자산 `고객사 준비 데모/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`, 국문 병행 표기는 `팬우주`로 확정됐다.** 발주사 공식 검토 회신서의
|
||||
표지 지시에 따라 종전 CD-01(`Fancake` 확정)은 철회·대체했다(W-01).
|
||||
|
||||
## 컬러 운용 규율 (60-30-10)
|
||||
- 60 배경/서피스 · 30 중립 텍스트·보더 · **10 악센트**. 악센트는 **행동·선택·포커스에만** 쓴다.
|
||||
@@ -242,4 +238,6 @@ contrast_checks:
|
||||
|
||||
| 일자 | 내용 |
|
||||
|---|---|
|
||||
| 2026-08-08 | **서비스명 확인 질문 삭제 — CD-01 로 이미 확정된 사항이었음.** 「① 서비스명 표기가 FANUZU 인지 팬우주(FANUZU) 인지」를 확인 필요 항목으로 남겨 두었으나, 변경 대장 **CD-01(서비스명 FANUZU 확정)** 으로 닫힌 건이라 질문을 지우고 확정 문구로 교체했다. **디자인 결정 변경은 없고**, 이미 확정된 것을 미확정처럼 적어 둔 표기를 바로잡은 것이다. 로고 원본 제공 여부(**Q6**)는 실제 미확정이라 그대로 남겼다 |
|
||||
| 2026-08-13 | **공식 자산 반영.** `고객사 준비 데모/LOGO/fanuzu logo only.png`를 공식 FANUZU 심볼 원천으로 확정하고 시안 로고·앱 아이콘에 반영했다. 제안 로고/Q6 대기는 종료했다 |
|
||||
| 2026-08-11 | **W-01 브랜드 정정.** 발주사 공식 검토 회신서 표지에 따라 서비스명을 `FANUZU`(국문 `팬우주`)로 교체하고, 종전 CD-01(`Fancake` 확정)을 철회했다 |
|
||||
| 2026-08-08 | **당시 CD-01 기준 정리.** 인수 데모를 근거로 `Fancake`를 확정값으로 보아 서비스명 확인 질문을 삭제했다. 이후 발주사 공식 회신으로 이 판단이 철회됐으며 위 2026-08-11 행이 현재값이다 |
|
||||
|
||||