Skip to content

Commit 5b039bc

Browse files
committed
feat: use subject filters in archive
1 parent 4f3a49a commit 5b039bc

7 files changed

Lines changed: 70 additions & 55 deletions

File tree

content

src/app/archive/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Metadata } from 'next';
22
import { Noto_Serif_KR } from 'next/font/google';
3-
import { getAllPosts, getCategories, getSeries } from '@/lib/posts';
3+
import { getAllPosts, getCategories, getSubjects } from '@/lib/posts';
44
import ArchiveClient from '@/components/archive/ArchiveClient';
55

66
const notoSerifKR = Noto_Serif_KR({
@@ -35,8 +35,8 @@ export default function ArchivePage() {
3535
const enPosts = getAllPosts('en');
3636
const koCategories = getCategories('ko');
3737
const enCategories = getCategories('en');
38-
const koSeries = getSeries('ko');
39-
const enSeries = getSeries('en');
38+
const koSubjects = getSubjects('ko');
39+
const enSubjects = getSubjects('en');
4040

4141
return (
4242
<>
@@ -50,8 +50,8 @@ export default function ArchivePage() {
5050
enPosts={enPosts}
5151
koCategories={koCategories}
5252
enCategories={enCategories}
53-
koSeries={koSeries}
54-
enSeries={enSeries}
53+
koSubjects={koSubjects}
54+
enSubjects={enSubjects}
5555
fontClassName="font-[family-name:var(--font-noto-serif-kr)]"
5656
/>
5757
</main>

src/components/archive/ArchiveClient.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client';
22

3-
import type { Post, Category, Series } from '@/types/archive';
3+
import type { Post, Category, Subject } from '@/types/archive';
44
import { useLang } from '@/components/layout/LangProvider';
55
import BookArchive from './BookArchive';
66

@@ -9,8 +9,8 @@ interface ArchiveClientProps {
99
enPosts: Post[];
1010
koCategories: Category[];
1111
enCategories: Category[];
12-
koSeries: Series[];
13-
enSeries: Series[];
12+
koSubjects: Subject[];
13+
enSubjects: Subject[];
1414
fontClassName: string;
1515
}
1616

@@ -19,21 +19,21 @@ export default function ArchiveClient({
1919
enPosts,
2020
koCategories,
2121
enCategories,
22-
koSeries,
23-
enSeries,
22+
koSubjects,
23+
enSubjects,
2424
fontClassName,
2525
}: ArchiveClientProps) {
2626
const { lang } = useLang();
2727

2828
const posts = lang === 'ko' ? koPosts : enPosts;
2929
const categories = lang === 'ko' ? koCategories : enCategories;
30-
const series = lang === 'ko' ? koSeries : enSeries;
30+
const subjects = lang === 'ko' ? koSubjects : enSubjects;
3131

3232
return (
3333
<BookArchive
3434
posts={posts}
3535
categories={categories}
36-
series={series}
36+
subjects={subjects}
3737
fontClassName={fontClassName}
3838
/>
3939
);

src/components/archive/BookArchive.tsx

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
import { useState, useEffect } from 'react';
44
import Link from 'next/link';
55
import { Search } from 'lucide-react';
6-
import type { Post, Category, Series } from '@/types/archive';
6+
import type { Post, Category, Subject } from '@/types/archive';
77
import { useLang } from '@/components/layout/LangProvider';
88
import i18n from '@/lib/i18n';
99

1010
interface BookArchiveProps {
1111
posts: Post[];
1212
categories: Category[];
13-
series: Series[];
13+
subjects: Subject[];
1414
fontClassName: string;
1515
}
1616

@@ -48,50 +48,63 @@ function Ornament() {
4848
);
4949
}
5050

51-
export default function BookArchive({ posts, categories, series, fontClassName }: BookArchiveProps) {
51+
export default function BookArchive({ posts, categories, subjects, fontClassName }: BookArchiveProps) {
5252
const [selectedCategory, setSelectedCategory] = useState('all');
53-
const [selectedSeries, setSelectedSeries] = useState('');
53+
const [selectedSubject, setSelectedSubject] = useState('');
5454
const [mobilePage, setMobilePage] = useState<'posts' | 'index'>('posts');
5555
const { lang } = useLang();
5656
const t = i18n[lang];
5757

58-
const SERIES_MAP: Record<string, string> = {
58+
const SUBJECT_MAP: Record<string, string> = {
5959
'프롬프트 가이드': 'Prompt Guide',
6060
'SQL 완전 정복': 'SQL Mastery',
6161
'GA4 완전 정복': 'GA4 Mastery',
6262
'애자일 가이드': 'Agile Guide',
6363
};
64-
const SERIES_MAP_REVERSE = Object.fromEntries(
65-
Object.entries(SERIES_MAP).map(([k, v]) => [v, k])
64+
const SUBJECT_MAP_REVERSE = Object.fromEntries(
65+
Object.entries(SUBJECT_MAP).map(([k, v]) => [v, k])
6666
);
6767

6868
useEffect(() => {
69-
if (!selectedSeries) return;
69+
if (!selectedSubject) return;
7070
if (lang === 'en') {
71-
const mapped = SERIES_MAP[selectedSeries];
72-
if (mapped) setSelectedSeries(mapped);
73-
else setSelectedSeries('');
71+
const mapped = SUBJECT_MAP[selectedSubject];
72+
if (mapped) setSelectedSubject(mapped);
73+
else setSelectedSubject('');
7474
} else {
75-
const mapped = SERIES_MAP_REVERSE[selectedSeries];
76-
if (mapped) setSelectedSeries(mapped);
77-
else setSelectedSeries('');
75+
const mapped = SUBJECT_MAP_REVERSE[selectedSubject];
76+
if (mapped) setSelectedSubject(mapped);
77+
else setSelectedSubject('');
7878
}
7979
// eslint-disable-next-line react-hooks/exhaustive-deps
8080
}, [lang]);
8181

8282
const filtered = posts
8383
.filter((p) => {
8484
const catMatch = selectedCategory === 'all' || p.category === selectedCategory;
85-
const seriesMatch = !selectedSeries || p.series === selectedSeries;
86-
return catMatch && seriesMatch;
85+
const subjectMatch = !selectedSubject || p.subject === selectedSubject;
86+
return catMatch && subjectMatch;
8787
})
8888
.sort((a, b) => {
89-
if (selectedSeries) return a.date < b.date ? -1 : 1;
89+
if (selectedSubject) return a.date < b.date ? -1 : 1;
9090
return a.date > b.date ? -1 : 1;
9191
});
9292

93-
const isActive = (catId: string, seriesId = '') =>
94-
seriesId ? selectedSeries === seriesId : selectedCategory === catId && !selectedSeries;
93+
const isActive = (catId: string, subjectId = '') =>
94+
subjectId ? selectedSubject === subjectId : selectedCategory === catId && !selectedSubject;
95+
96+
const visibleSubjects = selectedCategory === 'all'
97+
? subjects
98+
: Array.from(
99+
posts
100+
.filter((post) => post.category === selectedCategory && post.subject)
101+
.reduce((countMap, post) => {
102+
const subject = post.subject;
103+
if (!subject) return countMap;
104+
countMap.set(subject, (countMap.get(subject) ?? 0) + 1);
105+
return countMap;
106+
}, new Map<string, number>()),
107+
).map(([id, count]) => ({ id, label: id, count }));
95108

96109
const BOOK_HEIGHT = '74vh';
97110

@@ -147,7 +160,7 @@ export default function BookArchive({ posts, categories, series, fontClassName }
147160
{[{ id: 'all', label: t.allPosts, count: posts.length }, ...categories.filter(c => c.id !== 'all')].map((cat) => (
148161
<button
149162
key={cat.id}
150-
onClick={() => { setSelectedCategory(cat.id); setSelectedSeries(''); setMobilePage('posts'); }}
163+
onClick={() => { setSelectedCategory(cat.id); setSelectedSubject(''); setMobilePage('posts'); }}
151164
className="text-left flex items-center justify-between py-2 text-sm transition-all"
152165
style={{ color: isActive(cat.id) ? TEXT_ACTIVE : TEXT_INACTIVE }}
153166
>
@@ -160,15 +173,15 @@ export default function BookArchive({ posts, categories, series, fontClassName }
160173
))}
161174
</div>
162175
{/* 시리즈 */}
163-
{series.length > 0 && (
176+
{visibleSubjects.length > 0 && (
164177
<>
165178
<Ornament />
166179
<div className="flex flex-col mt-1 gap-0.5">
167-
<p className="text-xs tracking-[0.3em] uppercase mb-2" style={{ color: TEXT_LABEL }}>{t.seriesLabel}</p>
168-
{series.map((s) => (
180+
<p className="text-xs tracking-[0.3em] uppercase mb-2" style={{ color: TEXT_LABEL }}>{t.subjectLabel}</p>
181+
{visibleSubjects.map((s) => (
169182
<button
170183
key={s.id}
171-
onClick={() => { setSelectedSeries(s.id); setSelectedCategory('all'); setMobilePage('posts'); }}
184+
onClick={() => { setSelectedSubject(s.id); setMobilePage('posts'); }}
172185
className="text-left flex items-center justify-between py-2 text-sm transition-all"
173186
style={{ color: isActive('', s.id) ? TEXT_ACTIVE : TEXT_INACTIVE }}
174187
>
@@ -192,7 +205,7 @@ export default function BookArchive({ posts, categories, series, fontClassName }
192205
<RulingLines />
193206
<div className="mt-4 mb-4">
194207
<p className="text-xs tracking-[0.35em] uppercase" style={{ color: TEXT_LABEL }}>
195-
{selectedSeries || (selectedCategory === 'all' ? t.allPosts : selectedCategory)}
208+
{selectedSubject || (selectedCategory === 'all' ? t.allPosts : selectedCategory)}
196209
</p>
197210
</div>
198211
<div className="flex flex-col gap-4">
@@ -284,7 +297,7 @@ export default function BookArchive({ posts, categories, series, fontClassName }
284297
{[{ id: 'all', label: t.allPosts, count: posts.length }, ...categories.filter(c => c.id !== 'all')].map((cat) => (
285298
<button
286299
key={cat.id}
287-
onClick={() => { setSelectedCategory(cat.id); setSelectedSeries(''); }}
300+
onClick={() => { setSelectedCategory(cat.id); setSelectedSubject(''); }}
288301
className="group text-left flex items-center justify-between py-2 text-sm transition-all"
289302
style={{ color: isActive(cat.id) ? TEXT_ACTIVE : TEXT_INACTIVE }}
290303
>
@@ -301,17 +314,17 @@ export default function BookArchive({ posts, categories, series, fontClassName }
301314
</div>
302315

303316
{/* 시리즈 */}
304-
{series.length > 0 && (
317+
{visibleSubjects.length > 0 && (
305318
<>
306319
<Ornament />
307320
<div className="flex flex-col mt-1 gap-0.5">
308321
<p className="text-xs tracking-[0.3em] uppercase mb-2" style={{ color: TEXT_LABEL }}>
309-
{t.seriesLabel}
322+
{t.subjectLabel}
310323
</p>
311-
{series.map((s) => (
324+
{visibleSubjects.map((s) => (
312325
<button
313326
key={s.id}
314-
onClick={() => { setSelectedSeries(s.id); setSelectedCategory('all'); }}
327+
onClick={() => { setSelectedSubject(s.id); }}
315328
className="text-left flex items-center justify-between py-2 text-sm transition-all"
316329
style={{ color: isActive('', s.id) ? TEXT_ACTIVE : TEXT_INACTIVE }}
317330
>
@@ -364,7 +377,7 @@ export default function BookArchive({ posts, categories, series, fontClassName }
364377

365378
<div className="mt-6 mb-5">
366379
<p className="text-[9px] tracking-[0.35em] uppercase" style={{ color: TEXT_LABEL }}>
367-
{selectedSeries || (selectedCategory === 'all' ? t.allPosts : selectedCategory)}
380+
{selectedSubject || (selectedCategory === 'all' ? t.allPosts : selectedCategory)}
368381
</p>
369382
</div>
370383

src/lib/i18n.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ const i18n = {
5454
infiniteArchive: '무한서고',
5555
allPosts: '전체 기록',
5656
categoryLabel: 'Category',
57-
seriesLabel: 'Series',
57+
subjectLabel: 'Subject',
5858
episodes: '편',
5959

6060
// GuestBook
@@ -129,7 +129,7 @@ const i18n = {
129129
infiniteArchive: 'Infinite Archive',
130130
allPosts: 'All Posts',
131131
categoryLabel: 'Category',
132-
seriesLabel: 'Series',
132+
subjectLabel: 'Subject',
133133
episodes: 'ep.',
134134

135135
// GuestBook

src/lib/posts.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'fs';
22
import path from 'path';
33
import matter from 'gray-matter';
4-
import type { Post, PostLang, PostStatus, Series } from '@/types/archive';
4+
import type { Post, PostLang, PostStatus, Subject } from '@/types/archive';
55

66
const POSTS_DIR = path.join(process.cwd(), 'content', 'posts');
77

@@ -39,6 +39,7 @@ function parsePost(filename: string): Post | null {
3939
status: (data.status as PostStatus) ?? 'draft',
4040
lang,
4141
series: data.series ?? '',
42+
subject: data.subject ?? '',
4243
readingTime: calcReadingTime(content, lang),
4344
content,
4445
};
@@ -70,22 +71,22 @@ export function getSeriesPosts(series: string, lang: PostLang = 'ko'): Post[] {
7071
.sort((a, b) => (a.date < b.date ? -1 : 1)); // 오래된 순 (1편→N편)
7172
}
7273

73-
export function getSeries(lang: PostLang = 'ko'): Series[] {
74+
export function getSubjects(lang: PostLang = 'ko'): Subject[] {
7475
const posts = getAllPosts(lang);
7576
const countMap = new Map<string, number>();
7677

7778
for (const post of posts) {
78-
if (post.series) {
79-
countMap.set(post.series, (countMap.get(post.series) ?? 0) + 1);
79+
if (post.subject) {
80+
countMap.set(post.subject, (countMap.get(post.subject) ?? 0) + 1);
8081
}
8182
}
8283

83-
const series: Series[] = [];
84+
const subjects: Subject[] = [];
8485
countMap.forEach((count, id) => {
85-
series.push({ id, label: id, count });
86+
subjects.push({ id, label: id, count });
8687
});
8788

88-
return series;
89+
return subjects;
8990
}
9091

9192
export function getCategories(lang: PostLang = 'ko') {

src/types/archive.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export interface Category {
44
count: number;
55
}
66

7-
export interface Series {
7+
export interface Subject {
88
id: string;
99
label: string;
1010
count: number;
@@ -24,7 +24,8 @@ export interface Post {
2424
thumbnail?: string; // 파일명 (e.g. "next-app-router.webp"), 없으면 기본 이미지 사용
2525
status: PostStatus;
2626
lang: PostLang;
27-
series?: string; // 연재글 묶음 이름 (없으면 빈 문자열)
27+
series?: string; // 상세 페이지 이전/다음 호환용. Archive 목록은 subject를 우선 사용
28+
subject?: string; // Archive 주제 묶음 이름 (없으면 빈 문자열)
2829
readingTime: number; // 파싱 시 자동 계산 (분 단위)
2930
content: string; // 마크다운 본문
3031
}

0 commit comments

Comments
 (0)