Skip to content

Commit bc03ab6

Browse files
junzero741claude
andcommitted
feat(frontend): show post title and handle expired/missing state before unlock
unlock 페이지 진입 시 비밀번호 입력 전에 게시글 제목을 표시하고, 존재하지 않는 문서와 만료된 문서를 각각 별도 UI로 안내. - api.ts: getPostMetadata 함수 추가 - page.tsx: 서버에서 메타데이터 fetch → generateMetadata에 실제 제목 반영 - post-unlock.tsx: meta prop 수신, 제목 표시 / null → 404 UI / isExpired → 만료 UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 350fcc6 commit bc03ab6

4 files changed

Lines changed: 72 additions & 7 deletions

File tree

apps/frontend/next-env.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="next" />
22
/// <reference types="next/image-types/global" />
3-
import "./.next/types/routes.d.ts";
3+
import "./.next/dev/types/routes.d.ts";
44

55
// NOTE: This file should not be edited
66
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

apps/frontend/src/app/[slug]/page.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
import type { Metadata } from 'next';
2+
import { getPostMetadata } from '@/lib/api';
23
import PostUnlock from './post-unlock';
34

4-
export function generateMetadata(): Metadata {
5+
export async function generateMetadata({
6+
params,
7+
}: {
8+
params: Promise<{ slug: string }>;
9+
}): Promise<Metadata> {
10+
const { slug } = await params;
11+
const meta = await getPostMetadata(slug);
12+
const title = meta ? `${meta.title} - 님보` : '비밀 문서 - 님보';
13+
514
return {
6-
title: '비밀 문서 - 님보',
15+
title,
716
description: '비밀번호로 보호된 문서입니다.',
817
robots: {
918
index: false,
@@ -29,5 +38,6 @@ export default async function PostPage({
2938
params: Promise<{ slug: string }>;
3039
}) {
3140
const { slug } = await params;
32-
return <PostUnlock slug={slug} />;
41+
const meta = await getPostMetadata(slug);
42+
return <PostUnlock slug={slug} meta={meta} />;
3343
}

apps/frontend/src/app/[slug]/post-unlock.tsx

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
import { useState } from 'react';
44
import { viewPost } from '@/lib/api';
5+
import type { GetPostMetadataResponse } from '@/lib/api';
56
import { sanitizeHtml } from '@/lib/sanitize';
67
import ReportModal from '@/components/ReportModal';
78

89
type Step = 'auth' | 'view';
910

10-
export default function PostUnlock({ slug }: { slug: string }) {
11+
interface Props {
12+
slug: string;
13+
meta: GetPostMetadataResponse | null;
14+
}
15+
16+
export default function PostUnlock({ slug, meta }: Props) {
1117
const [step, setStep] = useState<Step>('auth');
1218
const [password, setPassword] = useState('');
1319
const [post, setPost] = useState<{ title: string; content: string } | null>(null);
@@ -57,6 +63,45 @@ export default function PostUnlock({ slug }: { slug: string }) {
5763
);
5864
}
5965

66+
// 존재하지 않는 게시글
67+
if (meta === null) {
68+
return (
69+
<section className="mx-auto max-w-md px-4 py-16">
70+
<div className="rounded-xl border border-border bg-surface-card p-8 shadow-md text-center">
71+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-error-bg">
72+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6 text-error">
73+
<circle cx="12" cy="12" r="10" />
74+
<line x1="12" x2="12" y1="8" y2="12" />
75+
<line x1="12" x2="12.01" y1="16" y2="16" />
76+
</svg>
77+
</div>
78+
<h1 className="text-lg font-semibold text-text-primary">존재하지 않는 문서</h1>
79+
<p className="mt-1 text-sm text-text-secondary">링크가 잘못되었거나 삭제된 문서입니다.</p>
80+
</div>
81+
</section>
82+
);
83+
}
84+
85+
// 만료된 게시글
86+
if (meta.isExpired) {
87+
return (
88+
<section className="mx-auto max-w-md px-4 py-16">
89+
<div className="rounded-xl border border-border bg-surface-card p-8 shadow-md text-center">
90+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-error-bg">
91+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6 text-error">
92+
<circle cx="12" cy="12" r="10" />
93+
<polyline points="12 6 12 12 16 14" />
94+
</svg>
95+
</div>
96+
<h1 className="text-lg font-semibold text-text-primary">만료된 문서</h1>
97+
<p className="mt-1 text-sm text-text-secondary">
98+
열람 기간이 지나 더 이상 접근할 수 없습니다.
99+
</p>
100+
</div>
101+
</section>
102+
);
103+
}
104+
60105
return (
61106
<section className="mx-auto max-w-md px-4 py-16">
62107
<div className="rounded-xl border border-border bg-surface-card p-8 shadow-md text-center">
@@ -68,7 +113,7 @@ export default function PostUnlock({ slug }: { slug: string }) {
68113
</svg>
69114
</div>
70115

71-
<h1 className="text-lg font-semibold text-text-primary">보호된 문서</h1>
116+
<h1 className="text-lg font-semibold text-text-primary">{meta.title}</h1>
72117
<p className="mt-1 text-sm text-text-secondary">
73118
이 문서는 비밀번호로 보호되어 있습니다.
74119
</p>

apps/frontend/src/lib/api.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@ import {
22
API_ROUTES,
33
CreatePostRequest,
44
CreatePostResponse,
5+
GetPostMetadataResponse,
56
UnlockPostRequest,
67
UnlockPostResponse,
78
ReportReason,
89
} from '@private-board/shared';
910

10-
export type { CreatePostRequest, CreatePostResponse, UnlockPostRequest, UnlockPostResponse, ReportReason };
11+
export type { CreatePostRequest, CreatePostResponse, GetPostMetadataResponse, UnlockPostRequest, UnlockPostResponse, ReportReason };
1112

1213
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:4000';
1314

@@ -22,6 +23,15 @@ export async function createPost(data: CreatePostRequest): Promise<CreatePostRes
2223
return res.json();
2324
}
2425

26+
export async function getPostMetadata(slug: string): Promise<GetPostMetadataResponse | null> {
27+
const res = await fetch(`${API_BASE}${API_ROUTES.posts.metadata(slug)}`, {
28+
cache: 'no-store',
29+
});
30+
if (res.status === 404) return null;
31+
if (!res.ok) return null;
32+
return res.json();
33+
}
34+
2535
export async function viewPost(slug: string, data: UnlockPostRequest): Promise<UnlockPostResponse> {
2636
const res = await fetch(`${API_BASE}${API_ROUTES.posts.unlock(slug)}`, {
2737
method: 'POST',

0 commit comments

Comments
 (0)