191 lines
7.5 KiB
TypeScript
191 lines
7.5 KiB
TypeScript
|
|
"use client";
|
||
|
|
import useSWR from "swr";
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { useToast } from "@/app/components/ui/ToastProvider";
|
||
|
|
import { Modal } from "@/app/components/ui/Modal";
|
||
|
|
|
||
|
|
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
||
|
|
|
||
|
|
export default function MainPageSettingsPage() {
|
||
|
|
const { show } = useToast();
|
||
|
|
const { data: settingsResp, mutate: mutateSettings } = useSWR<{ settings: any }>("/api/admin/mainpage-settings", fetcher);
|
||
|
|
const { data: boardsResp } = useSWR<{ boards: any[] }>("/api/admin/boards", fetcher);
|
||
|
|
const { data: catsResp } = useSWR<{ categories: any[] }>("/api/admin/categories", fetcher);
|
||
|
|
|
||
|
|
const settings = settingsResp?.settings ?? {};
|
||
|
|
const boards = boardsResp?.boards ?? [];
|
||
|
|
const categories = catsResp?.categories ?? [];
|
||
|
|
|
||
|
|
const [saving, setSaving] = useState(false);
|
||
|
|
const [showBoardModal, setShowBoardModal] = useState(false);
|
||
|
|
const [draft, setDraft] = useState({
|
||
|
|
showBanner: settings.showBanner ?? true,
|
||
|
|
showPartnerShops: settings.showPartnerShops ?? true,
|
||
|
|
visibleBoardIds: settings.visibleBoardIds ?? [],
|
||
|
|
});
|
||
|
|
|
||
|
|
// settings가 로드되면 draft 동기화
|
||
|
|
useEffect(() => {
|
||
|
|
if (settingsResp?.settings) {
|
||
|
|
setDraft({
|
||
|
|
showBanner: settings.showBanner ?? true,
|
||
|
|
showPartnerShops: settings.showPartnerShops ?? true,
|
||
|
|
visibleBoardIds: settings.visibleBoardIds ?? [],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}, [settingsResp, settings]);
|
||
|
|
|
||
|
|
async function save() {
|
||
|
|
setSaving(true);
|
||
|
|
try {
|
||
|
|
const res = await fetch("/api/admin/mainpage-settings", {
|
||
|
|
method: "POST",
|
||
|
|
headers: { "content-type": "application/json" },
|
||
|
|
body: JSON.stringify(draft),
|
||
|
|
});
|
||
|
|
if (!res.ok) throw new Error("save_failed");
|
||
|
|
const data = await res.json();
|
||
|
|
// 저장된 설정으로 즉시 업데이트
|
||
|
|
await mutateSettings({ settings: data.settings }, { revalidate: false });
|
||
|
|
show("저장되었습니다.");
|
||
|
|
} catch (e) {
|
||
|
|
console.error(e);
|
||
|
|
show("저장 중 오류가 발생했습니다.");
|
||
|
|
} finally {
|
||
|
|
setSaving(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-6">
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<h1 className="text-xl md:text-2xl font-bold text-neutral-900">메인페이지 설정</h1>
|
||
|
|
<button
|
||
|
|
onClick={save}
|
||
|
|
disabled={saving}
|
||
|
|
className="h-9 px-4 rounded-md bg-neutral-900 text-white text-sm shadow disabled:opacity-60"
|
||
|
|
>
|
||
|
|
{saving ? "저장 중..." : "저장"}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-4">
|
||
|
|
{/* 배너 표시 */}
|
||
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-4">
|
||
|
|
<label className="flex items-center gap-2 text-sm font-medium">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={draft.showBanner}
|
||
|
|
onChange={(e) => setDraft({ ...draft, showBanner: e.target.checked })}
|
||
|
|
/>
|
||
|
|
메인 배너 표시
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 제휴 샾 목록 표시 */}
|
||
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-4">
|
||
|
|
<label className="flex items-center gap-2 text-sm font-medium">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={draft.showPartnerShops}
|
||
|
|
onChange={(e) => setDraft({ ...draft, showPartnerShops: e.target.checked })}
|
||
|
|
/>
|
||
|
|
제휴 샾 목록 표시
|
||
|
|
</label>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 보일 게시판 선택 */}
|
||
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-4">
|
||
|
|
<div className="flex items-center justify-between mb-3">
|
||
|
|
<label className="block text-sm font-medium">보일 게시판 선택</label>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setShowBoardModal(true)}
|
||
|
|
className="h-8 px-3 rounded-md border border-neutral-300 text-sm hover:bg-neutral-50"
|
||
|
|
>
|
||
|
|
추가
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div className="space-y-2 min-h-[60px]">
|
||
|
|
{draft.visibleBoardIds.length === 0 ? (
|
||
|
|
<p className="text-sm text-neutral-400 py-4 text-center">선택된 게시판이 없습니다.</p>
|
||
|
|
) : (
|
||
|
|
draft.visibleBoardIds.map((boardId: string) => {
|
||
|
|
const board = boards.find((b: any) => b.id === boardId);
|
||
|
|
if (!board) return null;
|
||
|
|
const category = categories.find((c: any) => c.id === board.categoryId);
|
||
|
|
return (
|
||
|
|
<div key={boardId} className="flex items-center justify-between py-2 px-3 bg-neutral-50 rounded-md">
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span className="text-sm font-medium">{board.name}</span>
|
||
|
|
{category && (
|
||
|
|
<span className="text-xs text-neutral-500">({category.name})</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => {
|
||
|
|
setDraft({ ...draft, visibleBoardIds: draft.visibleBoardIds.filter((id: string) => id !== boardId) });
|
||
|
|
}}
|
||
|
|
className="text-xs text-red-600 hover:text-red-800"
|
||
|
|
>
|
||
|
|
삭제
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* 게시판 선택 모달 */}
|
||
|
|
<Modal open={showBoardModal} onClose={() => setShowBoardModal(false)}>
|
||
|
|
<div className="w-[600px] max-w-[90vw] max-h-[80vh] overflow-y-auto">
|
||
|
|
<div className="p-6">
|
||
|
|
<h2 className="text-lg font-bold mb-4">게시판 선택</h2>
|
||
|
|
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
||
|
|
{boards
|
||
|
|
.filter((b: any) => !draft.visibleBoardIds.includes(b.id))
|
||
|
|
.map((b: any) => {
|
||
|
|
const category = categories.find((c: any) => c.id === b.categoryId);
|
||
|
|
return (
|
||
|
|
<button
|
||
|
|
key={b.id}
|
||
|
|
type="button"
|
||
|
|
onClick={() => {
|
||
|
|
setDraft({ ...draft, visibleBoardIds: [...draft.visibleBoardIds, b.id] });
|
||
|
|
setShowBoardModal(false);
|
||
|
|
}}
|
||
|
|
className="w-full text-left px-3 py-2 rounded-md border border-neutral-200 hover:bg-neutral-50 transition-colors"
|
||
|
|
>
|
||
|
|
<div className="flex items-center justify-between">
|
||
|
|
<span className="text-sm font-medium">{b.name}</span>
|
||
|
|
{category && (
|
||
|
|
<span className="text-xs text-neutral-500">({category.name})</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</button>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
{boards.filter((b: any) => !draft.visibleBoardIds.includes(b.id)).length === 0 && (
|
||
|
|
<p className="text-sm text-neutral-400 py-4 text-center">추가할 수 있는 게시판이 없습니다.</p>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div className="mt-4 flex justify-end">
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={() => setShowBoardModal(false)}
|
||
|
|
className="h-9 px-4 rounded-md border border-neutral-300 text-sm hover:bg-neutral-50"
|
||
|
|
>
|
||
|
|
닫기
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|