255 lines
7.3 KiB
TypeScript
255 lines
7.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useTransition } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { 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 [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;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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>
|
|
{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>
|
|
);
|
|
}
|