Phase 10. 마에스트로 정보 수정 및 학생 목록 페이지네이션 적용
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user