deploy first
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user