deploy first

This commit is contained in:
2025-09-10 04:31:53 +00:00
parent 2171d5e744
commit 364e91c47a
13 changed files with 235 additions and 135 deletions

View File

@@ -10,13 +10,14 @@ export default function AdminSettingsPage() {
const [saving, setSaving] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [okMsg, setOkMsg] = useState<string>("");
const [toast, setToast] = useState<null | { msg: string; type: 'ok' | 'err' }>(null);
const [handles, setHandles] = useState<Array<{
id: string;
email: string;
handle: string;
icon: string;
isApproved: boolean;
createtime: string;
costPerView?: number;
}>>([]);
const [loadingHandles, setLoadingHandles] = useState<boolean>(false);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
@@ -42,20 +43,20 @@ export default function AdminSettingsPage() {
};
useEffect(() => {
(async () => {
setLoading(true);
setError("");
try {
const res = await fetch('/api/cost_per_view', { cache: 'no-store' });
const data = await res.json();
if (!res.ok) throw new Error(data?.error ?? '로드 실패');
if (typeof data.value === 'number') setValue(String(data.value));
} catch (e: any) {
setError(e?.message ?? '로드 실패');
} finally {
setLoading(false);
}
})();
// (async () => {
// setLoading(true);
// setError("");
// try {
// const res = await fetch('/api/cost_per_view', { cache: 'no-store' });
// const data = await res.json();
// if (!res.ok) throw new Error(data?.error ?? '로드 실패');
// if (typeof data.value === 'number') setValue(String(data.value));
// } catch (e: any) {
// setError(e?.message ?? '로드 실패');
// } finally {
// setLoading(false);
// }
// })();
}, []);
useEffect(() => {
@@ -97,23 +98,23 @@ export default function AdminSettingsPage() {
};
const onSave = async () => {
setSaving(true);
setError("");
setOkMsg("");
try {
const res = await fetch('/api/cost_per_view', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ value }),
});
const data = await res.json();
if (!res.ok || !data?.ok) throw new Error(data?.error ?? '저장 실패');
setOkMsg('저장되었습니다');
} catch (e: any) {
setError(e?.message ?? '저장 실패');
} finally {
setSaving(false);
}
// setSaving(true);
// setError("");
// setOkMsg("");
// try {
// const res = await fetch('/api/cost_per_view', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({ value }),
// });
// const data = await res.json();
// if (!res.ok || !data?.ok) throw new Error(data?.error ?? '저장 실패');
// setOkMsg('저장되었습니다');
// } catch (e: any) {
// setError(e?.message ?? '저장 실패');
// } finally {
// setSaving(false);
// }
};
const toggleApprove = async (id: string, approve: boolean) => {
@@ -138,8 +139,18 @@ export default function AdminSettingsPage() {
}
};
const showToast = (msg: string, type: 'ok' | 'err' = 'ok') => {
setToast({ msg, type });
setTimeout(() => setToast(null), 2000);
};
return (
<div className="w-full max-w-[900px] mx-auto p-4">
<div className="w-full max-w-[900px] mx-auto p-4 overflow-y-auto h-full">
{toast && (
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded-md shadow-lg text-white ${toast.type === 'ok' ? 'bg-green-600' : 'bg-red-600'}`}>
{toast.msg}
</div>
)}
<div className="w-full flex justify-between mb-3">
<button
className="px-3 py-1 rounded-md border-1 border-border-pale hover:border-[#F94B37] hover:text-[#F94B37] text-sm"
@@ -150,7 +161,7 @@ export default function AdminSettingsPage() {
</div>
<h1 className="text-2xl font-bold mb-4"> </h1>
<div className="border-1 border-[#e6e9ef] rounded-lg bg-white p-4">
<div className="mb-2 font-semibold">Cost Per View</div>
{/* <div className="mb-2 font-semibold">Cost Per View</div>
<div className="flex items-center gap-2">
<input
type="number"
@@ -170,12 +181,12 @@ export default function AdminSettingsPage() {
</div>
{loading && <div className="text-sm text-[#848484] mt-2">불러오는 중...</div>}
{error && <div className="text-sm text-red-600 mt-2">{error}</div>}
{okMsg && <div className="text-sm text-green-600 mt-2">{okMsg}</div>}
{okMsg && <div className="text-sm text-green-600 mt-2">{okMsg}</div>} */}
</div>
<div className="border-1 border-[#e6e9ef] rounded-lg bg-white p-4 mt-6">
<div className="mb-2 font-semibold flex items-center justify-between">
<span>User Handle </span>
<span>Handle </span>
<button
className="px-3 py-1 rounded-md border-1 border-border-pale hover:border-[#F94B37] hover:text-[#F94B37] text-sm"
onClick={loadHandles}
@@ -185,18 +196,16 @@ export default function AdminSettingsPage() {
<table className="w-full min-w-[680px] table-fixed border-separate border-spacing-y-1">
<colgroup>
<col className="w-[48px]" />
<col className="w-[200px]" />
<col className="w-[220px]" />
<col className="w-[120px]" />
<col className="w-[160px]" />
<col className="w-[200px]" />
</colgroup>
<thead>
<tr className="h-[40px]">
<th className="text-left"></th>
<th className="text-left"></th>
<th className="text-left"></th>
<th className="text-left"></th>
<th className="text-left"></th>
<th className="text-left">CPV(/)</th>
</tr>
</thead>
<tbody>
@@ -212,25 +221,43 @@ export default function AdminSettingsPage() {
</div>
</td>
<td className="px-2 font-semibold">{h.handle}</td>
<td className="px-2 text-sm text-[#555]">{h.email}</td>
<td className="px-2 text-sm">{h.createtime?.split('T')[0]}</td>
<td className="px-2">
<div className="flex items-center gap-2">
<span className={`${h.isApproved ? '' : 'text-[#F94B37]'}`}>{h.isApproved ? '승인' : '미승인'}</span>
<input
type="number"
step="0.1"
defaultValue={h.costPerView ?? 0}
className="border-1 border-border-pale rounded-md px-2 py-1 w-[120px]"
onChange={(e) => {
const v = Number(e.target.value);
setHandles(prev => prev.map(x => x.id === h.id ? { ...x, costPerView: v } : x));
}}
/>
<button
className="px-2 py-1 text-xs rounded-md border-1 border-border-pale hover:border-[#F94B37] hover:text-[#F94B37] disabled:opacity-60"
onClick={() => toggleApprove(h.id, !h.isApproved)}
disabled={pendingIds.has(h.id)}
>
{h.isApproved ? '미승인으로' : '승인'}
</button>
onClick={async () => {
const row = handles.find(x => x.id === h.id);
const v = Number(row?.costPerView ?? 0);
const res = await fetch('/api/admin/handle/cpv', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: h.id, costPerView: v }),
});
if (res.ok) {
showToast('적용되었습니다', 'ok');
} else {
showToast('저장 실패', 'err');
}
}}
></button>
</div>
</td>
<td className="px-2 text-sm">{h.createtime?.split('T')[0]}</td>
</tr>
))}
{handles.length === 0 && (
<tr>
<td colSpan={5} className="text-center py-4 text-[#848484]">
<td colSpan={4} className="text-center py-4 text-[#848484]">
{loadingHandles ? '불러오는 중...' : '데이터가 없습니다'}
</td>
</tr>