1
0
forked from baron/baron-sso

users 정보 페이지 구현

This commit is contained in:
2026-01-29 14:47:20 +09:00
parent df03771121
commit ee4c07f66d
12 changed files with 1139 additions and 14 deletions

View File

@@ -0,0 +1,206 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { ArrowLeft, Loader2, Save } from "lucide-react";
import * as React from "react";
import { useForm } from "react-hook-form";
import { Link, useNavigate } from "react-router-dom";
import { Button } from "../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../components/ui/card";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { createUser, type UserCreateRequest } from "../../lib/adminApi";
function UserCreatePage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [error, setError] = React.useState<string | null>(null);
const {
register,
handleSubmit,
formState: { errors },
} = useForm<UserCreateRequest>({
defaultValues: {
email: "",
password: "",
name: "",
phone: "",
role: "user",
companyCode: "",
department: "",
},
});
const mutation = useMutation({
mutationFn: createUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
navigate("/users");
},
onError: (err: AxiosError<{ error?: string }>) => {
setError(err.response?.data?.error || "사용자 생성에 실패했습니다.");
},
});
const onSubmit = (data: UserCreateRequest) => {
setError(null);
mutation.mutate(data);
};
return (
<div className="max-w-3xl space-y-8">
<header className="flex flex-wrap items-center justify-between gap-4">
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-[var(--color-muted)]">
<Link to="/users" className="hover:underline">
Users
</Link>
<span>/</span>
<span className="text-foreground">New</span>
</div>
<h2 className="text-3xl font-semibold"> </h2>
</div>
<Button variant="ghost" asChild>
<Link to="/users">
<ArrowLeft size={16} className="mr-2" />
</Link>
</Button>
</header>
<Card>
<CardHeader>
<CardTitle> </CardTitle>
<CardDescription>
.
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
{error && (
<div className="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
{error}
</div>
)}
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input
id="email"
placeholder="user@example.com"
{...register("email", { required: "이메일은 필수입니다." })}
/>
{errors.email && (
<p className="text-xs text-destructive">{errors.email.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password"></Label>
<Input
id="password"
type="password"
placeholder="********"
{...register("password", {
required: "비밀번호는 필수입니다.",
minLength: { value: 6, message: "6자 이상 입력해주세요." },
})}
/>
<p className="text-xs text-muted-foreground">
.
</p>
{errors.password && (
<p className="text-xs text-destructive">{errors.password.message}</p>
)}
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input
id="name"
placeholder="홍길동"
{...register("name", { required: "이름은 필수입니다." })}
/>
{errors.name && (
<p className="text-xs text-destructive">{errors.name.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input
id="phone"
placeholder="010-1234-5678"
{...register("phone")}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="companyCode"> </Label>
<Input
id="companyCode"
placeholder="HMAC"
{...register("companyCode")}
/>
</div>
<div className="space-y-2">
<Label htmlFor="department"></Label>
<Input
id="department"
placeholder="개발팀"
{...register("department")}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="role"> (Role)</Label>
<div className="relative">
<select
id="role"
className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
{...register("role")}
>
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<p className="text-xs text-muted-foreground">
.
</p>
</div>
<div className="flex justify-end gap-4">
<Button
type="button"
variant="outline"
onClick={() => navigate("/users")}
>
</Button>
<Button type="submit" disabled={mutation.isPending}>
{mutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
<Save className="mr-2 h-4 w-4" />
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
}
export default UserCreatePage;