@@ -7,8 +7,10 @@ const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
|
||||
export default function AdminPartnersPage() {
|
||||
const { data, mutate } = useSWR<{ partners: any[] }>("/api/admin/partners", fetcher);
|
||||
const { data: catData, mutate: mutateCategories } = useSWR<{ categories: any[] }>("/api/admin/partner-categories", fetcher);
|
||||
const partners = data?.partners ?? [];
|
||||
const [form, setForm] = useState({ name: "", category: "", latitude: "", longitude: "", address: "", imageUrl: "" });
|
||||
const categories = catData?.categories ?? [];
|
||||
const [form, setForm] = useState({ name: "", latitude: "", longitude: "", address: "", imageUrl: "", categoryId: "" });
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const editFileInputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -62,8 +64,8 @@ export default function AdminPartnersPage() {
|
||||
|
||||
async function create() {
|
||||
// 필수값 검증: 이름/카테고리/위도/경도
|
||||
if (!form.name || !form.category || !String(form.latitude).trim() || !String(form.longitude).trim()) {
|
||||
alert("이름, 카테고리, 위도, 경도를 모두 입력해 주세요.");
|
||||
if (!form.name || !form.categoryId || !String(form.latitude).trim() || !String(form.longitude).trim()) {
|
||||
alert("이름, 카테고리, 위도, 경도를 모두 선택/입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
const lat = Number(form.latitude);
|
||||
@@ -72,12 +74,21 @@ export default function AdminPartnersPage() {
|
||||
alert("위도/경도는 숫자여야 합니다.");
|
||||
return;
|
||||
}
|
||||
const payload = { ...form, latitude: lat, longitude: lon } as any;
|
||||
const payload = { ...form, latitude: lat, longitude: lon, categoryId: form.categoryId } as any;
|
||||
const r = await fetch("/api/admin/partners", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
|
||||
if (r.ok) {
|
||||
setForm({ name: "", category: "", latitude: "", longitude: "", address: "", imageUrl: "" });
|
||||
setForm({ name: "", latitude: "", longitude: "", address: "", imageUrl: "", categoryId: "" });
|
||||
mutate();
|
||||
setShowCreateModal(false);
|
||||
} else {
|
||||
let msg = "저장에 실패했습니다.";
|
||||
try {
|
||||
const j = await r.json();
|
||||
msg = j?.message || j?.error || msg;
|
||||
if (r.status === 409 && j?.error === "duplicate_name") msg = "이미 존재하는 업체명입니다.";
|
||||
if (r.status === 400) msg = msg || "입력값을 확인해 주세요.";
|
||||
} catch {}
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +118,12 @@ export default function AdminPartnersPage() {
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>카테고리</label>
|
||||
<input style={inputStyle} value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })} />
|
||||
<select style={inputStyle} value={form.categoryId} onChange={(e) => setForm({ ...form, categoryId: e.target.value })}>
|
||||
<option value="">(없음)</option>
|
||||
{categories.map((c: any) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>위도</label>
|
||||
@@ -178,7 +194,12 @@ export default function AdminPartnersPage() {
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>카테고리</label>
|
||||
<input style={inputStyle} value={editDraft?.category ?? ""} onChange={(e) => setEditDraft({ ...editDraft, category: e.target.value })} />
|
||||
<select style={inputStyle} value={editDraft?.categoryId ?? ""} onChange={(e) => setEditDraft({ ...editDraft, categoryId: e.target.value })}>
|
||||
<option value="">(없음)</option>
|
||||
{categories.map((c: any) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>위도</label>
|
||||
@@ -216,7 +237,23 @@ export default function AdminPartnersPage() {
|
||||
)}
|
||||
<div style={{ gridColumn: "span 6", display: "flex", justifyContent: "flex-end", gap: 8 }}>
|
||||
<button
|
||||
onClick={async () => { if (!editingId) return; await fetch(`/api/admin/partners/${editingId}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...editDraft }) }); setEditingId(null); setEditDraft(null); setShowEditModal(false); mutate(); }}
|
||||
onClick={async () => {
|
||||
if (!editingId) return;
|
||||
const resp = await fetch(`/api/admin/partners/${editingId}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ ...editDraft, categoryId: (editDraft?.categoryId || null) }) });
|
||||
if (!resp.ok) {
|
||||
let msg = "저장에 실패했습니다.";
|
||||
try {
|
||||
const j = await resp.json();
|
||||
msg = j?.message || j?.error || msg;
|
||||
} catch {}
|
||||
alert(msg);
|
||||
return;
|
||||
}
|
||||
setEditingId(null);
|
||||
setEditDraft(null);
|
||||
setShowEditModal(false);
|
||||
mutate();
|
||||
}}
|
||||
className="h-9 px-4 rounded-md bg-neutral-900 text-white text-sm shadow cursor-pointer hover:bg-neutral-800 active:translate-y-px transition"
|
||||
>
|
||||
저장
|
||||
@@ -250,14 +287,14 @@ export default function AdminPartnersPage() {
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<div><strong>{p.name}</strong> <span style={{ marginLeft: 6, fontSize: 12, opacity: .7 }}>{p.category}</span></div>
|
||||
<div><strong>{p.name}</strong> {p.categoryRef ? <span style={{ marginLeft: 6, fontSize: 12, opacity: .7 }}>[{p.categoryRef.name}]</span> : null}</div>
|
||||
<div style={{ fontSize: 12, opacity: .7 }}>{p.address || "(주소 없음)"}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, opacity: .7 }}>위도 {p.latitude}</div>
|
||||
<div style={{ fontSize: 12, opacity: .7 }}>경도 {p.longitude}</div>
|
||||
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
||||
<button
|
||||
onClick={() => { setEditingId(p.id); setEditDraft({ name: p.name, category: p.category, latitude: String(p.latitude), longitude: String(p.longitude), address: p.address ?? "", imageUrl: p.imageUrl ?? "", sortOrder: p.sortOrder ?? 0 }); setShowEditModal(true); }}
|
||||
onClick={() => { setEditingId(p.id); setEditDraft({ name: p.name, categoryId: p.categoryId ?? "", latitude: String(p.latitude), longitude: String(p.longitude), address: p.address ?? "", imageUrl: p.imageUrl ?? "", sortOrder: p.sortOrder ?? 0 }); setShowEditModal(true); }}
|
||||
className="h-8 px-3 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition"
|
||||
>
|
||||
수정
|
||||
@@ -288,8 +325,49 @@ export default function AdminPartnersPage() {
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div>
|
||||
<hr style={{ margin: "24px 0" }} />
|
||||
<h2 className="text-lg font-bold mb-2">카테고리 관리</h2>
|
||||
<CategoryManager categories={categories} onChanged={mutateCategories} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function CategoryManager({ categories, onChanged }: { categories: any[]; onChanged: () => void }) {
|
||||
const [name, setName] = useState("");
|
||||
return (
|
||||
<div className="border border-neutral-200 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<input
|
||||
placeholder="카테고리 이름"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="h-9 px-3 rounded-md border border-neutral-300 flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={async () => { if (!name.trim()) return; const r = await fetch("/api/admin/partner-categories", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ name: name.trim() }) }); if (r.ok) { setName(""); onChanged(); } else { try { const j = await r.json(); alert(j?.message || j?.error || "생성 실패"); } catch { alert("생성 실패"); } } }}
|
||||
className="h-9 px-4 rounded-md bg-neutral-900 text-white text-sm cursor-pointer hover:bg-neutral-800 active:translate-y-px transition"
|
||||
>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
<ul className="flex flex-wrap gap-2">
|
||||
{categories.map((c: any) => (
|
||||
<li key={c.id} className="flex items-center gap-2 px-2 py-1 rounded-full border border-neutral-300 text-sm">
|
||||
<span>{c.name}</span>
|
||||
<button
|
||||
title="삭제"
|
||||
onClick={async () => { const r = await fetch(`/api/admin/partner-categories/${c.id}`, { method: "DELETE" }); if (r.ok) onChanged(); else { try { const j = await r.json(); alert(j?.message || j?.error || "삭제 실패"); } catch { alert("삭제 실패"); } } }}
|
||||
className="px-2 h-6 rounded-md border border-red-200 text-red-600 hover:bg-red-100 hover:border-red-300 hover:text-red-700"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user