From c47c130fc371416a8ecbfba878bec4b2165e16c2 Mon Sep 17 00:00:00 2001 From: bicochan Date: Tue, 8 Sep 2026 10:29:27 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=ED=8C=8C=EC=9D=BC=EB=B3=B4?= =?UTF-8?q?=EC=99=84=20=EC=9A=94=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/fnrs-cnsltn/lib/button-config.ts | 7 +- .../fnrs-cnsltn/lib/save-payload-utils.ts | 18 ++ .../model/file-vrfc-splmnt-columns.tsx | 73 ++++++++ .../ui/components/file-vrfc-splmnt-dialog.tsx | 168 ++++++++++++++++++ .../ui/components/floating-action-buttons.tsx | 36 +++- .../pages/fnrs-cnsltn/ui/fnrs-form-page.tsx | 4 +- 6 files changed, 298 insertions(+), 8 deletions(-) create mode 100644 apps/as/src/pages/fnrs-cnsltn/model/file-vrfc-splmnt-columns.tsx create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/file-vrfc-splmnt-dialog.tsx diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts index 770707b39..3293edec3 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -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 > = { 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: [ diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts index 21e307f53..6a81db9ee 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/save-payload-utils.ts @@ -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, diff --git a/apps/as/src/pages/fnrs-cnsltn/model/file-vrfc-splmnt-columns.tsx b/apps/as/src/pages/fnrs-cnsltn/model/file-vrfc-splmnt-columns.tsx new file mode 100644 index 000000000..c677c9924 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/model/file-vrfc-splmnt-columns.tsx @@ -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(); + +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 }) => ( + table.toggleAllRowsSelected(!!checked)} + onClick={(event) => event.stopPropagation()} + aria-label="전체 선택" + /> + ), + meta: { align: "center", width: 48 }, + cell: ({ row }) => ( + row.toggleSelected(!!checked)} + onClick={(event) => event.stopPropagation()} + aria-label="행 선택" + /> + ), + }), + columnHelper.accessor("fileSeComCd", { + header: () =>

파일유형

, + meta: { align: "left" }, + cell: ({ getValue }) => ( +

{formatOptional(FILE_SE_COM_CD_MAP[getValue() as keyof typeof FILE_SE_COM_CD_MAP])}

+ ), + }), + columnHelper.accessor("fileSeNm", { + header: () =>

파일구분

, + meta: { align: "left" }, + cell: ({ getValue }) =>

{formatOptional(getValue())}

, + }), + columnHelper.accessor("vrfcRsltCn", { + header: "검증내역", + meta: { align: "left" }, + cell: ({ getValue }) => formatOptional(getValue()), + }), + ]; +} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/file-vrfc-splmnt-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/file-vrfc-splmnt-dialog.tsx new file mode 100644 index 000000000..1395883d2 --- /dev/null +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/file-vrfc-splmnt-dialog.tsx @@ -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 { + 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({}); + + 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>, + [ + 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 ( + + + + 파일보완 + + +
{tableComponent}
+ + + + + +
+
+ ); +} + +export { FileVrfcSplmntDialog }; +export type { FileVrfcSplmntDialogProps }; diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx index d8283220e..63e6526c9 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx @@ -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 ( <>
@@ -456,9 +474,9 @@ function FloatingActionButtons({ 기술검토 의뢰 )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileVrfc) && ( - )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && ( @@ -523,6 +541,14 @@ function FloatingActionButtons({ onConfirm={handleDmndSplmntConfirm} /> + setActiveDialog(null)} + onConfirm={handleFileSplmntConfirm} + /> + setActiveDialog(open ? "elaprApproval" : null)} @@ -644,7 +670,7 @@ const buttonActionHandlers: Record = sncnAprv: handleChangePrgrsSttsComCd, rvwRply: handleTechReview, rqstTechRvw: handleRequestExamine, - fileVrfc: () => Promise.resolve(), + fileSplmnt: handleDmndSplmnt, dsgntRvwr: handleDsgntRvwr, pwrkCnsltnDocCrt: () => Promise.resolve(), pwrkCnsltnDocMng: () => Promise.resolve(), diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx index 66a6a307c..6271183f9 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-form-page.tsx @@ -262,7 +262,9 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps)
- +
+ +
{initialData?.detDto?.fnrsCnsltnDocNo && (