From 65f73280aa79843bf624af68e4c0d9d9ce421457 Mon Sep 17 00:00:00 2001 From: Sn-Kinos Date: Tue, 6 Oct 2026 04:06:36 +0900 Subject: [PATCH] =?UTF-8?q?HOME=20=ED=80=98=EC=8A=A4=ED=8A=B8=20=EC=A4=84?= =?UTF-8?q?=20=EB=B6=84=EB=A5=98=20=C2=B7=20=EC=9D=BC=EC=8B=9C=20=EC=A0=95?= =?UTF-8?q?=EC=A7=80=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/lib/features/home/home_data.dart | 8 +++- .../home/home_repository_firestore.dart | 11 +++++- app/lib/features/home/widgets/quest_row.dart | 5 ++- .../quest/quest_repository_firestore.dart | 13 ++++++- .../quest/widgets/quest_card_tile.dart | 38 ++++++++++++------- app/test/backend_repository_test.dart | 26 +++++++++++++ 6 files changed, 79 insertions(+), 22 deletions(-) diff --git a/app/lib/features/home/home_data.dart b/app/lib/features/home/home_data.dart index dc16042..6ed3ffc 100644 --- a/app/lib/features/home/home_data.dart +++ b/app/lib/features/home/home_data.dart @@ -11,7 +11,7 @@ library; import 'package:flutter/painting.dart' show Color; import '../../core/i18n/app_locale.dart'; import '../../core/ui/planet_25d.dart' show planetColors; -import '../quest/quest_data.dart' show QuestType; +import '../quest/quest_data.dart' show QuestSession, QuestType; import '../inside/inside_data.dart' show InsideQuestion, SampleInsideRepository; /// 팬덤 행성의 성장 상태. 정책 근거는 정책 설계서 §P-08. @@ -182,12 +182,16 @@ class QuestSummary { this.endsAt, this.type, this.category, + this.pausedSession, }); + /// 일시 정지 세션(원장 C-83). 있으면 보조 줄이 「카테고리 · 일시 정지 · n/m」. + final QuestSession? pausedSession; + final String title; /// 보조 줄 — 퀘스트 카테고리(사본 「오늘의 출석체크」 · 「커뮤니티」). 퀘스트 목록의 - /// 카테고리 이름과 같은 원천(`QuestType.label`). + /// 카테고리 이름과 같은 원천(`QuestType.category`). final String? category; /// `quests.type`. 줄 왼쪽 아이콘을 고르는 데만 쓴다(없으면 제목으로 고른다). diff --git a/app/lib/features/home/home_repository_firestore.dart b/app/lib/features/home/home_repository_firestore.dart index c0b96ef..02e08db 100644 --- a/app/lib/features/home/home_repository_firestore.dart +++ b/app/lib/features/home/home_repository_firestore.dart @@ -11,7 +11,7 @@ import 'home_data.dart'; import '../inside/inside_repository_firestore.dart' show insideQuestionFrom; import '../quest/quest_data.dart' show QuestType; import '../quest/quest_repository_firestore.dart' - show openTimeAttack, questTitle, questToday; + show FirestoreQuestRepository, openTimeAttack, questTitle, questToday; import '../../core/i18n/app_locale.dart'; /// 홈의 실 서버 구현 — 화면 대조 원장 F1. @@ -413,7 +413,14 @@ class FirestoreHomeRepository implements HomeRepository { return QuestSummary( title: questTitle(quest), type: QuestType.parse(quest.strOr('type')), - category: QuestType.parse(quest.strOr('type')).label, + // 퀘스트 목록 · 상세와 같은 분류 표기(Figma `264:46867` 줄 아래 「커뮤니티」). + category: QuestType.parse(quest.strOr('type')).category, + pausedSession: progress == null + ? null + : FirestoreQuestRepository.pausedSessionOf( + QuestType.parse(quest.strOr('type')), + progress, + ), rewardPop: openTimeAttack(quest)?.rewardPop ?? QuestType.parse(quest.strOr('type')).rewardFrom(quest, policy), diff --git a/app/lib/features/home/widgets/quest_row.dart b/app/lib/features/home/widgets/quest_row.dart index 465dc52..1ad88c8 100644 --- a/app/lib/features/home/widgets/quest_row.dart +++ b/app/lib/features/home/widgets/quest_row.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import '../../../core/theme/tokens.dart'; import '../../../core/ui/app_icons.dart'; import '../../quest/widgets/quest_card_tile.dart' - show QuestIconBox, questImageFor; + show QuestIconBox, questImageFor, questSubLine; import '../home_data.dart'; /// 홈 「TODAY'S QUEST」 묶음 — Figma 「홈」 `2097:16428`: 343×270 · r12 안에 343×90 줄 @@ -98,7 +98,8 @@ class QuestRow extends StatelessWidget { ), ), const SizedBox(height: 4), - _sub(quest.category ?? ''), + // 진행중 탭 카드와 같은 일시 정지 표기(원장 C-83). + _sub(questSubLine(quest.category ?? '', quest.pausedSession)), ], ), ), diff --git a/app/lib/features/quest/quest_repository_firestore.dart b/app/lib/features/quest/quest_repository_firestore.dart index 337c097..4f0970e 100644 --- a/app/lib/features/quest/quest_repository_firestore.dart +++ b/app/lib/features/quest/quest_repository_firestore.dart @@ -156,8 +156,17 @@ class FirestoreQuestRepository implements QuestRepository { return _walkSteps > 0 ? _walkSteps : null; } + /// 목록 카드가 「일시 정지」로 보는 세션 — 진행완료가 아니고 Walk 가 아닌 남은 세션(앱이 꺼진 + /// inProgress 포함). 카드 단계 판정([QuestStage.paused])과 같은 규칙이다. 아니면 null. + static QuestSession? pausedSessionOf(QuestType type, Map progress) { + final s = sessionOf(progress); + return s == null || type == QuestType.steps || s.state == QuestStage.completed + ? null + : s; + } + /// 진행 세션(`questProgress.session` — `fn-quest-progress` 만 쓴다 · 원장 C-83). - static QuestSession? _sessionOf(Map progress) { + static QuestSession? sessionOf(Map progress) { final raw = progress['session']; if (raw is! Map) return null; final m = Map.from(raw); @@ -229,7 +238,7 @@ class FirestoreQuestRepository implements QuestRepository { : type == QuestType.musicShake ? quest.intOr('bpm') : dailyLimit; - final session = _sessionOf(progress); + final session = sessionOf(progress); final cardGoal = authorUid.isEmpty ? goal : quest.intOr('goal', 1); // 걸음은 화면 밖 걸음까지 실제 값(C-62) — 오늘 걸음, 없으면 세션의 마지막 값. final walked = steps && session != null ? walk ?? session.progress : null; diff --git a/app/lib/features/quest/widgets/quest_card_tile.dart b/app/lib/features/quest/widgets/quest_card_tile.dart index c4f39fe..1c4d4a1 100644 --- a/app/lib/features/quest/widgets/quest_card_tile.dart +++ b/app/lib/features/quest/widgets/quest_card_tile.dart @@ -71,16 +71,28 @@ class QuestIconBox extends StatelessWidget { }; /// 일시 정지 표기 — 「카테고리 · 일시 정지 · 43/60」. 카드와 탭 위 캡슐이 같이 쓴다(원장 C-83). -String pausedLabel(QuestCard card, {bool title = false}) { - final session = card.session; - return [ - if (!title) card.categoryName, - appL10n.questPaused, - if (title) card.title, - if (session != null && session.total > 0) - '${formatPop(session.progress)}/${formatPop(session.total)}', - ].join(' · '); -} +String pausedLabel(QuestCard card, {bool title = false}) => + title ? _paused(card.title, card.session, titleLast: true) + : _paused(card.categoryName, card.session); + +String _paused(String lead, QuestSession? session, {bool titleLast = false}) => [ + if (!titleLast) lead, + appL10n.questPaused, + if (titleLast) lead, + if (session != null && session.total > 0) + '${formatPop(session.progress)}/${formatPop(session.total)}', +].join(' · '); + +/// 퀘스트 줄 · 카드의 보조 줄 — 일시 정지면 「카테고리 · 일시 정지 · 43/60」, 아니면 카테고리. +/// 진행중 탭 카드 · 「전체」 탭 맨 위 줄 · HOME TODAY'S QUEST 줄이 모두 이 표기를 쓴다 +/// (10/6 실기기 — 맨 위 줄 · HOME 줄에만 일시 정지가 빠져 있었다). +/// [pausedSession] 은 `FirestoreQuestRepository.pausedSessionOf` 가 고른 세션이다. +String questSubLine(String category, QuestSession? pausedSession) => + pausedSession == null ? category : _paused(category, pausedSession); + +String questCardSubLine(QuestCard card) => card.stage == QuestStage.paused + ? pausedLabel(card) + : card.categoryName; /// 목록의 퀘스트 카드 한 장 — 시안 `264:46867` 카드(#1a1a1c r12 · 안쪽 20). /// @@ -148,9 +160,7 @@ class QuestCardTile extends StatelessWidget { const SizedBox(height: 6), Text( // 진행 상태 값 「일시 정지」 · 멈춘 진행값(원장 C-83 — 기존 카드 영역 안). - card.stage == QuestStage.paused - ? pausedLabel(card) - : card.categoryName, + questCardSubLine(card), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w400, @@ -385,7 +395,7 @@ class QuestAdminRow extends StatelessWidget { ), const SizedBox(height: 4), Text( - card.categoryName, + questCardSubLine(card), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w500, diff --git a/app/test/backend_repository_test.dart b/app/test/backend_repository_test.dart index 799fc6d..1e3a651 100644 --- a/app/test/backend_repository_test.dart +++ b/app/test/backend_repository_test.dart @@ -17,6 +17,7 @@ import 'package:fancake/features/planet/planet_data.dart'; import 'package:fancake/features/planet/planet_repository_firestore.dart'; import 'package:fancake/features/quest/quest_data.dart'; import 'package:fancake/features/quest/quest_repository_firestore.dart'; +import 'package:fancake/features/quest/widgets/quest_card_tile.dart' show questSubLine; import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -249,6 +250,31 @@ void main() { expect(snapshot.quests.single.rewardPop, 50); }); + test('TODAY\'S QUEST 줄 아래는 활동명이 아니라 퀘스트 목록과 같은 분류다 — 10/6 B-4', () async { + final db = await _seed(); + await db.doc('quests/shake').set({ + 'active': true, 'title': '박자 맞춰 흔들기', 'type': 'musicShake', + }); + final quest = (await FirestoreHomeRepository(_backend(db)).load()).quests.single; + expect(quest.category, QuestType.musicShake.category); + expect(quest.category, isNot(QuestType.musicShake.label)); + }); + + test('일시 정지 세션은 HOME 줄에도 「분류 · 일시 정지 · n/m」로 보인다 — 10/6 A-1', () async { + final db = await _seed(); + await db.doc('quests/shake').set({ + 'active': true, 'title': '박자 맞춰 흔들기', 'type': 'musicShake', + }); + await db.doc('users/$_uid/questProgress/shake').set({ + 'session': {'state': 'paused', 'progress': 14, 'total': 32}, + }); + final quest = (await FirestoreHomeRepository(_backend(db)).load()).quests.single; + expect( + questSubLine(quest.category!, quest.pausedSession), + '${QuestType.musicShake.category} · 일시 정지 · 14/32', + ); + }); + test('지난 날의 진행 행은 완료로 보지 않는다 — 목록 · 상세와 같은 판정(실기기 4-1)', () async { final db = await _seed(); await db.doc('quests/attend').set({