diff --git a/app/firebase/firestore.rules b/app/firebase/firestore.rules index d3d8e6b..8f822cd 100644 --- a/app/firebase/firestore.rules +++ b/app/firebase/firestore.rules @@ -228,7 +228,7 @@ service cloud.firestore { // 생성 주체를 Auth 트리거 하나로 고정한다. allow create: if false; // 클라이언트가 바꿀 수 있는 것은 프로필 표시 항목뿐이다. - // `nickname` 은 설계서 v1.1 §2.1 이 정한 프로필 표시 이름이다(원장 B-52 — + // `nickname` 은 설계서 v1.1 §2.1 이 정한 프로필 표시 이름이다(정책 원장 B-52 — // 2~20자 · 중복 허용 · 유니크 인덱스 제거). 목록에 없으면 MY 의 닉네임 변경이 // permission-denied 로 떨어지는데, 원인이 화면처럼 보여 찾기 어렵다. // `displayName` 은 Auth 트리거가 처음 채우는 필드라(§8.5 fn-auth-oncreate) 함께 남긴다. @@ -342,7 +342,7 @@ service cloud.firestore { } // 결제 기록. `fn-iap-verify` 가 쓰고 환불 회수는 `fn-iap-refund-webhook` 가 바꾼다. - // 화면: MY/설정/서비스 결제 내역 · 상세 · 환불 요청 (원장 B-02 · B-72) + // 화면: MY/설정/서비스 결제 내역 · 상세 · 환불 요청 (정책 원장 B-02 · B-72) // 본인이 할 수 있는 것은 **지급 완료 → 환불 요청** 한 가지 전이뿐이다. 금액·상태의 // 다른 전이(승인·반려·회수)는 Admin 과 서버가 한다. match /users/{uid}/purchases/{purchaseId} { @@ -511,7 +511,7 @@ service cloud.firestore { allow write: if false; // 수령은 fn-herodrop-claim 경유 } - // Hero Gift 상품 Catalog — 원장 B-85. 문서 ID = 스토어 상품 ID. + // Hero Gift 상품 Catalog — 정책 원장 B-85. 문서 ID = 스토어 상품 ID. // 화면: 홈 「아이템 구입」 · Admin `관리 마스터_아이템 관리`(fnAdminWrite 경유) // 지급량(buyerPop · dropPop)은 `fn-iap-verify` 가 여기서 읽으므로 클라이언트 쓰기를 막는다. match /heroGiftProducts/{productId} { @@ -529,7 +529,7 @@ service cloud.firestore { // ───────────────────────── 4. 퀘스트 · 장소 · 브리핑 ───────────────────────── // 퀘스트 정의·보상 파라미터. 화면: S-H-HOME · S-AD-QUEST - // 위치 Quest 의 QR 코드(원장 C-03) — 생성자가 현장에 붙일 QR 을 띄우려고 읽는다. 참여자가 읽으면 + // 위치 Quest 의 QR 코드(정책 원장 C-03) — 생성자가 현장에 붙일 QR 을 띄우려고 읽는다. 참여자가 읽으면 // 찍지 않고 값을 보낼 수 있으므로 생성자 · 조회 등급만. `fn-quest-create` 가 발급하고 고치지 않는다. match /questSecrets/{questId} { allow read: if signedIn() && (resource.data.authorUid == request.auth.uid || isViewer()); diff --git a/app/firebase/functions/src/account/account.ts b/app/firebase/functions/src/account/account.ts index 8ceae9b..be09239 100644 --- a/app/firebase/functions/src/account/account.ts +++ b/app/firebase/functions/src/account/account.ts @@ -86,12 +86,12 @@ export async function planetColorKeys(): Promise { /** * 운영값 기본 — 🟡 `config/policy` 가 덮는다. - * - 성운 전환: 원장 B-10 「10명 이상 + 1,000 POP 이상 + 기본 목표 7일」. + * - 성운 전환: 정책 원장 B-10 「10명 이상 + 1,000 POP 이상 + 기본 목표 7일」. * `nebulaConversionDeadlineEnforced` — 7일을 넘긴 성운의 전환을 막을지. B-10 「미달성 시 유지」와 * 「7일 이내 판정」이 갈려 발주사 확인 중이며, 회신 전에는 막지 않는다(false). - * - 대표 팬덤: 원장 C-15 「변경 무료 · 재변경까지 초기 30일 대기 · 운영자 조정 가능」. + * - 대표 팬덤: 정책 원장 C-15 「변경 무료 · 재변경까지 초기 30일 대기 · 운영자 조정 가능」. * - `primaryOnLeave` — 대표 팬덤에서 탈퇴할 때 남은 가입 팬덤 가운데 무엇을 대표로 둘지. - * 원장 C-101 확정값은 `latest`(가장 최근 가입). `earliest` · `none` 은 확정 전 남겨 둔 운영 스위치. + * 정책 원장 C-101 확정값은 `latest`(가장 최근 가입). `earliest` · `none` 은 확정 전 남겨 둔 운영 스위치. * 자동 지정은 C-15 대기에 세지 않는다. */ const FANDOM_DEFAULTS = { @@ -122,7 +122,7 @@ function primaryOf(user: Data): string | null { } /** - * 대표 팬덤을 [fandomId] 로 바꾸는 필드 — 원장 C-15. + * 대표 팬덤을 [fandomId] 로 바꾸는 필드 — 정책 원장 C-15. * * 대표가 없던 계정의 첫 지정은 「변경」이 아니므로 대기를 걸지 않고 시각도 찍지 않는다. * 이미 대표가 있으면 `primaryChangedAt + 대기일` 이 지나야 바뀐다. `patch` 가 null 이면 @@ -145,7 +145,7 @@ function primaryPatch(user: Data, fandomId: string, cooldownDays: number, now: n /** * 멤버십 한 건을 트랜잭션 안에서 쓴다. 호출 측이 「아직 멤버가 아님」을 확인한 뒤에만 부른다. * - * - `fandoms/{id}/members/{uid}` — 가입 사실 · 가입 순번(`memberNo`, 원장 B-68 #001~ 기록). + * - `fandoms/{id}/members/{uid}` — 가입 사실 · 가입 순번(`memberNo`, 정책 원장 B-68 #001~ 기록). * 순번은 탈퇴로 줄지 않는 `memberSeq` 에서 뽑아 재사용하지 않는다. * - `users/{uid}/memberships/{id}` — 「가입 또는 생성한 행성/성운」 목록(명세 §2.58)의 원천. * - `memberCount` 는 같은 트랜잭션에서 올린다 — 전환 조건(B-10)이 이 값을 읽는다. @@ -183,7 +183,7 @@ function writeJoin(tx: Transaction, uid: string, fandomId: string, fandom: Data) } /** - * `fn-nebula-create` — 성운(Stage0) 생성. 원장 B-08 · B-09 · B-10 · C-15. + * `fn-nebula-create` — 성운(Stage0) 생성. 정책 원장 B-08 · B-09 · B-10 · C-15. * * 팬덤 문서 자체가 공용 성운/행성이다(팬덤 1 : 행성 1). 만든 사람은 창립 멤버 #001 이다. * 확인 다이얼로그 「선택한 행성을 기본 행성으로 변경할까요?」대로 대표 팬덤을 바꾸되, C-15 @@ -237,7 +237,7 @@ export const fnNebulaCreate = onCall(async (req) => { }); /** - * `fn-fandom-join` — 행성/성운 가입. 원장 C-15 「복수 팬덤 가입」. + * `fn-fandom-join` — 행성/성운 가입. 정책 원장 C-15 「복수 팬덤 가입」. * * 이미 멤버면 아무것도 쓰지 않는다(두 번 눌러도 `memberCount` 는 한 번만 오른다). * 대표 팬덤이 없던 계정은 처음 가입한 팬덤이 대표가 된다 — 그 외에는 대표를 바꾸지 않는다. @@ -276,10 +276,10 @@ export const fnFandomJoin = onCall(async (req) => { * 과거 기여 · Passport 기록(원장 · growthEvents · popTransactions)은 지우지 않는다(C-15). * 순번은 `memberSeq` 를 되돌리지 않으므로 다른 사용자에게 다시 주지 않는다(C-100 · B-68). * - * - 성운(Stage0) 창립자도 탄생 전 떠날 수 있다(원장 C-100). 탄생 권한(`founderUid`)은 남은 멤버 + * - 성운(Stage0) 창립자도 탄생 전 떠날 수 있다(정책 원장 C-100). 탄생 권한(`founderUid`)은 남은 멤버 * 가운데 `joinedAt` 이 가장 이른 사용자에게 넘기고 알림을 남긴다. 남은 멤버가 없으면 비워 두고 * 다음 가입자가 이어받는다(원장 S20 확인 전 유지 기준). `ownerUid`(행성 문서) 등 최초 생성자 기록은 그대로다. - * - 대표 팬덤을 떠나면 남은 팬덤 중 가장 최근 가입한 팬덤을 대표로 둔다(원장 C-101). 자동 지정이라 + * - 대표 팬덤을 떠나면 남은 팬덤 중 가장 최근 가입한 팬덤을 대표로 둔다(정책 원장 C-101). 자동 지정이라 * `primaryChangedAt` 을 찍지 않는다 — 사용자의 다음 직접 변경을 30일 대기로 막지 않는다. */ export const fnFandomLeave = onCall(async (req) => { @@ -343,7 +343,7 @@ export const fnFandomLeave = onCall(async (req) => { }); /** - * `fn-primary-fandom-set` — 대표 팬덤 변경. 원장 C-15 · 명세 §2.56 「기본 행성 변경」. + * `fn-primary-fandom-set` — 대표 팬덤 변경. 정책 원장 C-15 · 명세 §2.56 「기본 행성 변경」. * * 가입한 팬덤만 대표가 될 수 있다. 변경은 무료이고, 재변경은 `primaryChangedAt + 30일` * (운영값 `primaryCooldownDays`) 이 지나야 한다. 과거 기여 · Passport 는 건드리지 않는다. @@ -369,11 +369,11 @@ export const fnPrimaryFandomSet = onCall(async (req) => { }); /** - * `fn-planet-create` — 성운(Stage0) → 정식 행성(Stage1) 탄생. 원장 B-09 · B-10 · C-50. + * `fn-planet-create` — 성운(Stage0) → 정식 행성(Stage1) 탄생. 정책 원장 B-09 · B-10 · C-50. * * 형태(`planetType`)와 컬러(`colorKey`) 두 가지만 이때 한 번 고른다. 행성 문서 ID 는 * 팬덤 안에서 고정(`planet`)이라 팬덤당 1건이 문서 ID 충돌로 원자적으로 지켜진다 - * (Firestore 설계서 §2.4 정정). 렌더링이 팬덤 문서 하나로 끝나도록 두 값을 팬덤 문서에도 + * (Firestore 설계서 §2.4). 렌더링이 팬덤 문서 하나로 끝나도록 두 값을 팬덤 문서에도 * 싣는다. 생성 후 두 값을 바꾸는 경로는 없다(보안 규칙이 클라이언트 쓰기를 전부 닫음). * * 전환 조건(인원 · POP)은 **트랜잭션 안에서 읽은** 팬덤 문서로 판정한다 — 가입 · 탈퇴와 @@ -505,7 +505,7 @@ export async function hasLocationConsent(uid: string): Promise { export const SIGNUP_REQUIRED_TERMS = ['service', 'privacy', 'age'] as const; /** - * `fn-signup-consent` — 가입 약관 동의를 한 번에 기록한다(FR-CMN-04 · 원장 C-134). + * `fn-signup-consent` — 가입 약관 동의를 한 번에 기록한다(FR-CMN-04 · 정책 원장 C-134). * * 항목마다 `consents` 에 append-only 행을 남기고(약관 버전 포함), 사용자 문서에 `termsAgreedAt` 을 * 찍는다. 앱은 이 값이 없으면 로그인 직후 온보딩(약관부터)으로 보낸다 — 「로그인」 버튼으로 처음 diff --git a/app/firebase/functions/src/account/referral.ts b/app/firebase/functions/src/account/referral.ts index b88dce4..4f5a95f 100644 --- a/app/firebase/functions/src/account/referral.ts +++ b/app/firebase/functions/src/account/referral.ts @@ -67,7 +67,7 @@ export async function inviteCodeOf(uid: string): Promise { /** `fn-invite-code-get` — 내 초대 코드(없으면 만들어서). 공유 링크 `?ref=` 에 붙는다. */ export const fnInviteCodeGet = onCall(async (req) => ({ code: await inviteCodeOf(callerUid(req)) })); -/** 추천 보상 보류 시간 — 원장 **B-71** 「기록 → 보상 pending/24h」. 지난 뒤 정산 작업이 확정한다. */ +/** 추천 보상 보류 시간 — 정책 원장 **B-71** 「기록 → 보상 pending/24h」. 지난 뒤 정산 작업이 확정한다. */ export const REFERRAL_HOLD_MS = 24 * 60 * 60 * 1000; /** @@ -109,7 +109,7 @@ export const fnReferralAttribute = onCall(async (req) => { // 자기 추천. 계정 하나로 POP 을 만들어 내는 가장 쉬운 길이다. fail(ERR.questAbnormal, '본인 추천 코드는 쓸 수 없습니다.'); } - // 추천인 1명당 하루 보상 상한은 두지 않는다 — 10/3 통합회신 7항 「삭제」(원장 C-120). + // 추천인 1명당 하루 보상 상한은 두지 않는다 — 10/3 통합회신 7항 「삭제」(정책 원장 C-120). // 자기 · 상호 · 중복 · 이상 패턴은 아래 판정과 Risk Engine 이 맡는다. const { referralAttachDays } = await loadPolicy(); @@ -244,7 +244,7 @@ export async function settleReferral(uid: string): Promise<'paid' | 'released' | uid: referrerUid, fandomId: fandomOfUser(referrer), axis: 'referral', units: 1, activity: 'referral', sourceId: referralFpiSource(fresh), occurredAt: new Date(), refPath: `referrals/${fresh.get('attributionId')}`, - // 추천인은 이 활동을 직접 하지 않았다 — EXP · DAU 에서 뺀다(리뷰 M5). FPI REFERRAL 축만. + // 추천인은 이 활동을 직접 하지 않았다 — EXP · DAU 에서 뺀다. FPI REFERRAL 축만. grantsExp: false, }, referrer); }); diff --git a/app/firebase/functions/src/account/translation.ts b/app/firebase/functions/src/account/translation.ts index 2726901..915205b 100644 --- a/app/firebase/functions/src/account/translation.ts +++ b/app/firebase/functions/src/account/translation.ts @@ -66,7 +66,7 @@ export function parseTranslationInput(data: unknown): TranslationInput { } /** - * 등록 시점 판정 — 원장 B-63 「중복·도배 제외 · 일일상한 초과분 미지급」. + * 등록 시점 판정 — 정책 원장 B-63 「중복·도배 제외 · 일일상한 초과분 미지급」. * 상한의 [TRANSLATION_HARD_LIMIT_FACTOR] 배를 넘으면 등록 자체를 거부(E-TRN-02), * 상한까지는 보상, 그 사이는 등록만 받고 보상 0. */ diff --git a/app/firebase/functions/src/admin/admin.ts b/app/firebase/functions/src/admin/admin.ts index 7a32a3f..43fdffa 100644 --- a/app/firebase/functions/src/admin/admin.ts +++ b/app/firebase/functions/src/admin/admin.ts @@ -15,7 +15,7 @@ import { voidCompletions } from '../ledger/fpi.js'; */ /** - * 원장 · 지표 · 감사 기록은 이 경로로 못 쓴다 — **master 도 불가**(리뷰 H3). + * 원장 · 지표 · 감사 기록은 이 경로로 못 쓴다 — **master 도 불가**. * 반드시 원장 · 집계 함수를 지나야 하고, 감사 로그는 고칠 수 없어야 감사 로그다. */ const FORBIDDEN = [ diff --git a/app/firebase/functions/src/campaign/create.ts b/app/firebase/functions/src/campaign/create.ts index 5405c9f..755227a 100644 --- a/app/firebase/functions/src/campaign/create.ts +++ b/app/firebase/functions/src/campaign/create.ts @@ -10,7 +10,7 @@ import { verifiedQuestCountOf } from '../ledger/fpi.js'; import { publicNameOf } from '../core/names.js'; /** - * `fn-campaign-create` — 사용자 캠페인 개설(원장 B-56 자격 · B-57 등록비). + * `fn-campaign-create` — 사용자 캠페인 개설(정책 원장 B-56 자격 · B-57 등록비). * * 예전에는 앱이 `campaigns` 에 `draft` 로 직접 썼는데 개설자는 status 를 바꿀 수 없어 * 심사(`review`)로 넘어갈 길이 없었다. 개설 · 등록비 차감 · `review` 진입을 diff --git a/app/firebase/functions/src/campaign/settle.ts b/app/firebase/functions/src/campaign/settle.ts index a067e2d..cd753a9 100644 --- a/app/firebase/functions/src/campaign/settle.ts +++ b/app/firebase/functions/src/campaign/settle.ts @@ -25,7 +25,7 @@ const REFUND_DEADLINE_HOURS = 72; /** * 자동 환불 패스 상한. 실패가 남은 패스가 이만큼 쌓이면 Admin Exception Queue 로 넘기고 - * 자동 재시도를 멈춘다(원장 B-36 「부분 실패는 자동 재시도 후 Admin Exception Queue」). + * 자동 재시도를 멈춘다(정책 원장 B-36 「부분 실패는 자동 재시도 후 Admin Exception Queue」). */ export const MAX_AUTO_REFUND_ATTEMPTS = 3; @@ -369,7 +369,7 @@ export const fnCampaignRefundBatch = onCall({ timeoutSeconds: TIMEOUT_SECONDS }, return refundCampaign(str(req.data, 'campaignId'), { manual: true }); }); -/** 결과보고 사진·영수증 상한 — 원장 B-44 「이미지 5장 · 10MB」. 용량·형식은 Storage 규칙이 본다. */ +/** 결과보고 사진·영수증 상한 — 정책 원장 B-44 「이미지 5장 · 10MB」. 용량·형식은 Storage 규칙이 본다. */ export const REPORT_MAX_PHOTOS = 5; const REPORT_MAX_CHARS = 5000; const BUDGET_MAX_LINES = 50; @@ -379,7 +379,7 @@ const CAMPAIGN_ID = /^[A-Za-z0-9_-]{1,128}$/; export interface BudgetLine { label: string; amount: number } /** - * `fn-campaign-report-submit` — 성공 캠페인 결과보고(원장 B-35 · FR-ADM-05 · FR-CMP-06). + * `fn-campaign-report-submit` — 성공 캠페인 결과보고(정책 원장 B-35 · FR-ADM-05 · FR-CMP-06). * * **운영(master·ops)만 올린다.** B-35 는 funded 이후 집행·증빙·결과보고를 Admin 수동 운영으로 * 두었다. 집행 사진 · 영수증 · 예산 사용 내역을 함께 받아 보고와 상태 `report` 를 한 diff --git a/app/firebase/functions/src/core/config.ts b/app/firebase/functions/src/core/config.ts index 9705e1e..d0eb1ef 100644 --- a/app/firebase/functions/src/core/config.ts +++ b/app/firebase/functions/src/core/config.ts @@ -10,22 +10,22 @@ export interface PolicyConfig { /** 출석 1회 보상 활동 POP. */ attendPop: number; /** - * 사용자 생성 Quest 의 광고 추가 보상 — 원장 **C-02** 고정형 **기여 POP** / 회. + * 사용자 생성 Quest 의 광고 추가 보상 — 정책 원장 **C-02** 고정형 **기여 POP** / 회. * 기본 보상액에 비례하지 않고 회사 재원이다(B-28). */ adRewardPop: number; /** - * 운영(관리자) Quest 의 광고 배수 — 원장 **B-03** 기본 x2. 지정 Quest 는 + * 운영(관리자) Quest 의 광고 배수 — 정책 원장 **B-03** 기본 x2. 지정 Quest 는 * `quests/{id}.adMultiplier`(예: 3)가 덮는다. 지급은 **증분만**(기본 보상 × (배수 − 1)) * 기여 POP 이다. */ adMultiplier: number; /** - * POP 묶음 상품(`pop.support.*`) 판매 스위치 — 원장 **B-02** 「POP 자체 직접 판매 없음」 + * POP 묶음 상품(`pop.support.*`) 판매 스위치 — 정책 원장 **B-02** 「POP 자체 직접 판매 없음」 * 이라 기본 false. 켜지 않으면 `fn-iap-verify` 가 E-PAY-01 로 거부한다. */ popPackSalesEnabled: boolean; - /** Focus 구독 일일 Claim 기여 POP — 원장 **B-04** 확정 40. */ + /** Focus 구독 일일 Claim 기여 POP — 정책 원장 **B-04** 확정 40. */ focusDailyPop: number; /** 광고 보상 일일 상한 횟수 — E-AD-02 판정 기준. */ adDailyLimit: number; @@ -111,14 +111,14 @@ export interface PolicyConfig { * HUD 「LIVE」 = 최근 이 분(分) 안에 앱을 쓴 팬덤 멤버 수 — 해석 확인 요청 **Q-GM-03** A안. 🟡 운영값 5. */ liveWindowMinutes: number; - /** 추천 보상 — 원장 **B-69** 확정 추천인 일반 POP 100 · 피추천인 20. 피추천인 최초 검증 활동 뒤 지급. */ + /** 추천 보상 — 정책 원장 **B-69** 확정 추천인 일반 POP 100 · 피추천인 20. 피추천인 최초 검증 활동 뒤 지급. */ referralReferrerPop: number; referralReferredPop: number; /** 가입 후 추천 코드를 등록할 수 있는 기간(일). 🟡 운영값 7. */ referralAttachDays: number; /** 긴급 돌발 타임어택 지속 시간 상한(분) — 원장 「최대 24시간」. 🟡 운영값. */ timeAttackMaxMinutes: number; - /** Music Shake 판정 · 성공 · 보상 운영값(원장 B-24). 아래 [MusicShakePolicy] 참고. */ + /** Music Shake 판정 · 성공 · 보상 운영값(정책 원장 B-24). 아래 [MusicShakePolicy] 참고. */ musicShake: MusicShakePolicy; } @@ -128,7 +128,7 @@ export type ShakeGrade = 'S' | 'A' | 'B' | 'C' | 'F'; /** * Music Shake 운영값 — `config/policy.musicShake`. * - * **🟡 임시값 — 회신 대기. 근거 없음.** 원장 B-24 가 확정한 것은 「BPM 기반 Shake 판정이 성공 / + * **🟡 임시값 — 회신 대기. 근거 없음.** 정책 원장 B-24 가 확정한 것은 「BPM 기반 Shake 판정이 성공 / * 보상 기준 · 완청 보상 아님 · 보상액은 운영값」뿐이다. 판정 창 · 성공 기준 · 등급(S/A/B/C) · * 등급 배수는 PRD-04 · 상세정의 v1.6.1 §20 · 별첨 B 어디에도 수치가 없어 **저희가 임시로 둔 * 값**이며, 해석 확인 요청 「2026-10-07 Music Shake」(Q-MS-04 · Q-MS-05) 회신으로 바꾼다. diff --git a/app/firebase/functions/src/fandom/eclipse_target.ts b/app/firebase/functions/src/fandom/eclipse_target.ts index 3ddd343..b7e4ba0 100644 --- a/app/firebase/functions/src/fandom/eclipse_target.ts +++ b/app/firebase/functions/src/fandom/eclipse_target.ts @@ -3,7 +3,7 @@ import { dayKey } from '../core/time.js'; /** * 2026-09-16 EC02·EC05: 누락된 집계를 0으로 채우지 않는다. - * 원장 C-26 `target = max(1, ceil(avgDau × rate))` · C-27 생성 후 완료 집계일 ≤ [windowDays] 일 평균 · + * 정책 원장 C-26 `target = max(1, ceil(avgDau × rate))` · C-27 생성 후 완료 집계일 ≤ [windowDays] 일 평균 · * C-21 집계 기간은 운영값(기본 7일). */ export async function eclipseTarget( diff --git a/app/firebase/functions/src/fandom/evolution.ts b/app/firebase/functions/src/fandom/evolution.ts index 0443797..00c7b4f 100644 --- a/app/firebase/functions/src/fandom/evolution.ts +++ b/app/firebase/functions/src/fandom/evolution.ts @@ -102,7 +102,7 @@ const FINAL_STAGE = 6; export interface Promotion { fandomId: string; fromStage: number; toStage: number } /** - * 정식 행성 Level · Stage 판정 — 수치표 v1.4(`planet_level.ts`) · 원장 **B-11 · B-53 · B-54**. + * 정식 행성 Level · Stage 판정 — 수치표 v1.4(`planet_level.ts`) · 정책 원장 **B-11 · B-53 · B-54**. * * - 레벨업 POP 진행도(`levelPop`) = 생성 기준점 이후 Growth POP 증가분 × ZAP 참여도 가중치(`zapWeight`, * 일일 마감이 정함). 가중치는 진행도에만 쓰고 `growthPop`(원장 누계)은 그대로 둔다. @@ -156,7 +156,7 @@ export async function promoteStages(fandomIds?: string[]): Promise if (!result) continue; const { name, ...promotion } = result; promoted.push(promotion); - // 원장 C-78 ③ 성취 · C-77 「Evolution」 메시지. 발송 실패는 승급을 되돌리지 않는다. + // 정책 원장 C-78 ③ 성취 · C-77 「Evolution」 메시지. 발송 실패는 승급을 되돌리지 않는다. await sendFandomPush({ fandomId: ref.id, category: 'fandomEvent', diff --git a/app/firebase/functions/src/fandom/fandom.ts b/app/firebase/functions/src/fandom/fandom.ts index df13147..4320dac 100644 --- a/app/firebase/functions/src/fandom/fandom.ts +++ b/app/firebase/functions/src/fandom/fandom.ts @@ -156,7 +156,7 @@ export async function defendEclipse(input: { const room = Math.max(0, settings.perUserMaxDefense - myTotal); const accepted = Math.min(shakes, settings.perCallMaxShakes, room); const clamped = shakes - accepted; - // 원장 C-25: 서버가 인정한 인정 횟수(기본 60회, 운영값)에 처음 도달할 때 회차당 한 명만 센다. + // 정책 원장 C-25: 서버가 인정한 인정 횟수(기본 60회, 운영값)에 처음 도달할 때 회차당 한 명만 센다. // 회차 문서에 고정한 값을 쓴다 — 진행 중 운영값 변경이 같은 회차의 기준을 바꾸지 않는다. const need = (eclipse.get('qualifyShakes') as number | undefined) ?? settings.qualifyShakes; const qualified = myTotal < need && myTotal + accepted >= need; @@ -204,7 +204,7 @@ export async function defendEclipse(input: { }); if (result.accepted === 0) return result; - // **성공 판정을 1 분 집계에만 맡기지 않는다**(검수 D2). 목표에 닿은 순간 세워야 + // **성공 판정을 1 분 집계에만 맡기지 않는다**. 목표에 닿은 순간 세워야 // 그 뒤의 참여가 헛돌지 않고, 화면이 성공을 곧 본다. await markDefendedIfReached(eclipseRef); return result; @@ -300,22 +300,22 @@ export interface EclipseSettings { /** 이 팬덤에 이클립스를 발동하는가. Admin 「활성화」. */ enabled: boolean; /** - * 공통 발생 일정 — 원장 **C-24** · 설계서 v1.2 `config/eclipse.schedule[]`. + * 공통 발생 일정 — 정책 원장 **C-24** · 설계서 v1.2 `config/eclipse.schedule[]`. * 배열 길이가 곧 하루 회차 수다(**하루 2회는 고정 제한이 아니다**). 팬덤 문서의 * `eclipseSchedule[]` 이 있으면 그것이 우선한다. */ schedule: EclipseSlot[]; /** 1회 발동당 FPI 침식률(%). Admin 「FPI 감소 수치」 · 수치표 v1.4 「침식 0.25% FPI」. */ erosionPercent: number; - /** 사전 알림 시간(초). 원장 C-22 「발생 3분 전 푸시」 = 180. `warnAt` 에 발송한다. */ + /** 사전 알림 시간(초). 정책 원장 C-22 「발생 3분 전 푸시」 = 180. `warnAt` 에 발송한다. */ warnSeconds: number; - /** 평균 DAU 대비 고유 유효 참여자 목표율. 원장 C-25 기본 5%. */ + /** 평균 DAU 대비 고유 유효 참여자 목표율. 정책 원장 C-25 기본 5%. */ targetRate: number; - /** 목표 분모의 DAU 평균 기간(일). 원장 C-21 기본 7일 · 운영값. */ + /** 목표 분모의 DAU 평균 기간(일). 정책 원장 C-21 기본 7일 · 운영값. */ dauWindowDays: number; - /** 공통 방어 창(초). 원장 C-22 기본 60초 · 운영값. */ + /** 공통 방어 창(초). 정책 원장 C-22 기본 60초 · 운영값. */ windowSec: number; - /** 고유 참여자 1명으로 인정하는 개인 유효 Shake 누적. 원장 C-25 기본 60회 · 운영값. */ + /** 고유 참여자 1명으로 인정하는 개인 유효 Shake 누적. 정책 원장 C-25 기본 60회 · 운영값. */ qualifyShakes: number; /** 한 호출이 실을 수 있는 최대 흔들기 수. 서버 상한이며 단말 값을 믿지 않는다. */ perCallMaxShakes: number; @@ -391,7 +391,7 @@ function kstMoment(day: string, slot: EclipseSlot | undefined): Date | null { /** * `fn-eclipse-trigger` — 이클립스 발동 · 사전 알림 · 종료. * - * 원장 **C-24** — *기본 KST 07:50 · 17:50, 팬덤별 발생 횟수 · 각 시각 별도 설정, 미설정 시 + * 정책 원장 **C-24** — *기본 KST 07:50 · 17:50, 팬덤별 발생 횟수 · 각 시각 별도 설정, 미설정 시 * 공통값. 하루 2회는 고정 제한이 아님. 일정 변경 시 푸시 · 종료 시각 함께 계산*. * * **중복 발동을 조건문으로 막지 않는다.** 문서 ID 를 `{KST 날짜}-{회차}` 로 고정하고 @@ -400,7 +400,7 @@ function kstMoment(day: string, slot: EclipseSlot | undefined): Date | null { * * **사전 알림 시각부터 문서를 만들고 `status: 'scheduled'` 로 둔다.** 발동 시각에야 * 문서를 만들면 `warnAt` 이 이미 지난 시각이라 사전 알림을 보낼 수 없다. 푸시 - * 예고(원장 C-22 「발생 3분 전 푸시」)는 이 함수가 `warnAt` 에 바로 보내고, 앱의 경보 + * 예고(정책 원장 C-22 「발생 3분 전 푸시」)는 이 함수가 `warnAt` 에 바로 보내고, 앱의 경보 * 배너는 `active` 만 구독하므로 시작 전에 배너가 뜨지는 않는다. * * 시작 시 FPI 스냅샷만 남기고, 마감 후 실패 확정 때에만 그 기준으로 차감한다. @@ -602,7 +602,7 @@ async function closeExpiredEclipses( occurredAt, createdAt: FieldValue.serverTimestamp(), }); tx.update(fandomRef, { - // FPI 는 0 아래로 내려가지 않는다(리뷰 M2). 감소량 기록(penalty)은 그대로 남긴다. + // FPI 는 0 아래로 내려가지 않는다. 감소량 기록(penalty)은 그대로 남긴다. fpiScore: Math.max(0, (fandom.get('fpiScore') as number) - penalty), eclipsePenaltyRevision: FieldValue.increment(1), }); @@ -938,7 +938,7 @@ export async function closeDay(now: Date = new Date()): Promise<{ day: string; f tx.set(fandom.ref, { sfpi: FieldValue.increment(fpiScore) }, { merge: true }); }); } - // 원장 B-54 — sFPI 누적 직후 누적 Growth POP 과 함께 Stage 승급을 판정한다. + // 정책 원장 B-54 — sFPI 누적 직후 누적 Growth POP 과 함께 Stage 승급을 판정한다. const promoted = await promoteStages(); return { day, fandoms: fandoms.size, promoted: promoted.length }; } @@ -1134,7 +1134,7 @@ export const fnCampaignFund = onCall(async (req) => fundCampaign({ /** * 후원 한 건의 코어. 테스트가 HTTP 왕복 없이 부르도록 떼어 둔다. * - * - **기여 POP 만 쓴다**(원장 B-33). 클라이언트가 보낸 `popType` 을 따르지 않는다. + * - **기여 POP 만 쓴다**(정책 원장 B-33). 클라이언트가 보낸 `popType` 을 따르지 않는다. * - 목표는 규칙·설계서의 스키마 이름 `targetPop` 이다. `goalPop` 은 옛 이름이라 보조로만 * 읽는다 — 예전에는 `goalPop` 만 읽어 목표 초과 차단(E-CMP-01)이 한 번도 걸리지 않았다. * - 성장 귀속 팬덤은 **캠페인 문서의 `fandomId`** 다. 클라이언트가 보낸 값을 믿지 않는다. diff --git a/app/firebase/functions/src/fandom/herodrop.ts b/app/firebase/functions/src/fandom/herodrop.ts index a1fb189..b6045a3 100644 --- a/app/firebase/functions/src/fandom/herodrop.ts +++ b/app/firebase/functions/src/fandom/herodrop.ts @@ -25,7 +25,7 @@ interface DropConfig { * 총량 하나에 운영값(1인 상한 % · 시간)을 붙여 Drop 규격을 만든다. * * [totalPop] 을 받으면 운영 기본 총량 대신 그 값을 쓴다 — Hero Gift 구매는 상품별 - * Admin 확정값이 총량이다(원장 B-84 · B-85). + * Admin 확정값이 총량이다(정책 원장 B-84 · B-85). */ export async function dropConfig(totalPop?: number): Promise { const snap = await getFirestore().doc('config/policy').get(); diff --git a/app/firebase/functions/src/ledger/ledger.ts b/app/firebase/functions/src/ledger/ledger.ts index b6b3b94..4f5de05 100644 --- a/app/firebase/functions/src/ledger/ledger.ts +++ b/app/firebase/functions/src/ledger/ledger.ts @@ -522,7 +522,7 @@ async function spendParts( if (growthSpend) { // FPI 는 기여 **행위 단위**다(C-34). 금액이 아니라 Zap 횟수(`fn-pop-spend` 가 서버에서 // 금액 ÷ zapPop 으로 계산) · 후원 1건. EXP 는 주지 않는다 — POP 을 쓰는 행위는 1 POP - // 단위로 쪼개 반복할 수 있어 건당 EXP 가 파밍된다(C-18 · 리뷰 B1). + // 단위로 쪼개 반복할 수 있어 건당 EXP 가 파밍된다(C-18). writeFpiEvent(tx, { uid: input.uid, fandomId: fandomId ?? null, axis: 'contribution', units: input.reasonCode === 'ZAP_CONTRIBUTION' ? input.count ?? 1 : 1, diff --git a/app/firebase/functions/src/ledger/pop.ts b/app/firebase/functions/src/ledger/pop.ts index e0c87a3..33474ab 100644 --- a/app/firebase/functions/src/ledger/pop.ts +++ b/app/firebase/functions/src/ledger/pop.ts @@ -47,7 +47,7 @@ export const fnPopGrant = onCall(async (req) => { * 사용자가 직접 부를 수 있는 차감 사유 — **허용 목록**. * * 다른 사유는 각자의 검증 함수를 지나야 한다. 예전에는 사유를 그대로 받아 `CAMPAIGN_FUND` - * 를 캠페인 검증 없이 쓰거나(B-33 · B-34 우회) 원장 사유를 위조할 수 있었다(리뷰 H1 · R03). + * 를 캠페인 검증 없이 쓰거나(B-33 · B-34 우회) 원장 사유를 위조할 수 있었다. */ const USER_SPEND_REASONS: readonly ReasonCode[] = ['ZAP_CONTRIBUTION']; @@ -55,9 +55,9 @@ const USER_SPEND_REASONS: readonly ReasonCode[] = ['ZAP_CONTRIBUTION']; * `fn-pop-spend` — 본인 계정에서만 차감한다. 지금 열린 사유는 Zap 하나다. * * **Zap 횟수는 서버가 센다** — `금액 ÷ zapPop`(운영값). 클라이언트 `count` 를 믿으면 - * 1 POP 으로 FPI CONTRIBUTION 축을 무제한 올린다(리뷰 B1). 귀속 팬덤은 Zap 한 행성 + * 1 POP 으로 FPI CONTRIBUTION 축을 무제한 올린다. 귀속 팬덤은 Zap 한 행성 * `fandomId` 다(상세정의 5장 · 10/3 #10). 원장이 같은 트랜잭션에서 가입 여부를 확인하므로 - * 가입하지 않은 행성은 거부된다(리뷰 M1 — 남의 행성 성장을 올리는 길을 막는다). + * 가입하지 않은 행성은 거부된다(남의 행성 성장을 올리는 길을 막는다). */ export const fnPopSpend = onCall(async (req) => { const uid = callerUid(req); diff --git a/app/firebase/functions/src/notify/push.ts b/app/firebase/functions/src/notify/push.ts index 014ca08..d3e7075 100644 --- a/app/firebase/functions/src/notify/push.ts +++ b/app/firebase/functions/src/notify/push.ts @@ -126,7 +126,7 @@ export async function sendUserPush( } /** - * 이클립스 사전 알림 — 원장 **C-22** 「발생 3분 전 푸시」 · C-24 「푸시 = 시작 − 3분」. + * 이클립스 사전 알림 — 정책 원장 **C-22** 「발생 3분 전 푸시」 · C-24 「푸시 = 시작 − 3분」. * * `fn-eclipse-trigger` 가 회차의 `warnAt`(시작 − `warnSeconds`) 이 지난 첫 점검에서 * 부른다. 활성화 뒤에 보내면 예고가 아니라 사후 통지가 된다. diff --git a/app/firebase/functions/src/payment/iap.ts b/app/firebase/functions/src/payment/iap.ts index 1335bdb..31ffe71 100644 --- a/app/firebase/functions/src/payment/iap.ts +++ b/app/firebase/functions/src/payment/iap.ts @@ -48,7 +48,7 @@ function platformOf(data: unknown): Platform { const iapKey = (claim: ReceiptClaim) => `iap:${claim.platform}:${claim.transactionId}`; -/** POP 묶음 상품 ID 접두 — 원장 B-02 로 기본 판매 중지(`popPackSalesEnabled`). */ +/** POP 묶음 상품 ID 접두 — 정책 원장 B-02 로 기본 판매 중지(`popPackSalesEnabled`). */ const POP_PACK_PREFIX = 'pop.support.'; const PAY_METHOD: Readonly> = { @@ -59,7 +59,7 @@ const PAY_METHOD: Readonly> = { interface HeroGiftProduct { name: string; buyerPop: number; dropPop: number } /** - * Hero Gift 상품 Catalog — 원장 B-85. 문서 ID 가 스토어 상품 ID 다. + * Hero Gift 상품 Catalog — 정책 원장 B-85. 문서 ID 가 스토어 상품 ID 다. * * Admin `관리 마스터_아이템 관리` 가 `fnAdminWrite` 로 쓴다. 상품을 늘려도 앱을 * 다시 배포하지 않는다(B-40 검증 기준). Catalog 에 없으면 null — 일반 상품 표로 넘어간다. @@ -408,7 +408,7 @@ export const fnSubscriptionWebhook = onCall(async (req) => { }); /** - * `fn-focus-daily-claim` — Focus 구독 혜택, 원장 **B-04**. + * `fn-focus-daily-claim` — Focus 구독 혜택, 정책 원장 **B-04**. * * *광고 제거 + 일 40 기여 POP, 당일 Claim, 미수령 누적 없음.* 하루는 서버 일 단위(KST * `dayKey`, 퀘스트 한도와 같은 경계)다. 멱등키가 `focus:{uid}:{day}` 라 같은 날 두 번 지급되지 diff --git a/app/firebase/functions/src/quest/link_preview.ts b/app/firebase/functions/src/quest/link_preview.ts index 183637f..4928ccf 100644 --- a/app/firebase/functions/src/quest/link_preview.ts +++ b/app/firebase/functions/src/quest/link_preview.ts @@ -9,7 +9,7 @@ import { getFirestore, Timestamp } from 'firebase-admin/firestore'; import { callerUid, str } from '../core/callable.js'; /** - * `fn-link-preview` — 링크 기반 Quest 의 원본 제목 · 썸네일 · 서비스명(원장 **C-05** · R10). + * `fn-link-preview` — 링크 기반 Quest 의 원본 제목 · 썸네일 · 서비스명(정책 원장 **C-05** · R10). * * 서버가 사용자가 준 URL 을 대신 여는 경로라 **SSRF 방어가 이 파일의 본체**다. * - http / https 만, 계정 정보(`user:pass@`) · 기본 외 포트 거부 diff --git a/app/firebase/functions/src/quest/quest.ts b/app/firebase/functions/src/quest/quest.ts index 868356b..34e1ab6 100644 --- a/app/firebase/functions/src/quest/quest.ts +++ b/app/firebase/functions/src/quest/quest.ts @@ -56,7 +56,7 @@ export async function dailyLimitOf(questId: string): Promise { } /** - * 참여할 퀘스트 문서를 확인한다 — 리뷰 H2 · B2. + * 참여할 퀘스트 문서를 확인한다. * * **문서가 없거나 유형이 다르면 거부한다.** 예전에는 `questId` 가 진행 문서 ID 로만 쓰여 * 있지도 않은 ID 를 바꿔 가며 부르면 일일 한도 · 1회 제한이 매번 새로 열렸다. @@ -140,7 +140,7 @@ function count(data: Record, key: string, max: number): number } /** - * `fn-quest-progress` — Quest 진행 상태(진행중 · 일시 정지 · 진행완료)를 진행 문서에 남긴다(원장 C-83). + * `fn-quest-progress` — Quest 진행 상태(진행중 · 일시 정지 · 진행완료)를 진행 문서에 남긴다(정책 원장 C-83). * * 화면을 다시 열거나 앱을 다시 켜도 「진행중」 탭 · 「이어하기」 · 목록 「보상받기」가 유지되게 하는 * **표시용 상태**다. 지급 판정은 여기서 하지 않는다 — 보상은 그대로 검증 함수가 증거로 판정한다. @@ -271,7 +271,7 @@ export const fnQuestAttend = onCall(async (req) => { }); /** - * `fn-quest-stream-participate` — Streaming · YouTube 시청 Quest **무보상 기본 참여**(원장 C-04 · B-25). + * `fn-quest-stream-participate` — Streaming · YouTube 시청 Quest **무보상 기본 참여**(정책 원장 C-04 · B-25). * * POP 원장을 쓰지 않는다(검증 기준 「POP 지급 원장 0건」). 참여 기록(`questProgress`)과 FPI 원천 * 이벤트만 같은 트랜잭션에 남긴다 — 보상이 없는 유효 활동도 FPI 에 들어간다(C-34). @@ -320,7 +320,7 @@ export const fnQuestVerifySteps = onCall(async (req) => { const at = occurredAt(req.data, now); const key = dayKey(at); // 퀘스트 문서가 있어야 하고 걸음 유형이어야 한다. 없는 ID 로 바꿔 부르면 진행 문서 · 멱등키가 - // 새로 열려 구간 보상이 다시 나간다(리뷰 재검토 blocker). 대표 팬덤 자격(C-16)도 본다. + // 새로 열려 구간 보상이 다시 나간다. 대표 팬덤 자격(C-16)도 본다. const questId = str(req.data, 'questId'); const quest = await questOf(uid, questId, ['walk', 'steps', 'health']); const steps = positiveInt(num(req.data, 'steps'), 'steps'); @@ -414,7 +414,7 @@ export const fnQuestVerifySteps = onCall(async (req) => { }); /** - * 광고 증분 — 원장 B-03 · C-02(B-28). + * 광고 증분 — 정책 원장 B-03 · C-02(B-28). * * - 사용자 생성 Quest(`authorUid` 있음): 고정 [PolicyConfig.adRewardPop] 기여 POP. * - 운영 Quest: 기본 보상 × (배수 − 1) 기여 POP. 배수는 Quest 지정값(`adMultiplier`, @@ -449,7 +449,7 @@ function questIdOfKey(key: unknown): string | null { * 앱은 방금 받은 **기본 보상의 원장 txId**(`baseTxId`)만 보낸다. 서버는 그 원장 행이 본인 · 오늘 · * 퀘스트 보상인지 확인하고, **AdMob 서버측 검증(SSV) 기록**(`adSsv`, [fnAdSsv])이 그 기본 보상에 * 붙어 있을 때만 증분을 기여 POP 으로 지급한다. 앱이 보낸 노출 ID 는 근거로 쓰지 않는다 — 함수만 - * 불러서는 기여 POP 이 나가지 않는다(원장 B-03 「광고 검증 실패 시 증분 미지급 · 횟수 미차감」). + * 불러서는 기여 POP 이 나가지 않는다(정책 원장 B-03 「광고 검증 실패 시 증분 미지급 · 횟수 미차감」). * 기본 보상 한 건에 광고 보상은 한 번이다(`users/{uid}/adBonuses/{baseTxId}`). * * - SSV 콜백이 아직 오지 않았으면 E-AD-03(횟수 미차감). 앱은 잠시 뒤 다시 부른다. diff --git a/app/firebase/functions/src/quest/verify.ts b/app/firebase/functions/src/quest/verify.ts index c37f697..98073db 100644 --- a/app/firebase/functions/src/quest/verify.ts +++ b/app/firebase/functions/src/quest/verify.ts @@ -52,7 +52,7 @@ export interface ShakeJudgement { export type { ShakeGrade }; /** - * 판정표 → 등급. F 면 실패(보상 0 — 원장 B-24). 문턱은 **운영값**(`config/policy.musicShake`)이며 + * 판정표 → 등급. F 면 실패(보상 0 — 정책 원장 B-24). 문턱은 **운영값**(`config/policy.musicShake`)이며 * 🟡 임시값 — 회신 대기다(근거 없음 · 해석 확인 요청 2026-10-07 Q-MS-05). */ export function gradeShake( @@ -93,7 +93,7 @@ export const fnQuestVerifyMusicShake = onCall(async (req) => { const now = new Date(); const data = (req.data ?? {}) as Record; // 곡과 하루 한도는 **퀘스트 문서**가 정한다. 예전 멱등키는 `trackId · 날짜` 라 곡 ID 만 - // 바꿔 부르면 하루에 무제한 지급됐다(리뷰 재검토 blocker). 곡은 퀘스트의 카탈로그와 대조하고, + // 바꿔 부르면 하루에 무제한 지급됐다. 곡은 퀘스트의 카탈로그와 대조하고, // 구간 수도 퀘스트에 정의돼 있으면 그 값만 받는다 — 판정표를 줄여 등급을 올리지 못하게. const questId = str(req.data, 'questId'); const quest = await questOf(uid, questId, ['musicshake', 'music_shake', 'shake']); @@ -160,7 +160,7 @@ export const fnQuestVerifyMusicShake = onCall(async (req) => { fail(ERR.questIncomplete, '박자 판정이 성공 기준에 미치지 못했습니다.'); } - // 원장 B-24 「보상액은 운영값」 — 퀘스트 문서(Admin 「설정 보상 POP」)가 정하고, 없으면 + // 정책 원장 B-24 「보상액은 운영값」 — 퀘스트 문서(Admin 「설정 보상 POP」)가 정하고, 없으면 // `config/policy.musicShake.pop` 으로 물러난다. 앱 화면도 같은 순서로 읽는다(`QuestType.rewardFrom`). // 등급 배수는 🟡 임시값 — 회신 대기(Q-MS-05). const base = quest.get('rewardPop'); @@ -224,7 +224,7 @@ export const fnQuestVerifyPlace = onCall(async (req) => { const lng = num(req.data, 'lng'); // 참여 횟수는 Quest 단위다(C-12 — 같은 장소의 다른 Quest 는 별개). 그래서 퀘스트 문서가 // 반드시 있어야 하고 **그 퀘스트의 장소**여야 한다 — 없는 ID 로 바꿔 부르면 1회 제한이 - // 매번 새로 열린다(리뷰 B2). 대표 팬덤 자격(C-16)도 여기서 본다. + // 매번 새로 열린다. 대표 팬덤 자격(C-16)도 여기서 본다. const questId = str(req.data, 'questId'); const questDoc = await questOf(uid, questId, ['place', 'location']); if (questDoc.get('authorUid') === uid) fail(ERR.permissionDenied, '내가 만든 퀘스트에는 참여할 수 없습니다.'); diff --git a/app/firebase/functions/test/admin_write.test.ts b/app/firebase/functions/test/admin_write.test.ts index 8111473..cc3e138 100644 --- a/app/firebase/functions/test/admin_write.test.ts +++ b/app/firebase/functions/test/admin_write.test.ts @@ -46,7 +46,7 @@ it('ops 는 master 전용이 아닌 문서는 그대로 쓴다 · 역할 없는 expect(await denials('admin-none')).toBe(1); }); -it('master 도 원장 · 지표 · 감사 기록과 서버 산출 필드는 못 쓴다 · Eclipse 운영값은 master 만(리뷰 H3)', async () => { +it('master 도 원장 · 지표 · 감사 기록과 서버 산출 필드는 못 쓴다 · Eclipse 운영값은 master 만', async () => { const denied = (role: string, path: string, patch?: Record) => expect(run(role, path, patch)).rejects.toMatchObject({ details: { code: 'E-PERM-01' } }); await denied('master', 'fandoms/f1/fpiEvents/e1'); diff --git a/app/firebase/functions/test/briefing.test.ts b/app/firebase/functions/test/briefing.test.ts index dff3b51..1e290ae 100644 --- a/app/firebase/functions/test/briefing.test.ts +++ b/app/firebase/functions/test/briefing.test.ts @@ -360,7 +360,7 @@ describe('푸시 토픽', () => { }); }); -describe('이클립스 사전 알림 — 시작 − 3분 (원장 C-22)', () => { +describe('이클립스 사전 알림 — 시작 − 3분 (정책 원장 C-22)', () => { /// 예약 회차 한 건을 심는다. [startInMin] 분 뒤 시작, 예고는 그 3분 전이다. async function round(id: string, startInMin: number | null, extra: Record = {}) { const ref = getFirestore().doc(`fandoms/${FANDOM}/eclipseRounds/${id}`); diff --git a/app/firebase/functions/test/eclipse.test.ts b/app/firebase/functions/test/eclipse.test.ts index b140fa9..f64a2cc 100644 --- a/app/firebase/functions/test/eclipse.test.ts +++ b/app/firebase/functions/test/eclipse.test.ts @@ -16,9 +16,9 @@ import { aggregateActiveEclipses, defendEclipse } from '../src/fandom/fandom.js' * 보는 것 * 1. **POP 이 움직이지 않는다** — 지갑과 원장이 그대로다. * 2. **분산 카운터 유실 0** — 샤드를 흩어도 합이 참여 총량과 같다(별첨 1 ① 예외 처리). - * 3. **멱등** — 재시도가 샤드·방어 로그·티커 이벤트를 두 번 올리지 않는다(검수 D1·D6). + * 3. **멱등** — 재시도가 샤드·방어 로그·티커 이벤트를 두 번 올리지 않는다. * 4. **서버 검증** — 단말이 보낸 흔들기 수를 상한과 속도로 다시 판정한다(별첨 1 ②). - * 5. **즉시 성공 판정** — 목표에 닿은 순간 `defended` 가 선다(검수 D2). + * 5. **즉시 성공 판정** — 목표에 닿은 순간 `defended` 가 선다. */ const FANDOM = 'test-ecl-fandom'; @@ -195,7 +195,7 @@ describe('분산 카운터 — 카운트 유실 0 (별첨 1 ① 예외 처리)', }); }); -describe('멱등 — 재시도가 이중 계산되지 않는다 (검수 D1·D6)', () => { +describe('멱등 — 재시도가 이중 계산되지 않는다', () => { it('같은 clientSeq 를 두 번 보내도 샤드·로그·티커가 한 번만 늘어난다', async () => { await seed(); const first = await defendEclipse({ @@ -338,7 +338,7 @@ describe('티커 이벤트 — Global Ticker 모양 (별첨 B)', () => { }); }); -describe('성공 판정 — 목표에 닿은 즉시 (검수 D2)', () => { +describe('성공 판정 — 목표에 닿은 즉시', () => { it('한 사람의 추가 Shake는 목표 두 명을 채울 수 없고 두 번째 60회에서만 성공한다', async () => { await seed({ target: 2, perCallMaxShakes: 60 }); const ref = getFirestore().doc(`fandoms/${FANDOM}/eclipseRounds/${ECLIPSE}`); @@ -485,7 +485,7 @@ describe('v1.5 유효 참여의 원자적 기록', () => { }); }); -describe('개인 인정 횟수는 운영값이다 (원장 C-25)', () => { +describe('개인 인정 횟수는 운영값이다 (정책 원장 C-25)', () => { it('회차에 고정된 인정 횟수로 판정하고 운영값 문서만 바꿔서는 진행 중 회차 기준이 바뀌지 않는다', async () => { await seed({ target: 1, perCallMaxShakes: 60, qualifyShakes: 60 }); const ref = getFirestore().doc(`fandoms/${FANDOM}/eclipseRounds/${ECLIPSE}`); diff --git a/app/firebase/functions/test/eclipse_trigger.test.ts b/app/firebase/functions/test/eclipse_trigger.test.ts index 4cff826..80c2d59 100644 --- a/app/firebase/functions/test/eclipse_trigger.test.ts +++ b/app/firebase/functions/test/eclipse_trigger.test.ts @@ -146,7 +146,7 @@ beforeEach(async () => { await wipeFandom(); }); -describe('발동 — 하루 정확히 2 회 (원장 B-55)', () => { +describe('발동 — 하루 정확히 2 회 (정책 원장 B-55)', () => { it('DAU 누락 회차는 취소 사유를 남기고 활성화·FPI 차감 없이 종료한다', async () => { await seedFandom(); await aggregateFpi(); @@ -278,7 +278,7 @@ describe('발동 — 하루 정확히 2 회 (원장 B-55)', () => { }); it('발동 시각·침식률을 운영값으로 바꾸면 재배포 없이 따라간다', async () => { - // 원장 B-55 검증 기준 「방어 수치가 Admin 에서 앱 재배포 없이 변경됨」. + // 정책 원장 B-55 검증 기준 「방어 수치가 Admin 에서 앱 재배포 없이 변경됨」. await seedFandom({ schedule: [{ hhmm: '06:30' }], erosionPercent: 1, warnSeconds: 60 }); await aggregateFpi(); @@ -318,14 +318,14 @@ describe('발동 — 하루 정확히 2 회 (원장 B-55)', () => { .get('status')).toBe('active'); await runEclipseSchedule(kst(DAY, '13:31')); - // 검수 D3 — 마감이 지나면 `ended` 로 닫아 배너 질의에서 빠진다. + // 마감이 지나면 `ended` 로 닫아 배너 질의에서 빠진다. expect((await getFirestore().doc(`fandoms/${FANDOM}/eclipseRounds/${DAY}-1`).get()) .get('status')).toBe('ended'); }); }); -describe('일정 — 배열 길이 = 하루 회차 수 · 팬덤별 우선 (원장 C-24)', () => { +describe('일정 — 배열 길이 = 하루 회차 수 · 팬덤별 우선 (정책 원장 C-24)', () => { const ids = async () => (await getFirestore().collection(`fandoms/${FANDOM}/eclipseRounds`).get()) .docs.map((d) => [d.id, (d.get('startAt') as Timestamp).toMillis()]).sort(); diff --git a/app/firebase/functions/test/fpi_snapshot.test.ts b/app/firebase/functions/test/fpi_snapshot.test.ts index 329636e..360110b 100644 --- a/app/firebase/functions/test/fpi_snapshot.test.ts +++ b/app/firebase/functions/test/fpi_snapshot.test.ts @@ -62,7 +62,7 @@ it('KST 자정 실행은 전날 키로 스냅샷 · DAU 를 남기고 DAU 는 expect((await ref.get()).get('sfpi')).toBe(50 + 125 * 3); }); -it('Eclipse 차감만 있는 팬덤은 FPI 0 아래로 내려가지 않고 랭킹 모집단에 들지 않는다(리뷰 M2)', async () => { +it('Eclipse 차감만 있는 팬덤은 FPI 0 아래로 내려가지 않고 랭킹 모집단에 들지 않는다', async () => { const ref = getFirestore().doc(`fandoms/${id}`); await ref.set({ stage: 1 }, { merge: true }); await ref.collection('fpiEvents').add({ diff --git a/app/firebase/functions/test/growth.test.ts b/app/firebase/functions/test/growth.test.ts index 780d9f2..5ac7cd5 100644 --- a/app/firebase/functions/test/growth.test.ts +++ b/app/firebase/functions/test/growth.test.ts @@ -155,7 +155,7 @@ it('캠페인 후원은 기여 POP 만 쓰고 targetPop 초과를 막으며 캠 await db.recursiveDelete(db.doc(`campaigns/${cid}`)); }); -it('fn-pop-spend 는 Zap 만 받고 횟수를 서버가 세며 Zap 한 가입 행성에 귀속하고 EXP 를 주지 않는다(리뷰 B1 · H1 · M1)', async () => { +it('fn-pop-spend 는 Zap 만 받고 횟수를 서버가 세며 Zap 한 가입 행성에 귀속하고 EXP 를 주지 않는다', async () => { const db = getFirestore(); const { fnPopSpend } = await import('../src/ledger/pop.js'); const run = (data: Record) => fnPopSpend.run( @@ -168,7 +168,7 @@ it('fn-pop-spend 는 Zap 만 받고 횟수를 서버가 세며 Zap 한 가입 await expect(run({ popType: 'activity', amount: 5, reasonCode: 'ZAP_CONTRIBUTION', idempotencyKey: 'zap:odd' })) .rejects.toMatchObject({ details: { code: 'E-POP-02' } }); - // 가입하지 않은 행성 b 에는 Zap 할 수 없다 — 남의 행성 성장을 올리지 못한다(리뷰 M1). + // 가입하지 않은 행성 b 에는 Zap 할 수 없다 — 남의 행성 성장을 올리지 못한다. await expect(run({ popType: 'activity', amount: 30, fandomId: b, reasonCode: 'ZAP_CONTRIBUTION', idempotencyKey: 'zap:b0' })) .rejects.toMatchObject({ details: { code: 'E-PERM-01' } }); diff --git a/app/firebase/functions/test/ledger.test.ts b/app/firebase/functions/test/ledger.test.ts index d21db29..6d095a5 100644 --- a/app/firebase/functions/test/ledger.test.ts +++ b/app/firebase/functions/test/ledger.test.ts @@ -118,7 +118,7 @@ describe('차감', () => { const user = await db.doc(`users/${UID}`).get(); expect(user.get('totalContributedPop')).toBe(40); // 기여 POP 문턱 등급은 폐기됐다(C-18). 후원 · Zap 은 1 POP 단위로 쪼개 반복할 수 있어 - // EXP 를 주지 않는다(리뷰 B1) — EXP 는 Quest 인정 활동에서만 쌓인다. + // EXP 를 주지 않는다 — EXP 는 Quest 인정 활동에서만 쌓인다. expect(user.get('tier')).toBeUndefined(); expect(user.get('userExp')).toBeUndefined(); const member = await db.doc(`fandoms/fandom-a/members/${UID}`).get(); @@ -279,7 +279,7 @@ describe('User EXP · Level · Passport 갱신 (C-18 · B-65)', () => { }); const user = await getFirestore().doc(`users/${UID}`).get(); expect(user.get('totalContributedPop')).toBe(3_000); - // 3,000 POP 후원도 EXP 를 만들지 않는다(C-18 기여 POP 치환 금지 · 리뷰 B1). + // 3,000 POP 후원도 EXP 를 만들지 않는다(C-18 기여 POP 치환 금지). expect(user.get('userExp')).toBeUndefined(); expect(user.get('passportTier')).toBeUndefined(); expect(user.get('tier')).toBeUndefined(); diff --git a/app/firebase/functions/test/planet_create.test.ts b/app/firebase/functions/test/planet_create.test.ts index c760427..59e25e6 100644 --- a/app/firebase/functions/test/planet_create.test.ts +++ b/app/firebase/functions/test/planet_create.test.ts @@ -6,7 +6,7 @@ import { fnFandomJoin, fnFandomLeave, fnNebulaCreate, fnPlanetCreate, fnPrimaryFandomSet, isValidNebulaName, } from '../src/account/account.js'; -/** 성운 생성 → 가입 · 대표 팬덤 → 행성 탄생 — 원장 B-08 · B-09 · B-10 · C-15 · C-50. */ +/** 성운 생성 → 가입 · 대표 팬덤 → 행성 탄생 — 정책 원장 B-08 · B-09 · B-10 · C-15 · C-50. */ const UID = 'test-planet-create'; const OTHER = 'test-planet-create-other'; const db = () => getFirestore(); @@ -143,7 +143,7 @@ describe('가입 · 탈퇴 · 대표 팬덤', () => { }); }); -describe('창립자 탈퇴 승계 · 대표 팬덤 자동 지정(원장 C-100 · C-101 · S20)', () => { +describe('창립자 탈퇴 승계 · 대표 팬덤 자동 지정(정책 원장 C-100 · C-101 · S20)', () => { const setPolicy = (patch: Record) => db().doc('config/policy').set(patch, { merge: true }); const THIRD = 'test-planet-create-third'; afterEach(async () => { diff --git a/app/firebase/functions/test/pure.test.ts b/app/firebase/functions/test/pure.test.ts index 03589b7..8b9a712 100644 --- a/app/firebase/functions/test/pure.test.ts +++ b/app/firebase/functions/test/pure.test.ts @@ -98,7 +98,7 @@ describe('참여형 번역 등록 (FR-I18N-02 · B-63)', () => { expect(translationSettleOf({ ...base, valid: 3, invalid: 5, ageHours: 80 })).toBe('release'); }); - it('보상 기본값은 원장 B-63 의 5 POP 이다', () => { + it('보상 기본값은 정책 원장 B-63 의 5 POP 이다', () => { expect(DEFAULT_POLICY.translationPop).toBe(5); }); diff --git a/app/firebase/functions/test/quest_e2e.test.ts b/app/firebase/functions/test/quest_e2e.test.ts index a47b5de..e18bd12 100644 --- a/app/firebase/functions/test/quest_e2e.test.ts +++ b/app/firebase/functions/test/quest_e2e.test.ts @@ -128,7 +128,7 @@ beforeEach(async () => { }); describe('출석 — S-H-ATTEND (fn-quest-attend)', () => { - it('없는 퀘스트 · 출석이 아닌 퀘스트 ID 로는 지급하지 않는다(리뷰 H2)', async () => { + it('없는 퀘스트 · 출석이 아닌 퀘스트 ID 로는 지급하지 않는다', async () => { expect(await codeOf(() => call(fnQuestAttend, { questId: 'no-such-quest' }))).toBe('E-QST-01'); expect(await codeOf(() => call(fnQuestAttend, { questId: STEPS_QUEST }))).toBe('E-QST-01'); expect((await db().collection(`users/${UID}/popTransactions`).get()).size).toBe(0); @@ -314,7 +314,7 @@ describe('만보기 — S-H-WALK (fn-quest-verify-steps · 정책 원장 B-26)', }); }); -describe('클라이언트가 고른 ID 로 한도를 다시 열 수 없다 (리뷰 재검토)', () => { +describe('클라이언트가 고른 ID 로 한도를 다시 열 수 없다', () => { it('걸음: 없는 퀘스트 · 걸음이 아닌 퀘스트 ID 로는 지급하지 않는다', async () => { expect(await codeOf(() => call(fnQuestVerifySteps, { questId: 'no-such-walk', steps: 5_200 }))) .toBe('E-QST-01'); @@ -534,7 +534,7 @@ describe('방문 — S-H-PLACE (fn-quest-verify-place)', () => { expect((await progress.get()).get('count')).toBe(2); }); - it('없는 퀘스트 · 다른 장소의 퀘스트 · 다른 팬덤 퀘스트로는 지급하지 않는다(리뷰 B2 · C-16)', async () => { + it('없는 퀘스트 · 다른 장소의 퀘스트 · 다른 팬덤 퀘스트로는 지급하지 않는다(C-16)', async () => { expect(await codeOf(() => call(fnQuestVerifyPlace, { ...inRange, questId: 'no-such-quest' }))) .toBe('E-QST-01'); await db().doc('quests/place-elsewhere').set({ type: 'location', placeId: 'other-place' }); @@ -725,7 +725,7 @@ describe('POP → 행성 반영 (별첨 1 ② 완료 시나리오 1)', () => { // 후원할 금액을 만든다. 지갑만 올리지 않고 원장을 지난다 — 3자 대조가 깨지면 // 차감이 E-POP-02 로 막힌다. await grantPop({ - // 후원은 기여 POP 만 쓴다(원장 B-33) — 기여 잔액을 만든다. + // 후원은 기여 POP 만 쓴다(정책 원장 B-33) — 기여 잔액을 만든다. uid: UID, popType: 'support', amount: 3_000, reasonCode: 'ADMIN_GRANT', idempotencyKey: `seed:${UID}`, }); @@ -746,7 +746,7 @@ describe('POP → 행성 반영 (별첨 1 ② 완료 시나리오 1)', () => { expect(user.get('totalContributedPop')).toBe(3_000); // EXP 는 원장이 움직일 때 함께 갱신된다 — 출석 보상 10 + 후원 3,000(C-18 단순안). expect(user.get('tier')).toBeUndefined(); - // EXP 는 출석(인정 활동 1건)만 — 후원은 EXP 대상이 아니다(리뷰 B1). + // EXP 는 출석(인정 활동 1건)만 — 후원은 EXP 대상이 아니다. expect(user.get('userExp')).toBe(100); expect(user.get('userLevel')).toBe(1); const member = await db().doc(`fandoms/${FANDOM}/members/${UID}`).get(); @@ -765,7 +765,7 @@ describe('POP → 행성 반영 (별첨 1 ② 완료 시나리오 1)', () => { }); }); -describe('진행 상태 — 일시 정지 · 이어하기 · 진행완료 (fn-quest-progress · 원장 C-83)', () => { +describe('진행 상태 — 일시 정지 · 이어하기 · 진행완료 (fn-quest-progress · 정책 원장 C-83)', () => { const ref = () => db().doc(`users/${UID}/questProgress/${STEPS_QUEST}`); it('진행중 → 진행완료를 남기고, 일일 한도 · 완료 횟수는 건드리지 않는다', async () => { diff --git a/app/firebase/functions/test/translation.test.ts b/app/firebase/functions/test/translation.test.ts index 6e0d9bf..21377de 100644 --- a/app/firebase/functions/test/translation.test.ts +++ b/app/firebase/functions/test/translation.test.ts @@ -9,7 +9,7 @@ import { grantPop, reconcileUser, settlePendingGrant } from '../src/ledger/ledge /** * 참여형 번역 왕복 — 등록(보류 지급) → 마킹(자격 판정) → 24시간 뒤 정산(확정/해제). - * 원장 B-63: 1건 5 POP · 24시간 확정대기 · 자기 마킹 제외 · 중복 0 · 일일상한. + * 정책 원장 B-63: 1건 5 POP · 24시간 확정대기 · 자기 마킹 제외 · 중복 0 · 일일상한. */ const AUTHOR = 'test-trn-author'; diff --git a/app/firebase/seed-demo.py b/app/firebase/seed-demo.py index 10ecbed..f5dd73d 100644 --- a/app/firebase/seed-demo.py +++ b/app/firebase/seed-demo.py @@ -203,7 +203,7 @@ def main() -> None: print("▸ 기능 스위치(config/features)") put(token, "config/features", {"stationEnabled": False}) - # 원장 C-01 · C-02 — 4 카테고리 ↔ 10 활동 · 순서 · 노출 · 사용자 생성 허용(S03 초기값). + # 정책 원장 C-01 · C-02 — 4 카테고리 ↔ 10 활동 · 순서 · 노출 · 사용자 생성 허용(S03 초기값). # 서버 `DEFAULT_QUEST_CATALOG`(functions/src/quest/ugc.ts)와 같은 값이다. 운영 변경은 fn-admin-write. print("▸ 퀘스트 분류 마스터(config/questCatalog)") field = lambda f: [{"field": f, "required": True}] if f else [] @@ -409,7 +409,7 @@ def main() -> None: # 활성이 된다. 발동 스케줄을 확인할 때 이 값을 켠다. put(token, "config/eclipse", { "enabled": False, - # 원장 C-24 — 공통 일정(배열 길이 = 하루 회차 수). 팬덤별은 fandoms.eclipseSchedule[]. + # 정책 원장 C-24 — 공통 일정(배열 길이 = 하루 회차 수). 팬덤별은 fandoms.eclipseSchedule[]. "schedule": [{"hhmm": "07:50"}, {"hhmm": "17:50"}], "erosionPercent": 0.25, # C-23 실패 감소율(%) "warnSeconds": 180, # C-22 발생 3분 전 푸시 diff --git a/app/firebase/storage.rules b/app/firebase/storage.rules index 381ee0d..7639764 100644 --- a/app/firebase/storage.rules +++ b/app/firebase/storage.rules @@ -38,7 +38,7 @@ service firebase.storage { allow delete: if false; } - // 캠페인 결과보고 집행 사진 · 영수증 — 원장 B-35 · FR-ADM-05 「결과보고는 Admin 수동 운영」. + // 캠페인 결과보고 집행 사진 · 영수증 — 정책 원장 B-35 · FR-ADM-05 「결과보고는 Admin 수동 운영」. // 운영(master·ops)만 올린다. `fn-campaign-report-submit` 이 이 경로 접두와 파일 존재를 // 다시 본다. 결과보고는 공개 캠페인 상세에 뜨므로 읽기는 열어 둔다. 상한은 B-44 10MB. match /campaignReports/{campaignId}/{fileName} { @@ -69,7 +69,7 @@ service firebase.storage { allow delete: if isSelf(uid); } - // 위치 Quest 방문 인증 사진 — 원장 C-03 · PRD-05 「GPS · 사진 · QR 인증」. `fn-quest-verify-place` 가 + // 위치 Quest 방문 인증 사진 — 정책 원장 C-03 · PRD-05 「GPS · 사진 · QR 인증」. `fn-quest-verify-place` 가 // 이 경로 접두(`questProofs/{본인}/{questId}/`)와 파일 존재를 다시 본다. 운영 검토용이라 본인 · 서버만 읽는다. // 덮어쓰기 · 삭제 금지 — 제출한 증거가 바뀌면 안 된다. 상한은 B-44 이미지 10MB. match /questProofs/{uid}/{questId}/{fileName} { diff --git a/app/firebase/test/tests/firestore.rules.test.mjs b/app/firebase/test/tests/firestore.rules.test.mjs index 74cb44d..8fcf28f 100644 --- a/app/firebase/test/tests/firestore.rules.test.mjs +++ b/app/firebase/test/tests/firestore.rules.test.mjs @@ -1785,7 +1785,7 @@ describe('성운 일 1회 기록 — GALAXY 「성운 생성」 버튼(Figma 540 }); }); -describe('Quest 진행 세션 — 일시 정지 · 진행완료(원장 C-83 · fn-quest-progress 전용)', () => { +describe('Quest 진행 세션 — 일시 정지 · 진행완료(정책 원장 C-83 · fn-quest-progress 전용)', () => { test('본인도 진행 세션을 직접 쓰거나 지울 수 없다 — 함수로만', async () => { await seedDocuments(testEnvironment, { 'users/alice/questProgress/shake': { session: { state: 'paused', progress: 43, total: 60 } }, diff --git a/app/lib/app.dart b/app/lib/app.dart index 3118d51..63b0db5 100644 --- a/app/lib/app.dart +++ b/app/lib/app.dart @@ -211,7 +211,7 @@ class _FancakeAppState extends State with WidgetsBindingObserver { Future _startPush(PushMessaging push) async { // 알림 권한은 여기서 묻지 않는다 — 온보딩 ⑥ 권한 안내가 쓰임을 설명한 뒤 묻는다 - // (FR-CMN-08 · 원장 C-84, 로그인 직후 일괄 요청 금지). 거부해도 토큰과 구독은 + // (FR-CMN-08 · 정책 원장 C-84, 로그인 직후 일괄 요청 금지). 거부해도 토큰과 구독은 // 사용자가 나중에 설정에서 허용했을 때 그대로 살아 있어야 한다. await push.syncToken(); diff --git a/app/lib/core/data/invite_code.dart b/app/lib/core/data/invite_code.dart index 11408ef..4019df5 100644 --- a/app/lib/core/data/invite_code.dart +++ b/app/lib/core/data/invite_code.dart @@ -1,6 +1,6 @@ import 'backend.dart'; -/// 내 초대 코드 — 공유 링크 `?ref=` 에 붙는다(원장 B-69 추천). +/// 내 초대 코드 — 공유 링크 `?ref=` 에 붙는다(정책 원장 B-69 추천). /// /// 세션당 한 번 `fnInviteCodeGet` 을 부른다(없으면 서버가 만든다). 공유는 이 값을 기다리지 /// 않는다 — 아직 없거나 실패했으면 `?ref=` 없이 나간다. 계정이 바뀌면 다시 받는다. diff --git a/app/lib/core/monetization/rewarded_ad.dart b/app/lib/core/monetization/rewarded_ad.dart index b1ffad7..ddf4d8e 100644 --- a/app/lib/core/monetization/rewarded_ad.dart +++ b/app/lib/core/monetization/rewarded_ad.dart @@ -34,7 +34,7 @@ abstract interface class RewardedAdHandle { /// Google Mobile Ads 보상형 광고를 불러온다. /// /// 불러오지 못하면(채울 광고 없음 · 네트워크 · 시간 초과) `E-AD-01` 로 끊는다 — -/// 원장 B-03 「광고 로드 실패 시 기본 보상은 지급하고 추가 보상·횟수는 차감하지 않음」. +/// 정책 원장 B-03 「광고 로드 실패 시 기본 보상은 지급하고 추가 보상·횟수는 차감하지 않음」. /// /// [userId] · [customData] 는 **서버측 시청 검증(AdMob SSV)** 에 실린다 — Google 이 시청 완료를 /// 서버(`fn-ad-ssv`)로 직접 알릴 때 이 두 값이 「누가 · 어느 기본 보상(txId)에 붙여」 본 광고인지 diff --git a/app/lib/core/push/push_messaging.dart b/app/lib/core/push/push_messaging.dart index 597f010..a0311c3 100644 --- a/app/lib/core/push/push_messaging.dart +++ b/app/lib/core/push/push_messaging.dart @@ -350,10 +350,10 @@ class PushNotice { /// 라이브 띠 · Push 가 같은 사건을 가리키는 키 — Push `kind` 표기 + 팬덤. String liveTickerKey(String kind, String fandomId) => '$kind:$fandomId'; -/// 원장 C-78 「Push 우선순위」 — ① 위기 ② 공동행동 ③ 성취 ④ 결과, 그 밖(단순 보상 유도형 포함)은 맨 뒤. +/// 정책 원장 C-78 「Push 우선순위」 — ① 위기 ② 공동행동 ③ 성취 ④ 결과, 그 밖(단순 보상 유도형 포함)은 맨 뒤. enum PushTier { crisis, collective, achievement, result, other } -/// 원장 C-78 단계 배정 — Push 와 알림함이 같은 표를 쓴다. +/// 정책 원장 C-78 단계 배정 — Push 와 알림함이 같은 표를 쓴다. /// /// - 위기: 이클립스 사전 경고 · 발생 /// - 공동행동: TIME ATTACK 시작 — **임시값(해석 확인 Q-C-10 A안)** diff --git a/app/lib/core/router/app_router.dart b/app/lib/core/router/app_router.dart index ecb5c13..c87132f 100644 --- a/app/lib/core/router/app_router.dart +++ b/app/lib/core/router/app_router.dart @@ -257,7 +257,7 @@ class AppShell extends StatelessWidget { final StatefulNavigationShell shell; final FeatureFlags flags; - /// 일시 정지 Quest 캡슐(원장 C-83)을 읽는 저장소. null 이면 캡슐이 없다. + /// 일시 정지 Quest 캡슐(정책 원장 C-83)을 읽는 저장소. null 이면 캡슐이 없다. final QuestRepository? quests; @override @@ -271,7 +271,7 @@ class AppShell extends StatelessWidget { return Scaffold( body: shell, // Figma 탭 줄은 좌우 16 을 비우고 칸을 나눈다(350:215853). - // 그 바로 위에 일시 정지 Quest 캡슐 한 줄(원장 C-83) — 없으면 높이 0. + // 그 바로 위에 일시 정지 Quest 캡슐 한 줄(정책 원장 C-83) — 없으면 높이 0. bottomNavigationBar: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/app/lib/core/ui/planet_25d.dart b/app/lib/core/ui/planet_25d.dart index e38bffe..d5575e7 100644 --- a/app/lib/core/ui/planet_25d.dart +++ b/app/lib/core/ui/planet_25d.dart @@ -173,7 +173,7 @@ class Planet25D extends StatefulWidget { /// 이클립스 성공 회차. 바뀌면 광량 복원 뒤 짧은 Orbit Burst(v1.5 §5 「그림자 이탈 → 복원 → Orbit Burst」). final String? eclipseSuccessId; - /// 사전 경고 창(서버 회차 `warnAt` → `startAt`, 원장 C-22 발생 3분 전). 이 사이에만 + /// 사전 경고 창(서버 회차 `warnAt` → `startAt`, 정책 원장 C-22 발생 3분 전). 이 사이에만 /// v1.5 §5 「행성 뒤 어두운 그림자 접근, 별빛·Glow 점진 감소」를 그린다([eclipseApproach]). final DateTime? eclipseWarnAt; final DateTime? eclipseStartAt; diff --git a/app/lib/features/campaign/campaign_data.dart b/app/lib/features/campaign/campaign_data.dart index f90f6a1..57cd18a 100644 --- a/app/lib/features/campaign/campaign_data.dart +++ b/app/lib/features/campaign/campaign_data.dart @@ -244,7 +244,7 @@ class CampaignDetail { } /// 결과보고 — 요구사항 FR-CMP-06 「집행 사진 · 영수증 · 예산 사용 내역 공개」. -/// 서버 문서 `campaigns/{id}/reports/main`. 올리는 쪽은 운영이다(원장 B-35). +/// 서버 문서 `campaigns/{id}/reports/main`. 올리는 쪽은 운영이다(정책 원장 B-35). class CampaignReport { const CampaignReport({ required this.body, @@ -263,10 +263,10 @@ class CampaignReport { int get budgetTotal => budget.fold(0, (sum, l) => sum + l.amount); } -/// 사용자 캠페인 등록비 — 원장 B-57(🟡 운영값). 서버 `fn-campaign-create` 가 실제로 뺀다. +/// 사용자 캠페인 등록비 — 정책 원장 B-57(🟡 운영값). 서버 `fn-campaign-create` 가 실제로 뺀다. const int campaignRegistrationFeePop = 100; -/// Campaign Product Catalog 상품 하나 — 원장 B-59(상품 · 필요 기여 POP) · C-14(권장 모금 기간). +/// Campaign Product Catalog 상품 하나 — 정책 원장 B-59(상품 · 필요 기여 POP) · C-14(권장 모금 기간). class CampaignProduct { const CampaignProduct(this.name, this.goalPop, this.recommendedDays); diff --git a/app/lib/features/eclipse/eclipse_data.dart b/app/lib/features/eclipse/eclipse_data.dart index 65b7986..7a11815 100644 --- a/app/lib/features/eclipse/eclipse_data.dart +++ b/app/lib/features/eclipse/eclipse_data.dart @@ -74,7 +74,7 @@ class EclipseState { /// 없으면 이미 시작한 회차로 본다. final DateTime? startAt; - /// 사전 경고 시각(`warnAt` = 발생 − 사전 알림, 원장 C-22 3분). 행성 그림자 접근의 출발점. + /// 사전 경고 시각(`warnAt` = 발생 − 사전 알림, 정책 원장 C-22 3분). 행성 그림자 접근의 출발점. final DateTime? warnAt; /// 서버 회차가 아직 `scheduled`(경보 단계)인가. 방어 호출은 `active` 에서만 받는다. @@ -94,7 +94,7 @@ class EclipseState { double get progress => targetDefense <= 0 ? 0 : (defense / targetDefense).clamp(0.0, 1.0); - /// 마감이 지났는가. **지난 이클립스는 화면에 띄우지 않는다**(검수 D3) — + /// 마감이 지났는가. **지난 이클립스는 화면에 띄우지 않는다** — /// 서버가 `ended` 로 닫기 전이라도 화면이 먼저 접는다. bool isExpired(DateTime now) { final at = endsAt; diff --git a/app/lib/features/eclipse/eclipse_repository_firestore.dart b/app/lib/features/eclipse/eclipse_repository_firestore.dart index f79dec2..57b75af 100644 --- a/app/lib/features/eclipse/eclipse_repository_firestore.dart +++ b/app/lib/features/eclipse/eclipse_repository_firestore.dart @@ -187,7 +187,7 @@ class FirestoreEclipseRepository implements EclipseRepository { fandomName: data.strOr('fandomName', fandomId), erosionRate: data.intOr('erosionRate', 100), defense: shards.isEmpty ? data.intOr('qualifiedParticipants') : live.$1, - // 원장 C-25·C-26: 인정 횟수 이상인 고유 참여자 수 / DAU 기반 목표. + // 정책 원장 C-25·C-26: 인정 횟수 이상인 고유 참여자 수 / DAU 기반 목표. targetDefense: data.intOr('target'), qualifyShakes: data.intOr('qualifyShakes', 60), defenseCount: shards.isEmpty ? data.intOr('defenseCount') : live.$2, diff --git a/app/lib/features/eclipse/widgets/eclipse_banner.dart b/app/lib/features/eclipse/widgets/eclipse_banner.dart index 6adeb79..134432c 100644 --- a/app/lib/features/eclipse/widgets/eclipse_banner.dart +++ b/app/lib/features/eclipse/widgets/eclipse_banner.dart @@ -16,7 +16,7 @@ import '../../../core/i18n/app_locale.dart'; /// 배너는 세 상태를 그린다. /// - 활성 이클립스 없음(또는 마감이 지남) → 아무것도 그리지 않는다(빈 [SizedBox]). /// - 침식 진행 중 → 침식률 · 방어 진행률 · 남은 시간. -/// - 방어 성공(`defended`) → 회복 문구(원장 B-55 「방어 성공 시 침식분 회복」). +/// - 방어 성공(`defended`) → 회복 문구(정책 원장 B-55 「방어 성공 시 침식분 회복」). /// /// 방어는 **공동 Shake 참여**이며 POP 을 소모하지 않는다. 눌러 들어가는 곳은 /// 「흔들어 방어」 화면이다. diff --git a/app/lib/features/eclipse/widgets/eclipse_event_button.dart b/app/lib/features/eclipse/widgets/eclipse_event_button.dart index 1635695..8e50f17 100644 --- a/app/lib/features/eclipse/widgets/eclipse_event_button.dart +++ b/app/lib/features/eclipse/widgets/eclipse_event_button.dart @@ -10,7 +10,7 @@ import '../eclipse_defend_screen.dart'; /// /// 명세 `516:88125` *데이터: 이클립스 발생 잔여 시간 · 규칙: 성운(행성) 아래 중앙 정렬, /// 어드민에서 설정된 이클립스 이벤트 발생 시간의 N분 전 노출 · 동작: 클릭 시 이클립스 상세로 -/// 이동*. N분은 서버 회차의 `warnAt`(원장 C-22 발생 3분 전)이다 — 회차 문서가 그때부터 +/// 이동*. N분은 서버 회차의 `warnAt`(정책 원장 C-22 발생 3분 전)이다 — 회차 문서가 그때부터 /// `scheduled` 로 있으므로 그 문서가 보이는 동안 띄운다. 발생 뒤 방어 창(60초) 동안은 /// 남은 초로 바뀌고, 결과가 나거나 창이 닫히면 접는다. /// diff --git a/app/lib/features/eclipse/widgets/live_proof.dart b/app/lib/features/eclipse/widgets/live_proof.dart index e182b42..d741fef 100644 --- a/app/lib/features/eclipse/widgets/live_proof.dart +++ b/app/lib/features/eclipse/widgets/live_proof.dart @@ -46,7 +46,7 @@ typedef TickerLine = ({String text, bool hero, String? push}); /// ponytail: 전역 하나. HOME 이 띠를 하나만 그리므로 충분하다 — 띠가 여러 곳에 생기면 집합으로. String? liveTickerShowing; -/// 띠 한 줄 고르기 — 원장 C-78 순서(위기 → 성취)대로 핵심 이벤트가 먼저 오른다. +/// 띠 한 줄 고르기 — 정책 원장 C-78 순서(위기 → 성취)대로 핵심 이벤트가 먼저 오른다. /// /// 1. 이클립스 발생 소식 — 그 뒤 이벤트(방어 참여 등)가 아직 없을 때. /// 2. 방어 중 집약 — UX·FX v1.5 §1.1 「개인 이벤트를 연속 티커로 쌓기보다 집단 상태로 집약」 · diff --git a/app/lib/features/galaxy/galaxy_data.dart b/app/lib/features/galaxy/galaxy_data.dart index 1cfce44..7042424 100644 --- a/app/lib/features/galaxy/galaxy_data.dart +++ b/app/lib/features/galaxy/galaxy_data.dart @@ -42,7 +42,7 @@ class PlanetNode { /// 갤럭시 전체 순위. HUD 의 `Rank`. /// - /// Stage0 성운은 랭킹 모집단이 아니다(원장 B-08 · B-17). 성운에는 순위를 매기지 않고 + /// Stage0 성운은 랭킹 모집단이 아니다(정책 원장 B-08 · B-17). 성운에는 순위를 매기지 않고 /// 정식 행성 수 + 1 을 넣어 **가장 바깥 궤도 자리로만** 쓴다 — HUD 는 「-」로 그린다. /// 최근 7 일 유효 활동이 없는 정식 행성도 같다([ranked]). final int rank; @@ -65,7 +65,7 @@ class PlanetNode { /// 내 행성. 명세 규칙 「행성 — 나의행성 한가운데」. final bool isMine; - /// 맵 한가운데 서는 행성. 기본은 내 행성이고, Ranking View 에서는 FPI 1위다(원장 B-21). + /// 맵 한가운데 서는 행성. 기본은 내 행성이고, Ranking View 에서는 FPI 1위다(정책 원장 B-21). final bool isCenter; } @@ -128,7 +128,7 @@ class GalaxySnapshot { PlanetNode? get mine => planets.where((p) => p.isMine).firstOrNull; - /// Ranking View — 원장 B-21 「Ranking View 는 FPI 상위 중심」. FPI 1위 행성을 가운데 + /// Ranking View — 정책 원장 B-21 「Ranking View 는 FPI 상위 중심」. FPI 1위 행성을 가운데 /// 두고 나머지는 기본 보기와 같이 순위 궤도를 돈다. Stage0 성운은 랭킹 모집단이 아니므로 /// (B-17) 중심 후보에서 뺀다. 공동 1위면 목록 순서(C-19 고정 정렬키)의 첫 행성이다. GalaxySnapshot get rankingView { @@ -201,7 +201,7 @@ abstract interface class GalaxyRepository { required String clientSeq, }); - /// 대표 팬덤 변경 — `fn-primary-fandom-set`(원장 C-15). 30일 대기 중이면 + /// 대표 팬덤 변경 — `fn-primary-fandom-set`(정책 원장 C-15). 30일 대기 중이면 /// E-FDM-04 로 거부된다. Future setPrimary(String fandomId); diff --git a/app/lib/features/galaxy/galaxy_screen.dart b/app/lib/features/galaxy/galaxy_screen.dart index de0e91c..d953788 100644 --- a/app/lib/features/galaxy/galaxy_screen.dart +++ b/app/lib/features/galaxy/galaxy_screen.dart @@ -98,7 +98,7 @@ class _GalaxyScreenState extends State PlanetNode? _selected; int _selectedIndex = 0; - /// Ranking View(원장 B-21). 끄면 기본 보기 — 내 행성 중심. + /// Ranking View(정책 원장 B-21). 끄면 기본 보기 — 내 행성 중심. bool _ranking = false; /// 보기를 바꾸면 **새 중심 행성으로 날아간다** — 「Ranking View 전환 시 FPI 상위 중심으로 diff --git a/app/lib/features/home/home_data.dart b/app/lib/features/home/home_data.dart index 90e3a10..38f28d7 100644 --- a/app/lib/features/home/home_data.dart +++ b/app/lib/features/home/home_data.dart @@ -100,7 +100,7 @@ class PlanetGrowth { /// 「행성 성장률 N%」 · 「현재 / 목표」의 기준 — 두 조건 중 **진행률이 낮은 쪽(병목)**. /// /// Figma 「홈」(`2097:16428` `Frame 1984081073`)은 성장률 하나 · 수치 한 쌍이고, 정책은 - /// 두 조건 동시 충족이다(원장 B-11 · 성운은 POP + 창립 인원 B-10). 다음 진화는 늦은 쪽이 + /// 두 조건 동시 충족이다(정책 원장 B-11 · 성운은 POP + 창립 인원 B-10). 다음 진화는 늦은 쪽이 /// 채워질 때 오므로 그 축을 보인다. 두 조건 상세는 말풍선을 누르면 연다. /// ponytail: 해석 확인 전 임시 기준(HOME 정보위계 v1.0 §3 은 「단일 백분율로 합치지 않음」). /// 값을 모르는 축은 건너뛰고, 둘 다 모르면 null. @@ -185,7 +185,7 @@ class QuestSummary { this.pausedSession, }); - /// 일시 정지 세션(원장 C-83). 있으면 보조 줄이 「카테고리 · 일시 정지 · n/m」. + /// 일시 정지 세션(정책 원장 C-83). 있으면 보조 줄이 「카테고리 · 일시 정지 · n/m」. final QuestSession? pausedSession; final String title; @@ -464,7 +464,7 @@ class HomeWeekly { /// 아이템(Hero Gift) 상품 한 줄 — 사본 `아이템 구입`(`252:160233`) 카드. /// /// 구매하면 구매자에게 기여 POP, 팬덤 Drop Pool 에 같은 수량의 일반 POP 이 생긴다 -/// (원장 B-84). 수치는 Admin 저장값이며 표본은 사본 문구다. +/// (정책 원장 B-84). 수치는 Admin 저장값이며 표본은 사본 문구다. class ItemProduct { const ItemProduct({ this.productId = '', @@ -504,7 +504,7 @@ abstract interface class HomeRepository { /// 보상)이 HOME 헤더에도 바로 반영된다. Stream watchPopBalance(); - /// 성운(Stage0) 생성 — `fn-nebula-create`(원장 B-08 · B-09). 만든 팬덤 ID 와, 대표 팬덤이 + /// 성운(Stage0) 생성 — `fn-nebula-create`(정책 원장 B-08 · B-09). 만든 팬덤 ID 와, 대표 팬덤이 /// 바뀌었는지(C-15 30일 대기 중이면 false — 성운만 생기고 대표는 그대로)를 돌려준다. Future<({String fandomId, bool primaryChanged})> createNebula(String name); diff --git a/app/lib/features/home/home_repository_firestore.dart b/app/lib/features/home/home_repository_firestore.dart index 8b1e4fe..3347095 100644 --- a/app/lib/features/home/home_repository_firestore.dart +++ b/app/lib/features/home/home_repository_firestore.dart @@ -158,7 +158,7 @@ class FirestoreHomeRepository implements HomeRepository { .limit(30) .get(); final blocksF = db.collection('users/$uid/blocks').get(); - // Hero Gift Catalog(원장 B-85) — 판매 중인 것만. 소속 행성이 없는 사용자는 시트가 + // Hero Gift Catalog(정책 원장 B-85) — 판매 중인 것만. 소속 행성이 없는 사용자는 시트가 // 결제 창을 열기 전에 막는다(`fn-iap-verify` 도 거부한다). final itemsF = db .collection('heroGiftProducts') diff --git a/app/lib/features/home/nebula_create_screen.dart b/app/lib/features/home/nebula_create_screen.dart index c547788..87224ef 100644 --- a/app/lib/features/home/nebula_create_screen.dart +++ b/app/lib/features/home/nebula_create_screen.dart @@ -13,7 +13,7 @@ import '../../core/i18n/app_locale.dart'; /// 틀은 사본(이름 입력 → 성운 → 목표 영역 → 「성운 생성」)이고 숫자는 정책 원장이다. /// B-10 — 성운은 **10명 이상 + 1,000 POP 이상**을 기본 목표 **7일** 안에 채우면 정식 /// 행성이 된다. 목표가 고정값이라 사본의 목표 슬라이더 자리에는 조작할 수 없는 고정 -/// 표시를 둔다. 근거는 발주사 10/3 통합회신(원장 C-112) 「성운 생성은 10 명 + 1,000 POP + +/// 표시를 둔다. 근거는 발주사 10/3 통합회신(정책 원장 C-112) 「성운 생성은 10 명 + 1,000 POP + /// 기본 목표 7 일, 사용자 목표 POP 조작 없음」 — Figma `516:87407` 목표 POP · 창립멤버 핸들러를 /// 이 회신이 대체한다. B-19 — 진화 참여자에게는 /// 기념 배지만 지급하므로 사본의 「1T 등급 · 거버넌스」 문장은 그 내용으로 바꾼다. diff --git a/app/lib/features/home/planet_create_screen.dart b/app/lib/features/home/planet_create_screen.dart index c21f586..0ba28ca 100644 --- a/app/lib/features/home/planet_create_screen.dart +++ b/app/lib/features/home/planet_create_screen.dart @@ -14,7 +14,7 @@ import '../../core/i18n/app_locale.dart'; /// 정책 원장 C-50 — 생성 때 고르는 것은 **최초 기본 모델의 형태와 컬러 두 가지**뿐이고, /// 생성 뒤 사용자가 외형을 바꾸는 입구는 없다. 그래서 사본의 「오브젝트 색 선택」 줄과 /// 직접 색 고르기(스포이트) 칩은 두지 않는다. 사용자화(컬러 팔레트)를 빼는 근거는 발주사 -/// 10/3 통합회신 #6(원장 C-115) 「사용자 자유 HEX/RGB 컬러피커는 적용하지 않음」 — Figma +/// 10/3 통합회신 #6(정책 원장 C-115) 「사용자 자유 HEX/RGB 컬러피커는 적용하지 않음」 — Figma /// `2066:17910` 의 「사용자화」를 이 회신이 대체한다. 「오브젝트 색」 줄은 Q-A14 회신 대기. /// /// 형태 칸 · 미리보기는 행성 형태 시트 그림이다([planetShapeImage] — 6번 칸만 자리표시, Q-A14). diff --git a/app/lib/features/home/widgets/item_sheet.dart b/app/lib/features/home/widgets/item_sheet.dart index 7c96e40..8ab6153 100644 --- a/app/lib/features/home/widgets/item_sheet.dart +++ b/app/lib/features/home/widgets/item_sheet.dart @@ -13,7 +13,7 @@ import '../../../core/i18n/app_locale.dart'; /// 「아이템 선택」(ZAP 옵션 「아이템」 · `343:204868`). /// /// 두 시트는 제목과 카드 아랫줄만 다르다 — 구입은 「보유 : N」 + 가격, 선택은 「보유 : N개」. -/// 구입은 스토어 결제 → `fn-iap-verify` 검증 → 구매자 기여 POP · 팬덤 Drop(원장 B-84)이고 +/// 구입은 스토어 결제 → `fn-iap-verify` 검증 → 구매자 기여 POP · 팬덤 Drop(정책 원장 B-84)이고 /// 결과는 앱 전역 알림으로 온다([Store.messages]). 사본의 결제 시트(「앱 내 구입」)는 OS 결제 /// 창이라 앱이 그리지 않는다. 구매가 곧 발동이라 보유 재고가 없어 「선택」(사용)은 준비 중이다. Future showItemSheet( diff --git a/app/lib/features/home/widgets/quest_row.dart b/app/lib/features/home/widgets/quest_row.dart index 1ad88c8..90c8658 100644 --- a/app/lib/features/home/widgets/quest_row.dart +++ b/app/lib/features/home/widgets/quest_row.dart @@ -44,7 +44,7 @@ class QuestRows extends StatelessWidget { /// /// 보조 줄은 카테고리(사본 「오늘의 출석체크」 · 「커뮤니티」)다. 「홈」 프레임에는 보상 숫자 · /// 진행 상태가 없어 그리지 않는다. 타임어택 남은 시간도 그리지 않는다 — 설명 프레임 -/// `2097:16397` 데이터는 「어드민에서 등록된 퀘스트」뿐이고, FR-QST-06 · 원장 C-94 의 카운트다운 +/// `2097:16397` 데이터는 「어드민에서 등록된 퀘스트」뿐이고, FR-QST-06 · 정책 원장 C-94 의 카운트다운 /// 표시 위치는 퀘스트 목록 · 상세다. class QuestRow extends StatelessWidget { const QuestRow({ @@ -98,7 +98,7 @@ class QuestRow extends StatelessWidget { ), ), const SizedBox(height: 4), - // 진행중 탭 카드와 같은 일시 정지 표기(원장 C-83). + // 진행중 탭 카드와 같은 일시 정지 표기(정책 원장 C-83). _sub(questSubLine(quest.category ?? '', quest.pausedSession)), ], ), diff --git a/app/lib/features/inside/inside_data.dart b/app/lib/features/inside/inside_data.dart index 4f7398d..0a70a24 100644 --- a/app/lib/features/inside/inside_data.dart +++ b/app/lib/features/inside/inside_data.dart @@ -81,7 +81,7 @@ class InsideQuestion { /// 답변이 채택되어 현상금이 지급된 상태. final bool isSolved; - /// 채택 기한(원장 B-39 · 7일)이 지난 뒤의 환불 단계. + /// 채택 기한(정책 원장 B-39 · 7일)이 지난 뒤의 환불 단계. final InsideRefund refund; /// 채택을 받을 수 있는 상태 — 채택도 만료도 아니다. diff --git a/app/lib/features/my/my_data.dart b/app/lib/features/my/my_data.dart index b30e67d..c4a3313 100644 --- a/app/lib/features/my/my_data.dart +++ b/app/lib/features/my/my_data.dart @@ -524,7 +524,7 @@ PaymentState paymentStateOf(String state) => switch (state) { _ => PaymentState.paid, }; -/// Focus 구독 — 원장 **B-04** *광고 제거 + 일 40 기여 POP, 당일 Claim, 미수령 누적 없음*. +/// Focus 구독 — 정책 원장 **B-04** *광고 제거 + 일 40 기여 POP, 당일 Claim, 미수령 누적 없음*. class FocusStatus { const FocusStatus({required this.expiresAt, required this.claimedToday}); @@ -796,7 +796,7 @@ abstract interface class MyRepository { /// Focus 구독 상태. 구독 중(만료 전)이 아니면 null — MY 에 카드를 그리지 않는다. Future focus(); - /// Focus 오늘 몫 받기(원장 B-04). 받은 기여 POP 을 돌려준다. 구독이 아니면 E-PAY-03. + /// Focus 오늘 몫 받기(정책 원장 B-04). 받은 기여 POP 을 돌려준다. 구독이 아니면 E-PAY-03. Future claimFocusDaily(); Future> pushSettings(); diff --git a/app/lib/features/my/my_screen.dart b/app/lib/features/my/my_screen.dart index 569e85a..271d695 100644 --- a/app/lib/features/my/my_screen.dart +++ b/app/lib/features/my/my_screen.dart @@ -102,7 +102,7 @@ class _MyScreenState extends State { } } - /// Focus 오늘 몫 받기(원장 B-04). 지급은 서버가 하고 화면은 결과만 알린다. + /// Focus 오늘 몫 받기(정책 원장 B-04). 지급은 서버가 하고 화면은 결과만 알린다. Future _claimFocus() async { if (_claiming) return; setState(() => _claiming = true); @@ -501,7 +501,7 @@ class _MyHeader extends StatelessWidget { } /// 활동 탭의 한 줄 — 글자 16/600 · 끝 chevron. 줄 높이 24 · 간격 24. -/// Focus 구독 혜택 카드 — 원장 B-04 *일 40 기여 POP · 당일 Claim · 미수령 누적 없음*. +/// Focus 구독 혜택 카드 — 정책 원장 B-04 *일 40 기여 POP · 당일 Claim · 미수령 누적 없음*. /// /// Figma 에는 이 Claim 자리가 없다(퀘스트 완료 화면의 「광고 제거 보상 · 퀘스트 보상 × 2」 /// `518:88748` 은 B-04 가 대체한 과거 안). 반영 화면이 `MY` 로 정해져 있어 통계 카드 아래에 둔다. diff --git a/app/lib/features/my/profile_edit_screen.dart b/app/lib/features/my/profile_edit_screen.dart index f5f649a..f3dc8eb 100644 --- a/app/lib/features/my/profile_edit_screen.dart +++ b/app/lib/features/my/profile_edit_screen.dart @@ -12,7 +12,7 @@ import '../../core/ui/bottom_inset.dart'; /// 명세 `549:22136` — *완료 디폴트 비활성화 · 변경 사항 있을 경우 활성화 ·* /// ***중복된 닉네임일 경우 닉네임 필드 아래 헬프 텍스트로 오류 메시지 노출*** /// -/// 중복 오류는 **별첨 B 「닉네임」(2~20자 · 중복 허용 · 내부 UID 식별, 원장 B-52)** 이 대체했다 — +/// 중복 오류는 **별첨 B 「닉네임」(2~20자 · 중복 허용 · 내부 UID 식별, 정책 원장 B-52)** 이 대체했다 — /// 중복을 이유로 막지 않는다. 헬프 텍스트 자리는 길이 · 예약어 오류에 쓴다. /// /// 사진은 시안대로 MY 의 카메라 배지 → [ProfilePhotoScreen] 에서 바꾼다. diff --git a/app/lib/features/notification/notification_data.dart b/app/lib/features/notification/notification_data.dart index f812db1..dbc3c44 100644 --- a/app/lib/features/notification/notification_data.dart +++ b/app/lib/features/notification/notification_data.dart @@ -117,7 +117,7 @@ class AppNotification { /// 이클립스 항목의 단계 — `start` · `defended`. final String phase; - /// 원장 C-78 단계 — 앱 안 Push 와 같은 표([pushTierOf]). 알림함은 이 단계로 시각 위계를 준다(v1.5 8장). + /// 정책 원장 C-78 단계 — 앱 안 Push 와 같은 표([pushTierOf]). 알림함은 이 단계로 시각 위계를 준다(v1.5 8장). PushTier get tier => pushTierOf( eventType ?? switch (kind) { diff --git a/app/lib/features/notification/notification_screen.dart b/app/lib/features/notification/notification_screen.dart index 7f8025d..ad6c0ac 100644 --- a/app/lib/features/notification/notification_screen.dart +++ b/app/lib/features/notification/notification_screen.dart @@ -229,7 +229,7 @@ TextStyle _style(Color color, {double height = 1.3}) => TextStyle( ), }; -/// 원장 C-78 위계 — 위기 · 공동행동 · 성취 · 결과(v1.5 8장 「알림 우선순위 표현」). Figma 는 배지 획이 모두 +/// 정책 원장 C-78 위계 — 위기 · 공동행동 · 성취 · 결과(v1.5 8장 「알림 우선순위 표현」). Figma 는 배지 획이 모두 /// #333333 이라 결과 · 그 밖은 그대로 두고, 위 세 단계만 획 색으로 올린다. /// 색은 **임시값** — 자료에 위계별 색 정의가 없어 기존 토큰(위기 = 경고 적색 · 공동행동 = 강조 · 성취 = 금)을 쓴다. Color _tierRing(PushTier tier) => switch (tier) { diff --git a/app/lib/features/planet/planet_data.dart b/app/lib/features/planet/planet_data.dart index 2cef2e5..a243672 100644 --- a/app/lib/features/planet/planet_data.dart +++ b/app/lib/features/planet/planet_data.dart @@ -308,7 +308,7 @@ class PlanetDetail { /// hide」 · `543:19721` 「(타인 행성) 신고/차단 · (내 행성) 편집」 — 편집은 C-50 으로 두지 않는다. final bool isOwner; - /// 성운 → 정식 행성 전환 조건 — 서버와 같은 `config/policy`(원장 B-10 · `nebulaMinPop` · `nebulaMinMembers`). + /// 성운 → 정식 행성 전환 조건 — 서버와 같은 `config/policy`(정책 원장 B-10 · `nebulaMinPop` · `nebulaMinMembers`). final int nebulaTargetPop; final int nebulaTargetMembers; @@ -319,7 +319,7 @@ class PlanetDetail { /// 「가입중 / 가입하기」 알약이 이 값을 그린다(명세 §2.72 「가입 버튼 - 소속 상태」). final bool joined; - /// 서버 FPI 집계 시각(`fandoms.fpiUpdatedAt`, 원장 B-15). 없으면 null. + /// 서버 FPI 집계 시각(`fandoms.fpiUpdatedAt`, 정책 원장 B-15). 없으면 null. final DateTime? fpiUpdatedAt; /// 개설자 표시명 · 사진. `fandoms` 문서에 아직 자리가 없어 서버 상세는 null 이고, diff --git a/app/lib/features/planet/planet_detail_screen.dart b/app/lib/features/planet/planet_detail_screen.dart index 8b82235..c1960f2 100644 --- a/app/lib/features/planet/planet_detail_screen.dart +++ b/app/lib/features/planet/planet_detail_screen.dart @@ -214,7 +214,7 @@ class _PlanetDetailScreenState extends State { ); /// Figma `543:19721` — 더보기: (타인 행성) 신고/차단 · (내 행성) 편집. 편집은 행성 사용자 커스텀이라 - /// 두지 않는다(원장 C-50 「생성 후 사용자가 외형을 바꾸는 진입점 0건」). 내 행성에는 신고 · 차단도 없다. + /// 두지 않는다(정책 원장 C-50 「생성 후 사용자가 외형을 바꾸는 진입점 0건」). 내 행성에는 신고 · 차단도 없다. void _showMore(BuildContext context, String name, {required bool mine}) => showActionSheet(context, [ // 팬덤 역사는 시안 밖 기능이라 남기되 중립색으로 둔다. @@ -466,7 +466,7 @@ class _FpiTab extends StatelessWidget { children: [ // 침식·방어 진행 — `fn-eclipse-aggregate` 의 집계 문서와 샤드를 구독한다. // TOTAL FPI 카드 위에 두는 이유는 침식이 곧 이 카드의 값을 깎기 때문이다 - // (원장 B-55 — FPI 감소는 Eclipse 가 담당한다). + // (정책 원장 B-55 — FPI 감소는 Eclipse 가 담당한다). EclipseBanner( repository: eclipse, fandomId: detail.fandomId, @@ -544,7 +544,7 @@ class _FpiTab extends StatelessWidget { /// /// 「기여 현황」 카드(#1a1a1c r12 · 안쪽 12/16) — 성운 성장률 · 진행 막대 · 0 ~ 목표, /// 「실시간 기여 랭킹」 — 1위 왕관 · 빈 원 80 · 「아직 기여자가 없어요」. -/// 목표는 원장 B-10 의 1,000 POP(사본 500,000 은 과거 수치). 분자는 서버 `growthPop`. +/// 목표는 정책 원장 B-10 의 1,000 POP(사본 500,000 은 과거 수치). 분자는 서버 `growthPop`. /// 기여자 목록은 상세 데이터에 아직 없어 빈 상태로 둔다. class _NebulaFpi extends StatelessWidget { const _NebulaFpi({ diff --git a/app/lib/features/quest/music_shake_play_screen.dart b/app/lib/features/quest/music_shake_play_screen.dart index 9203729..40f2743 100644 --- a/app/lib/features/quest/music_shake_play_screen.dart +++ b/app/lib/features/quest/music_shake_play_screen.dart @@ -23,7 +23,7 @@ typedef ShakePlayerEvent = ({bool playing, Duration position, bool ended}); /// 재생되는 동안의 흔들기를 **영상 시각**으로 찍어 박마다 Perfect · Good · Miss · Combo 를 /// 띄운다. 판정 창은 운영값(🟡 임시값 — 회신 대기). 마지막 박의 창이 닫히거나 영상이 끝나면 /// 판정표를 돌려준다. 중간에 나가면 null 이다 — 호출자는 넘긴 [judge] 의 판정 · 영상 시각을 -/// 일시 정지 세션으로 남기고, 「이어하기」 때 그 판정표를 다시 넘겨 멈춘 자리부터 잇는다(원장 C-83). +/// 일시 정지 세션으로 남기고, 「이어하기」 때 그 판정표를 다시 넘겨 멈춘 자리부터 잇는다(정책 원장 C-83). /// /// Figma 에 이 진행 화면은 없다(상세 `480:14506` 은 「진행중」 버튼까지). 공통 기준 5항 — /// 기존 범위 기능의 정상 구현에 필요한 화면이다. 상세의 시각 문법(#141414 · #1a1a1c r24)을 따른다. diff --git a/app/lib/features/quest/qr_scan_screen.dart b/app/lib/features/quest/qr_scan_screen.dart index f675135..c35b920 100644 --- a/app/lib/features/quest/qr_scan_screen.dart +++ b/app/lib/features/quest/qr_scan_screen.dart @@ -4,7 +4,7 @@ import 'package:mobile_scanner/mobile_scanner.dart'; import '../../core/i18n/app_locale.dart'; import '../../core/theme/tokens.dart'; -/// 위치 Quest QR 인증 — 현장에 붙은 QR 을 스캔해 그 값을 돌려준다(원장 C-03 · PRD-05 「QR 인증」). +/// 위치 Quest QR 인증 — 현장에 붙은 QR 을 스캔해 그 값을 돌려준다(정책 원장 C-03 · PRD-05 「QR 인증」). /// /// 판정은 서버가 한다(생성 때 발급한 코드와 대조). 이 화면은 첫 번째로 읽힌 값으로 닫힐 뿐이다. /// 🟡 Figma 에 스캔 화면이 없어 카메라 전체 화면 + 닫기 버튼만 둔다(해석 확인 요청 영역B Q-B-10). diff --git a/app/lib/features/quest/quest_ad_screens.dart b/app/lib/features/quest/quest_ad_screens.dart index 5c9ec40..7f000a2 100644 --- a/app/lib/features/quest/quest_ad_screens.dart +++ b/app/lib/features/quest/quest_ad_screens.dart @@ -97,7 +97,7 @@ class _AdBonusDialog extends StatelessWidget { child: Text( bonusPop == null ? context.l10n.adBonusPlaceholder - // 원장 C-116 — 운영값 기반 「+N 기여 POP」(증분은 기여 POP · C-122). + // 정책 원장 C-116 — 운영값 기반 「+N 기여 POP」(증분은 기여 POP · C-122). : '+${formatPop(bonusPop!)} ${context.l10n.popKindContribution}', style: TextStyle( fontSize: 36, @@ -166,7 +166,7 @@ class _AdBonusDialog extends StatelessWidget { } } -/// Focus(광고 제거 구독 · 원장 B-04) 구입 버튼. 가격은 스토어가 준 현지 가격이다 — +/// Focus(광고 제거 구독 · 정책 원장 B-04) 구입 버튼. 가격은 스토어가 준 현지 가격이다 — /// 시안의 「₩3,900/월」은 확정가(US$1.99)와 달라 쓰지 않는다(공통 기준 2항). /// 스토어에 상품이 없으면 「스토어 상품 준비 중」으로 멈춘다. class _FocusOffer extends StatefulWidget { diff --git a/app/lib/features/quest/quest_create_screen.dart b/app/lib/features/quest/quest_create_screen.dart index 722389c..a1f7a61 100644 --- a/app/lib/features/quest/quest_create_screen.dart +++ b/app/lib/features/quest/quest_create_screen.dart @@ -319,7 +319,7 @@ class _QuestCreateScreenState extends State { trailing: const Icon(Icons.chevron_right), onTap: _pickPlace, ), - // 추가 인증 수단(원장 C-03 「Location 의 추가 인증 수단」 · PRD-05 「GPS · 사진 · QR 인증」). + // 추가 인증 수단(정책 원장 C-03 「Location 의 추가 인증 수단」 · PRD-05 「GPS · 사진 · QR 인증」). // GPS 는 항상 들어간다. 🟡 Figma 생성 화면에 이 줄이 없어 배치는 임시다(해석 확인 요청 영역B Q-B-10). Text(context.l10n.questExtraVerify, style: Theme.of(context).textTheme.bodySmall), const SizedBox(height: AppSpace.s2), diff --git a/app/lib/features/quest/quest_data.dart b/app/lib/features/quest/quest_data.dart index 7900e56..b996f1f 100644 --- a/app/lib/features/quest/quest_data.dart +++ b/app/lib/features/quest/quest_data.dart @@ -20,7 +20,7 @@ import 'quest_evidence.dart' show ShakeJudge, ShakeWindows; /// 진행 상태 — 명세가 **다섯 가지**를 못박았다. /// /// 명세 주석은 *「자세히」 · 「시작하기」 · 「진행중」 · 「보상받기」 · 「보상완료」* 이지만, -/// 버튼 문구는 시각 기준(원장 C-44)인 Figma 화면을 따른다 — 「퀘스트 시작하기」 · +/// 버튼 문구는 시각 기준(정책 원장 C-44)인 Figma 화면을 따른다 — 「퀘스트 시작하기」 · /// 「보상받기」 · 「보상 완료」(`264:46867` · `373:433420`). 상세 `272:245274` 는 「보상 받기」로 /// 띄어 썼지만 홈 · 목록 사본 표기로 통일한다. /// @@ -32,7 +32,7 @@ enum QuestStage { notStarted, inProgress, - /// 화면 안에서 진행하는 Quest(Music Shake)를 진행 화면 밖으로 나와 멈춘 상태 — 원장 C-83(10/3 통합회신 + /// 화면 안에서 진행하는 Quest(Music Shake)를 진행 화면 밖으로 나와 멈춘 상태 — 정책 원장 C-83(10/3 통합회신 /// Q-OB-01 A): 진행 상태 값 「일시 정지」 · 버튼 「이어하기」. Walk · 출석은 이 상태가 되지 않는다. paused, completed, @@ -72,11 +72,11 @@ enum QuestType { /// 안에서만 참여를 받는다. 판정 시각은 서버 시각이다(`fn-quest-timeattack`). timeAttack, - /// 스밍·YouTube — 무보상 기본 참여(원장 C-04 · B-25, 9/16 확정). POP 지급 없이 참여 기록과 + /// 스밍·YouTube — 무보상 기본 참여(정책 원장 C-04 · B-25, 9/16 확정). POP 지급 없이 참여 기록과 /// FPI 원천만 남는다(`fn-quest-stream-participate`). streaming, - /// 사용자 생성 Vote · SNS — 인증 자료 제출 → 생성자 · 운영자 검토(원장 C-08 · C-10). + /// 사용자 생성 Vote · SNS — 인증 자료 제출 → 생성자 · 운영자 검토(정책 원장 C-08 · C-10). vote, sns, @@ -134,7 +134,7 @@ enum QuestType { /// `quests.rewardPop` 을 보지 않는다. 그래서 출석만 운영값을 읽는다. 운영값이 /// 없으면 null — 숫자를 지어내지 않고 화면이 중립 문구를 쓴다. /// - /// 운영 걸음 Quest 는 `fn-quest-steps` 가 `config/policy.stepTiers` 구간표(원장 B-26 — + /// 운영 걸음 Quest 는 `fn-quest-steps` 가 `config/policy.stepTiers` 구간표(정책 원장 B-26 — /// 1 천/5 천/1 만/1.5 만보 = 5/6/7/8)대로 지급하고 `quests.rewardPop` 을 보지 않는다. 카드의 /// 목표 칸이 오늘 다음 구간(`[cleared]` 번째)이므로 보상 칸도 **그 구간의 지급액**이다. int? rewardFrom( @@ -152,7 +152,7 @@ enum QuestType { ]..sort((a, b) => a.steps.compareTo(b.steps)); if (tiers.isNotEmpty) return tiers[cleared.clamp(0, tiers.length - 1)].pop; } - // Music Shake 는 BPM 판정 성공 시 일반 POP(원장 B-24 · 보상액 운영값). 서버 + // Music Shake 는 BPM 판정 성공 시 일반 POP(정책 원장 B-24 · 보상액 운영값). 서버 // (`fn-quest-verify-music-shake`)가 퀘스트의 `rewardPop` 을, 없으면 // `config/policy.musicShake.pop` 을 기본액으로 쓴다. 운영값이 둘 다 없을 때만 중립 문구. final shake = policy['musicShake']; @@ -210,7 +210,7 @@ enum QuestAuthor { user, } -/// 퀘스트 분류 — `config/questCatalog.categories`(원장 C-01). 순서 · 노출은 Admin 운영값이다. +/// 퀘스트 분류 — `config/questCatalog.categories`(정책 원장 C-01). 순서 · 노출은 Admin 운영값이다. class QuestCategory { const QuestCategory({ required this.id, @@ -359,7 +359,7 @@ bool questFull(QuestCard card) => card.capacity! > 0 && card.participants >= card.capacity!; -/// 진행 세션 — `users/{uid}/questProgress/{questId}.session`(`fn-quest-progress` 만 쓴다 · 원장 C-83). +/// 진행 세션 — `users/{uid}/questProgress/{questId}.session`(`fn-quest-progress` 만 쓴다 · 정책 원장 C-83). /// /// **표시용 상태다.** 화면을 다시 열거나 앱을 다시 켜도 「진행중」 탭 · 「이어하기」 · 목록 「보상받기」가 /// 남게 한다. 지급 판정은 여전히 검증 함수가 증거로 한다. @@ -403,7 +403,7 @@ class QuestSession { final Map? judgement; } -/// 일시 정지 · 진행 세션이 바뀌었다 — 탭 화면 하단 캡슐(원장 C-83)이 다시 읽는다. +/// 일시 정지 · 진행 세션이 바뀌었다 — 탭 화면 하단 캡슐(정책 원장 C-83)이 다시 읽는다. final questSessionSignal = ValueNotifier(0); /// 목록 카드 하나. @@ -433,7 +433,7 @@ class QuestCard { final String title; final String categoryName; - /// 서버에 남은 진행 세션(진행중 · 일시 정지 · 진행완료) — 원장 C-83. 없으면 null. + /// 서버에 남은 진행 세션(진행중 · 일시 정지 · 진행완료) — 정책 원장 C-83. 없으면 null. final QuestSession? session; /// 운영값(첫 박 · 판 길이)이 바뀌어 이어갈 수 없는 Music Shake 세션이 서버에 남아 있다. @@ -467,10 +467,10 @@ class QuestCard { /// 서버 경로는 null 이고, 화면은 분모 없이 참여 수만 적는다. final int? capacity; - /// 운영 Quest 의 광고 배수 — 원장 **B-03** 기본 x2, 지정 Quest 는 `quests/{id}.adMultiplier`. + /// 운영 Quest 의 광고 배수 — 정책 원장 **B-03** 기본 x2, 지정 Quest 는 `quests/{id}.adMultiplier`. final int adMultiplier; - /// 사용자 생성 Quest 의 광고 추가 보상 — 원장 **C-02** 고정 10 기여 POP(운영값). + /// 사용자 생성 Quest 의 광고 추가 보상 — 정책 원장 **C-02** 고정 10 기여 POP(운영값). final int ugcAdPop; /// 참여 마감 시각(`quests.activeTo`). 타임어택만 채운다 — 화면이 남은 시간을 센다. @@ -566,7 +566,7 @@ class QuestDetail { final QuestCard card; - /// 위치 Quest 의 인증 수단(`quests.verifyMethods` — `gps` · `photo` · `qr`, 원장 C-03). PRD-05 + /// 위치 Quest 의 인증 수단(`quests.verifyMethods` — `gps` · `photo` · `qr`, 정책 원장 C-03). PRD-05 /// 「장소 상세에서 인증 방법 … 확인」. GPS 는 항상, 사진 · QR 은 생성 때 고른 추가 수단이다. final List verifyMethods; @@ -601,7 +601,7 @@ class QuestDetail { final List reviews; } -/// 위치 Quest 의 추가 인증 증거(원장 C-03 · PRD-05 「사진 인증 · QR 인증」). GPS 좌표는 저장소가 따로 잰다. +/// 위치 Quest 의 추가 인증 증거(정책 원장 C-03 · PRD-05 「사진 인증 · QR 인증」). GPS 좌표는 저장소가 따로 잰다. class PlaceProof { const PlaceProof({this.qr, this.photo}); @@ -630,7 +630,7 @@ enum QuestTab { QuestTab.all => true, // 명세 `518:88405` — *진행중 - 진행중 탭 / 진행완료 - 진행중 탭 / 보상완료 - 완료 탭*. // 보상을 받기 전(진행완료)까지는 진행중 탭에 두고, 받은 뒤에 완료 탭으로 옮긴다. - // 일시 정지도 진행 중이다(원장 C-83 — 진행 상태 값에 「일시 정지」를 더함). + // 일시 정지도 진행 중이다(정책 원장 C-83 — 진행 상태 값에 「일시 정지」를 더함). QuestTab.inProgress => card.stage == QuestStage.inProgress || card.stage == QuestStage.paused || @@ -658,7 +658,7 @@ class QuestDraft { this.extraVerify = const [], }); - /// 위치 Quest 의 추가 인증 수단(`photo` · `qr`, 원장 C-03). GPS 는 서버가 항상 넣는다. + /// 위치 Quest 의 추가 인증 수단(`photo` · `qr`, 정책 원장 C-03). GPS 는 서버가 항상 넣는다. final List extraVerify; /// 지도에 꽂은 핀(등록 장소가 아닐 때) — 서버가 그 좌표로 장소를 만든다. @@ -739,7 +739,7 @@ abstract interface class QuestRepository { QuestSession? session, }); - /// 가장 최근에 일시 정지한 Quest 1건 — 탭 화면 하단 캡슐(원장 C-83 · QA 3-34). 없으면 null. + /// 가장 최근에 일시 정지한 Quest 1건 — 탭 화면 하단 캡슐(정책 원장 C-83 · QA 3-34). 없으면 null. Future pausedQuest(); Future create(QuestDraft draft); @@ -805,7 +805,7 @@ class SampleQuestRepository implements QuestRepository { @override LocationConsent get locationConsent => SampleLocationConsent.shared; - /// 원장 C-01 매핑 중 사용자 생성 초기값(C-02 A안)으로 열린 활동만. + /// 정책 원장 C-01 매핑 중 사용자 생성 초기값(C-02 A안)으로 열린 활동만. static QuestCatalog get _catalog => QuestCatalog( categories: [ QuestCategory( diff --git a/app/lib/features/quest/quest_detail_screen.dart b/app/lib/features/quest/quest_detail_screen.dart index e808daa..965ea87 100644 --- a/app/lib/features/quest/quest_detail_screen.dart +++ b/app/lib/features/quest/quest_detail_screen.dart @@ -76,7 +76,7 @@ class _QuestDetailScreenState extends State { /// 방금 끝낸 Music Shake 판의 판정 합계 — 결과 화면 · 결과 카드에 쓴다. Map? _shakeResult; - /// 진행 세션(원장 C-83) — 서버에서 읽은 값을 이 화면에서 멈추면 바로 갈아 끼운다(「이어하기」 시작점). + /// 진행 세션(정책 원장 C-83) — 서버에서 읽은 값을 이 화면에서 멈추면 바로 갈아 끼운다(「이어하기」 시작점). QuestSession? _session; /// [QuestDetailScreen.autoAct] 를 한 번만 쓴다. @@ -125,7 +125,7 @@ class _QuestDetailScreenState extends State { } } - /// 진행 세션을 서버에 남긴다(원장 C-83). 실패해도 이 화면의 진행은 계속된다 — 표시용 상태다. + /// 진행 세션을 서버에 남긴다(정책 원장 C-83). 실패해도 이 화면의 진행은 계속된다 — 표시용 상태다. Future _persist( String questId, QuestStage next, [ @@ -267,7 +267,7 @@ class _QuestDetailScreenState extends State { value: '${formatPop(_live ?? card.progress)} / ${formatPop(card.goal)}', ), - // 일시 정지 — 멈춘 시점의 진행값을 그대로 둔다(원장 C-83 · 해석 확인 요청서 Q-OB-01 A 5절). + // 일시 정지 — 멈춘 시점의 진행값을 그대로 둔다(정책 원장 C-83 · 해석 확인 요청서 Q-OB-01 A 5절). if (stage == QuestStage.paused && _session != null) ...[ const SizedBox(height: 8), _Row( @@ -306,7 +306,7 @@ class _QuestDetailScreenState extends State { ), const SizedBox(height: 8), ], - // 인증 방법 — PRD-05 MVP 수용 기준 「장소 상세에서 인증 방법 · 보상 · 참여 현황 확인」 · 원장 C-03. + // 인증 방법 — PRD-05 MVP 수용 기준 「장소 상세에서 인증 방법 · 보상 · 참여 현황 확인」 · 정책 원장 C-03. if (card.type == QuestType.place) ...[ _Row( label: context.l10n.questVerifyMethods, @@ -841,7 +841,7 @@ class _QuestDetailScreenState extends State { } /// Music Shake 한 판 — 공식 영상을 보며 흔드는 진행 화면을 띄운다(PRD-04). - /// 끝까지 했으면 판정표를 남기고 「보상받기」로, 중간에 나왔으면 **일시 정지**로 둔다(원장 C-83). + /// 끝까지 했으면 판정표를 남기고 「보상받기」로, 중간에 나왔으면 **일시 정지**로 둔다(정책 원장 C-83). /// [resume] 이 있으면 멈춘 판정 · 영상 시각부터 잇는다(「이어하기」). Future _playShake(QuestDetail detail, {QuestSession? resume}) async { final MusicShakeSetup setup; @@ -937,7 +937,7 @@ class _QuestDetailScreenState extends State { _stage = QuestStage.completed; } }); - // 진행완료를 남긴다 — 목록 「보상받기」 · 진행중 탭이 다시 읽어도 같게(원장 C-83). + // 진행완료를 남긴다 — 목록 「보상받기」 · 진행중 탭이 다시 읽어도 같게(정책 원장 C-83). if (reached) { unawaited( _persist( @@ -1087,7 +1087,7 @@ class _QuestDetailScreenState extends State { return; } - // 기본 보상은 이미 원장에 있다. 광고는 그 위의 선택적 증분이다(원장 B-03 · C-122) — + // 기본 보상은 이미 원장에 있다. 광고는 그 위의 선택적 증분이다(정책 원장 B-03 · C-122) — // 광고가 실패해도 기본 보상은 그대로다. // 일 한도를 다 쓴 사람에게는 모달을 띄우지 않는다(별첨 1 ⑥ 「일일 한도 도달 시 노출 제한」) — // 띄우면 광고를 끝까지 보고도 증분 0 이다. @@ -1142,7 +1142,7 @@ class _QuestDetailScreenState extends State { /// 광고 시청 전 대기(`494:14332`)를 띄운 채 광고를 불러와 보여 준다. /// /// 끝까지 봤으면 노출 ID, 아니면 null 이다. 불러오지 못했거나 중간에 닫았으면 - /// 그 사실을 알리고 **기본 보상으로 넘어간다**(원장 B-03 — 실패 시 기본 보상 지급 · + /// 그 사실을 알리고 **기본 보상으로 넘어간다**(정책 원장 B-03 — 실패 시 기본 보상 지급 · /// 횟수 미차감). 대기 중 「건너뛰기」를 누르면 늦게 온 광고는 띄우지 않고 버린다. /// 이번 보상에서 광고가 실제로 화면에 떴는가 — skip 으로 닫았을 때도 시청 후 애니메이션을 거친다. bool _adShown = false; diff --git a/app/lib/features/quest/quest_evidence.dart b/app/lib/features/quest/quest_evidence.dart index c9b8a44..2eb0ce1 100644 --- a/app/lib/features/quest/quest_evidence.dart +++ b/app/lib/features/quest/quest_evidence.dart @@ -147,7 +147,7 @@ typedef ShakeLive = ({ ShakeHit? last, }); -/// Music Shake 구간 판정표 — `fn-quest-verify-music-shake` 가 받는 **증거**(원장 B-24). +/// Music Shake 구간 판정표 — `fn-quest-verify-music-shake` 가 받는 **증거**(정책 원장 B-24). /// /// 시각은 **영상 재생 시각**(0 = 영상 처음)이다 — 공식 YouTube 영상을 보며 흔드는 퀘스트라 /// (PRD-04) 박은 영상에 붙는다. 곡의 박(`quests.bpm`)마다 한 구간이고 첫 박은 `quests.firstBeatMs` 이며(판 길이 `totalSegments` 는 곡 전체를 덮도록 운영자가 정한다), 흔들기는 가장 가까운 박 @@ -163,7 +163,7 @@ class ShakeJudge { List hits = const [], this.position = Duration.zero, }) : assert(bpm > 0 && segments > 0) { - // 일시 정지에서 「이어하기」 — 멈추기 전 판정을 그대로 잇는다(원장 C-83). + // 일시 정지에서 「이어하기」 — 멈추기 전 판정을 그대로 잇는다(정책 원장 C-83). for (var i = 0; i < hits.length && i < segments; i++) { if (hits[i] >= 0) _best[i] = hits[i]; } diff --git a/app/lib/features/quest/quest_list_screen.dart b/app/lib/features/quest/quest_list_screen.dart index 362dcc8..03cb350 100644 --- a/app/lib/features/quest/quest_list_screen.dart +++ b/app/lib/features/quest/quest_list_screen.dart @@ -144,7 +144,7 @@ class _QuestListScreenState extends State { Future _act(QuestCard card) async { // 명세 `518:88405` *보상 받기 버튼 클릭 시 POP 수령* — 목록에서 바로 받는다. 일시 정지는 「이어하기」 - // (원장 C-83). 판정 · 광고 모달 · 보상 완료 화면은 상세의 같은 경로를 쓴다. + // (정책 원장 C-83). 판정 · 광고 모달 · 보상 완료 화면은 상세의 같은 경로를 쓴다. if (card.stage == QuestStage.completed || card.stage == QuestStage.paused) { await _open(card, act: true); return; diff --git a/app/lib/features/quest/quest_repository_firestore.dart b/app/lib/features/quest/quest_repository_firestore.dart index ce08f0a..7302965 100644 --- a/app/lib/features/quest/quest_repository_firestore.dart +++ b/app/lib/features/quest/quest_repository_firestore.dart @@ -146,7 +146,7 @@ class FirestoreQuestRepository implements QuestRepository { return cards; } - /// 진행 세션이 있는 걸음 Quest 의 오늘 걸음 — Walk 는 화면 밖 걸음까지 **실제 상태**로 보인다(원장 C-62 · C-83). + /// 진행 세션이 있는 걸음 Quest 의 오늘 걸음 — Walk 는 화면 밖 걸음까지 **실제 상태**로 보인다(정책 원장 C-62 · C-83). /// Health 를 한 번 읽고, 못 쓰면 이번 실행에서 센서로 센 값, 그것도 없으면 null(세션의 마지막 값). Future _walkFor(Map quest, Map progress) async { if (QuestType.parse(quest.strOr('type')) != QuestType.steps || progress['session'] is! Map) { @@ -183,7 +183,7 @@ class FirestoreQuestRepository implements QuestRepository { s != null && !s.fits(Duration(milliseconds: quest.intOr('firstBeatMs')), quest.intOr('totalSegments')); - /// 진행 세션(`questProgress.session` — `fn-quest-progress` 만 쓴다 · 원장 C-83). + /// 진행 세션(`questProgress.session` — `fn-quest-progress` 만 쓴다 · 정책 원장 C-83). static QuestSession? sessionOf(Map progress) { final raw = progress['session']; if (raw is! Map) return null; @@ -560,7 +560,7 @@ class FirestoreQuestRepository implements QuestRepository { 'description': description, }); - /// 진행중 · 일시 정지 · 진행완료 · 중단을 서버 진행 문서에 남긴다(`fn-quest-progress` · 원장 C-83). + /// 진행중 · 일시 정지 · 진행완료 · 중단을 서버 진행 문서에 남긴다(`fn-quest-progress` · 정책 원장 C-83). /// /// 진행 문서는 규칙상 서버 전용이라 함수로만 쓴다. 보상 상태는 여전히 [verify] 가 지급에 성공한 뒤 /// 서버 카운터로 확정되고, 지급되면 서버가 세션을 지운다. @@ -839,7 +839,7 @@ class FirestoreQuestRepository implements QuestRepository { Future> _verifyShake(QuestCard card) async { final run = _shakeRun; - // 끝낸 판을 서버 세션에 남겨 두었다면 그 판정표로 — 목록에서 바로 「보상받기」(원장 C-83). + // 끝낸 판을 서버 세션에 남겨 두었다면 그 판정표로 — 목록에서 바로 「보상받기」(정책 원장 C-83). final saved = card.session?.state == QuestStage.completed ? card.session : null; final judgement = run?.judgement() ?? saved?.judgement; final segments = run?.segments ?? saved?.total ?? 0; diff --git a/app/lib/features/quest/widgets/paused_quest_capsule.dart b/app/lib/features/quest/widgets/paused_quest_capsule.dart index 02d8ffd..dff395c 100644 --- a/app/lib/features/quest/widgets/paused_quest_capsule.dart +++ b/app/lib/features/quest/widgets/paused_quest_capsule.dart @@ -5,7 +5,7 @@ import '../quest_data.dart'; import '../quest_detail_screen.dart'; import 'quest_card_tile.dart' show pausedLabel; -/// 탭 화면(HOME · INSIDE · GALAXY · MY) 하단 탭 바로 위 한 줄 캡슐 — 원장 C-83(10/3 통합회신 Q-OB-01 A). +/// 탭 화면(HOME · INSIDE · GALAXY · MY) 하단 탭 바로 위 한 줄 캡슐 — 정책 원장 C-83(10/3 통합회신 Q-OB-01 A). /// /// 일시 정지한 Quest 가 있을 때만 가장 최근 1건을 「일시 정지 · Quest명 · 43/60」으로 띄우고, 누르면 그 /// Quest 상세로 간다. Walk · 출석은 대상이 아니다(일시 정지가 되지 않는다). 새 탭 · 독립 화면이 아니다. diff --git a/app/lib/features/quest/widgets/quest_card_tile.dart b/app/lib/features/quest/widgets/quest_card_tile.dart index 70474e2..50654e1 100644 --- a/app/lib/features/quest/widgets/quest_card_tile.dart +++ b/app/lib/features/quest/widgets/quest_card_tile.dart @@ -70,7 +70,7 @@ class QuestIconBox extends StatelessWidget { _ => (bg: AppColors.onCosmic, fg: Colors.black), }; -/// 일시 정지 표기 — 「카테고리 · 일시 정지 · 43/60」. 카드와 탭 위 캡슐이 같이 쓴다(원장 C-83). +/// 일시 정지 표기 — 「카테고리 · 일시 정지 · 43/60」. 카드와 탭 위 캡슐이 같이 쓴다(정책 원장 C-83). String pausedLabel(QuestCard card, {bool title = false}) => title ? _paused(card.title, card.session, titleLast: true) : _paused(card.categoryName, card.session); @@ -159,7 +159,7 @@ class QuestCardTile extends StatelessWidget { ), const SizedBox(height: 6), Text( - // 진행 상태 값 「일시 정지」 · 멈춘 진행값(원장 C-83 — 기존 카드 영역 안). + // 진행 상태 값 「일시 정지」 · 멈춘 진행값(정책 원장 C-83 — 기존 카드 영역 안). questCardSubLine(card), style: const TextStyle( fontSize: 14, diff --git a/app/lib/features/translation/translation_comments.dart b/app/lib/features/translation/translation_comments.dart index cf99ed4..726b615 100644 --- a/app/lib/features/translation/translation_comments.dart +++ b/app/lib/features/translation/translation_comments.dart @@ -6,7 +6,7 @@ import '../../core/theme/tokens.dart'; import '../../core/ui/app_icons.dart'; import 'translation_data.dart'; -/// 참여형 번역 — 「번역 댓글」 UI (FR-I18N-02 · 원장 B-63 · 해석 확인 요청서 i18n-참여형번역 2.1). +/// 참여형 번역 — 「번역 댓글」 UI (FR-I18N-02 · 정책 원장 B-63 · 해석 확인 요청서 i18n-참여형번역 2.1). /// /// Figma /// - `540:19136` · `527:21198` 댓글 입력 영역 — 「번역 댓글 작성 버튼(지구본) · 클릭 시 키보드 diff --git a/app/lib/features/translation/translation_data.dart b/app/lib/features/translation/translation_data.dart index 5f91850..06222df 100644 --- a/app/lib/features/translation/translation_data.dart +++ b/app/lib/features/translation/translation_data.dart @@ -6,7 +6,7 @@ import '../../core/data/backend.dart'; /// 참여형 번역 — 요구사항 FR-I18N-02 · 정책 원장 B-63 · 별첨 1 ⑧. /// -/// 데이터 관계(원장 B-63): 번역 등록 → 「번역함」 표시 → 다른 멤버 유효/무효 마킹 → +/// 데이터 관계(정책 원장 B-63): 번역 등록 → 「번역함」 표시 → 다른 멤버 유효/무효 마킹 → /// 24시간 pending → `fn-translation-settle` 확정 → 일반 POP 5. /// /// 화면에서는 댓글 영역의 「번역 댓글」이다(Figma `201:50554` · `540:19136` · 해석 확인 diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 9aa4d41..639e7ba 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -134,7 +134,7 @@ dependencies: # API)」 · 대조표 B-02). 2주차 선검증리포트 §2 가 채택한 13.3.1 그대로다. 못 쓰면 위 pedometer 로 센다(B-26). # 최소 OS 하한 영향 없음(선검증 §3.5 에 이미 들어간 값): Android minSdk 26(앱 29) · iOS 14.0(앱 15.0). health: ^13.3.1 - # 위치 Quest QR 인증 — 현장 QR 스캔(원장 C-03 · PRD-05). iOS 는 Apple Vision, Android 는 ML Kit 번들. + # 위치 Quest QR 인증 — 현장 QR 스캔(정책 원장 C-03 · PRD-05). iOS 는 Apple Vision, Android 는 ML Kit 번들. # 최소 OS 하한 영향 없음: Android minSdk 23(앱 29) · iOS 12.0(앱 15.0). mobile_scanner: ^7.4.2 # 생성자 상세의 「현장에 붙일 인증 QR」 그리기. 순수 Dart 라 네이티브 하한이 없다. diff --git a/app/test/backend_repository_test.dart b/app/test/backend_repository_test.dart index c27b9d4..55dfb5b 100644 --- a/app/test/backend_repository_test.dart +++ b/app/test/backend_repository_test.dart @@ -359,7 +359,7 @@ void main() { expect(snapshot.joined, isTrue); }); - // 원장 B-08 · B-17 — 성운은 랭킹 모집단이 아니다. 끼면 정식 행성 번호가 밀린다. + // 정책 원장 B-08 · B-17 — 성운은 랭킹 모집단이 아니다. 끼면 정식 행성 번호가 밀린다. test('Stage0 성운은 순위를 매기기 전에 빠지고 맨 바깥 궤도 자리에 선다', () async { final db = await _seed(); await db.doc('fandoms/neb').set({ @@ -374,7 +374,7 @@ void main() { expect(snapshot.planets.firstWhere((p) => p.id == 'neb').rankLabel, '-'); }); - // 원장 B-17 — 최근 7일 유효 활동이 없는 정식 행성(서버 rankingEligible: false)도 순위 밖이다. + // 정책 원장 B-17 — 최근 7일 유효 활동이 없는 정식 행성(서버 rankingEligible: false)도 순위 밖이다. test('최근 7일 활동이 없는 행성은 순위 없이 바깥 궤도에 서고 Ranking View 중심이 되지 않는다', () async { final db = await _seed(); await db.doc('fandoms/idle').set({ diff --git a/app/test/design_match/d_test.dart b/app/test/design_match/d_test.dart index 255b3d5..83fc61e 100644 --- a/app/test/design_match/d_test.dart +++ b/app/test/design_match/d_test.dart @@ -34,7 +34,7 @@ import 'package:flutter_test/flutter_test.dart'; const _width = 375.0; const _padding = FakeViewPadding(top: 48 * 2, bottom: 34 * 2); -/// Figma 사본의 표본 문구(BTS_Hong · hon******@gmail.com …). 숫자는 원장 체계(C-18)로 담는다. +/// Figma 사본의 표본 문구(BTS_Hong · hon******@gmail.com …). 숫자는 정책 원장 체계(C-18)로 담는다. class _FigmaMy extends SampleMyRepository { @override Future profile() async => const MyProfile( diff --git a/app/test/eclipse_figma_flow_test.dart b/app/test/eclipse_figma_flow_test.dart index ac521ae..6ff86bb 100644 --- a/app/test/eclipse_figma_flow_test.dart +++ b/app/test/eclipse_figma_flow_test.dart @@ -217,7 +217,7 @@ void main() { }); }); - group('Music Shake 보상 표시 (원장 B-24)', () { + group('Music Shake 보상 표시 (정책 원장 B-24)', () { test('퀘스트 rewardPop 이 없으면 서버와 같은 운영값 musicShake.pop 을 기본액으로 적는다', () { const policy = {'stepsPop': 50, 'musicShake': {'pop': 1}}; // 걸음 보상(stepsPop)을 빌려 쓰지 않는다. @@ -228,7 +228,7 @@ void main() { }); }); - group('걸음 보상 표시 (원장 B-26)', () { + group('걸음 보상 표시 (정책 원장 B-26)', () { const policy = { 'stepTiers': [ {'steps': 5000, 'pop': 6}, diff --git a/app/test/eclipse_live_test.dart b/app/test/eclipse_live_test.dart index d1919ba..81ca5b3 100644 --- a/app/test/eclipse_live_test.dart +++ b/app/test/eclipse_live_test.dart @@ -492,7 +492,7 @@ void main() { }); }); - group('구독 공유 — 팬덤당 하나 (검수 D5)', () { + group('구독 공유 — 팬덤당 하나', () { test('여러 화면이 같은 원본 하나를 나눠 쓴다', () async { // 화면마다 snapshots() 를 열면 한 팬덤에 리스너가 화면 수만큼 붙는다. var opened = 0; @@ -592,7 +592,7 @@ void main() { expect(LiveEventKind.of('무엇인가'), LiveEventKind.notice); }); - test('마감이 지난 이클립스는 만료로 읽는다 (검수 D3)', () { + test('마감이 지난 이클립스는 만료로 읽는다', () { final now = DateTime(2026, 9, 14, 12); expect( eclipseFixture(endsAt: now.subtract(const Duration(seconds: 1))) diff --git a/app/test/f2_screens_test.dart b/app/test/f2_screens_test.dart index 7e1613f..a590890 100644 --- a/app/test/f2_screens_test.dart +++ b/app/test/f2_screens_test.dart @@ -270,7 +270,7 @@ void main() { reason: '한 프레임에 순간이동하면 「행성 간 이동감」이 없다'); }); - // 리뷰 M3 — HUD 위/아래 뒤집힘이 비행 중에 일어나면 카드가 행성을 가로질러 + // HUD 위/아래 뒤집힘이 비행 중에 일어나면 카드가 행성을 가로질러 // 순간 이동한다. 선택 시점에 정한 방향을 전환이 끝날 때까지 붙들어야 한다. // // 표본 6행성은 어느 것도 화면 위쪽 끝에서 출발하지 않아 뒤집힘이 나지 않는다. @@ -328,7 +328,7 @@ void main() { expect(find.textContaining(RegExp(r'상위 \d+%')), findsNothing); }); - // 리뷰 M2 — 행성이 뷰포트 밖이면 HUD 를 아예 그리지 않는다. 카드만 가장자리에 + // 행성이 뷰포트 밖이면 HUD 를 아예 그리지 않는다. 카드만 가장자리에 // clamp 해 두면 어느 행성을 가리키는지 알 수 없고 연결선은 화면 밖에 그려진다. // 고른 행성은 카메라가 따라가므로(10/5) 확대·초점 이동으로도 화면 밖으로 밀리지 않는다. testWidgets('확대해도 고른 행성은 가운데에 남고 HUD 도 남는다', (tester) async { diff --git a/app/test/f3_screens_test.dart b/app/test/f3_screens_test.dart index 9539c47..e0d45a7 100644 --- a/app/test/f3_screens_test.dart +++ b/app/test/f3_screens_test.dart @@ -169,7 +169,7 @@ void main() { eclipse: NoEclipseRepository(), )); expect(find.text('TOTAL FPI : 284,310'), findsOneWidget); - // 원장 B-15 검증 기준 — 화면에 마지막 갱신 시각 노출(서버 시각이 없으면 --:--). + // 정책 원장 B-15 검증 기준 — 화면에 마지막 갱신 시각 노출(서버 시각이 없으면 --:--). expect(find.text('마지막 갱신 --:--'), findsOneWidget); expect(find.textContaining('30초'), findsNothing); // 개설자 이름은 서버에 없으면 그리지 않는다 — 시안 표본 이름을 박지 않는다. @@ -226,7 +226,7 @@ void main() { testWidgets('이클립스가 발동 중이면 침식·방어 진행을 띄운다', (tester) async { // 스펙 B — 「집계 문서를 갤럭시·행성 상세 HUD 에서 실시간으로 표시한다」. - // 침식은 TOTAL FPI 를 깎는 경로이므로(원장 B-55) 그 카드 위에 온다. + // 침식은 TOTAL FPI 를 깎는 경로이므로(정책 원장 B-55) 그 카드 위에 온다. await _pumpTall(tester, PlanetDetailScreen( repository: const SamplePlanetRepository(), eclipse: SampleEclipseRepository(), diff --git a/app/test/f4_screens_test.dart b/app/test/f4_screens_test.dart index 11df556..4601da9 100644 --- a/app/test/f4_screens_test.dart +++ b/app/test/f4_screens_test.dart @@ -310,7 +310,7 @@ void main() { await tester.tap(find.widgetWithText(FilledButton, '보상받기')); await tester.pumpAndSettle(); // 별첨 B B-28 — 배수형 철회. 추가분은 고정형 운영값(C-02 초기 10 기여 POP). - // 원장 C-116 — 「+N 기여 POP」 표기. + // 정책 원장 C-116 — 「+N 기여 POP」 표기. expect(find.text('+10 기여 POP'), findsOneWidget); expect(find.textContaining('배'), findsNothing); // 별첨1 ⑥ No-Force — 보지 않아도 기본 보상을 받는다. @@ -580,7 +580,7 @@ void main() { }); }); - group('일시 정지 · 이어하기 · 목록 보상받기 (원장 C-83 · 명세 518:88405)', () { + group('일시 정지 · 이어하기 · 목록 보상받기 (정책 원장 C-83 · 명세 518:88405)', () { testWidgets('목록 「보상받기」는 상세를 거쳐 바로 수령 흐름(보상 안내 모달)을 연다', (tester) async { await pumpWide(tester, const QuestListScreen(repository: _sample)); await tester.pump(); diff --git a/app/test/f5_screens_test.dart b/app/test/f5_screens_test.dart index 15a1424..32238ff 100644 --- a/app/test/f5_screens_test.dart +++ b/app/test/f5_screens_test.dart @@ -460,7 +460,7 @@ void main() { await tester.enterText( find.widgetWithText(TextField, '캠페인 설명을 작성해주세요.'), '스태프 응원', ); - // 원장 B-32 · B-59 — 목표는 입력이 아니라 상품 선택. C-14 — 권장 기간 자동 입력. + // 정책 원장 B-32 · B-59 — 목표는 입력이 아니라 상품 선택. C-14 — 권장 기간 자동 입력. expect(find.widgetWithText(TextField, '목표 POP'), findsNothing); await tester.tap(find.text('커피차 서포트')); await tester.pump(); diff --git a/app/test/galaxy_motion_test.dart b/app/test/galaxy_motion_test.dart index 4d765c1..ce2b63f 100644 --- a/app/test/galaxy_motion_test.dart +++ b/app/test/galaxy_motion_test.dart @@ -52,7 +52,7 @@ void main() { expect(clock.velocity, closeTo(GalaxyMotion.idleSpin, 1e-6)); }); - // 리뷰 H1 — onInteractionUpdate 가 focalPointDelta.dx 를 무조건 넘기면 + // onInteractionUpdate 가 focalPointDelta.dx 를 무조건 넘기면 // 세로 드래그·핀치 줌이 각속도를 덮어써 궤도가 선다. test('세로 드래그·핀치 줌은 각속도를 바꾸지 않는다', () { final dragged = GalaxyClock(); @@ -108,7 +108,7 @@ void main() { ); }); - // 리뷰 H2 — 기본 궤도 속도가 프로토타입의 1/4~1/5 이었다. + // 기본 궤도 속도가 프로토타입의 1/4~1/5 이었다. // 프로토타입은 `time = Date.now()*0.0005` 에 `speed = 0.8/(rank+1)` 를 곱한다. test('기본 궤도 각속도가 프로토타입과 같다 — 0.4/(rank+1) rad/s', () { for (final rank in [2, 3, 6, 12]) { @@ -136,7 +136,7 @@ void main() { ); }); - // 원장 B-21 — 기본은 내 Primary 중심, Ranking View 는 FPI 상위 중심. + // 정책 원장 B-21 — 기본은 내 Primary 중심, Ranking View 는 FPI 상위 중심. test('Ranking View 는 FPI 1위(성운 제외)를 가운데 두고 내 행성은 순위 궤도를 돈다', () { const snapshot = GalaxySnapshot( myPop: 0, @@ -173,7 +173,7 @@ void main() { expect(maxDy, lessThan(maxDx)); }); - // 리뷰 M4 — extent=900 에 간격 52 를 그대로 쓰면 8위부터 궤도가 맵 반쪽을 + // extent=900 에 간격 52 를 그대로 쓰면 8위부터 궤도가 맵 반쪽을 // 넘어 Stack 에 잘리고, 반지름은 19위부터 음수가 된다. test('행성이 많아도 전부 맵 안에 남는다 — 6·30·100개', () { for (final n in [6, 30, 100]) { diff --git a/app/test/i18n_test.dart b/app/test/i18n_test.dart index c80c9c4..84dc3a9 100644 --- a/app/test/i18n_test.dart +++ b/app/test/i18n_test.dart @@ -42,7 +42,7 @@ Widget _app(LocaleController c, Widget home) => ListenableBuilder( void main() { setUp(() => SharedPreferences.setMockInitialValues({})); - group('지원 언어 (원장 B-76)', () { + group('지원 언어 (정책 원장 B-76)', () { test('supportedLocales 가 정확히 ko · en · ja · zh-CN · es 다', () { expect(appLocales.map(localeTag).toList(), [ 'ko', diff --git a/app/test/monetization_test.dart b/app/test/monetization_test.dart index 64f504b..7ce555c 100644 --- a/app/test/monetization_test.dart +++ b/app/test/monetization_test.dart @@ -14,7 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:in_app_purchase/in_app_purchase.dart'; -/// 결제 경계 — 별첨 1 ⑥ 「영수증 서버검증 통과」 · 원장 B-02 · B-72. +/// 결제 경계 — 별첨 1 ⑥ 「영수증 서버검증 통과」 · 정책 원장 B-02 · B-72. /// /// 스토어와 서버는 대역이다. 보는 것은 두 가지다 — **서버가 받아들인 뒤에만 스토어에 /// 구매 완료를 알리는가**, 그리고 MY 결제 내역이 서버 문서를 화면 상태로 옳게 옮기는가. diff --git a/app/test/quest_react_push_test.dart b/app/test/quest_react_push_test.dart index 45d16c0..146ffcb 100644 --- a/app/test/quest_react_push_test.dart +++ b/app/test/quest_react_push_test.dart @@ -14,7 +14,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'eclipse_stubs.dart'; /// UX·FX v1.5 2차 약속분(10/8) — 진척현황 부록 B-3 1-2 · 4-2 · 4-3(퀘스트 완료 → 성공 햅틱 · 행성 반응), -/// 1-4 · 8-6(Push ↔ 라이브 띠 중복 제어 · 알림 위계 · Push 탭 실제 화면 이동), 원장 C-78 단계 배정. +/// 1-4 · 8-6(Push ↔ 라이브 띠 중복 제어 · 알림 위계 · Push 탭 실제 화면 이동), 정책 원장 C-78 단계 배정. void main() { group('C-78 단계 배정 — Push · 알림함 같은 표', () { test('위기 · 공동행동 · 성취 · 결과 · 그 밖', () { diff --git a/app/test/quest_repository_test.dart b/app/test/quest_repository_test.dart index 24ae2de..40e970f 100644 --- a/app/test/quest_repository_test.dart +++ b/app/test/quest_repository_test.dart @@ -630,7 +630,7 @@ service cloud.firestore { }); }); - group('진행 상태 저장 · 일시 정지 · 최신순 (원장 C-83 · 명세 518:88395)', () { + group('진행 상태 저장 · 일시 정지 · 최신순 (정책 원장 C-83 · 명세 518:88395)', () { Future shakeSeed() async { final db = await _seed(); await db.doc('quests/shake').set({ diff --git a/app/test/translation_test.dart b/app/test/translation_test.dart index 0ba0eb7..73adc8e 100644 --- a/app/test/translation_test.dart +++ b/app/test/translation_test.dart @@ -15,7 +15,7 @@ import 'package:fancake/core/theme/app_theme.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; -/// 참여형 번역 — FR-I18N-02 · 원장 B-63 · E-TRN-01. +/// 참여형 번역 — FR-I18N-02 · 정책 원장 B-63 · E-TRN-01. /// 화면에서는 댓글 영역의 「번역 댓글」이다(Figma `201:50554` · `540:19136`). void main() { const target = 'questions/q1';