??
Some checks failed
deploy-on-main / deploy (push) Failing after 22s

This commit is contained in:
koreacomp5
2025-11-07 21:36:34 +09:00
parent 5f72e6ce7c
commit ab81a3da3d
16 changed files with 345 additions and 19 deletions

View File

@@ -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>
);
}