[as] feat: 필수첨부파일관리 페이지 작업
This commit is contained in:
@@ -72,10 +72,8 @@ export interface EsntlAtchFileMngService {
|
||||
|
||||
/** 필수첨부파일관리 저장 요청 DTO */
|
||||
export interface EsntlAtchFileMngSaveRequest {
|
||||
/** 필수첨부파일아이디 */
|
||||
esntlAtchFileId: string;
|
||||
/** 문서구분공통코드 */
|
||||
docSeComCd?: string;
|
||||
docSeComCd: string;
|
||||
/** 필수첨부파일명 */
|
||||
esntlAtchFileNm?: string;
|
||||
/** 필수첨부여부 ("Y" | "N") */
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { EsntlAtchFileMngSearchParams } from "@/entities/esntl";
|
||||
|
||||
export const ESNTL_ATCH_FILE_ALL_DOC_SE = "";
|
||||
|
||||
export const DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE = 100;
|
||||
|
||||
export const DEFAULT_ESNTL_ATCH_FILE_SORT = "regDt,desc";
|
||||
|
||||
export function createDefaultEsntlAtchFileSearchParams(): EsntlAtchFileMngSearchParams {
|
||||
return {
|
||||
page: 0,
|
||||
size: DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE,
|
||||
sort: DEFAULT_ESNTL_ATCH_FILE_SORT,
|
||||
};
|
||||
}
|
||||
|
||||
export function toEsntlAtchFileQueryParams(
|
||||
params: EsntlAtchFileMngSearchParams,
|
||||
): EsntlAtchFileMngSearchParams {
|
||||
const { docSeComCd, esntlAtchFileNm, esntlAtchYn, page, size, sort } = params;
|
||||
|
||||
return {
|
||||
...(docSeComCd ? { docSeComCd } : {}),
|
||||
...(esntlAtchFileNm ? { esntlAtchFileNm } : {}),
|
||||
...(esntlAtchYn ? { esntlAtchYn } : {}),
|
||||
page: page ?? 0,
|
||||
size: size ?? DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE,
|
||||
sort: sort ?? DEFAULT_ESNTL_ATCH_FILE_SORT,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
const EAF_ID_PREFIX = "EAF-";
|
||||
const EAF_ID_PATTERN = /^EAF-(\d+)$/;
|
||||
|
||||
export function generateEsntlAtchFileId(existingIds: string[]): string {
|
||||
const maxSeq = existingIds.reduce((max, id) => {
|
||||
const match = id.match(EAF_ID_PATTERN);
|
||||
if (!match) return max;
|
||||
return Math.max(max, Number(match[1]));
|
||||
}, 0);
|
||||
|
||||
return `${EAF_ID_PREFIX}${String(maxSeq + 1).padStart(4, "0")}`;
|
||||
}
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
} from "@common/components";
|
||||
|
||||
export interface EsntlAtchFileFormValues {
|
||||
esntlAtchFileId: string;
|
||||
docSeComCd: string;
|
||||
esntlAtchFileNm: string;
|
||||
esntlAtchYn: "Y" | "N";
|
||||
@@ -32,7 +31,6 @@ interface EsntlAtchFileFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
initial?: EsntlAtchFileMng | null;
|
||||
generatedId?: string;
|
||||
isSubmitting?: boolean;
|
||||
onSubmit: (values: EsntlAtchFileFormValues) => void;
|
||||
}
|
||||
@@ -42,13 +40,11 @@ function EsntlAtchFileFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
initial,
|
||||
generatedId,
|
||||
isSubmitting,
|
||||
onSubmit,
|
||||
}: EsntlAtchFileFormDialogProps) {
|
||||
const { register, handleSubmit, reset, setValue, watch } = useForm<EsntlAtchFileFormValues>({
|
||||
defaultValues: {
|
||||
esntlAtchFileId: "",
|
||||
docSeComCd: "",
|
||||
esntlAtchFileNm: "",
|
||||
esntlAtchYn: "Y",
|
||||
@@ -64,7 +60,6 @@ function EsntlAtchFileFormDialog({
|
||||
|
||||
if (mode === "edit" && initial) {
|
||||
reset({
|
||||
esntlAtchFileId: initial.esntlAtchFileId,
|
||||
docSeComCd: initial.docSeComCd ?? "",
|
||||
esntlAtchFileNm: initial.esntlAtchFileNm ?? "",
|
||||
esntlAtchYn: initial.esntlAtchYn ?? "Y",
|
||||
@@ -74,13 +69,12 @@ function EsntlAtchFileFormDialog({
|
||||
}
|
||||
|
||||
reset({
|
||||
esntlAtchFileId: generatedId ?? "",
|
||||
docSeComCd: "",
|
||||
esntlAtchFileNm: "",
|
||||
esntlAtchYn: "Y",
|
||||
esntlAtchFileRmrk: "",
|
||||
});
|
||||
}, [open, mode, initial, generatedId, reset]);
|
||||
}, [open, mode, initial, reset]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
@@ -88,15 +82,15 @@ function EsntlAtchFileFormDialog({
|
||||
<DialogHeader>
|
||||
<DialogTitle>{mode === "create" ? "필수첨부파일 등록" : "필수첨부파일 수정"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleSubmit(onSubmit)}>
|
||||
{mode === "edit" ? (
|
||||
<form className="space-y-6" onSubmit={handleSubmit(onSubmit)}>
|
||||
{mode === "edit" && initial ? (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="esntlAtchFileId">필수첨부파일아이디</Label>
|
||||
<p className="p-2 text-xl">{watch("esntlAtchFileId")}</p>
|
||||
<p id="esntlAtchFileId" className="p-1 text-[1.3rem]">
|
||||
{initial.esntlAtchFileId}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<input type="hidden" {...register("esntlAtchFileId", { required: true })} />
|
||||
)}
|
||||
) : null}
|
||||
<div className="space-y-2">
|
||||
<Label>문서구분</Label>
|
||||
<CodeSelect
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import { getCoreRowModel, type PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
@@ -9,49 +10,71 @@ import { useCodeList } from "@/entities/code";
|
||||
import {
|
||||
type EsntlAtchFileMng,
|
||||
type EsntlAtchFileMngSearchParams,
|
||||
useCreateEsntlAtchFileMng,
|
||||
useDeleteEsntlAtchFileMng,
|
||||
useEsntlAtchFileMngPage,
|
||||
useUpdateEsntlAtchFileMng,
|
||||
} from "@/entities/esntl";
|
||||
import { Button } from "@common/components";
|
||||
|
||||
import { createEsntlAtchFileColumns } from "../model/esntl-atch-file-columns";
|
||||
import { toEsntlAtchFileQueryParams } from "../model/esntl-atch-file-search-params";
|
||||
|
||||
import {
|
||||
EsntlAtchFileFormDialog,
|
||||
type EsntlAtchFileFormValues,
|
||||
} from "./esntl-atch-file-form-dialog";
|
||||
|
||||
interface EsntlAtchFileListProps {
|
||||
searchParams: EsntlAtchFileMngSearchParams;
|
||||
pagination: PaginationState;
|
||||
onPaginationChange: (pagination: PaginationState) => void;
|
||||
onEdit: (row: EsntlAtchFileMng) => void;
|
||||
onDelete: (row: EsntlAtchFileMng) => void;
|
||||
}
|
||||
|
||||
function EsntlAtchFileList({
|
||||
searchParams,
|
||||
pagination,
|
||||
onPaginationChange,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: EsntlAtchFileListProps) {
|
||||
const { confirm } = useConfirm();
|
||||
const { data: docSeCodes } = useCodeList("ASDOCSECD");
|
||||
|
||||
const queryParams = useMemo(
|
||||
() =>
|
||||
toEsntlAtchFileQueryParams({
|
||||
...searchParams,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
}),
|
||||
[pagination.pageIndex, pagination.pageSize, searchParams],
|
||||
);
|
||||
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
|
||||
const [editingRow, setEditingRow] = useState<EsntlAtchFileMng | null>(null);
|
||||
|
||||
const createMutation = useCreateEsntlAtchFileMng();
|
||||
const updateMutation = useUpdateEsntlAtchFileMng();
|
||||
const deleteMutation = useDeleteEsntlAtchFileMng();
|
||||
|
||||
const queryParams = useMemo(() => {
|
||||
return {
|
||||
docSeComCd: searchParams.docSeComCd,
|
||||
page: pagination.pageIndex,
|
||||
size: pagination.pageSize,
|
||||
sort: searchParams.sort,
|
||||
};
|
||||
}, [searchParams, pagination]);
|
||||
|
||||
const pageQuery = useEsntlAtchFileMngPage(queryParams);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (row: EsntlAtchFileMng) => {
|
||||
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||
await deleteMutation.mutateAsync(row.esntlAtchFileId);
|
||||
},
|
||||
[confirm, deleteMutation],
|
||||
);
|
||||
|
||||
const columns = useMemo(
|
||||
() =>
|
||||
createEsntlAtchFileColumns({
|
||||
docSeCodes,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onEdit: (row) => {
|
||||
setEditingRow(row);
|
||||
setDialogMode("edit");
|
||||
},
|
||||
onDelete: handleDelete,
|
||||
}),
|
||||
[docSeCodes, onDelete, onEdit],
|
||||
[docSeCodes, handleDelete],
|
||||
);
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
@@ -71,12 +94,59 @@ function EsntlAtchFileList({
|
||||
},
|
||||
});
|
||||
|
||||
const handleFormSubmit = async (values: EsntlAtchFileFormValues) => {
|
||||
if (!values.docSeComCd || !values.esntlAtchFileNm || !values.esntlAtchYn) return;
|
||||
|
||||
const body = {
|
||||
docSeComCd: values.docSeComCd,
|
||||
esntlAtchFileNm: values.esntlAtchFileNm,
|
||||
esntlAtchYn: values.esntlAtchYn,
|
||||
esntlAtchFileRmrk: values.esntlAtchFileRmrk || undefined,
|
||||
};
|
||||
|
||||
if (dialogMode === "create") {
|
||||
if (!(await confirm("등록하시겠습니까?"))) return;
|
||||
await createMutation.mutateAsync(body);
|
||||
} else if (dialogMode === "edit" && editingRow) {
|
||||
await updateMutation.mutateAsync({
|
||||
esntlAtchFileId: editingRow.esntlAtchFileId,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
setDialogMode(null);
|
||||
setEditingRow(null);
|
||||
};
|
||||
|
||||
const handleOpenCreate = () => {
|
||||
setEditingRow(null);
|
||||
setDialogMode("create");
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="card card-border mx-0 bg-background pt-0">
|
||||
<div className="card-header">
|
||||
<div className="card-header flex items-center justify-between">
|
||||
<h3 className="text-2xl font-semibold">필수첨부파일 목록</h3>
|
||||
<Button type="button" color="green" onClick={handleOpenCreate}>
|
||||
등록
|
||||
</Button>
|
||||
</div>
|
||||
{tableComponent}
|
||||
{dialogMode ? (
|
||||
<EsntlAtchFileFormDialog
|
||||
mode={dialogMode}
|
||||
open={Boolean(dialogMode)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setDialogMode(null);
|
||||
setEditingRow(null);
|
||||
}
|
||||
}}
|
||||
initial={dialogMode === "edit" ? editingRow : null}
|
||||
isSubmitting={createMutation.isPending || updateMutation.isPending}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
||||
import type { PaginationState } from "@tanstack/react-table";
|
||||
|
||||
import { useCodeList } from "@/entities/code";
|
||||
import {
|
||||
type EsntlAtchFileMng,
|
||||
type EsntlAtchFileMngSearchParams,
|
||||
esntlAtchFileMngService,
|
||||
useCreateEsntlAtchFileMng,
|
||||
useDeleteEsntlAtchFileMng,
|
||||
useUpdateEsntlAtchFileMng,
|
||||
} from "@/entities/esntl";
|
||||
import type { EsntlAtchFileMngSearchParams } from "@/entities/esntl";
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
@@ -24,121 +16,51 @@ import {
|
||||
SelectValue,
|
||||
} from "@common/components";
|
||||
|
||||
import {
|
||||
createDefaultEsntlAtchFileSearchParams,
|
||||
DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE,
|
||||
ESNTL_ATCH_FILE_ALL_DOC_SE,
|
||||
} from "../model/esntl-atch-file-search-params";
|
||||
import { generateEsntlAtchFileId } from "../model/generate-esntl-atch-file-id";
|
||||
|
||||
import {
|
||||
EsntlAtchFileFormDialog,
|
||||
type EsntlAtchFileFormValues,
|
||||
} from "./esntl-atch-file-form-dialog";
|
||||
import { EsntlAtchFileList } from "./esntl-atch-file-list";
|
||||
|
||||
const ALL_DOC_SE_VALUE = "__ALL__";
|
||||
const ESNTL_ATCH_FILE_ALL_DOC_SE = "";
|
||||
|
||||
const DEFAULT_ESNTL_ATCH_FILE_PAGE_INDEX = 0;
|
||||
const DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE = 10;
|
||||
const DEFAULT_ESNTL_ATCH_FILE_SORT = "regDt,desc";
|
||||
|
||||
const DEFAULT_SEARCH_PARAMS = {
|
||||
page: DEFAULT_ESNTL_ATCH_FILE_PAGE_INDEX,
|
||||
size: DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE,
|
||||
sort: DEFAULT_ESNTL_ATCH_FILE_SORT,
|
||||
};
|
||||
|
||||
function EsntlAtchFileMngPage() {
|
||||
const { confirm } = useConfirm();
|
||||
const { data: docSeCodes, isPending: isDocSeLoading } = useCodeList("ASDOCSECD");
|
||||
|
||||
const [draftDocSeComCd, setDraftDocSeComCd] = useState(ESNTL_ATCH_FILE_ALL_DOC_SE);
|
||||
const [searchParams, setSearchParams] = useState<EsntlAtchFileMngSearchParams>(
|
||||
createDefaultEsntlAtchFileSearchParams(),
|
||||
);
|
||||
const [docSeComCd, setDocSeComCd] = useState(ESNTL_ATCH_FILE_ALL_DOC_SE);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageIndex: DEFAULT_ESNTL_ATCH_FILE_PAGE_INDEX,
|
||||
pageSize: DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE,
|
||||
});
|
||||
const [dialogMode, setDialogMode] = useState<"create" | "edit" | null>(null);
|
||||
const [editingRow, setEditingRow] = useState<EsntlAtchFileMng | null>(null);
|
||||
const [generatedId, setGeneratedId] = useState("");
|
||||
|
||||
const createMutation = useCreateEsntlAtchFileMng();
|
||||
const updateMutation = useUpdateEsntlAtchFileMng();
|
||||
const deleteMutation = useDeleteEsntlAtchFileMng();
|
||||
const [searchParams, setSearchParams] =
|
||||
useState<EsntlAtchFileMngSearchParams>(DEFAULT_SEARCH_PARAMS);
|
||||
|
||||
const handleSearch = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
setPagination((prev) => ({ ...prev, pageIndex: DEFAULT_ESNTL_ATCH_FILE_PAGE_INDEX }));
|
||||
setSearchParams({
|
||||
...createDefaultEsntlAtchFileSearchParams(),
|
||||
...(draftDocSeComCd ? { docSeComCd: draftDocSeComCd } : {}),
|
||||
...DEFAULT_SEARCH_PARAMS,
|
||||
docSeComCd,
|
||||
});
|
||||
};
|
||||
|
||||
const handleOpenCreate = async () => {
|
||||
const page = await esntlAtchFileMngService.findPage({
|
||||
page: 0,
|
||||
size: DEFAULT_ESNTL_ATCH_FILE_PAGE_SIZE,
|
||||
sort: "esntlAtchFileId,desc",
|
||||
});
|
||||
const existingIds = page.content.map((row) => row.esntlAtchFileId);
|
||||
setGeneratedId(generateEsntlAtchFileId(existingIds));
|
||||
setEditingRow(null);
|
||||
setDialogMode("create");
|
||||
};
|
||||
|
||||
const handleEdit = useCallback((row: EsntlAtchFileMng) => {
|
||||
setEditingRow(row);
|
||||
setDialogMode("edit");
|
||||
}, []);
|
||||
|
||||
const handleDelete = useCallback(
|
||||
async (row: EsntlAtchFileMng) => {
|
||||
if (!(await confirm("삭제하시겠습니까?"))) return;
|
||||
await deleteMutation.mutateAsync(row.esntlAtchFileId);
|
||||
},
|
||||
[confirm, deleteMutation],
|
||||
);
|
||||
|
||||
const handleFormSubmit = async (values: EsntlAtchFileFormValues) => {
|
||||
if (!values.docSeComCd || !values.esntlAtchFileNm || !values.esntlAtchYn) return;
|
||||
|
||||
const body = {
|
||||
esntlAtchFileId: values.esntlAtchFileId,
|
||||
docSeComCd: values.docSeComCd,
|
||||
esntlAtchFileNm: values.esntlAtchFileNm,
|
||||
esntlAtchYn: values.esntlAtchYn,
|
||||
esntlAtchFileRmrk: values.esntlAtchFileRmrk || undefined,
|
||||
};
|
||||
|
||||
if (dialogMode === "create") {
|
||||
if (!(await confirm("등록하시겠습니까?"))) return;
|
||||
await createMutation.mutateAsync(body);
|
||||
alert("저장되었습니다.");
|
||||
} else if (dialogMode === "edit" && editingRow) {
|
||||
await updateMutation.mutateAsync({
|
||||
esntlAtchFileId: editingRow.esntlAtchFileId,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
setDialogMode(null);
|
||||
setEditingRow(null);
|
||||
setGeneratedId("");
|
||||
};
|
||||
|
||||
const closeDialog = () => {
|
||||
setDialogMode(null);
|
||||
setEditingRow(null);
|
||||
setGeneratedId("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 px-[1.8rem] pb-6">
|
||||
<section className="card card-border mx-0 bg-background">
|
||||
<form
|
||||
className="flex flex-wrap items-end justify-between gap-4 p-4"
|
||||
onSubmit={handleSearch}
|
||||
>
|
||||
<form className="flex flex-wrap items-end gap-4 p-4" onSubmit={handleSearch}>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Label htmlFor="docSeComCd">문서구분</Label>
|
||||
<Select
|
||||
value={draftDocSeComCd || ALL_DOC_SE_VALUE}
|
||||
value={docSeComCd || ALL_DOC_SE_VALUE}
|
||||
onValueChange={(value) =>
|
||||
setDraftDocSeComCd(value === ALL_DOC_SE_VALUE ? ESNTL_ATCH_FILE_ALL_DOC_SE : value)
|
||||
setDocSeComCd(value === ALL_DOC_SE_VALUE ? ESNTL_ATCH_FILE_ALL_DOC_SE : value)
|
||||
}
|
||||
disabled={isDocSeLoading}
|
||||
>
|
||||
@@ -156,9 +78,6 @@ function EsntlAtchFileMngPage() {
|
||||
</Select>
|
||||
<Button type="submit">조회</Button>
|
||||
</div>
|
||||
<Button type="button" onClick={handleOpenCreate}>
|
||||
등록
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -166,23 +85,7 @@ function EsntlAtchFileMngPage() {
|
||||
searchParams={searchParams}
|
||||
pagination={pagination}
|
||||
onPaginationChange={setPagination}
|
||||
onEdit={handleEdit}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
|
||||
{dialogMode ? (
|
||||
<EsntlAtchFileFormDialog
|
||||
mode={dialogMode}
|
||||
open={Boolean(dialogMode)}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) closeDialog();
|
||||
}}
|
||||
initial={dialogMode === "edit" ? editingRow : null}
|
||||
generatedId={generatedId}
|
||||
isSubmitting={createMutation.isPending || updateMutation.isPending}
|
||||
onSubmit={handleFormSubmit}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user