INSIDE · STATION · 참여형 번역 · 알림 · MY 화면

This commit is contained in:
Sn-Kinos
2026-09-29 17:06:48 +09:00
parent 18afc9cb98
commit 3aca678072
31 changed files with 10046 additions and 25 deletions
+213
View File
@@ -0,0 +1,213 @@
import 'package:flutter/material.dart';
import '../campaign/campaign_data.dart';
import '../campaign/campaign_detail_screen.dart';
import '../campaign/widgets/campaign_list_card.dart';
import '../inside/inside_data.dart';
import '../inside/inside_detail_screen.dart';
import '../inside/inside_screen.dart' show InsideQuestionCard;
import 'widgets/my_cards.dart';
import '../../core/i18n/app_locale.dart';
/// 내 인사이드 · 내 캠페인 — 화면 대조 원장 F6.
///
/// Figma `370:228289`(내 인사이드) · `370:229665`(내 캠페인).
/// 명세 `554:22382`(게시글 리스트 영역) 계열이며 **머리글과 빈 상태만 다르고**
/// 구조가 같아 한 화면에 종류를 받아 쓴다.
enum MyListKind {
inside,
campaign;
String get title => switch (this) {
inside => appL10n.myListInside,
campaign => appL10n.myListCampaign,
};
String get emptyMessage => switch (this) {
inside => appL10n.myListInsideEmpty,
campaign => appL10n.myListCampaignEmpty,
};
}
/// 정렬 칩 — 시안 `370:228289` 「최신순 · POP순 · 채택 완료」 · `370:229665` 「최신순 · 달성 완료」.
enum _MySort { latest, pop, solved, achieved }
class MyListsScreen extends StatefulWidget {
const MyListsScreen({super.key, required this.kind, this.campaigns});
final MyListKind kind;
/// 내 캠페인의 원천. null 이면 표본(목 모드). 저장소 선택은 repositories.dart.
final CampaignRepository? campaigns;
@override
State<MyListsScreen> createState() => _MyListsScreenState();
}
class _MyListsScreenState extends State<MyListsScreen> {
_MySort _sort = _MySort.latest;
/// 표본 — 시안 `370:228289` 문구. 서버가 붙으면 INSIDE 저장소의 내 글로 옮긴다.
static final _insides = () {
final now = DateTime.now();
return <InsideQuestion>[
InsideQuestion(
id: 'i1',
title: 'BTS 컴백 알려주세요!',
body: 'BTS 한국 컴백 시기가 궁급합니다!\n아시는 분들 댓글 부탁드려요 ㅠㅠ',
authorName: 'BTS_Hong',
createdAt: now,
bountyPop: 100,
isMine: true,
),
InsideQuestion(
id: 'i2',
title: '화양연화 세계관 해석해주실 분?',
body: '이번 뮤비랑 연결되는 부분이 헷갈리네요. 타임라인 정리해주실 천재 아미 구합니다!',
authorName: 'BTS_Hong',
createdAt: now.subtract(const Duration(minutes: 10)),
bountyPop: 500,
viewCount: 3,
isMine: true,
),
InsideQuestion(
id: 'i3',
title: 'BTS 이 포즈 뭔가요?',
body: '아미 분들 이 포즈 뭔지 알려주실 수 있나요?',
authorName: 'BTS_Hong',
createdAt: now.subtract(const Duration(days: 30)),
bountyPop: 800,
answerCount: 24,
viewCount: 82,
isMine: true,
isSolved: true,
),
];
}();
List<_MySort> get _sorts => switch (widget.kind) {
MyListKind.inside => const [_MySort.latest, _MySort.pop, _MySort.solved],
MyListKind.campaign => const [_MySort.latest, _MySort.achieved],
};
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: Text(widget.kind.title),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(36),
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 2, 16, 2),
child: Row(
children: [
for (final (i, sort) in _sorts.indexed) ...[
if (i > 0) const SizedBox(width: 8),
MySortChip(
label: switch (sort) {
_MySort.latest => context.l10n.sortLatest,
_MySort.pop => context.l10n.sortPop,
_MySort.solved => context.l10n.sortSolved,
_MySort.achieved => context.l10n.sortAchieved,
},
icon: switch (sort) {
_MySort.latest => MySortChip.clock,
_MySort.pop => MySortChip.bolt,
_ => MySortChip.fileCheck,
},
selected: _sort == sort,
onTap: () => setState(() => _sort = sort),
),
],
],
),
),
),
),
),
body: switch (widget.kind) {
MyListKind.inside => _insideList(context),
MyListKind.campaign => _campaignList(context),
},
);
Widget _insideList(BuildContext context) {
final items = switch (_sort) {
_MySort.pop => [
..._insides,
]..sort((a, b) => b.bountyPop.compareTo(a.bountyPop)),
_MySort.solved => _insides.where((q) => q.isSolved).toList(),
_ => _insides,
};
if (items.isEmpty) {
return Center(
child: Text(
widget.kind.emptyMessage,
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
// 카드는 INSIDE 목록과 같은 것을 쓴다(시안도 같은 컴포넌트다).
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: items.length,
separatorBuilder: (_, _) => const Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: SizedBox(height: 1, child: ColoredBox(color: Color(0xFF1A1A1A))),
),
itemBuilder: (context, i) => InsideQuestionCard(
question: items[i],
onTap: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => InsideDetailScreen(question: items[i]),
),
),
),
);
}
Widget _campaignList(BuildContext context) =>
FutureBuilder<List<CampaignCardData>>(
future: (widget.campaigns ?? SampleCampaignRepository()).mine(),
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const Center(child: CircularProgressIndicator());
}
// 내가 만든 것만 남긴다. 「달성 완료」는 목표를 채운 것만.
final mine = (snap.data ?? const [])
.where((c) => c.mine)
.where(
(c) =>
_sort != _MySort.achieved ||
c.status == CampaignStatus.success,
)
.toList();
if (mine.isEmpty) {
return Center(
child: Text(
widget.kind.emptyMessage,
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: mine.length,
separatorBuilder: (_, _) => const SizedBox(height: 16),
itemBuilder: (context, i) => CampaignListCard(
card: mine[i],
mine: true,
onOpen: () => Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute<void>(
builder: (_) => CampaignDetailScreen(
campaignId: mine[i].id,
repository: widget.campaigns,
),
),
),
),
);
},
);
}