[as] feat: 파일보완 요청 기능 구현
This commit is contained in:
@@ -8,7 +8,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = {
|
||||
sncnAprv: "sncnAprv",
|
||||
rvwRply: "rvwRply",
|
||||
rqstTechRvw: "rqstTechRvw",
|
||||
fileVrfc: "fileVrfc",
|
||||
fileSplmnt: "fileSplmnt",
|
||||
dsgntRvwr: "dsgntRvwr",
|
||||
pwrkCnsltnDocCrt: "pwrkCnsltnDocCrt",
|
||||
pwrkCnsltnDocMng: "pwrkCnsltnDocMng",
|
||||
@@ -53,7 +53,10 @@ const ROLE_USER01_ATRZ_STATUS_ACTIONS: Record<
|
||||
Record<string, FnrsCnsltnViewButtonAction[]>
|
||||
> = {
|
||||
ER: {
|
||||
ATRZ01: [FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc, FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr],
|
||||
ATRZ01: [
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt,
|
||||
FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr,
|
||||
],
|
||||
},
|
||||
TE: {
|
||||
ATRZ02: [
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
BzScaleItem,
|
||||
CklstResultItem,
|
||||
FnrsCnsltnApplySaveRequestDto,
|
||||
FnrsCnsltnFileVrfcRsltResponseDto,
|
||||
FnrsCnsltnMngDetResponseDto,
|
||||
FnrsCnsltnMngResponseDto,
|
||||
FnrsCnsltnSplmntDmndItem,
|
||||
@@ -111,6 +112,23 @@ export function buildSupplementReqPayload(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildFileSplmntReqPayload(
|
||||
data: FnrsCnsltnMngDetResponseDto,
|
||||
selected: FnrsCnsltnFileVrfcRsltResponseDto[],
|
||||
checklistFormState: ChecklistFormState,
|
||||
): FnrsCnsltnApplySaveRequestDto {
|
||||
const newItems: FnrsCnsltnSplmntDmndItem[] = selected.map((row) => ({
|
||||
splmntArtclComCd: row.fileSeComCd,
|
||||
splmntDmndCn: row.vrfcRsltCn,
|
||||
cmptnYn: "N",
|
||||
}));
|
||||
|
||||
return {
|
||||
...buildSavePayload(data, checklistFormState, "LOCAL", "SL"),
|
||||
splmntDmndList: [...newItems],
|
||||
};
|
||||
}
|
||||
|
||||
export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem {
|
||||
return {
|
||||
splmntDmndSn: item.splmntDmndSn,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { createColumnHelper } from "@tanstack/react-table";
|
||||
|
||||
import type { FnrsCnsltnFileVrfcRsltResponseDto } from "@/entities/fnrs-cnsltn";
|
||||
import { Checkbox } from "@common/components";
|
||||
|
||||
const FILE_SE_COM_CD_MAP = {
|
||||
ESNTL: "필수",
|
||||
ETC: "기타",
|
||||
};
|
||||
|
||||
const columnHelper = createColumnHelper<FnrsCnsltnFileVrfcRsltResponseDto>();
|
||||
|
||||
function formatOptional(value: string | undefined) {
|
||||
return value?.trim() ? value : "-";
|
||||
}
|
||||
|
||||
export function getFileVrfcRsltRowKey(row: FnrsCnsltnFileVrfcRsltResponseDto, index: number) {
|
||||
if (row.atflId) return row.atflId;
|
||||
return [row.esntlAtchFileId, row.fileSeComCd, row.orgnlFileNm, String(index)]
|
||||
.filter(Boolean)
|
||||
.join("::");
|
||||
}
|
||||
|
||||
export function createFileVrfcSplmntColumns() {
|
||||
return [
|
||||
columnHelper.display({
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllRowsSelected()
|
||||
? true
|
||||
: table.getIsSomeRowsSelected()
|
||||
? "indeterminate"
|
||||
: false
|
||||
}
|
||||
onCheckedChange={(checked) => table.toggleAllRowsSelected(!!checked)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="전체 선택"
|
||||
/>
|
||||
),
|
||||
meta: { align: "center", width: 48 },
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
disabled={!row.getCanSelect()}
|
||||
onCheckedChange={(checked) => row.toggleSelected(!!checked)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
aria-label="행 선택"
|
||||
/>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("fileSeComCd", {
|
||||
header: () => <p className="whitespace-nowrap">파일유형</p>,
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => (
|
||||
<p>{formatOptional(FILE_SE_COM_CD_MAP[getValue() as keyof typeof FILE_SE_COM_CD_MAP])}</p>
|
||||
),
|
||||
}),
|
||||
columnHelper.accessor("fileSeNm", {
|
||||
header: () => <p className="whitespace-nowrap">파일구분</p>,
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => <p className="whitespace-nowrap">{formatOptional(getValue())}</p>,
|
||||
}),
|
||||
columnHelper.accessor("vrfcRsltCn", {
|
||||
header: "검증내역",
|
||||
meta: { align: "left" },
|
||||
cell: ({ getValue }) => formatOptional(getValue()),
|
||||
}),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { UseQueryResult } from "@tanstack/react-query";
|
||||
import { getCoreRowModel, type RowSelectionState } from "@tanstack/react-table";
|
||||
|
||||
import { useDataTable } from "@/shared/lib";
|
||||
import type { PageResponse } from "@/shared/model";
|
||||
import {
|
||||
type FnrsCnsltnFileVrfcRsltResponseDto,
|
||||
useFnrsCnsltnFileVrfcResults,
|
||||
} from "@/entities/fnrs-cnsltn";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@common/components";
|
||||
|
||||
import {
|
||||
createFileVrfcSplmntColumns,
|
||||
getFileVrfcRsltRowKey,
|
||||
} from "../../model/file-vrfc-splmnt-columns";
|
||||
|
||||
interface FileVrfcSplmntDialogProps {
|
||||
open: boolean;
|
||||
fnrsCnsltnDocNo: string;
|
||||
isSubmitting?: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (selected: FnrsCnsltnFileVrfcRsltResponseDto[]) => void;
|
||||
}
|
||||
|
||||
function toFileVrfcPageResponse(
|
||||
rows: FnrsCnsltnFileVrfcRsltResponseDto[],
|
||||
): PageResponse<FnrsCnsltnFileVrfcRsltResponseDto> {
|
||||
const content = rows.filter((row) => row.vrfcSttsComCd === "WARNING");
|
||||
const totalElements = content.length;
|
||||
return {
|
||||
content,
|
||||
pageable: {
|
||||
sort: { empty: true, sorted: false, unsorted: true },
|
||||
offset: 0,
|
||||
pageNumber: 0,
|
||||
pageSize: totalElements || 1,
|
||||
paged: false,
|
||||
unpaged: true,
|
||||
},
|
||||
sort: { empty: true, sorted: false, unsorted: true },
|
||||
totalPages: 1,
|
||||
totalElements,
|
||||
number: 0,
|
||||
numberOfElements: totalElements,
|
||||
size: totalElements || 1,
|
||||
first: true,
|
||||
last: true,
|
||||
empty: totalElements === 0,
|
||||
};
|
||||
}
|
||||
|
||||
function FileVrfcSplmntDialog({
|
||||
open,
|
||||
fnrsCnsltnDocNo,
|
||||
isSubmitting = false,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: FileVrfcSplmntDialogProps) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
|
||||
const listQuery = useFnrsCnsltnFileVrfcResults(open ? fnrsCnsltnDocNo : null);
|
||||
|
||||
const pageData = useMemo(
|
||||
() => (listQuery.data ? toFileVrfcPageResponse(listQuery.data) : undefined),
|
||||
[listQuery.data],
|
||||
);
|
||||
|
||||
const pageQuery = useMemo(
|
||||
() =>
|
||||
({
|
||||
isPending: listQuery.isPending,
|
||||
isError: listQuery.isError,
|
||||
isSuccess: listQuery.isSuccess,
|
||||
data: pageData,
|
||||
error: listQuery.error,
|
||||
status: listQuery.status,
|
||||
fetchStatus: listQuery.fetchStatus,
|
||||
}) as UseQueryResult<PageResponse<FnrsCnsltnFileVrfcRsltResponseDto>>,
|
||||
[
|
||||
listQuery.error,
|
||||
listQuery.fetchStatus,
|
||||
listQuery.isError,
|
||||
listQuery.isPending,
|
||||
listQuery.isSuccess,
|
||||
listQuery.status,
|
||||
pageData,
|
||||
],
|
||||
);
|
||||
|
||||
const columns = useMemo(() => createFileVrfcSplmntColumns(), []);
|
||||
|
||||
const { tableComponent } = useDataTable({
|
||||
query: pageQuery,
|
||||
tableOptions: {
|
||||
columns,
|
||||
state: { rowSelection },
|
||||
enableRowSelection: true,
|
||||
enableMultiRowSelection: true,
|
||||
getRowId: (row, index) => getFileVrfcRsltRowKey(row, index),
|
||||
onRowSelectionChange: (updater) => {
|
||||
const next = typeof updater === "function" ? updater(rowSelection) : updater;
|
||||
setRowSelection(next);
|
||||
},
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
},
|
||||
dataTableOptions: {
|
||||
viewPagination: false,
|
||||
},
|
||||
});
|
||||
|
||||
const selectedRows = useMemo(() => {
|
||||
const selectedIds = new Set(Object.keys(rowSelection));
|
||||
const content = pageData?.content ?? [];
|
||||
return content.filter((row, index) => selectedIds.has(getFileVrfcRsltRowKey(row, index)));
|
||||
}, [pageData?.content, rowSelection]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) setRowSelection({});
|
||||
}, [open]);
|
||||
|
||||
function handleOpenChange(nextOpen: boolean) {
|
||||
if (!nextOpen) onClose();
|
||||
}
|
||||
|
||||
function handleConfirm() {
|
||||
if (selectedRows.length === 0) return;
|
||||
onConfirm(selectedRows);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-w-[80rem]" aria-describedby={undefined}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>파일보완</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">{tableComponent}</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" color="white" onClick={onClose} disabled={isSubmitting}>
|
||||
취소
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleConfirm}
|
||||
disabled={isSubmitting || selectedRows.length === 0}
|
||||
>
|
||||
보완요청
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export { FileVrfcSplmntDialog };
|
||||
export type { FileVrfcSplmntDialogProps };
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import {
|
||||
type DocPrgrsUpdateRequestDto,
|
||||
type FnrsCnsltnApplySaveRequestDto,
|
||||
type FnrsCnsltnFileVrfcRsltResponseDto,
|
||||
fnrsCnsltnKeys,
|
||||
type FnrsCnsltnMngDetResponseDto,
|
||||
fnrsCnsltnMngService,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
} from "../../lib/button-config";
|
||||
import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils";
|
||||
import {
|
||||
buildFileSplmntReqPayload,
|
||||
buildSavePayload,
|
||||
buildSupplementReqPayload,
|
||||
buildTechReviewReplyPayload,
|
||||
@@ -46,6 +48,8 @@ import {
|
||||
import { type ChecklistFormState } from "../../model/view-types";
|
||||
import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opinion-section";
|
||||
|
||||
import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog";
|
||||
|
||||
interface FloatingActionButtonsProps {
|
||||
data: FnrsCnsltnMngDetResponseDto;
|
||||
docSeComCd: string;
|
||||
@@ -60,7 +64,7 @@ interface FloatingActionButtonsProps {
|
||||
isAiPending?: boolean;
|
||||
}
|
||||
|
||||
type ActiveDialog = "elaprTmplt" | "rjct" | "dmndSplmnt" | "elaprApproval" | null;
|
||||
type ActiveDialog = "elaprTmplt" | "rjct" | "dmndSplmnt" | "fileSplmnt" | "elaprApproval" | null;
|
||||
|
||||
function FloatingActionButtons({
|
||||
data,
|
||||
@@ -397,6 +401,20 @@ function FloatingActionButtons({
|
||||
});
|
||||
}
|
||||
|
||||
function handleFileSplmntClick() {
|
||||
setActiveDialog("fileSplmnt");
|
||||
}
|
||||
|
||||
async function handleFileSplmntConfirm(selected: FnrsCnsltnFileVrfcRsltResponseDto[]) {
|
||||
setActiveDialog(null);
|
||||
const requestPayload = buildFileSplmntReqPayload(data, selected, checklistFormState);
|
||||
await executeAction({
|
||||
action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt,
|
||||
request: requestPayload,
|
||||
successMessage: "파일보완 요청이 처리되었습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="sticky bottom-0 z-40 !-mb-2 border-t bg-white pb-8 pt-6 shadow-[0_-4px_12px_rgba(0,0,0,0.06)]">
|
||||
@@ -456,9 +474,9 @@ function FloatingActionButtons({
|
||||
기술검토 의뢰
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc) && (
|
||||
<Button color="gray" onClick={() => {}} disabled={isProcessing}>
|
||||
파일 검증
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt) && (
|
||||
<Button color="gray" onClick={handleFileSplmntClick} disabled={isProcessing}>
|
||||
파일보완 요청
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && (
|
||||
@@ -523,6 +541,14 @@ function FloatingActionButtons({
|
||||
onConfirm={handleDmndSplmntConfirm}
|
||||
/>
|
||||
|
||||
<FileVrfcSplmntDialog
|
||||
open={activeDialog === "fileSplmnt"}
|
||||
fnrsCnsltnDocNo={docNo}
|
||||
isSubmitting={isProcessing}
|
||||
onClose={() => setActiveDialog(null)}
|
||||
onConfirm={handleFileSplmntConfirm}
|
||||
/>
|
||||
|
||||
<ElaprApprovalDialog
|
||||
open={activeDialog === "elaprApproval"}
|
||||
onOpenChange={(open) => setActiveDialog(open ? "elaprApproval" : null)}
|
||||
@@ -644,7 +670,7 @@ const buttonActionHandlers: Record<FnrsCnsltnViewButtonAction, ActionHandler> =
|
||||
sncnAprv: handleChangePrgrsSttsComCd,
|
||||
rvwRply: handleTechReview,
|
||||
rqstTechRvw: handleRequestExamine,
|
||||
fileVrfc: () => Promise.resolve(),
|
||||
fileSplmnt: handleDmndSplmnt,
|
||||
dsgntRvwr: handleDsgntRvwr,
|
||||
pwrkCnsltnDocCrt: () => Promise.resolve(),
|
||||
pwrkCnsltnDocMng: () => Promise.resolve(),
|
||||
|
||||
@@ -262,7 +262,9 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
|
||||
<FormProvider {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSave)} className="mt-4">
|
||||
<SectionCollapsibleGroup>
|
||||
<SectionCollapsibleToggle />
|
||||
<div className="sticky top-[6rem] z-30 bg-container px-4 py-2">
|
||||
<SectionCollapsibleToggle />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{initialData?.detDto?.fnrsCnsltnDocNo && (
|
||||
<ProgressStatusSection
|
||||
|
||||
Reference in New Issue
Block a user