feat: implement post existence check and detail page

This commit is contained in:
암냥 2026-04-18 06:48:21 +09:00
commit b18cff8b1a
No known key found for this signature in database
10 changed files with 646 additions and 43 deletions

View file

@ -1,6 +1,7 @@
"use client";
import { FormEvent, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import Header from "../../components/header";
type SourceType = "twitter" | "pixiv";
@ -26,6 +27,22 @@ type Me = {
role: "admin" | "writer" | "reader";
};
type UploadApiResponse = {
success: boolean;
message: string;
savedCount?: number;
failedCount?: number;
};
type ExistsApiResponse = {
success: boolean;
message: string;
exists: boolean;
source: SourceType | null;
postId: string | null;
documentId: string | null;
};
function detectSource(url: string): SourceType | null {
if (/^https?:\/\/(www\.)?(x\.com|twitter\.com|fxtwitter\.com|fixupx\.com|vxwitter\.com)\//.test(url)) {
return "twitter";
@ -59,6 +76,7 @@ export default function AddPage() {
const [lastFetchedUrl, setLastFetchedUrl] = useState("");
const [viewerRole, setViewerRole] = useState<Me["role"] | "guest">("guest");
const [loadingRole, setLoadingRole] = useState(true);
const [existingDetailId, setExistingDetailId] = useState<string | null>(null);
const selectedCount = useMemo(
() => selected.filter(Boolean).length,
@ -110,6 +128,7 @@ export default function AddPage() {
setLastFetchedUrl("");
setError(null);
setSuccess(null);
setExistingDetailId(null);
}
async function fetchPreview(targetUrl?: string) {
@ -125,8 +144,25 @@ export default function AddPage() {
setLoadingPreview(true);
setSourceType(source);
setExistingDetailId(null);
try {
const existsResponse = await fetch(`/api/post/exists?url=${encodeURIComponent(trimmedUrl)}`, {
cache: "no-store",
});
if (existsResponse.ok) {
const existsData = (await existsResponse.json()) as ExistsApiResponse;
if (existsData.exists) {
setPreviewItems([]);
setSelected([]);
setLastFetchedUrl(trimmedUrl);
setExistingDetailId(existsData.documentId);
setSuccess(existsData.message);
return;
}
}
if (source === "twitter") {
const response = await fetch(`/api/tweet/fetch?url=${encodeURIComponent(trimmedUrl)}`, {
cache: "no-store",
@ -255,17 +291,28 @@ export default function AddPage() {
}),
});
if (!response.ok) {
const responseText = await response.text();
throw new Error(responseText || `업로드 실패: ${response.status}`);
let data: UploadApiResponse | null = null;
try {
data = (await response.json()) as UploadApiResponse;
} catch {
data = null;
}
const uploadedCount = selectedCount;
setUrl("");
setAuthor("");
setTagsText("");
resetPreview();
setSuccess(`${uploadedCount}개 이미지 업로드를 요청했습니다.`);
const message = data?.message || `업로드 실패: ${response.status}`;
if (!response.ok || !data?.success) {
throw new Error(message);
}
const uploadedCount = data.savedCount ?? selectedCount;
if (uploadedCount > 0) {
setUrl("");
setAuthor("");
setTagsText("");
resetPreview();
}
setSuccess(message);
} catch (submitError) {
setError(submitError instanceof Error ? submitError.message : "업로드에 실패했습니다.");
} finally {
@ -351,7 +398,14 @@ export default function AddPage() {
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
{success ? <p className="text-sm text-emerald-600">{success}</p> : null}
{success ? <p className="text-sm text-emerald-600">{success} {existingDetailId ? (
<Link
href={`/detail/${existingDetailId}`}
className="underline"
>
</Link>
) : null}</p> : null}
{loadingPreview ? (
<div className="border-l-2 border-border/80 pl-3 text-xs text-foreground/65">