코드 주석을 이 저장소 기준으로 정리

This commit is contained in:
Sn-Kinos
2026-10-16 18:04:14 +09:00
parent 80b90aad4c
commit 4236d75fd1
89 changed files with 212 additions and 212 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ class _FancakeAppState extends State<FancakeApp> with WidgetsBindingObserver {
Future<void> _startPush(PushMessaging push) async {
// 알림 권한은 여기서 묻지 않는다 — 온보딩 ⑥ 권한 안내가 쓰임을 설명한 뒤 묻는다
// (FR-CMN-08 · 원장 C-84, 로그인 직후 일괄 요청 금지). 거부해도 토큰과 구독은
// (FR-CMN-08 · 정책 원장 C-84, 로그인 직후 일괄 요청 금지). 거부해도 토큰과 구독은
// 사용자가 나중에 설정에서 허용했을 때 그대로 살아 있어야 한다.
await push.syncToken();
+1 -1
View File
@@ -1,6 +1,6 @@
import 'backend.dart';
/// 내 초대 코드 — 공유 링크 `?ref=` 에 붙는다(원장 B-69 추천).
/// 내 초대 코드 — 공유 링크 `?ref=` 에 붙는다(정책 원장 B-69 추천).
///
/// 세션당 한 번 `fnInviteCodeGet` 을 부른다(없으면 서버가 만든다). 공유는 이 값을 기다리지
/// 않는다 — 아직 없거나 실패했으면 `?ref=` 없이 나간다. 계정이 바뀌면 다시 받는다.
+1 -1
View File
@@ -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)에 붙여」 본 광고인지
+2 -2
View File
@@ -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안)**
+2 -2
View File
@@ -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: [
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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);
+2 -2
View File
@@ -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;
@@ -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,
@@ -16,7 +16,7 @@ import '../../../core/i18n/app_locale.dart';
/// 배너는 세 상태를 그린다.
/// - 활성 이클립스 없음(또는 마감이 지남) → 아무것도 그리지 않는다(빈 [SizedBox]).
/// - 침식 진행 중 → 침식률 · 방어 진행률 · 남은 시간.
/// - 방어 성공(`defended`) → 회복 문구(원장 B-55 「방어 성공 시 침식분 회복」).
/// - 방어 성공(`defended`) → 회복 문구(정책 원장 B-55 「방어 성공 시 침식분 회복」).
///
/// 방어는 **공동 Shake 참여**이며 POP 을 소모하지 않는다. 눌러 들어가는 곳은
/// 「흔들어 방어」 화면이다.
@@ -10,7 +10,7 @@ import '../eclipse_defend_screen.dart';
///
/// 명세 `516:88125` *데이터: 이클립스 발생 잔여 시간 · 규칙: 성운(행성) 아래 중앙 정렬,
/// 어드민에서 설정된 이클립스 이벤트 발생 시간의 N분 전 노출 · 동작: 클릭 시 이클립스 상세로
/// 이동*. N분은 서버 회차의 `warnAt`(원장 C-22 발생 3분 전)이다 — 회차 문서가 그때부터
/// 이동*. N분은 서버 회차의 `warnAt`(정책 원장 C-22 발생 3분 전)이다 — 회차 문서가 그때부터
/// `scheduled` 로 있으므로 그 문서가 보이는 동안 띄운다. 발생 뒤 방어 창(60초) 동안은
/// 남은 초로 바뀌고, 결과가 나거나 창이 닫히면 접는다.
///
@@ -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 「개인 이벤트를 연속 티커로 쌓기보다 집단 상태로 집약」 ·
+4 -4
View File
@@ -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<void> setPrimary(String fandomId);
+1 -1
View File
@@ -98,7 +98,7 @@ class _GalaxyScreenState extends State<GalaxyScreen>
PlanetNode? _selected;
int _selectedIndex = 0;
/// Ranking View(원장 B-21). 끄면 기본 보기 — 내 행성 중심.
/// Ranking View(정책 원장 B-21). 끄면 기본 보기 — 내 행성 중심.
bool _ranking = false;
/// 보기를 바꾸면 **새 중심 행성으로 날아간다** — 「Ranking View 전환 시 FPI 상위 중심으로
+4 -4
View File
@@ -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<int> 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);
@@ -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')
@@ -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 등급 · 거버넌스」 문장은 그 내용으로 바꾼다.
@@ -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).
@@ -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<void> showItemSheet(
+2 -2
View File
@@ -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)),
],
),
+1 -1
View File
@@ -81,7 +81,7 @@ class InsideQuestion {
/// 답변이 채택되어 현상금이 지급된 상태.
final bool isSolved;
/// 채택 기한(원장 B-39 · 7일)이 지난 뒤의 환불 단계.
/// 채택 기한(정책 원장 B-39 · 7일)이 지난 뒤의 환불 단계.
final InsideRefund refund;
/// 채택을 받을 수 있는 상태 — 채택도 만료도 아니다.
+2 -2
View File
@@ -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<FocusStatus?> focus();
/// Focus 오늘 몫 받기(원장 B-04). 받은 기여 POP 을 돌려준다. 구독이 아니면 E-PAY-03.
/// Focus 오늘 몫 받기(정책 원장 B-04). 받은 기여 POP 을 돌려준다. 구독이 아니면 E-PAY-03.
Future<int> claimFocusDaily();
Future<Map<PushTopic, bool>> pushSettings();
+2 -2
View File
@@ -102,7 +102,7 @@ class _MyScreenState extends State<MyScreen> {
}
}
/// Focus 오늘 몫 받기(원장 B-04). 지급은 서버가 하고 화면은 결과만 알린다.
/// Focus 오늘 몫 받기(정책 원장 B-04). 지급은 서버가 하고 화면은 결과만 알린다.
Future<void> _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` 로 정해져 있어 통계 카드 아래에 둔다.
+1 -1
View File
@@ -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] 에서 바꾼다.
@@ -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) {
@@ -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) {
+2 -2
View File
@@ -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 이고,
@@ -214,7 +214,7 @@ class _PlanetDetailScreenState extends State<PlanetDetailScreen> {
);
/// 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({
@@ -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)을 따른다.
+1 -1
View File
@@ -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).
+2 -2
View File
@@ -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 {
@@ -319,7 +319,7 @@ class _QuestCreateScreenState extends State<QuestCreateScreen> {
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),
+18 -18
View File
@@ -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<String, int>? judgement;
}
/// 일시 정지 · 진행 세션이 바뀌었다 — 탭 화면 하단 캡슐(원장 C-83)이 다시 읽는다.
/// 일시 정지 · 진행 세션이 바뀌었다 — 탭 화면 하단 캡슐(정책 원장 C-83)이 다시 읽는다.
final questSessionSignal = ValueNotifier<int>(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<String> verifyMethods;
@@ -601,7 +601,7 @@ class QuestDetail {
final List<QuestSubmission> 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<String> 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<QuestCard?> pausedQuest();
Future<String> 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(
@@ -76,7 +76,7 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
/// 방금 끝낸 Music Shake 판의 판정 합계 — 결과 화면 · 결과 카드에 쓴다.
Map<String, int>? _shakeResult;
/// 진행 세션(원장 C-83) — 서버에서 읽은 값을 이 화면에서 멈추면 바로 갈아 끼운다(「이어하기」 시작점).
/// 진행 세션(정책 원장 C-83) — 서버에서 읽은 값을 이 화면에서 멈추면 바로 갈아 끼운다(「이어하기」 시작점).
QuestSession? _session;
/// [QuestDetailScreen.autoAct] 를 한 번만 쓴다.
@@ -125,7 +125,7 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
}
}
/// 진행 세션을 서버에 남긴다(원장 C-83). 실패해도 이 화면의 진행은 계속된다 — 표시용 상태다.
/// 진행 세션을 서버에 남긴다(정책 원장 C-83). 실패해도 이 화면의 진행은 계속된다 — 표시용 상태다.
Future<void> _persist(
String questId,
QuestStage next, [
@@ -267,7 +267,7 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
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<QuestDetailScreen> {
),
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<QuestDetailScreen> {
}
/// Music Shake 한 판 — 공식 영상을 보며 흔드는 진행 화면을 띄운다(PRD-04).
/// 끝까지 했으면 판정표를 남기고 「보상받기」로, 중간에 나왔으면 **일시 정지**로 둔다(원장 C-83).
/// 끝까지 했으면 판정표를 남기고 「보상받기」로, 중간에 나왔으면 **일시 정지**로 둔다(정책 원장 C-83).
/// [resume] 이 있으면 멈춘 판정 · 영상 시각부터 잇는다(「이어하기」).
Future<void> _playShake(QuestDetail detail, {QuestSession? resume}) async {
final MusicShakeSetup setup;
@@ -937,7 +937,7 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
_stage = QuestStage.completed;
}
});
// 진행완료를 남긴다 — 목록 「보상받기」 · 진행중 탭이 다시 읽어도 같게(원장 C-83).
// 진행완료를 남긴다 — 목록 「보상받기」 · 진행중 탭이 다시 읽어도 같게(정책 원장 C-83).
if (reached) {
unawaited(
_persist(
@@ -1087,7 +1087,7 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
return;
}
// 기본 보상은 이미 원장에 있다. 광고는 그 위의 선택적 증분이다(원장 B-03 · C-122) —
// 기본 보상은 이미 원장에 있다. 광고는 그 위의 선택적 증분이다(정책 원장 B-03 · C-122) —
// 광고가 실패해도 기본 보상은 그대로다.
// 일 한도를 다 쓴 사람에게는 모달을 띄우지 않는다(별첨 1 ⑥ 「일일 한도 도달 시 노출 제한」) —
// 띄우면 광고를 끝까지 보고도 증분 0 이다.
@@ -1142,7 +1142,7 @@ class _QuestDetailScreenState extends State<QuestDetailScreen> {
/// 광고 시청 전 대기(`494:14332`)를 띄운 채 광고를 불러와 보여 준다.
///
/// 끝까지 봤으면 노출 ID, 아니면 null 이다. 불러오지 못했거나 중간에 닫았으면
/// 그 사실을 알리고 **기본 보상으로 넘어간다**(원장 B-03 — 실패 시 기본 보상 지급 ·
/// 그 사실을 알리고 **기본 보상으로 넘어간다**(정책 원장 B-03 — 실패 시 기본 보상 지급 ·
/// 횟수 미차감). 대기 중 「건너뛰기」를 누르면 늦게 온 광고는 띄우지 않고 버린다.
/// 이번 보상에서 광고가 실제로 화면에 떴는가 — skip 으로 닫았을 때도 시청 후 애니메이션을 거친다.
bool _adShown = false;
+2 -2
View File
@@ -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<double> 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];
}
@@ -144,7 +144,7 @@ class _QuestListScreenState extends State<QuestListScreen> {
Future<void> _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;
@@ -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<int?> _walkFor(Map<String, dynamic> quest, Map<String, dynamic> 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<String, dynamic> 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<Map<String, dynamic>> _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;
@@ -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 · 출석은 대상이 아니다(일시 정지가 되지 않는다). 새 탭 · 독립 화면이 아니다.
@@ -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,
@@ -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` 댓글 입력 영역 — 「번역 댓글 작성 버튼(지구본) · 클릭 시 키보드
@@ -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` · 해석 확인