Skip to content

Commit 3036012

Browse files
committed
fix
1 parent c873570 commit 3036012

2 files changed

Lines changed: 52 additions & 59 deletions

File tree

src/app/api/posts/route.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { NextResponse } from "next/server";
2+
import { getAllPosts } from "@/lib/posts";
3+
4+
export async function GET() {
5+
const posts = await getAllPosts();
6+
return NextResponse.json(
7+
posts.map((p) => ({ slug: p.slug, title: p.title, category: p.category }))
8+
);
9+
}

src/app/stats/page.tsx

Lines changed: 43 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -21,64 +21,44 @@ function Skeleton({ className }: { className?: string }) {
2121
export default function StatsPage() {
2222
const [stats, setStats] = useState<PostStat[]>([]);
2323
const [loading, setLoading] = useState(true);
24-
const [totalPosts, setTotalPosts] = useState(0);
2524

2625
useEffect(() => {
2726
async function load() {
2827
try {
29-
// Fetch posts list from the existing posts API proxy
30-
const postsRes = await fetch("/api/admin/posts");
31-
// posts API might not be public — fall back to reactions/bulk approach
32-
// We use the reactions bulk endpoint to discover slugs, and views/top for views
33-
const [viewsRes, reactionsTopRes] = await Promise.all([
34-
fetch("/api/views/top?limit=100").then((r) => r.ok ? r.json() : []),
35-
fetch("/api/views/bulk?slugs=").catch(() => ({})),
28+
// 1. 전체 포스트 목록 (slug, title, category)
29+
const postsRes = await fetch("/api/posts");
30+
const posts: { slug: string; title: string; category: string }[] =
31+
postsRes.ok ? await postsRes.json() : [];
32+
33+
if (posts.length === 0) { setLoading(false); return; }
34+
35+
const slugs = posts.map((p) => p.slug).join(",");
36+
const enc = encodeURIComponent(slugs);
37+
38+
// 2. 조회수 + 반응수 병렬 fetch
39+
const [viewsData, reactionsData] = await Promise.all([
40+
fetch(`/api/views/bulk?slugs=${enc}`)
41+
.then((r) => r.ok ? r.json() : {})
42+
.catch(() => ({})) as Promise<Record<string, number>>,
43+
fetch(`/api/reactions/bulk?slugs=${enc}`)
44+
.then((r) => r.ok ? r.json() : {})
45+
.catch(() => ({})) as Promise<Record<string, Record<string, number>>>,
3646
]);
3747

38-
// viewsRes: [{slug, views}]
39-
const viewMap: Record<string, number> = {};
40-
let maxViews = 0;
41-
for (const { slug, views } of viewsRes as { slug: string; views: number }[]) {
42-
viewMap[slug] = views;
43-
if (views > maxViews) maxViews = views;
44-
}
45-
46-
// Get reaction counts for the slugs we know about
47-
const knownSlugs = (viewsRes as { slug: string }[]).map((r) => r.slug);
48-
let reactionMap: Record<string, number> = {};
49-
if (knownSlugs.length > 0) {
50-
const reacRes = await fetch(
51-
`/api/reactions/bulk?slugs=${encodeURIComponent(knownSlugs.join(","))}`
52-
);
53-
if (reacRes.ok) {
54-
const reacData: Record<string, Record<string, number>> = await reacRes.json();
55-
for (const [slug, emojis] of Object.entries(reacData)) {
56-
reactionMap[slug] = Object.values(emojis).reduce((a, b) => a + b, 0);
57-
}
58-
}
59-
}
60-
61-
const combined: PostStat[] = (viewsRes as { slug: string; views: number }[]).map(
62-
({ slug, views }) => {
63-
// Extract category from slug (first segment)
64-
const parts = slug.split("/");
65-
const category = parts.length > 1 ? parts[0] : "";
66-
// Use slug as title if we can't get the real title
67-
const title = slug.split("/").pop()?.replace(/-/g, " ") ?? slug;
68-
return {
69-
slug,
70-
title,
71-
category,
72-
views,
73-
reactions: reactionMap[slug] ?? 0,
74-
};
75-
}
76-
);
48+
const combined: PostStat[] = posts.map((p) => ({
49+
slug: p.slug,
50+
title: p.title,
51+
category: p.category,
52+
views: viewsData[p.slug] ?? 0,
53+
reactions: Object.values(reactionsData[p.slug] ?? {}).reduce(
54+
(a: number, b: number) => a + b,
55+
0
56+
),
57+
}));
7758

7859
setStats(combined);
79-
setTotalPosts(combined.length);
8060
} catch {
81-
// ignore errors
61+
/* ignore */
8262
} finally {
8363
setLoading(false);
8464
}
@@ -90,15 +70,18 @@ export default function StatsPage() {
9070
const totalReactions = stats.reduce((s, p) => s + p.reactions, 0);
9171

9272
const byViews = [...stats].sort((a, b) => b.views - a.views);
93-
const byReactions = [...stats].sort((a, b) => b.reactions - a.reactions).filter((p) => p.reactions > 0);
73+
const byReactions = [...stats]
74+
.sort((a, b) => b.reactions - a.reactions)
75+
.filter((p) => p.reactions > 0);
9476

95-
// Category breakdown
9677
const catViews: Record<string, number> = {};
9778
for (const p of stats) {
9879
const key = p.category || "기타";
9980
catViews[key] = (catViews[key] ?? 0) + p.views;
10081
}
101-
const sortedCats = Object.entries(catViews).sort((a, b) => b[1] - a[1]);
82+
const sortedCats = Object.entries(catViews)
83+
.filter(([, v]) => v > 0)
84+
.sort((a, b) => b[1] - a[1]);
10285
const maxCatViews = sortedCats[0]?.[1] ?? 1;
10386

10487
return (
@@ -121,7 +104,7 @@ export default function StatsPage() {
121104
))
122105
) : (
123106
[
124-
{ label: "조회된 글", value: totalPosts, unit: "개" },
107+
{ label: "전체 글", value: stats.length, unit: "개" },
125108
{ label: "누적 조회", value: totalViews.toLocaleString(), unit: "회" },
126109
{ label: "누적 반응", value: totalReactions.toLocaleString(), unit: "개" },
127110
].map((s) => (
@@ -171,9 +154,9 @@ export default function StatsPage() {
171154
</section>
172155
)}
173156

174-
{(!loading && sortedCats.length > 0) && <div className="h-px bg-black/8 dark:bg-white/8" />}
157+
{!loading && <div className="h-px bg-black/8 dark:bg-white/8" />}
175158

176-
{/* 조회수 TOP */}
159+
{/* 조회수 TOP 10 */}
177160
<section className="space-y-3">
178161
<h2 className="text-xs font-semibold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest">
179162
조회수 TOP 10
@@ -184,13 +167,13 @@ export default function StatsPage() {
184167
<Skeleton key={i} className="h-12 w-full rounded-xl" />
185168
))}
186169
</div>
187-
) : byViews.length === 0 ? (
170+
) : byViews.filter((p) => p.views > 0).length === 0 ? (
188171
<p className="text-sm text-zinc-400 dark:text-zinc-500 text-center py-8">
189172
아직 조회 데이터가 없어요
190173
</p>
191174
) : (
192175
<ol className="space-y-2">
193-
{byViews.slice(0, 10).map((p, i) => (
176+
{byViews.filter((p) => p.views > 0).slice(0, 10).map((p, i) => (
194177
<li key={p.slug}>
195178
<Link
196179
href={`/posts/${p.slug}`}
@@ -200,7 +183,7 @@ export default function StatsPage() {
200183
{i + 1}
201184
</span>
202185
<span className="flex-1 text-sm font-medium text-zinc-800 dark:text-zinc-200 group-hover:text-violet-700 dark:group-hover:text-violet-300 transition-colors truncate">
203-
{p.slug}
186+
{p.title}
204187
</span>
205188
<div className="flex items-center gap-3 shrink-0 text-xs text-zinc-400 dark:text-zinc-500 tabular-nums">
206189
<span className="flex items-center gap-1">
@@ -219,6 +202,7 @@ export default function StatsPage() {
219202
)}
220203
</section>
221204

205+
{/* 반응 TOP 10 */}
222206
{!loading && byReactions.length > 0 && (
223207
<>
224208
<div className="h-px bg-black/8 dark:bg-white/8" />
@@ -237,7 +221,7 @@ export default function StatsPage() {
237221
{i + 1}
238222
</span>
239223
<span className="flex-1 text-sm font-medium text-zinc-800 dark:text-zinc-200 group-hover:text-violet-700 dark:group-hover:text-violet-300 transition-colors truncate">
240-
{p.slug}
224+
{p.title}
241225
</span>
242226
<span className="shrink-0 text-xs tabular-nums text-zinc-400 dark:text-zinc-500">
243227
{p.reactions}개 반응

0 commit comments

Comments
 (0)