Files
chocoadmin/app/(admin)/maestros/[maestroId]/MaestroEditForm.tsx
T

302 lines
8.6 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { useRouter } from "next/navigation";
import { KeyRound, Save } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
ACCOUNT_TYPE_LABELS,
ACCOUNT_TYPES,
ACTIVATE_STATUS_LABELS,
ACTIVATE_STATUSES,
} from "@/lib/constants";
import { formatDate, formatDateTime, toDatetimeLocal } from "@/lib/utils";
import type { MaestroDetail } from "@/lib/maestros";
type MaestroEditFormProps = {
maestro: MaestroDetail["maestro"];
playerCount: number;
};
const paidAccountTypeOptions = [
ACCOUNT_TYPES.BASIC_20,
ACCOUNT_TYPES.STANDARD_50,
ACCOUNT_TYPES.PRO_100,
ACCOUNT_TYPES.SCHOOL_500,
ACCOUNT_TYPES.SCHOOL_1000,
] as const;
const activateStatusOptions = [
ACTIVATE_STATUSES.PENDING,
ACTIVATE_STATUSES.TRIAL,
ACTIVATE_STATUSES.ACTIVE,
ACTIVATE_STATUSES.CANCELED,
] as const;
const inputClass =
"h-9 w-full rounded-md border bg-background px-3 text-sm outline-none transition-colors focus:border-ring focus:ring-3 focus:ring-ring/30 disabled:opacity-50";
export function MaestroEditForm({ maestro, playerCount }: MaestroEditFormProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [isSaving, setIsSaving] = useState(false);
const [isResettingPassword, setIsResettingPassword] = useState(false);
const [name, setName] = useState(maestro.name);
const [email, setEmail] = useState(maestro.email);
const [activateStatus, setActivateStatus] = useState(maestro.activateStatus);
const [accountType, setAccountType] = useState(maestro.accountType);
const [availableActivateDateTime, setAvailableActivateDateTime] = useState(
toDatetimeLocal(maestro.availableActivateDateTime)
);
const [allowEditEnterCode, setAllowEditEnterCode] = useState(
maestro.allowEditEnterCode
);
const [errorMessage, setErrorMessage] = useState("");
const [successMessage, setSuccessMessage] = useState("");
const isDisabled = isSaving || isPending || isResettingPassword;
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!window.confirm("마에스트로 정보를 저장하시겠습니까?")) return;
setIsSaving(true);
setErrorMessage("");
setSuccessMessage("");
try {
const response = await fetch(`/api/maestros/${maestro.maestroID}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name,
email,
activateStatus,
accountType,
availableActivateDateTime,
allowEditEnterCode,
}),
});
const body = (await response.json().catch(() => null)) as {
changed?: boolean;
message?: string;
} | null;
if (!response.ok) {
throw new Error(body?.message ?? "저장에 실패했습니다.");
}
if (body?.changed) {
setSuccessMessage("저장 완료");
startTransition(() => {
router.refresh();
});
} else {
setSuccessMessage("변경 사항이 없습니다.");
}
} catch (error) {
setErrorMessage(
error instanceof Error ? error.message : "저장에 실패했습니다."
);
} finally {
setIsSaving(false);
}
}
async function handleResetPassword() {
if (
!window.confirm(
`${maestro.name} 계정의 비밀번호를 123456으로 초기화 하시겠습니까?`
)
)
return;
setIsResettingPassword(true);
setErrorMessage("");
setSuccessMessage("");
try {
const response = await fetch(
`/api/maestros/${maestro.maestroID}/reset-password`,
{ method: "POST" }
);
const body = (await response.json().catch(() => null)) as {
message?: string;
} | null;
if (!response.ok) {
throw new Error(body?.message ?? "암호 초기화에 실패했습니다.");
}
setSuccessMessage("암호 초기화 완료");
} catch (error) {
setErrorMessage(
error instanceof Error ? error.message : "암호 초기화에 실패했습니다."
);
} finally {
setIsResettingPassword(false);
}
}
return (
<form onSubmit={(e) => void handleSubmit(e)} className="mt-4 space-y-5">
<div className="grid gap-x-8 gap-y-4 text-sm md:grid-cols-2 xl:grid-cols-3">
<FormField label="이름">
<input
className={inputClass}
disabled={isDisabled}
maxLength={50}
onChange={(e) => setName(e.target.value)}
required
type="text"
value={name}
/>
</FormField>
<FormField label="이메일">
<input
className={inputClass}
disabled={isDisabled}
maxLength={50}
onChange={(e) => setEmail(e.target.value)}
required
type="email"
value={email}
/>
</FormField>
<FormField label="상태">
<select
className={inputClass}
disabled={isDisabled}
onChange={(e) => setActivateStatus(Number(e.target.value))}
value={activateStatus}
>
{activateStatusOptions.map((s) => (
<option key={s} value={s}>
{ACTIVATE_STATUS_LABELS[s]}
</option>
))}
</select>
</FormField>
<FormField label="계정 유형">
<select
className={inputClass}
disabled={isDisabled}
onChange={(e) => setAccountType(Number(e.target.value))}
value={accountType}
>
{paidAccountTypeOptions.map((t) => (
<option key={t} value={t}>
{ACCOUNT_TYPE_LABELS[t]}
</option>
))}
</select>
</FormField>
<FormField label="사용 가능일">
<input
className={inputClass}
disabled={isDisabled}
onChange={(e) => setAvailableActivateDateTime(e.target.value)}
required
type="datetime-local"
value={availableActivateDateTime}
/>
</FormField>
<FormField label="입장코드 수정">
<select
className={inputClass}
disabled={isDisabled}
onChange={(e) => setAllowEditEnterCode(Number(e.target.value))}
value={allowEditEnterCode}
>
<option value={1}></option>
<option value={0}></option>
</select>
</FormField>
</div>
<div className="grid gap-x-8 gap-y-3 border-t pt-4 text-sm md:grid-cols-2 xl:grid-cols-3">
<ReadOnlyItem
label="약관 동의일"
value={formatDateTime(maestro.acceptClausesDateTime)}
/>
<ReadOnlyItem
label="DB 학생 수"
value={playerCount.toLocaleString()}
/>
<ReadOnlyItem
label="PlayerCount"
value={maestro.playerCount.toLocaleString()}
/>
<ReadOnlyItem
label="체험 MaestroID"
value={maestro.maestroTestID?.toString() ?? "-"}
/>
<ReadOnlyItem
label="사용 가능일 (현재)"
value={formatDate(maestro.availableActivateDateTime)}
/>
</div>
<div className="flex items-center gap-3">
<Button disabled={isDisabled} size="sm" type="submit">
<Save aria-hidden="true" className="size-4" />
</Button>
<Button
disabled={isDisabled}
onClick={() => void handleResetPassword()}
size="sm"
type="button"
variant="outline"
>
<KeyRound aria-hidden="true" className="size-4" />
</Button>
{successMessage ? (
<p className="text-sm text-green-700 dark:text-green-400">
{successMessage}
</p>
) : null}
{errorMessage ? (
<p className="text-sm text-destructive">{errorMessage}</p>
) : null}
</div>
</form>
);
}
function FormField({
label,
children,
}: {
label: string;
children: React.ReactNode;
}) {
return (
<label className="space-y-1.5">
<span className="text-xs font-medium text-muted-foreground">{label}</span>
{children}
</label>
);
}
function ReadOnlyItem({ label, value }: { label: string; value: string }) {
return (
<div>
<dt className="text-xs font-medium text-muted-foreground">{label}</dt>
<dd className="mt-1 font-medium">{value}</dd>
</div>
);
}