"use client"; import { FormEvent, useEffect, useMemo, useState } from "react"; import Link from "next/link"; import Header from "../../components/header"; type SourceType = "twitter" | "pixiv"; type PreviewItem = { url: string; }; type TweetApiResponse = { tweet?: { author?: { name?: string }; media?: { photos?: Array<{ url?: string }> }; }; }; type PixivApiResponse = { image_proxy_urls?: string[]; author_name?: string; tags?: string[]; }; 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"; } if (/^https?:\/\/(www\.)?pixiv\.net\//.test(url)) { return "pixiv"; } return null; } function splitTags(text: string) { return text .split(/[\n,]/) .map((tag) => tag.trim().replace(/^#/, "")) .filter((tag) => tag.length > 0); } export default function AddPage() { const [url, setUrl] = useState(""); const [author, setAuthor] = useState(""); const [tagsText, setTagsText] = useState(""); const [previewItems, setPreviewItems] = useState([]); const [selected, setSelected] = useState([]); const [sourceType, setSourceType] = useState(null); const [loadingPreview, setLoadingPreview] = useState(false); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [lastFetchedUrl, setLastFetchedUrl] = useState(""); const [viewerRole, setViewerRole] = useState("guest"); const [loadingRole, setLoadingRole] = useState(true); const [existingDetailId, setExistingDetailId] = useState(null); const selectedCount = useMemo( () => selected.filter(Boolean).length, [selected], ); const canPreview = url.trim().length > 0 && !loadingPreview; const canSubmit = !submitting && previewItems.length > 0 && selectedCount > 0; const tags = useMemo(() => splitTags(tagsText), [tagsText]); const sourceLabel = sourceType === "twitter" ? "๐•" : sourceType === "pixiv" ? "Pixiv" : "-"; const canManagePost = viewerRole === "admin" || viewerRole === "writer"; useEffect(() => { let active = true; async function loadMe() { try { const response = await fetch("/api/auth/me", { cache: "no-store" }); if (!response.ok) { if (active) setViewerRole("guest"); return; } const me = (await response.json()) as Me; if (active) { setViewerRole(me.role); } } catch { if (active) { setViewerRole("guest"); } } finally { if (active) { setLoadingRole(false); } } } void loadMe(); return () => { active = false; }; }, []); function resetPreview() { setPreviewItems([]); setSelected([]); setSourceType(null); setLastFetchedUrl(""); setError(null); setSuccess(null); setExistingDetailId(null); } async function fetchPreview(targetUrl?: string) { const trimmedUrl = (targetUrl ?? url).trim(); const source = detectSource(trimmedUrl); setError(null); setSuccess(null); if (!source) { setError("์ง€์›ํ•˜์ง€ ์•Š๋Š” URL ํ˜•์‹์ž…๋‹ˆ๋‹ค. Twitter(X) ๋˜๋Š” Pixiv URL์„ ์ž…๋ ฅํ•ด ์ฃผ์„ธ์š”."); return; } 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", }); if (!response.ok) { throw new Error(`ํŠธ์œ„ํ„ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ ์‹คํŒจ: ${response.status}`); } const data = (await response.json()) as TweetApiResponse; const items = (data.tweet?.media?.photos ?? []) .map((photo) => photo.url) .filter((photoUrl): photoUrl is string => typeof photoUrl === "string" && photoUrl.length > 0) .map((photoUrl) => ({ url: photoUrl })); if (items.length === 0) { throw new Error("์ด๋ฏธ์ง€๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); } setPreviewItems(items); setSelected(items.map(() => true)); if (!author.trim()) { setAuthor(data.tweet?.author?.name ?? ""); } setLastFetchedUrl(trimmedUrl); return; } const response = await fetch(`/api/pixiv/fetch?url=${encodeURIComponent(trimmedUrl)}`, { cache: "no-store", }); if (!response.ok) { throw new Error(`ํ”ฝ์‹œ๋ธŒ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์š”์ฒญ ์‹คํŒจ: ${response.status}`); } const data = (await response.json()) as PixivApiResponse; const items = (data.image_proxy_urls ?? []) .filter((imageUrl): imageUrl is string => typeof imageUrl === "string" && imageUrl.length > 0) .map((imageUrl) => ({ url: imageUrl })); if (items.length === 0) { throw new Error("์ด๋ฏธ์ง€๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); } setPreviewItems(items); setSelected(items.map(() => true)); if (!author.trim()) { setAuthor(data.author_name ?? ""); } if (!tagsText.trim()) { const pixivTags = (data.tags ?? []).map((tag) => tag.replace(/^#/, "")).join(", "); setTagsText(pixivTags); } setLastFetchedUrl(trimmedUrl); } catch (fetchError) { setPreviewItems([]); setSelected([]); setLastFetchedUrl(""); setError(fetchError instanceof Error ? fetchError.message : "๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค."); } finally { setLoadingPreview(false); } } useEffect(() => { if (loadingPreview) { return; } const trimmed = url.trim(); if (!trimmed || trimmed === lastFetchedUrl) { return; } if (!detectSource(trimmed)) { return; } const timer = window.setTimeout(() => { void fetchPreview(trimmed); }, 450); return () => window.clearTimeout(timer); }, [lastFetchedUrl, loadingPreview, url]); async function submit(event: FormEvent) { event.preventDefault(); setError(null); setSuccess(null); if (!canManagePost) { setError("writer ๋˜๋Š” admin ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค."); return; } if (!sourceType) { setError("๋จผ์ € ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๋ถˆ๋Ÿฌ์™€ ์ฃผ์„ธ์š”."); return; } if (previewItems.length === 0) { setError("์—…๋กœ๋“œํ•  ์ด๋ฏธ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); return; } if (selectedCount === 0) { setError("์ตœ์†Œ ํ•œ ์žฅ ์ด์ƒ ์„ ํƒํ•ด ์ฃผ์„ธ์š”."); return; } setSubmitting(true); try { const tags = splitTags(tagsText); const response = await fetch("/api/post/upload", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ url: url.trim(), author: author.trim() || undefined, tag: tags.length > 0 ? tags : undefined, selected, }), }); let data: UploadApiResponse | null = null; try { data = (await response.json()) as UploadApiResponse; } catch { data = null; } 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 { setSubmitting(false); } } return (

Add Post

์•„๋ž˜์™€ ๊ฐ™์€ ์œ ํ˜•์˜ URL์„ ์ง€์›ํ•ด์š”!

  • - https://x.com, https://twitter.com (๊ธฐํƒ€ FxEmbed URL)
  • - https://pixiv.net (#R-18 ํƒœ๊ทธ๊ฐ€ ๋“ค์–ด๊ฐˆ ์‹œ ๊ฑฐ๋ถ€)

Source: {sourceLabel}

Selected: {selectedCount}/{previewItems.length}

{!loadingRole && !canManagePost ? (
์—…๋กœ๋“œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. writer ๋˜๋Š” admin ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
) : null}
{ setUrl(event.target.value); if (previewItems.length > 0 || sourceType) { resetPreview(); } }} placeholder="https://x.com/... or https://www.pixiv.net/artworks/..." className="w-full border-b border-border bg-transparent px-2 py-2 text-sm outline-none ring-0 transition focus:border-foreground/40" disabled={loadingPreview} required />

URL ๋ณ€๊ฒฝ ์‹œ ๊ธฐ์กด ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ์ž๋™์œผ๋กœ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.

setAuthor(event.target.value)} className="w-full border-b border-border bg-transparent px-2 py-2 text-sm outline-none transition focus:border-foreground/40" />

* ์ž๋™์œผ๋กœ ์ž‘๊ฐ€๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ๋งŒ์•ฝ ์ž˜๋ชป๋œ ์ด๋ฆ„์ด๋ผ๋ฉด ์ˆ˜๋™์œผ๋กœ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

setTagsText(event.target.value)} placeholder="" className="w-full border-b border-border bg-transparent px-2 py-2 text-sm outline-none transition focus:border-foreground/40" />

* ์ž๋™์œผ๋กœ ํƒœ๊ทธ๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. ์‰ผํ‘œ(,)๋‚˜ ์ค„๋ฐ”๊ฟˆ์œผ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ์—ฌ๋Ÿฌ ๊ฐœ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

{error ?

{error}

: null} {success ?

{success} {existingDetailId ? ( ์ƒ์„ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ ) : null}

: null} {loadingPreview ? (
๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค. ์ด๋ฏธ์ง€ ๊ฐœ์ˆ˜์— ๋”ฐ๋ผ ๋ช‡ ์ดˆ ์ •๋„ ์†Œ์š”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
) : null}
source: {sourceLabel} selected: {selectedCount}/{previewItems.length} tags: {tags.length}
{previewItems.length > 0 ? (

Preview

{selectedCount}์žฅ ์„ ํƒ๋จ
{previewItems.map((item, index) => ( ))}
) : null}
); }