[as] feat: 결재선 관리 페이지 구현
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
export { AprvrPickerDialog } from "./ui/aprvr-picker-dialog";
|
||||
export type { ReviewerOption } from "./model/mock-reviewers";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { fakerKO as faker } from "@faker-js/faker";
|
||||
|
||||
export interface ReviewerOption {
|
||||
uuid: string;
|
||||
name: string;
|
||||
deptNm: string;
|
||||
}
|
||||
|
||||
/** TBD: 검토인력 목록 API 연동 전 목 데이터 */
|
||||
export function createMockReviewers(count = 12): ReviewerOption[] {
|
||||
faker.seed(42);
|
||||
return Array.from({ length: count }, () => ({
|
||||
uuid: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
deptNm: faker.commerce.department(),
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import {
|
||||
Button,
|
||||
DataTable,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
} from "@common/components";
|
||||
import {
|
||||
createColumnHelper,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import type { ReviewerOption } from "../model/mock-reviewers";
|
||||
import { createMockReviewers } from "../model/mock-reviewers";
|
||||
|
||||
interface AprvrPickerDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSelect: (reviewer: ReviewerOption) => void;
|
||||
}
|
||||
|
||||
const columnHelper = createColumnHelper<ReviewerOption>();
|
||||
|
||||
function AprvrPickerDialog({ open, onOpenChange, onSelect }: AprvrPickerDialogProps) {
|
||||
const [keyword, setKeyword] = useState("");
|
||||
const reviewers = useMemo(() => createMockReviewers(), []);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = keyword.trim().toLowerCase();
|
||||
if (!q) return reviewers;
|
||||
return reviewers.filter(
|
||||
(r) =>
|
||||
r.name.toLowerCase().includes(q) || r.deptNm.toLowerCase().includes(q),
|
||||
);
|
||||
}, [keyword, reviewers]);
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", { header: "성명" }),
|
||||
columnHelper.accessor("deptNm", { header: "부서" }),
|
||||
columnHelper.display({
|
||||
id: "actions",
|
||||
header: "선택",
|
||||
cell: ({ row }) => (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
onSelect(row.original);
|
||||
onOpenChange(false);
|
||||
}}
|
||||
>
|
||||
선택
|
||||
</Button>
|
||||
),
|
||||
}),
|
||||
],
|
||||
[onOpenChange, onSelect],
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filtered,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>검토인력 선택</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
placeholder="성명·부서 검색"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
/>
|
||||
<DataTable table={table} variant="line" />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
닫기
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { AprvrPickerDialog };
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ElaprTmpltFormDialog } from "./ui/elapr-tmplt-form-dialog";
|
||||
export type { ElaprTmpltFormValues } from "./ui/elapr-tmplt-form-dialog";
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
Input,
|
||||
Label,
|
||||
Textarea,
|
||||
} from "@common/components";
|
||||
|
||||
import { CodeSelect, UP_CD } from "@/entities/code";
|
||||
import type { ElaprTmplt } from "@/entities/elapr";
|
||||
import { generateClientId } from "@/shared/lib";
|
||||
|
||||
export interface ElaprTmpltFormValues {
|
||||
elaprTmpltId: string;
|
||||
elaprTmpltNm: string;
|
||||
docSeComCd: string;
|
||||
elaprTmpltExpln: string;
|
||||
}
|
||||
|
||||
interface ElaprTmpltFormDialogProps {
|
||||
mode: "create" | "edit";
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initial?: ElaprTmplt | null;
|
||||
isSubmitting?: boolean;
|
||||
onSubmit: (values: ElaprTmpltFormValues) => void;
|
||||
}
|
||||
|
||||
function ElaprTmpltFormDialog({
|
||||
mode,
|
||||
open,
|
||||
onOpenChange,
|
||||
initial,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
}: ElaprTmpltFormDialogProps) {
|
||||
const { register, handleSubmit, reset, setValue, watch } = useForm<ElaprTmpltFormValues>({
|
||||
defaultValues: {
|
||||
elaprTmpltId: "",
|
||||
elaprTmpltNm: "",
|
||||
docSeComCd: "",
|
||||
elaprTmpltExpln: "",
|
||||
},
|
||||
});
|
||||
|
||||
const docSeComCd = watch("docSeComCd");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (mode === "edit" && initial) {
|
||||
reset({
|
||||
elaprTmpltId: initial.elaprTmpltId,
|
||||
elaprTmpltNm: initial.elaprTmpltNm,
|
||||
docSeComCd: initial.docSeComCd ?? "",
|
||||
elaprTmpltExpln: initial.elaprTmpltExpln ?? "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
reset({
|
||||
// TBD: elaprTmpltId 발급 정책 — 현재 클라이언트 임시 ID
|
||||
elaprTmpltId: generateClientId("TMPL"),
|
||||
elaprTmpltNm: "",
|
||||
docSeComCd: "",
|
||||
elaprTmpltExpln: "",
|
||||
});
|
||||
}, [open, mode, initial, reset]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "create" ? "결재서식 등록" : "결재서식 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltId">전자결재서식아이디</Label>
|
||||
<Input
|
||||
id="elaprTmpltId"
|
||||
readOnly
|
||||
{...register("elaprTmpltId", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltNm">결재서식명</Label>
|
||||
<Input
|
||||
id="elaprTmpltNm"
|
||||
{...register("elaprTmpltNm", { required: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>문서구분</Label>
|
||||
<CodeSelect
|
||||
upCd={UP_CD.DOC_SE}
|
||||
value={docSeComCd}
|
||||
onValueChange={(v) => setValue("docSeComCd", v)}
|
||||
placeholder="문서구분 선택"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="elaprTmpltExpln">서식 설명</Label>
|
||||
<Textarea id="elaprTmpltExpln" rows={3} {...register("elaprTmpltExpln")} />
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{mode === "create" ? "등록" : "저장"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { ElaprTmpltFormDialog };
|
||||
Reference in New Issue
Block a user