Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions app/compare/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import type { Metadata } from "next";
import Link from "next/link";
import { COMPARISONS, CATEGORY_LABELS } from "@/lib/compare-pages/comparisons";
import { COMPARISONS } from "@/lib/compare-pages/comparisons";
import type { ComparisonDef } from "@/lib/compare-pages/comparisons";

export const metadata: Metadata = {
title: "제품 비교 모음 — Axis",
description:
"노트북·스마트폰·이어폰·태블릿 인기 비교 모음. 공식 스펙과 실사용 기준으로 어떤 걸 살지 바로 알려드립니다.",
};
import { localizedComparisonTitle } from "@/lib/compare-pages/localized-title";
import { getDictionary } from "@/lib/i18n";
import { getLocale } from "@/lib/i18n/server";

const CATEGORIES: ComparisonDef["category"][] = ["laptop", "smartphone", "earphones", "tablet"];

export default function ComparePage() {
export async function generateMetadata(): Promise<Metadata> {
const locale = await getLocale();
const t = getDictionary(locale);
return {
title: t.home.compareIndex.metaTitle,
description: t.home.compareIndex.metaDescription,
};
}

export default async function ComparePage() {
const locale = await getLocale();
const t = getDictionary(locale);
const byCategory = Object.fromEntries(
CATEGORIES.map((cat) => [cat, COMPARISONS.filter((c) => c.category === cat)])
) as Record<ComparisonDef["category"], ComparisonDef[]>;
Expand All @@ -25,21 +33,21 @@ export default function ComparePage() {
</header>

<section className="hero compact">
<h1>제품 비교 모음</h1>
<p className="sub">
공식 스펙 + AI 분석으로 뭘 살지 바로 결론 내드립니다.
</p>
<h1>{t.home.compareIndex.title}</h1>
<p className="sub">{t.home.compareIndex.sub}</p>
</section>

{CATEGORIES.map((cat) => (
<section key={cat} className="compare-index-section">
<h2 className="compare-index-category">{CATEGORY_LABELS[cat]}</h2>
<h2 className="compare-index-category">{t.home.categoryLabels[cat]}</h2>
<ul className="compare-index-list">
{byCategory[cat].map((c) => (
<li key={c.slug} className="compare-index-item">
<Link href={`/compare/${c.slug}`} className="compare-index-link">
<span className="compare-index-title">{c.title}</span>
<p className="compare-index-desc">{c.description.slice(0, 72)}...</p>
<span className="compare-index-title">{localizedComparisonTitle(c, locale)}</span>
{locale === "ko" ? (
<p className="compare-index-desc">{c.description.slice(0, 72)}...</p>
) : null}
</Link>
</li>
))}
Expand All @@ -48,9 +56,9 @@ export default function ComparePage() {
))}

<section className="compare-cta">
<p className="compare-cta-copy">목록에 없는 제품? 직접 비교해보세요.</p>
<p className="compare-cta-copy">{t.home.compareIndex.ctaCopy}</p>
<Link href="/" className="btn-primary compare-cta-btn">
직접 비교하기 →
{t.home.compareIndex.cta}
</Link>
</section>
</main>
Expand Down
10 changes: 8 additions & 2 deletions app/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export default function Loading() {
import { getLocale } from "@/lib/i18n/server";
import { getDictionary } from "@/lib/i18n";

export default async function Loading() {
const locale = await getLocale();
const t = getDictionary(locale);

return (
<main className="container narrow">
<p className="hint">불러오는 중...</p>
<p className="hint">{t.error.loading}</p>
</main>
);
}
11 changes: 6 additions & 5 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { COMPARISONS } from "@/lib/compare-pages/comparisons";
import { createServiceClientSafe } from "@/lib/supabase-server";
import { aggregatePopularQueries } from "@/lib/popular-queries";
import { coverageNoteYear } from "@/lib/specs/coverage";
import { localizedComparisonTitle } from "@/lib/compare-pages/localized-title";

type PopularQuery = { query: string; count: number };

Expand Down Expand Up @@ -41,9 +42,9 @@ export default async function Home() {
const locale = await getLocale();
const t = getDictionary(locale);

// 인기 비교: DB 기반 + 부족하면 하드코딩 페이지로 채움
const popularQueries = await getPopularQueries(10);
const hasRealData = popularQueries.length >= 3;
// 인기 비교: KO만 DB 실데이터를 쓰고, en/ja는 로케일 타이틀 정적 목록을 쓴다.
const popularQueries = locale === "ko" ? await getPopularQueries(10) : [];
const hasRealData = locale === "ko" && popularQueries.length >= 3;

return (
<main className="container">
Expand Down Expand Up @@ -88,7 +89,7 @@ export default async function Home() {

<section className="home-section home-method">
<div className="section-copy">
<p className="section-kicker">Decision method</p>
<p className="section-kicker">{t.home.methodKicker}</p>
<h2>{t.home.methodTitle}</h2>
<p>
<strong className="method-em">{t.home.methodSubEm}</strong>{" "}
Expand Down Expand Up @@ -127,7 +128,7 @@ export default async function Home() {
<li key={c.slug} className="home-rank-item">
<Link href={`/compare/${c.slug}`} className="home-rank-link">
<span className="home-rank-num">{i + 1}</span>
<span className="home-rank-text">{c.title}</span>
<span className="home-rank-text">{localizedComparisonTitle(c, locale)}</span>
<span className="home-rank-arrow" aria-hidden>→</span>
</Link>
</li>
Expand Down
150 changes: 150 additions & 0 deletions lib/compare-pages/localized-title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type { Locale } from "@/lib/i18n";
import type { ComparisonDef } from "./comparisons";

/** Locale-facing titles for curated comparison pages. Korean keeps the source title. */
const TITLE_I18N: Record<string, Partial<Record<Exclude<Locale, "ko">, string>>> = {
"macbook-air-m4-vs-macbook-pro-m4": {
en: "MacBook Air M4 vs MacBook Pro M4",
ja: "MacBook Air M4 vs MacBook Pro M4",
},
"macbook-air-m3-vs-macbook-pro-m3": {
en: "MacBook Air M3 vs MacBook Pro M3",
ja: "MacBook Air M3 vs MacBook Pro M3",
},
"macbook-air-13-vs-macbook-air-15": {
en: "MacBook Air 13-inch vs 15-inch",
ja: "MacBook Air 13インチ vs 15インチ",
},
"macbook-air-m3-vs-lg-gram-16": {
en: "MacBook Air M3 vs LG gram 16",
ja: "MacBook Air M3 vs LG gram 16",
},
"macbook-air-m3-vs-galaxy-book4-pro": {
en: "MacBook Air M3 vs Galaxy Book4 Pro",
ja: "MacBook Air M3 vs Galaxy Book4 Pro",
},
"lg-gram-16-vs-galaxy-book4-pro": {
en: "LG gram 16 vs Galaxy Book4 Pro",
ja: "LG gram 16 vs Galaxy Book4 Pro",
},
"macbook-air-m4-vs-lg-gram-16": {
en: "MacBook Air M4 vs LG gram 16",
ja: "MacBook Air M4 vs LG gram 16",
},
"macbook-air-m4-vs-galaxy-book5-pro": {
en: "MacBook Air M4 vs Galaxy Book5 Pro",
ja: "MacBook Air M4 vs Galaxy Book5 Pro",
},
"macbook-air-m4-vs-lg-gram-pro-16": {
en: "MacBook Air M4 vs LG gram Pro 16",
ja: "MacBook Air M4 vs LG gram Pro 16",
},
"macbook-air-m3-vs-macbook-air-m4": {
en: "MacBook Air M3 vs M4 — upgrade value",
ja: "MacBook Air M3 vs M4 — アップグレード価値",
},
"macbook-air-m2-vs-macbook-air-m3": {
en: "MacBook Air M2 vs M3 — used vs new",
ja: "MacBook Air M2 vs M3 — 中古 vs 新型",
},
"lg-gram-pro-16-vs-galaxy-book5-pro": {
en: "LG gram Pro 16 vs Galaxy Book5 Pro",
ja: "LG gram Pro 16 vs Galaxy Book5 Pro",
},
"lg-gram-16-vs-lg-gram-pro-16": {
en: "LG gram 16 vs gram Pro 16",
ja: "LG gram 16 vs gram Pro 16",
},
"galaxy-book5-pro-vs-galaxy-book4-pro": {
en: "Galaxy Book5 Pro vs Book4 Pro — one-year gap",
ja: "Galaxy Book5 Pro vs Book4 Pro — 1年差の価値",
},
"iphone-16-vs-galaxy-s25": {
en: "iPhone 16 vs Galaxy S25",
ja: "iPhone 16 vs Galaxy S25",
},
"iphone-16-pro-vs-galaxy-s25-ultra": {
en: "iPhone 16 Pro vs Galaxy S25 Ultra",
ja: "iPhone 16 Pro vs Galaxy S25 Ultra",
},
"iphone-16-vs-iphone-16-pro": {
en: "iPhone 16 vs iPhone 16 Pro",
ja: "iPhone 16 vs iPhone 16 Pro",
},
"galaxy-s25-vs-s25-ultra": {
en: "Galaxy S25 vs S25 Ultra",
ja: "Galaxy S25 vs S25 Ultra",
},
"iphone-15-vs-iphone-16": {
en: "iPhone 15 vs iPhone 16 — worth upgrading?",
ja: "iPhone 15 vs iPhone 16 — アップグレード価値は?",
},
"galaxy-s24-vs-s25": {
en: "Galaxy S24 vs S25 — reason to switch?",
ja: "Galaxy S24 vs S25 — 乗り換える理由は?",
},
"iphone-16-pro-max-vs-galaxy-s25-ultra": {
en: "iPhone 16 Pro Max vs Galaxy S25 Ultra",
ja: "iPhone 16 Pro Max vs Galaxy S25 Ultra",
},
"iphone-14-pro-vs-iphone-15-pro": {
en: "iPhone 14 Pro vs iPhone 15 Pro",
ja: "iPhone 14 Pro vs iPhone 15 Pro",
},
"iphone-15-pro-vs-iphone-16-pro": {
en: "iPhone 15 Pro vs iPhone 16 Pro",
ja: "iPhone 15 Pro vs iPhone 16 Pro",
},
"galaxy-s24-ultra-vs-s25-ultra": {
en: "Galaxy S24 Ultra vs S25 Ultra",
ja: "Galaxy S24 Ultra vs S25 Ultra",
},
"airpods-pro-2-vs-galaxy-buds3-pro": {
en: "AirPods Pro 2 vs Galaxy Buds3 Pro",
ja: "AirPods Pro 2 vs Galaxy Buds3 Pro",
},
"airpods-pro-2-vs-sony-wf-1000xm5": {
en: "AirPods Pro 2 vs Sony WF-1000XM5",
ja: "AirPods Pro 2 vs Sony WF-1000XM5",
},
"sony-wh-1000xm5-vs-bose-qc-ultra": {
en: "Sony WH-1000XM5 vs Bose QC Ultra",
ja: "Sony WH-1000XM5 vs Bose QC Ultra",
},
"airpods-4-vs-galaxy-buds3": {
en: "AirPods 4 vs Galaxy Buds3",
ja: "AirPods 4 vs Galaxy Buds3",
},
"airpods-pro-2-vs-airpods-4": {
en: "AirPods Pro 2 vs AirPods 4 — need Pro?",
ja: "AirPods Pro 2 vs AirPods 4 — Proは必要?",
},
"galaxy-buds3-pro-vs-sony-wf-1000xm5": {
en: "Galaxy Buds3 Pro vs Sony WF-1000XM5",
ja: "Galaxy Buds3 Pro vs Sony WF-1000XM5",
},
"ipad-pro-m4-vs-galaxy-tab-s10-ultra": {
en: "iPad Pro M4 vs Galaxy Tab S10 Ultra",
ja: "iPad Pro M4 vs Galaxy Tab S10 Ultra",
},
"ipad-air-m2-vs-galaxy-tab-s9-plus": {
en: "iPad Air M2 vs Galaxy Tab S9+",
ja: "iPad Air M2 vs Galaxy Tab S9+",
},
"ipad-pro-m4-vs-ipad-air-m2": {
en: "iPad Pro M4 vs iPad Air M2 — need Pro?",
ja: "iPad Pro M4 vs iPad Air M2 — Proは必要?",
},
"ipad-10-vs-galaxy-tab-s9-fe": {
en: "iPad 10th gen vs Galaxy Tab S9 FE",
ja: "iPad 第10世代 vs Galaxy Tab S9 FE",
},
};

export function localizedComparisonTitle(
comparison: Pick<ComparisonDef, "slug" | "title">,
locale: Locale
): string {
if (locale === "ko") return comparison.title;
return TITLE_I18N[comparison.slug]?.[locale] ?? comparison.title;
}
10 changes: 10 additions & 0 deletions lib/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const en = {
}
],
methodTitle: "How Axis decides",
methodKicker: "Decision method",
methodSubEm: "Not marketing copy —",
methodSub: "Axis starts with verifiable information and your real-world situation.",
features: [
Expand Down Expand Up @@ -69,6 +70,15 @@ export const en = {
earphones: "Earphones & Headphones",
tablet: "Tablets",
},
compareIndex: {
title: "Product comparisons",
metaTitle: "Product comparisons — Axis",
metaDescription:
"Popular laptop, smartphone, earphone, and tablet comparisons. Official specs and real-world fit help you decide what to buy.",
sub: "Official specs + AI analysis so you can decide what to buy.",
ctaCopy: "Looking for something not listed? Compare it yourself.",
cta: "Compare now →",
},
},

input: {
Expand Down
10 changes: 10 additions & 0 deletions lib/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const ja = {
}
],
methodTitle: "Axisの判断方法",
methodKicker: "判断の方法",
methodSubEm: "広告文ではなく、",
methodSub: "確認できる情報とあなたの状況を先に見ます。",
features: [
Expand Down Expand Up @@ -69,6 +70,15 @@ export const ja = {
earphones: "イヤホン・ヘッドホン",
tablet: "タブレット",
},
compareIndex: {
title: "製品比較まとめ",
metaTitle: "製品比較まとめ — Axis",
metaDescription:
"ノートPC・スマホ・イヤホン・タブレットの人気比較まとめ。公式スペックと実用基準で買うものをすぐ判断できます。",
sub: "公式スペックとAI分析で、買うものをすぐ結論づけます。",
ctaCopy: "一覧にない製品? 自分で比較してみてください。",
cta: "自分で比較する →",
},
},

input: {
Expand Down
10 changes: 10 additions & 0 deletions lib/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const ko = {
}
],
methodTitle: "Axis가 판단하는 방식",
methodKicker: "결정 방식",
methodSubEm: "광고 문구가 아니라,",
methodSub: "확인 가능한 정보와 사용자의 상황을 먼저 봅니다.",
features: [
Expand Down Expand Up @@ -72,6 +73,15 @@ export const ko = {
earphones: "이어폰·헤드폰",
tablet: "태블릿",
},
compareIndex: {
title: "제품 비교 모음",
metaTitle: "제품 비교 모음 — Axis",
metaDescription:
"노트북·스마트폰·이어폰·태블릿 인기 비교 모음. 공식 스펙과 실사용 기준으로 어떤 걸 살지 바로 알려드립니다.",
sub: "공식 스펙 + AI 분석으로 뭘 살지 바로 결론 내드립니다.",
ctaCopy: "목록에 없는 제품? 직접 비교해보세요.",
cta: "직접 비교하기 →",
},
},

// Input
Expand Down
Loading