Skip to content

Commit f553913

Browse files
authored
Merge pull request #2 from ph-value/refactor/markdown-structure
2 parents c6cd997 + be401dd commit f553913

File tree

7 files changed

+304
-56
lines changed

7 files changed

+304
-56
lines changed

lib/PostListPage.dart

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter/services.dart';
3+
import 'package:intl/intl.dart';
4+
import 'dart:html' as html;
5+
import 'package:markdown_widget/markdown_widget.dart';
6+
import 'package:sando_diary/PostMeta.dart';
7+
8+
class PostListPage extends StatefulWidget {
9+
const PostListPage({super.key});
10+
@override
11+
State<PostListPage> createState() => _PostListPageState();
12+
}
13+
14+
class _PostListPageState extends State<PostListPage> {
15+
late Future<List<MdDoc>> future;
16+
late DateFormat df;
17+
bool isShowPostDetail = false;
18+
late MdDoc currentPost;
19+
20+
@override
21+
void initState() {
22+
super.initState();
23+
future = loadMarkdownDocs(context);
24+
df = DateFormat('yyyy-MM-dd'); // 한국어 로케일이면 intl 초기화 추가 가능
25+
}
26+
27+
void _launchURLInNewTab(String url) {
28+
html.window.open(url, '_blank');
29+
}
30+
31+
void _copyCurrentPostUrlToClipboard() {
32+
final currentUrl = html.window.location.href; // 현재 페이지의 URL 가져오기
33+
Clipboard.setData(ClipboardData(text: currentUrl)); // URL을 클립보드에 복사
34+
35+
ScaffoldMessenger.of(context).showSnackBar(
36+
SnackBar(content: Text('URL copied to clipboard!')),
37+
);
38+
}
39+
40+
Widget _postDetail(MdDoc currentDoc) {
41+
return Padding(
42+
padding: const EdgeInsets.all(16.0),
43+
child: Stack(
44+
children: [
45+
Align(
46+
alignment: Alignment.topRight,
47+
child: Text(currentDoc.meta.date != null
48+
? DateFormat('yyyy-MM-dd').format(currentDoc.meta.date!)
49+
: '날짜 정보 없음'),
50+
),
51+
MarkdownWidget(
52+
data: currentDoc.body,
53+
config: MarkdownConfig(configs: [
54+
LinkConfig(
55+
style: TextStyle(
56+
color: Colors.cyan,
57+
decoration: TextDecoration.underline,
58+
),
59+
onTap: (url) {
60+
_launchURLInNewTab(url);
61+
},
62+
)
63+
]),
64+
),
65+
],
66+
),
67+
);
68+
}
69+
70+
@override
71+
Widget build(BuildContext context) {
72+
return Scaffold(
73+
appBar: !isShowPostDetail
74+
? AppBar(
75+
title: Text('Blog Posts'),
76+
)
77+
: AppBar(
78+
elevation: 0,
79+
scrolledUnderElevation: 0,
80+
shadowColor: Colors.transparent,
81+
surfaceTintColor: Colors.transparent,
82+
forceMaterialTransparency: true,
83+
title: Text(currentPost.meta.title),
84+
leading: BackButton(
85+
onPressed: () => setState(() {
86+
isShowPostDetail = false;
87+
html.window.history.pushState(null, 'Posts', '/');
88+
}),
89+
),
90+
actions: [
91+
IconButton(
92+
icon: const Icon(Icons.share),
93+
tooltip: 'Share this Post',
94+
onPressed: _copyCurrentPostUrlToClipboard,
95+
),
96+
],
97+
),
98+
body: !isShowPostDetail
99+
? FutureBuilder<List<MdDoc>>(
100+
future: future,
101+
builder: (context, snap) {
102+
if (snap.connectionState != ConnectionState.done) {
103+
return const Center(child: CircularProgressIndicator());
104+
}
105+
if (snap.hasError) {
106+
return Center(child: Text('로드 실패: ${snap.error}'));
107+
}
108+
final docs = snap.data ?? [];
109+
if (docs.isEmpty) {
110+
return const Center(child: Text('문서가 없습니다.'));
111+
}
112+
return ListView.separated(
113+
itemCount: docs.length,
114+
separatorBuilder: (_, __) => const Divider(height: 1),
115+
itemBuilder: (context, i) {
116+
final d = docs[i];
117+
final dateStr = d.meta.date != null
118+
? df.format(d.meta.date!)
119+
: '날짜 정보 없음';
120+
return ListTile(
121+
title: Text(d.meta.title,
122+
maxLines: 1, overflow: TextOverflow.ellipsis),
123+
subtitle: Text('${d.meta.category} · $dateStr'),
124+
trailing: const Icon(Icons.chevron_right),
125+
onTap: () {
126+
setState(() {
127+
isShowPostDetail = true;
128+
currentPost = d;
129+
});
130+
},
131+
);
132+
},
133+
);
134+
},
135+
)
136+
: _postDetail(currentPost),
137+
);
138+
}
139+
}

lib/PostMeta.dart

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import 'dart:convert';
2+
import 'package:cosmic_frontmatter/cosmic_frontmatter.dart';
3+
import 'package:flutter/services.dart' show rootBundle;
4+
import 'package:flutter/widgets.dart';
5+
6+
class PostMeta {
7+
final String title;
8+
final String category;
9+
final DateTime? date;
10+
11+
PostMeta({required this.title, required this.category, this.date});
12+
13+
factory PostMeta.fromJson(Map<String, dynamic> json) {
14+
final rawTitle = (json['title'] ?? '').toString().trim();
15+
final rawCategory = (json['category'] ?? 'Uncategorized').toString().trim();
16+
final rawDate = (json['date'] ?? '').toString().trim();
17+
DateTime? dt;
18+
if (rawDate.isNotEmpty) {
19+
// ISO8601 또는 YYYY-MM-DD 가정
20+
try {
21+
dt = DateTime.parse(rawDate);
22+
} catch (_) {}
23+
}
24+
return PostMeta(title: rawTitle, category: rawCategory, date: dt);
25+
}
26+
27+
PostMeta withFallbacks({required String fallbackTitle}) => PostMeta(
28+
title: title.isNotEmpty ? title : fallbackTitle,
29+
category: category.isNotEmpty ? category : 'Uncategorized',
30+
date: date,
31+
);
32+
}
33+
34+
class MdDoc {
35+
final String path;
36+
final PostMeta meta;
37+
final String body;
38+
MdDoc({required this.path, required this.meta, required this.body});
39+
}
40+
41+
Future<List<MdDoc>> loadMarkdownDocs(BuildContext context) async {
42+
final manifestJson =
43+
await DefaultAssetBundle.of(context).loadString('AssetManifest.json');
44+
final Map<String, dynamic> manifest = json.decode(manifestJson);
45+
46+
final mdPaths = manifest.keys
47+
.where((k) => k.startsWith('post/'))
48+
.where((k) => k.toLowerCase().endsWith('.md'))
49+
.toList()
50+
..sort();
51+
52+
final List<MdDoc> docs = [];
53+
for (final path in mdPaths) {
54+
final raw = await rootBundle.loadString(path);
55+
// front matter 파싱 + 모델 매핑
56+
final doc = parseFrontmatter<PostMeta>(
57+
content: raw,
58+
frontmatterBuilder: (map) => PostMeta.fromJson(map),
59+
);
60+
// 제목/카테고리 폴백 처리 (없을 경우 파일명 사용)
61+
final filename = path.split('/').last.replaceAll('.md', '');
62+
final meta = doc.frontmatter
63+
.withFallbacks(fallbackTitle: _firstH1(doc.body) ?? filename);
64+
65+
docs.add(MdDoc(path: path, meta: meta, body: doc.body));
66+
}
67+
68+
// 날짜 내림차순 정렬(날짜 없는 글은 뒤로)
69+
docs.sort((a, b) {
70+
final da = a.meta.date;
71+
final db = b.meta.date;
72+
if (da == null && db == null) return 0;
73+
if (da == null) return 1;
74+
if (db == null) return -1;
75+
return db.compareTo(da);
76+
});
77+
78+
return docs;
79+
}
80+
81+
String? _firstH1(String s) {
82+
final r = RegExp(r'^\s*#\s+(.+)$', multiLine: true);
83+
final m = r.firstMatch(s);
84+
return m?.group(1)?.trim();
85+
}

lib/main.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:flutter/material.dart';
22
import 'package:sando_diary/AboutPage.dart';
33
import 'package:sando_diary/BlogPostListPage.dart';
4+
import 'package:sando_diary/PostListPage.dart';
45
import 'package:sando_diary/customDecoration.dart';
56
import 'package:sando_diary/guestBookPage.dart';
67
import 'package:sando_diary/ProjectListPage.dart';
@@ -33,7 +34,6 @@ class MyApp extends StatelessWidget {
3334
return MaterialApp(
3435
debugShowCheckedModeBanner: false,
3536
home: CustomAppBarScreenTest(),
36-
3737
);
3838
}
3939
}
@@ -419,15 +419,17 @@ class _CustomAppBarScreenState extends State<CustomAppBarScreenTest> {
419419
}
420420

421421
class Page1 extends StatelessWidget {
422+
const Page1({super.key});
423+
422424
@override
423425
Widget build(BuildContext context) {
424-
// return MarkdownPage(data: '');
425-
return BlogPostListPage();
426+
return const PostListPage();
426427
}
427428
}
428429

429-
430430
class Page3 extends StatelessWidget {
431+
const Page3({super.key});
432+
431433
@override
432434
Widget build(BuildContext context) {
433435
return AboutPage();

0 commit comments

Comments
 (0)