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
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ pnpm test:loading # 빌드 후 로딩 분리, 재시도, SSG 회귀 검사

### Reader and browser regression checks
- `scripts/image-assets.ts`에서 본문 이미지의 WebP와 크기 메타데이터를 생성합니다. 원본 파일은 유지합니다.
- 글 본문은 최대 800px, 1280px 이상에서 옆 목차, 작은 화면에서 접는 목차를 사용합니다.
- 글 본문과 목차 전체는 최대 1152px로 중앙 정렬합니다. 1280px 이상에서 본문 최대 912px + 간격 32px + 목차 208px로 배치하고, 작은 화면에서는 접는 목차를 사용합니다.
- SSR 본문에 목차 대상 제목이 있으면 접힌 목차의 48px 높이를 hydration 전부터 확보합니다.
- 태그·시리즈·소개·분석의 열 전환은 페이지나 목록의 실제 가용폭을 기준으로 합니다.
- `useHydratedSearchParams`로 첫 렌더를 SSG와 맞춘 후 필터를 적용합니다. 404 프리렌더 표시가 있으면 클라이언트도 404 라우트로 시작합니다.
- `pnpm preview`는 GitHub Pages의 실제 404 응답을 재현합니다.
- 이동·hydration·접근성 변경 후 `pnpm build`, `pnpm test:browser`를 실행합니다.
Expand Down
2 changes: 1 addition & 1 deletion src/components/PostImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export function PostImage({ src, alt = "", ...props }: ComponentPropsWithoutRef<
const asset = src ? images[src] : undefined
return <img {...props} alt={alt} src={asset?.src ?? src} srcSet={asset?.srcSet ?? props.srcSet}
width={asset?.width ?? props.width} height={asset?.height ?? props.height}
sizes={asset ? "(max-width: 767px) calc(100vw - 2rem), 800px" : props.sizes}
sizes={asset ? "(max-width: 767px) calc(100vw - 2rem), 912px" : props.sizes}
loading={props.loading ?? "lazy"} decoding="async" />
}
9 changes: 5 additions & 4 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -386,17 +386,18 @@ html.theme-transition *::after {
}

.page-container-article {
--page-container-max-width: 72rem;
--page-container-max-width: 57rem;
}

.page-container-wide {
--page-container-max-width: 72rem;
}

/* 글 본문과 목차를 화면 크기에 맞춰 배치한다. */
.post-page-layout { display: flex; flex-direction: column; gap: 1.5rem; }
.page-container-article { --page-container-max-width: 50rem; }
.post-page-layout { display: flex; flex-direction: column; gap: 1.5rem; width: 100%; max-width: 72rem; margin-inline: auto; }
.post-toc-layer { order: -1; position: sticky; top: 3rem; z-index: 30; }
/* SSR 본문에 목차 대상이 있으면 hydration 전부터 접힌 목차 높이를 확보한다. */
.post-page-layout:has(.prose h2[id], .prose h3[id]) .post-toc-layer { min-height: 3rem; }
.post-toc-panel { padding: .5rem .75rem; background: var(--background); }
.post-toc-desktop-title { display: none; }
.post-toc-list { display: none; margin-top: .75rem; max-height: 50vh; overflow-y: auto; }
Expand All @@ -408,7 +409,7 @@ html.theme-transition *::after {
.prose h2, .prose h3 { scroll-margin-top: 5rem; }
}
@media (min-width: 1280px) {
.post-page-layout { display: grid; grid-template-columns: minmax(0, 50rem) 13rem; gap: 2rem; max-width: 65rem; margin-inline: auto; align-items: start; }
.post-page-layout { display: grid; grid-template-columns: minmax(0, 1fr) 13rem; gap: 2rem; align-items: start; }
.post-article-centered { grid-column: 1; grid-row: 1; }
.post-toc-layer { grid-column: 2; grid-row: 1; top: 2rem; }
.post-toc-panel { padding: .75rem; }
Expand Down
18 changes: 9 additions & 9 deletions src/pages/AboutPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ function CompanySection({
const t = useT()

return (
<div className="grid grid-cols-1 md:grid-cols-[160px_1fr] gap-4 md:gap-8">
<div className="grid grid-cols-1 gap-4 @min-[48rem]/about:grid-cols-[160px_minmax(0,1fr)] @min-[48rem]/about:gap-8">
{/* Left: Company Info */}
<div>
<h3 className="font-bold">{company.name}</h3>
Expand All @@ -70,7 +70,7 @@ function CompanySection({
</div>

{/* Right: Projects */}
<div className="space-y-3">
<div className="min-w-0 space-y-3">
{company.projects.map((ps, index) => {
const project = projects.find((p) => p.slug === ps.slug)
if (!project) return null
Expand Down Expand Up @@ -214,17 +214,17 @@ export function AboutPage() {
}

return (
<PageContainer variant="wide" className="space-y-10">
<PageContainer variant="wide" className="@container/about space-y-10">
{/* Profile Header */}
<div className="flex flex-col sm:flex-row items-center sm:items-start gap-6">
<Avatar className="h-24 w-24">
<div className="flex flex-col items-center gap-6 @min-[30rem]/about:flex-row @min-[30rem]/about:items-start">
<Avatar className="h-24 w-24 shrink-0">
<AvatarImage src="https://github.com/devy1540.png" alt={resume.profile.name} />
<AvatarFallback className="text-2xl">HJ</AvatarFallback>
</Avatar>
<div className="text-center sm:text-left space-y-2">
<div className="min-w-0 w-full space-y-2 text-center @min-[30rem]/about:flex-1 @min-[30rem]/about:text-left">
<h1 className="text-3xl font-bold tracking-tight">{resume.profile.name}</h1>
<p className="text-lg text-muted-foreground">{t.about.description}</p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground justify-center sm:justify-start">
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-sm text-muted-foreground @min-[30rem]/about:justify-start">
<a href={`mailto:${resume.profile.email}`} className="inline-flex items-center gap-1.5 hover:text-foreground transition-colors">
<Mail className="h-3.5 w-3.5" />
{resume.profile.email}
Expand All @@ -234,7 +234,7 @@ export function AboutPage() {
{displayPhone}
</a>
</div>
<div className="flex gap-2 justify-center sm:justify-start">
<div className="flex flex-wrap justify-center gap-2 @min-[30rem]/about:justify-start">
<Button asChild variant="outline" size="sm">
<a href={resume.profile.github} target="_blank" rel="noopener noreferrer">
<Github className="mr-2 h-4 w-4" />
Expand Down Expand Up @@ -279,7 +279,7 @@ export function AboutPage() {
{/* Development approach */}
<section>
<h2 className="text-2xl font-semibold mb-4">{t.about.aiNativeDevelopment}</h2>
<div className="grid gap-3 md:grid-cols-3">
<div className="grid gap-3 @min-[48rem]/about:grid-cols-3">
{resume.aiNativeWorkflow.map((item) => (
<div key={item.title} className="rounded-lg border bg-card p-4">
<h3 className="font-semibold">{item.title}</h3>
Expand Down
50 changes: 25 additions & 25 deletions src/pages/AnalyticsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,11 @@ export function AnalyticsPage() {
]

return (
<PageContainer>
<PageContainer className="@container/analytics">
<section className="mb-8">
<h1 className="text-4xl font-bold tracking-tight mb-2">{t.common.analytics}</h1>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
<p className="text-muted-foreground flex-1">{t.analytics.description}</p>
<div className="flex flex-col gap-3 @min-[40rem]/analytics:flex-row @min-[40rem]/analytics:items-center">
<p className="min-w-0 text-muted-foreground flex-1">{t.analytics.description}</p>
<div className="flex flex-wrap items-center gap-2">
<ToggleGroup
type="single"
Expand Down Expand Up @@ -260,14 +260,14 @@ export function AnalyticsPage() {
</section>

{/* Summary Cards */}
<section className="grid grid-cols-2 gap-3 mb-8 md:grid-cols-4">
<section className="grid grid-cols-1 gap-3 mb-8 @min-[22rem]/analytics:grid-cols-2 @min-[48rem]/analytics:grid-cols-4">
{summaryCards.map((card) => (
<Card key={card.label} className="py-4">
<Card key={card.label} className="min-w-0 py-4">
<CardContent className="flex items-center gap-3 px-4">
<card.icon className="size-5 text-muted-foreground shrink-0" />
<div>
<p className="text-xs text-muted-foreground">{card.label}</p>
<p className="text-2xl font-bold tabular-nums">{card.value}</p>
<div className="min-w-0">
<p className="text-xs text-muted-foreground wrap-anywhere">{card.label}</p>
<p className="text-2xl font-bold tabular-nums break-all">{card.value}</p>
</div>
</CardContent>
</Card>
Expand All @@ -285,21 +285,21 @@ export function AnalyticsPage() {

{/* Traffic Momentum */}
<section className="mb-8">
<div className="mb-4 flex items-center justify-between gap-3">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
{t.analytics.trafficMomentum}
</h2>
<Badge variant="outline">{rangeDays === 7 ? t.analytics.range7d : rangeDays === 14 ? t.analytics.range14d : t.analytics.range30d}</Badge>
</div>
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
<div className="grid grid-cols-1 gap-3 @min-[22rem]/analytics:grid-cols-2 @min-[48rem]/analytics:grid-cols-4">
{momentumCards.map((card) => (
<Card key={card.label} className="py-4">
<Card key={card.label} className="min-w-0 py-4">
<CardContent className="px-4">
<div className="mb-3 flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground">{card.label}</p>
<p className="min-w-0 text-xs text-muted-foreground wrap-anywhere">{card.label}</p>
<card.icon className="size-4 text-muted-foreground shrink-0" />
</div>
<p className="text-2xl font-bold tabular-nums">{card.value}</p>
<p className="text-2xl font-bold tabular-nums break-all">{card.value}</p>
<p className="mt-1 text-xs text-muted-foreground truncate">{card.detail}</p>
</CardContent>
</Card>
Expand All @@ -309,14 +309,14 @@ export function AnalyticsPage() {

{/* Content Insights */}
<section className="mb-8">
<div className="mb-4 flex items-center justify-between gap-3">
<div className="mb-4 flex flex-wrap items-center justify-between gap-3">
<h2 className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
{t.analytics.contentInsights}
</h2>
<Badge variant="secondary">{t.analytics.allTimeBasis}</Badge>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card>
<div className="grid gap-4 @min-[48rem]/analytics:grid-cols-2">
<Card className="min-w-0">
<CardHeader>
<CardTitle className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
{t.analytics.fastestPosts}
Expand Down Expand Up @@ -353,7 +353,7 @@ export function AnalyticsPage() {
</CardContent>
</Card>

<Card>
<Card className="min-w-0">
<CardHeader>
<CardTitle className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
{t.analytics.tagPerformance}
Expand All @@ -366,9 +366,9 @@ export function AnalyticsPage() {
<div className="space-y-3">
{tagPerformance.map((item) => (
<div key={item.tag}>
<div className="mb-1 flex items-center gap-2">
<span className="text-sm font-medium truncate">{item.tag}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
<div className="mb-1 flex flex-wrap items-center gap-x-2 gap-y-1">
<span className="min-w-0 text-sm font-medium truncate">{item.tag}</span>
<span className="ml-auto text-xs text-muted-foreground tabular-nums wrap-anywhere">
{formatMetric(item.views)} · {formatMetric(item.averageViews, 1)} {t.analytics.averageViews}
</span>
</div>
Expand All @@ -385,7 +385,7 @@ export function AnalyticsPage() {
</CardContent>
</Card>

<Card className="lg:col-span-2">
<Card className="min-w-0 @min-[48rem]/analytics:col-span-2">
<CardHeader>
<CardTitle className="text-sm font-semibold uppercase tracking-widest text-muted-foreground">
{t.analytics.seriesPerformance}
Expand All @@ -395,12 +395,12 @@ export function AnalyticsPage() {
{seriesPerformance.length === 0 ? (
<p className="text-sm text-muted-foreground">{t.analytics.noSeriesData}</p>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
<div className="grid gap-3 @min-[32rem]/analytics:grid-cols-2 @min-[64rem]/analytics:grid-cols-5">
{seriesPerformance.map((item) => (
<div key={item.series} className="rounded-md border p-3">
<div key={item.series} className="min-w-0 rounded-md border p-3">
<p className="mb-2 line-clamp-2 min-h-10 text-sm font-medium">{item.series}</p>
<p className="text-xl font-bold tabular-nums">{formatMetric(item.views)}</p>
<p className="text-xs text-muted-foreground">
<p className="text-xl font-bold tabular-nums break-all">{formatMetric(item.views)}</p>
<p className="text-xs text-muted-foreground wrap-anywhere">
{item.count} {t.analytics.postsCount} · {formatMetric(item.averageViews, 1)} {t.analytics.averageViews}
</p>
</div>
Expand Down
24 changes: 12 additions & 12 deletions src/pages/SeriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ export function SeriesPage() {
const hasFilters = Boolean(query.trim() || selectedSeriesName || sortMode !== "recent")

return (
<PageContainer variant="wide">
<div className="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<PageContainer variant="wide" className="@container/taxonomy">
<div className="mb-8 flex flex-col flex-wrap gap-4 @min-[32rem]/taxonomy:flex-row @min-[32rem]/taxonomy:items-end @min-[32rem]/taxonomy:justify-between">
<div>
<h1 className="text-4xl font-bold tracking-tight">{t.common.series}</h1>
<p className="mt-3 text-sm text-muted-foreground">
Expand All @@ -169,7 +169,7 @@ export function SeriesPage() {
</div>

{activeSeries && (
<Badge variant="outline" className="w-fit rounded-full px-3 py-1 text-sm">
<Badge variant="outline" className="max-w-full whitespace-normal break-words rounded-full px-3 py-1 text-sm">
{t.series.selectedLabel(activeSeries.name)}
</Badge>
)}
Expand All @@ -187,7 +187,7 @@ export function SeriesPage() {
/>
</div>

<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-col gap-3 @min-[32rem]/taxonomy:flex-row @min-[32rem]/taxonomy:items-center @min-[32rem]/taxonomy:justify-between">
<ToggleGroup
type="single"
value={sortMode}
Expand All @@ -205,7 +205,7 @@ export function SeriesPage() {
type="button"
variant="ghost"
size="sm"
className="w-fit self-start sm:self-auto"
className="w-fit self-start @min-[32rem]/taxonomy:self-auto"
onClick={resetFilters}
>
<XIcon className="size-3" />
Expand All @@ -218,14 +218,14 @@ export function SeriesPage() {
{allSeries.length === 0 ? (
<p className="text-muted-foreground">{t.series.noSeries}</p>
) : (
<div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_24rem]">
<div className="space-y-9">
<div className="grid gap-10 @min-[56rem]/taxonomy:grid-cols-[minmax(0,1fr)_24rem]">
<div className="@container/taxonomy-list min-w-0 space-y-9">
<section>
<div className="mb-3 flex items-center justify-between gap-4">
<h2 className="text-xs font-semibold uppercase tracking-widest text-muted-foreground">
{t.series.topSeries}
</h2>
<span className="hidden text-xs text-muted-foreground sm:inline">{t.series.topSeriesNote}</span>
<span className="hidden text-xs text-muted-foreground @min-[32rem]/taxonomy-list:inline">{t.series.topSeriesNote}</span>
</div>

{matchingSeries.length === 0 ? (
Expand All @@ -236,7 +236,7 @@ export function SeriesPage() {
<Link
key={series.name}
to={seriesPathFor(series.name)}
className="grid min-h-20 grid-cols-1 gap-1 border-b py-4 transition-colors hover:bg-accent/40 sm:grid-cols-[minmax(0,1fr)_5rem_7rem] sm:items-center sm:gap-4 sm:px-0"
className="grid min-h-20 grid-cols-1 gap-1 border-b py-4 transition-colors hover:bg-accent/40 @min-[28rem]/taxonomy-list:grid-cols-[minmax(0,1fr)_5rem_7rem] @min-[28rem]/taxonomy-list:items-center @min-[28rem]/taxonomy-list:gap-4"
>
<span className="min-w-0">
<span className="flex min-w-0 items-center gap-2 font-medium">
Expand Down Expand Up @@ -271,7 +271,7 @@ export function SeriesPage() {
{groupedSeries.length === 0 ? (
<p className="text-sm text-muted-foreground">{t.series.noMatchingSeries}</p>
) : (
<div className="grid gap-x-8 gap-y-7 sm:grid-cols-2">
<div className="grid gap-x-8 gap-y-7 @min-[28rem]/taxonomy-list:grid-cols-2">
{groupedSeries.map((group) => (
<div key={group.id} className="min-w-0">
<h3 className="mb-3 text-base font-semibold">{group.label}</h3>
Expand All @@ -295,9 +295,9 @@ export function SeriesPage() {
</div>

{activeSeries && (
<aside className="border-t pt-8 lg:sticky lg:top-20 lg:border-l lg:border-t-0 lg:pl-7 lg:pt-0">
<aside className="min-w-0 border-t pt-8 @min-[56rem]/taxonomy:sticky @min-[56rem]/taxonomy:top-20 @min-[56rem]/taxonomy:border-l @min-[56rem]/taxonomy:border-t-0 @min-[56rem]/taxonomy:pl-7 @min-[56rem]/taxonomy:pt-0">
<div className="mb-2 flex items-start justify-between gap-3">
<h2 className="break-keep text-3xl font-bold tracking-tight">{activeSeries.name}</h2>
<h2 className="min-w-0 break-keep text-3xl font-bold tracking-tight [overflow-wrap:anywhere]">{activeSeries.name}</h2>
<Badge className="shrink-0 rounded-full">{t.series.postCountShort(activeSeries.postCount)}</Badge>
</div>

Expand Down
Loading
Loading