ㄱㄱ
This commit is contained in:
@@ -7,6 +7,7 @@ const navItems = [
|
||||
{ href: "/admin/mainpage-settings", label: "메인페이지 설정" },
|
||||
{ href: "/admin/boards", label: "게시판" },
|
||||
{ href: "/admin/banners", label: "배너" },
|
||||
{ href: "/admin/partners", label: "제휴업체" },
|
||||
{ href: "/admin/users", label: "사용자" },
|
||||
{ href: "/admin/logs", label: "로그" },
|
||||
];
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Modal } from "@/app/components/ui/Modal";
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
|
||||
@@ -8,21 +9,112 @@ export default function AdminBannersPage() {
|
||||
const { data, mutate } = useSWR<{ banners: any[] }>("/api/admin/banners", fetcher);
|
||||
const banners = data?.banners ?? [];
|
||||
const [form, setForm] = useState({ title: "", imageUrl: "", linkUrl: "", active: true, sortOrder: 0 });
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
async function onSelectFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const inputEl = e.currentTarget; // 이벤트 풀링 대비 사본 보관
|
||||
const file = inputEl.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
setUploading(true);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const r = await fetch("/api/uploads", { method: "POST", body: fd });
|
||||
const json = await r.json();
|
||||
if (!r.ok) throw new Error(json?.error || "upload_failed");
|
||||
setForm((f) => ({ ...f, imageUrl: json.url }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("이미지 업로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (inputEl) inputEl.value = "";
|
||||
}
|
||||
}
|
||||
async function create() {
|
||||
const r = await fetch("/api/admin/banners", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(form) });
|
||||
if (r.ok) { setForm({ title: "", imageUrl: "", linkUrl: "", active: true, sortOrder: 0 }); mutate(); }
|
||||
if (r.ok) { setForm({ title: "", imageUrl: "", linkUrl: "", active: true, sortOrder: 0 }); mutate(); setShowCreateModal(false); }
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>배너 관리</h1>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 12 }}>
|
||||
<input placeholder="제목" value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
||||
<input placeholder="이미지 URL" value={form.imageUrl} onChange={(e) => setForm({ ...form, imageUrl: e.target.value })} />
|
||||
<input placeholder="링크 URL(선택)" value={form.linkUrl} onChange={(e) => setForm({ ...form, linkUrl: e.target.value })} />
|
||||
<label><input type="checkbox" checked={form.active} onChange={(e) => setForm({ ...form, active: e.target.checked })} /> 활성</label>
|
||||
<input type="number" placeholder="정렬" value={form.sortOrder} onChange={(e) => setForm({ ...form, sortOrder: Number(e.target.value) })} style={{ width: 80 }} />
|
||||
<button onClick={create}>추가</button>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
|
||||
<h1>배너 관리</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
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"
|
||||
>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
<Modal open={showCreateModal} onClose={() => setShowCreateModal(false)}>
|
||||
<div className="w-[720px] max-w-[90vw]">
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-bold mb-4">배너 추가</h2>
|
||||
{(() => {
|
||||
const inputStyle: React.CSSProperties = { border: "1px solid #ddd", borderRadius: 6, padding: "8px 10px", width: "100%" };
|
||||
return (
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(6, minmax(0, 1fr))", gap: 12, alignItems: "end" }}>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>제목</label>
|
||||
<input style={inputStyle} value={form.title} onChange={(e) => setForm({ ...form, title: e.target.value })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>링크 URL(선택)</label>
|
||||
<input style={inputStyle} value={form.linkUrl} onChange={(e) => setForm({ ...form, linkUrl: e.target.value })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>이미지 URL</label>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<input style={inputStyle} value={form.imageUrl} onChange={(e) => setForm({ ...form, imageUrl: e.target.value })} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
className="h-9 px-4 w-36 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 hover:border-neutral-400 active:translate-y-px transition"
|
||||
>
|
||||
업로드
|
||||
</button>
|
||||
<input ref={fileInputRef} type="file" accept="image/*" onChange={onSelectFile} style={{ display: "none" }} />
|
||||
{uploading && <span style={{ fontSize: 12, opacity: .7 }}>업로드 중...</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>활성</label>
|
||||
<label className="inline-flex items-center gap-2 text-sm"><input type="checkbox" checked={form.active} onChange={(e) => setForm({ ...form, active: e.target.checked })} /> 활성화</label>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>정렬</label>
|
||||
<input type="number" style={inputStyle} value={form.sortOrder} onChange={(e) => setForm({ ...form, sortOrder: Number(e.target.value) })} />
|
||||
</div>
|
||||
{form.imageUrl && (
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<div style={{ fontSize: 12, opacity: .7, marginBottom: 6 }}>미리보기</div>
|
||||
<img src={form.imageUrl} alt="미리보기" style={{ width: 320, height: 160, objectFit: "cover", borderRadius: 6, border: "1px solid #eee" }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ gridColumn: "span 6", display: "flex", justifyContent: "flex-end", gap: 8 }}>
|
||||
<button
|
||||
onClick={create}
|
||||
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"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="h-9 px-4 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ul style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{banners.map((b) => (
|
||||
<li key={b.id} style={{ border: "1px solid #eee", borderRadius: 8, padding: 12, display: "flex", gap: 12, alignItems: "center" }}>
|
||||
@@ -31,8 +123,8 @@ export default function AdminBannersPage() {
|
||||
<div><strong>{b.title}</strong> {b.linkUrl && <a style={{ marginLeft: 8 }} href={b.linkUrl}>링크</a>}</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.7 }}>정렬 {b.sortOrder} · {b.active ? "활성" : "비활성"}</div>
|
||||
</div>
|
||||
<button onClick={async () => { await fetch(`/api/admin/banners/${b.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ active: !b.active }) }); mutate(); }}>{b.active ? "비활성" : "활성"}</button>
|
||||
<button onClick={async () => { await fetch(`/api/admin/banners/${b.id}`, { method: "DELETE" }); mutate(); }}>삭제</button>
|
||||
<button onClick={async () => { await fetch(`/api/admin/banners/${b.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ active: !b.active }) }); mutate(); }} className="h-8 px-3 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition">{b.active ? "비활성" : "활성"}</button>
|
||||
<button onClick={async () => { await fetch(`/api/admin/banners/${b.id}`, { method: "DELETE" }); mutate(); }} className="h-8 px-3 rounded-md border border-red-200 text-red-600 text-sm cursor-pointer hover:bg-red-100 hover:border-red-300 hover:text-red-700 active:translate-y-px transition">삭제</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
45
src/app/admin/partner-shops/page.tsx
Normal file
45
src/app/admin/partner-shops/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
import useSWR from "swr";
|
||||
import { useState } from "react";
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((r) => r.json());
|
||||
|
||||
export default function AdminPartnerShopsPage() {
|
||||
const { data, mutate } = useSWR<{ items: any[] }>("/api/admin/partner-shops", fetcher);
|
||||
const items = data?.items ?? [];
|
||||
const [form, setForm] = useState({ region: "", name: "", address: "", imageUrl: "", active: true, sortOrder: 0 });
|
||||
async function create() {
|
||||
const r = await fetch("/api/admin/partner-shops", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(form) });
|
||||
if (r.ok) { setForm({ region: "", name: "", address: "", imageUrl: "", active: true, sortOrder: 0 }); mutate(); }
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>제휴 샵 관리</h1>
|
||||
<div style={{ display: "flex", gap: 8, marginBottom: 12, flexWrap: "wrap" }}>
|
||||
<input placeholder="지역" value={form.region} onChange={(e) => setForm({ ...form, region: e.target.value })} />
|
||||
<input placeholder="이름" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
<input placeholder="주소" value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} style={{ width: 320 }} />
|
||||
<input placeholder="이미지 URL" value={form.imageUrl} onChange={(e) => setForm({ ...form, imageUrl: e.target.value })} style={{ width: 280 }} />
|
||||
<label><input type="checkbox" checked={form.active} onChange={(e) => setForm({ ...form, active: e.target.checked })} /> 활성</label>
|
||||
<input type="number" placeholder="정렬" value={form.sortOrder} onChange={(e) => setForm({ ...form, sortOrder: Number(e.target.value) })} style={{ width: 80 }} />
|
||||
<button onClick={create}>추가</button>
|
||||
</div>
|
||||
<ul style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{items.map((it) => (
|
||||
<li key={it.id} style={{ border: "1px solid #eee", borderRadius: 8, padding: 12, display: "flex", gap: 12, alignItems: "center" }}>
|
||||
<img src={it.imageUrl} alt={it.name} style={{ width: 80, height: 48, objectFit: "cover", borderRadius: 6 }} />
|
||||
<div style={{ flex: 1 }}>
|
||||
<div><strong>{it.name}</strong> <span style={{ marginLeft: 8, fontSize: 12, opacity: .7 }}>{it.region}</span></div>
|
||||
<div style={{ fontSize: 12, opacity: 0.7 }}>{it.address}</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.7 }}>정렬 {it.sortOrder} · {it.active ? "활성" : "비활성"}</div>
|
||||
</div>
|
||||
<button onClick={async () => { await fetch(`/api/admin/partner-shops/${it.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ active: !it.active }) }); mutate(); }}>{it.active ? "비활성" : "활성"}</button>
|
||||
<button onClick={async () => { await fetch(`/api/admin/partner-shops/${it.id}`, { method: "DELETE" }); mutate(); }}>삭제</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
295
src/app/admin/partners/page.tsx
Normal file
295
src/app/admin/partners/page.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
"use client";
|
||||
import useSWR from "swr";
|
||||
import { useState, useRef } from "react";
|
||||
import { Modal } from "@/app/components/ui/Modal";
|
||||
|
||||
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 partners = data?.partners ?? [];
|
||||
const [form, setForm] = useState({ name: "", category: "", latitude: "", longitude: "", address: "", imageUrl: "" });
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const editFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [editUploading, setEditUploading] = useState(false);
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editDraft, setEditDraft] = useState<any>(null);
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
|
||||
async function onSelectFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const inputEl = e.currentTarget;
|
||||
const file = inputEl.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
setUploading(true);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const r = await fetch("/api/uploads", { method: "POST", body: fd });
|
||||
const json = await r.json();
|
||||
if (!r.ok) throw new Error(json?.error || "upload_failed");
|
||||
setForm((f) => ({ ...f, imageUrl: json.url }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("이미지 업로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setUploading(false);
|
||||
if (inputEl) inputEl.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function onSelectEditFile(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const inputEl = e.currentTarget;
|
||||
const file = inputEl.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
setEditUploading(true);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const r = await fetch("/api/uploads", { method: "POST", body: fd });
|
||||
const json = await r.json();
|
||||
if (!r.ok) throw new Error(json?.error || "upload_failed");
|
||||
setEditDraft((d: any) => ({ ...(d || {}), imageUrl: json.url }));
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert("이미지 업로드 중 오류가 발생했습니다.");
|
||||
} finally {
|
||||
setEditUploading(false);
|
||||
if (inputEl) inputEl.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function create() {
|
||||
// 필수값 검증: 이름/카테고리/위도/경도
|
||||
if (!form.name || !form.category || !String(form.latitude).trim() || !String(form.longitude).trim()) {
|
||||
alert("이름, 카테고리, 위도, 경도를 모두 입력해 주세요.");
|
||||
return;
|
||||
}
|
||||
const lat = Number(form.latitude);
|
||||
const lon = Number(form.longitude);
|
||||
if (!isFinite(lat) || !isFinite(lon)) {
|
||||
alert("위도/경도는 숫자여야 합니다.");
|
||||
return;
|
||||
}
|
||||
const payload = { ...form, latitude: lat, longitude: lon } 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: "" });
|
||||
mutate();
|
||||
setShowCreateModal(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 12 }}>
|
||||
<h1>제휴업체 관리</h1>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
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"
|
||||
>
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
<Modal open={showCreateModal} onClose={() => setShowCreateModal(false)}>
|
||||
<div className="w-[720px] max-w-[90vw]">
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-bold mb-4">제휴업체 추가</h2>
|
||||
{(() => {
|
||||
const inputStyle: React.CSSProperties = { border: "1px solid #ddd", borderRadius: 6, padding: "8px 10px", width: "100%" };
|
||||
return (
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(6, minmax(0, 1fr))", gap: 12, alignItems: "end" }}>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>이름</label>
|
||||
<input style={inputStyle} value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
|
||||
</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 })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>위도</label>
|
||||
<input style={inputStyle} value={form.latitude} onChange={(e) => setForm({ ...form, latitude: e.target.value })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>경도</label>
|
||||
<input style={inputStyle} value={form.longitude} onChange={(e) => setForm({ ...form, longitude: e.target.value })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>이미지 URL</label>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<input style={inputStyle} value={form.imageUrl} onChange={(e) => setForm({ ...form, imageUrl: e.target.value })} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={uploading}
|
||||
className="h-9 px-4 w-36 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 hover:border-neutral-400 active:translate-y-px transition"
|
||||
>
|
||||
업로드
|
||||
</button>
|
||||
<input ref={fileInputRef} type="file" accept="image/*" onChange={onSelectFile} style={{ display: "none" }} />
|
||||
{uploading && <span style={{ fontSize: 12, opacity: .7 }}>업로드 중...</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>주소(선택)</label>
|
||||
<input style={inputStyle} value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
|
||||
</div>
|
||||
{form.imageUrl && (
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<div style={{ fontSize: 12, opacity: .7, marginBottom: 6 }}>미리보기</div>
|
||||
<img src={form.imageUrl} alt="미리보기" style={{ width: 240, height: 120, objectFit: "cover", borderRadius: 6, border: "1px solid #eee" }} />
|
||||
</div>
|
||||
)}
|
||||
<div style={{ gridColumn: "span 6", display: "flex", justifyContent: "flex-end", gap: 8 }}>
|
||||
<button
|
||||
onClick={create}
|
||||
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"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="h-9 px-4 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<Modal open={showEditModal} onClose={() => { setShowEditModal(false); setEditingId(null); setEditDraft(null); }}>
|
||||
<div className="w-[720px] max-w-[90vw]">
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-bold mb-4">제휴업체 수정</h2>
|
||||
{(() => {
|
||||
const inputStyle: React.CSSProperties = { border: "1px solid #ddd", borderRadius: 6, padding: "8px 10px", width: "100%" };
|
||||
return (
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(6, minmax(0, 1fr))", gap: 12, alignItems: "end" }}>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>이름</label>
|
||||
<input style={inputStyle} value={editDraft?.name ?? ""} onChange={(e) => setEditDraft({ ...editDraft, name: e.target.value })} />
|
||||
</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 })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>위도</label>
|
||||
<input style={inputStyle} value={editDraft?.latitude ?? ""} onChange={(e) => setEditDraft({ ...editDraft, latitude: e.target.value })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 3" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>경도</label>
|
||||
<input style={inputStyle} value={editDraft?.longitude ?? ""} onChange={(e) => setEditDraft({ ...editDraft, longitude: e.target.value })} />
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>이미지 URL</label>
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
||||
<input style={inputStyle} value={editDraft?.imageUrl ?? ""} onChange={(e) => setEditDraft({ ...editDraft, imageUrl: e.target.value })} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editFileInputRef.current?.click()}
|
||||
disabled={editUploading}
|
||||
className="h-9 px-4 w-36 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 hover:border-neutral-400 active:translate-y-px transition"
|
||||
>
|
||||
업로드
|
||||
</button>
|
||||
<input ref={editFileInputRef} type="file" accept="image/*" onChange={onSelectEditFile} style={{ display: "none" }} />
|
||||
{editUploading && <span style={{ fontSize: 12, opacity: .7 }}>업로드 중...</span>}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<label style={{ display: "block", fontSize: 12, fontWeight: 600, marginBottom: 6 }}>주소</label>
|
||||
<input style={inputStyle} value={editDraft?.address ?? ""} onChange={(e) => setEditDraft({ ...editDraft, address: e.target.value })} />
|
||||
</div>
|
||||
{editDraft?.imageUrl && (
|
||||
<div style={{ gridColumn: "span 6" }}>
|
||||
<div style={{ fontSize: 12, opacity: .7, marginBottom: 6 }}>미리보기</div>
|
||||
<img src={editDraft.imageUrl} alt="미리보기" style={{ width: 240, height: 120, objectFit: "cover", borderRadius: 6, border: "1px solid #eee" }} />
|
||||
</div>
|
||||
)}
|
||||
<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(); }}
|
||||
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"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setShowEditModal(false); setEditingId(null); setEditDraft(null); }}
|
||||
className="h-9 px-4 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<ul style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
||||
{partners.map((p) => (
|
||||
<li key={p.id} style={{ border: "1px solid #eee", borderRadius: 8, padding: 12, display: "grid", gridTemplateColumns: "auto 1fr 1fr 1fr auto", gap: 8, alignItems: "center" }}>
|
||||
<div style={{ width: 80 }}>
|
||||
{p.imageUrl ? (
|
||||
<img src={p.imageUrl} alt={p.name} style={{ width: 80, height: 48, objectFit: "cover", borderRadius: 6 }} />
|
||||
) : (
|
||||
<div style={{ width: 80, height: 48, border: "1px solid #eee", borderRadius: 6, background: "#fafafa" }} />
|
||||
)}
|
||||
</div>
|
||||
{false ? (
|
||||
<></>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<div><strong>{p.name}</strong> <span style={{ marginLeft: 6, fontSize: 12, opacity: .7 }}>{p.category}</span></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); }}
|
||||
className="h-8 px-3 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition"
|
||||
>
|
||||
수정
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => { await fetch(`/api/admin/partners/${p.id}`, { method: "DELETE" }); mutate(); }}
|
||||
className="h-8 px-3 rounded-md border border-red-200 text-red-600 text-sm cursor-pointer hover:bg-red-100 hover:border-red-300 hover:text-red-700 active:translate-y-px transition"
|
||||
>
|
||||
삭제
|
||||
</button>
|
||||
<button
|
||||
title="위로"
|
||||
onClick={async () => { await fetch(`/api/admin/partners/${p.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ sortOrder: (p.sortOrder ?? 0) - 1 }) }); mutate(); }}
|
||||
className="h-8 px-3 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition"
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
title="아래로"
|
||||
onClick={async () => { await fetch(`/api/admin/partners/${p.id}`, { method: "PATCH", headers: { "content-type": "application/json" }, body: JSON.stringify({ sortOrder: (p.sortOrder ?? 0) + 1 }) }); mutate(); }}
|
||||
className="h-8 px-3 rounded-md border border-neutral-300 text-sm cursor-pointer hover:bg-neutral-100 active:translate-y-px transition"
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user