From 01d5ed297e9b79ca25ba1e34b7ee57c5c48c3064 Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 11 Sep 2026 10:35:33 +0900 Subject: [PATCH 1/7] =?UTF-8?q?[as]=20chore:=20=EC=9E=AC=EC=9B=90=ED=98=91?= =?UTF-8?q?=EC=9D=98=20=EC=83=81=EC=84=B8=ED=99=94=EB=A9=B4=20=EA=B2=8C?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx index 55fbb04ad..efe663ab3 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/fnrs-detail-gate.tsx @@ -12,7 +12,7 @@ interface FnrsDetailGateProps { function FnrsDetailGate({ id }: FnrsDetailGateProps) { const userInfo = useUserStore((state) => state.userInfo); - const { data, isLoading, isFetching, isError } = useFnrsCnsltnMng(id); + const { data, isLoading, isError } = useFnrsCnsltnMng(id); const canEdit = canEditPbadmsDoc({ docSeComCd: "FNRS", prgrsSttsComCd: data?.detDto?.prgrsSttsComCd, @@ -22,9 +22,9 @@ function FnrsDetailGate({ id }: FnrsDetailGateProps) { return ( From d1ccddc1d31d65cf0d6b2bc0bce74e9edab163bb Mon Sep 17 00:00:00 2001 From: bicochan Date: Fri, 11 Sep 2026 17:02:09 +0900 Subject: [PATCH 2/7] =?UTF-8?q?[as]=20feat:=20=EC=9E=AC=EC=9B=90=ED=98=91?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EC=88=A0=EA=B2=80=ED=86=A0=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/pwrk-cnsltn-mng-service.ts | 6 + .../entities/pwrk-cnsltn-mng/api/queries.ts | 12 + apps/as/src/entities/pwrk-cnsltn-mng/index.ts | 8 +- .../model/pwrk-cnsltn-mng-interface.ts | 57 +++++ .../design-cnstn/ui/design-form-page.tsx | 4 +- .../pages/fnrs-cnsltn/lib/button-config.ts | 21 +- .../lib/fnrs-cnsltn-regist-schema.ts | 2 +- .../fnrs-cnsltn/lib/save-payload-utils.ts | 21 ++ .../ui/components/floating-action-buttons.tsx | 44 +++- .../components/pwrk-cnsltn-doc-mng-dialog.tsx | 12 +- .../pwrk-cnsltn-rvw-rslt-inq-dialog.tsx | 224 ++++++++++++++++++ .../pages/instl-sncn/ui/instl-form-page.tsx | 4 +- .../ui/sewer-bsc-plan-form-page.tsx | 4 +- apps/as/src/shared/model/pbadms-sprt.ts | 2 +- .../hangulgiangi/ui/hangul-giangi-toolbar.tsx | 12 +- .../widgets/hangulgiangi/ui/hangul-giangi.tsx | 3 + 16 files changed, 415 insertions(+), 21 deletions(-) create mode 100644 apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-rvw-rslt-inq-dialog.tsx diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts b/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts index ac27e8f5d..2654f544f 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/api/pwrk-cnsltn-mng-service.ts @@ -8,11 +8,17 @@ import type { const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_MNG; +async function findById(pwrkCnsltnDocNo: string): Promise { + const response = await client.get(`${BASE}/${pwrkCnsltnDocNo}`); + return response.data; +} + async function findByUpDocNo(upDocNo: string): Promise { const response = await client.get(`${BASE}/updocno/${upDocNo}`); return response.data; } export const pwrkCnsltnMngService = { + findById, findByUpDocNo, } satisfies PwrkCnsltnMngService; diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts b/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts index a5ba4cd8e..f3dbc0f68 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/api/queries.ts @@ -4,9 +4,21 @@ import { pwrkCnsltnMngService } from "./pwrk-cnsltn-mng-service"; export const pwrkCnsltnMngKeys = { all: ["pwrk-cnsltn-mng"] as const, + byId: (pwrkCnsltnDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-id", pwrkCnsltnDocNo] as const, byUpDocNo: (upDocNo: string) => [...pwrkCnsltnMngKeys.all, "by-up-doc-no", upDocNo] as const, }; +export function usePwrkCnsltnMng( + pwrkCnsltnDocNo: string | null | undefined, + isEnabled = true, +) { + return useQuery({ + queryKey: pwrkCnsltnMngKeys.byId(pwrkCnsltnDocNo!), + queryFn: () => pwrkCnsltnMngService.findById(pwrkCnsltnDocNo!), + enabled: Boolean(pwrkCnsltnDocNo) && isEnabled, + }); +} + export function usePwrkCnsltnMngByUpDocNo(upDocNo: string | null | undefined, isEnabled = true) { return useQuery({ queryKey: pwrkCnsltnMngKeys.byUpDocNo(upDocNo!), diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/index.ts b/apps/as/src/entities/pwrk-cnsltn-mng/index.ts index 58a3dc75d..1083b207b 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/index.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/index.ts @@ -1,9 +1,15 @@ export { pwrkCnsltnMngService } from "./api/pwrk-cnsltn-mng-service"; -export { pwrkCnsltnMngKeys, usePwrkCnsltnMngByUpDocNo } from "./api/queries"; +export { + pwrkCnsltnMngKeys, + usePwrkCnsltnMng, + usePwrkCnsltnMngByUpDocNo, +} from "./api/queries"; export { findCompletedPwrkDraft } from "./lib/find-completed-draft"; export type { + PwrkCnsltnCnsltnResponseDto, PwrkCnsltnDocCrtDmndResponseDto, PwrkCnsltnDocCrtRsltResponseDto, PwrkCnsltnMngDetResponseDto, PwrkCnsltnMngService, + PwrkCnsltnPjtcoAjmtResponseDto, } from "./model/pwrk-cnsltn-mng-interface"; diff --git a/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts index fe5c4e5a4..67065215a 100644 --- a/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts +++ b/apps/as/src/entities/pwrk-cnsltn-mng/model/pwrk-cnsltn-mng-interface.ts @@ -1,3 +1,5 @@ +import type { AtflMngClientResponseDto } from "@/shared/model"; + export interface PwrkCnsltnDocCrtRsltResponseDto { crtDmndUuid: string; verSn: number; @@ -10,25 +12,80 @@ export interface PwrkCnsltnDocCrtRsltResponseDto { export interface PwrkCnsltnDocCrtDmndResponseDto { crtDmndUuid: string; pwrkCnsltnDocNo: string; + orgnlDocNo?: string; + orgnlDocSeComCd?: string; + orgnlDocSeComCdNm?: string; + crtSysDmndId?: string; crtSttsComCd?: string; crtSttsComCdNm?: string; failRsn?: string; dmndDt?: string; cmptnDt?: string; + rqstrUuid?: string; + rqstrNm?: string; crtRslt?: PwrkCnsltnDocCrtRsltResponseDto | null; } +/** 백엔드 PwrkCnsltnMngDetResponseDto.CnsltnResponseDto */ +export interface PwrkCnsltnCnsltnResponseDto { + pwrkCnsltnDocNo?: string; + rvwOpnnSn?: number; + rvwOpnnClsfComCd?: string; + rvwOpnnClsfComCdNm?: string; + rvwOpnn?: string; + cnsltnCn?: string; + rfltYn?: string; + rmrk?: string; + atchFileGroupId?: string; + atchFileInfo?: AtflMngClientResponseDto; + rvwrNm?: string; + rvwrMblTelno?: string; + rvwrOgdpNm?: string; + rvwrEmlAddr?: string; + lclgvPicNm?: string; + lclgvPicTelno?: string; + lclgvPicOgdpNm?: string; + lclgvPicEmlAddr?: string; + crtDmndUuid?: string; + crtYn?: string; + hwrtChgYn?: string; + verSn?: number; + dtlSn?: number; +} + +/** 백엔드 PwrkCnsltnMngDetResponseDto.PjtcoAjmtResponseDto */ +export interface PwrkCnsltnPjtcoAjmtResponseDto { + pwrkCnsltnDocNo?: string; + rvwOpnnSn?: number; + cnstpSeCd?: string; + rvwOpnnClsfComCd?: string; + rvwOpnnClsfComCdNm?: string; + ognTpcoAmt?: number; + ognGvexpAmt?: number; + ognLcexpAmt?: number; + chgTpcoAmt?: number; + chgGvexpAmt?: number; + chgLcexpAmt?: number; +} + export interface PwrkCnsltnMngDetResponseDto { pwrkCnsltnDocNo: string; upDocNo: string; upDocSeComCd?: string; + upDocSeComCdNm?: string; prgrsSttsComCd?: string; + prgrsSttsComCdNm?: string; bizNm?: string; atchFileGroupId?: string; + regDt?: string; + rgtrNm?: string; + cnsltnList?: PwrkCnsltnCnsltnResponseDto[]; + pjtcoAjmtList?: PwrkCnsltnPjtcoAjmtResponseDto[]; crtDmndList?: PwrkCnsltnDocCrtDmndResponseDto[]; crtRsltList?: PwrkCnsltnDocCrtRsltResponseDto[]; } export interface PwrkCnsltnMngService { + findById(pwrkCnsltnDocNo: string): Promise; findByUpDocNo(upDocNo: string): Promise; } diff --git a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx index 4e5a5ad76..041e1d812 100644 --- a/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx +++ b/apps/as/src/pages/design-cnstn/ui/design-form-page.tsx @@ -169,7 +169,9 @@ function DesignFormPage({ mode, initialData, createDefaults }: DesignFormPagePro
- +
+ +
{initialData?.detDto?.designCnstnDocNo && ( ([ - FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn, + ...ROLE_USER01_ATRZ_STATUS_ACTIONS.TE.ATRZ02, FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv, FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzRply, ]); @@ -109,8 +114,14 @@ function resolveRoleUser01AtrzButtonActions( } return actions.filter((action) => { + const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action); + if (needsPendingApproval && options?.hasPendingApprovalRequest === false) return false; + if (prgrsSttsComCd === "TE" && options?.hasCompletedDraft != null) { - if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) { + if ( + action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng || + action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq + ) { return options.hasCompletedDraft; } if (action === FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) { @@ -118,9 +129,7 @@ function resolveRoleUser01AtrzButtonActions( } } - const needsPendingApproval = PENDING_APPROVAL_ACTIONS.has(action); - - return !needsPendingApproval || options?.hasPendingApprovalRequest !== false; + return true; }); } diff --git a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts index e925ab321..3bc32621a 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/fnrs-cnsltn-regist-schema.ts @@ -86,7 +86,7 @@ export const fnrsCnsltnRegistSchema = z.object({ splmntDmndList: z.array( z.object({ splmntDmndSn: z.number().nullish(), - splmntArtclComCd: z.string().optional(), + splmntArtclComCd: z.string().nullable(), splmntArtclCdNm: z.string().nullish(), splmntDmndCn: z.string().optional(), splmntDmndDt: z.string().optional(), 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 6a81db9ee..39ebd7582 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 @@ -10,6 +10,7 @@ import type { FnrsCnsltnMngResponseDto, FnrsCnsltnSplmntDmndItem, } from "@/entities/fnrs-cnsltn"; +import type { PwrkCnsltnCnsltnResponseDto } from "@/entities/pwrk-cnsltn-mng"; import { mapFnrsSharDetToFormList, type SplmntDmndDialogConfirmPayload, @@ -129,6 +130,26 @@ export function buildFileSplmntReqPayload( }; } +export function buildRvwRsltSplmntReqPayload( + data: FnrsCnsltnMngDetResponseDto, + selected: PwrkCnsltnCnsltnResponseDto[], + checklistFormState: ChecklistFormState, +): FnrsCnsltnApplySaveRequestDto { + const existingItems: FnrsCnsltnSplmntDmndItem[] = (data.splmntDmndList ?? []).map((item) => + toSplmntDmndSaveItem(item), + ); + + const newItems: FnrsCnsltnSplmntDmndItem[] = selected.map((row) => ({ + splmntDmndCn: row.rvwOpnn, + cmptnYn: "N", + })); + + return { + ...buildSavePayload(data, checklistFormState, "LOCAL", "SL"), + splmntDmndList: [...existingItems, ...newItems], + }; +} + export function toSplmntDmndSaveItem(item: FnrsCnsltnSplmntDmndItem): FnrsCnsltnSplmntDmndItem { return { splmntDmndSn: item.splmntDmndSn, 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 e3a6fad4f..f3bb8d088 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 @@ -28,7 +28,11 @@ import { fnrsCnsltnMngService, type FnrsExamineRequestDto, } from "@/entities/fnrs-cnsltn"; -import { findCompletedPwrkDraft, usePwrkCnsltnMngByUpDocNo } from "@/entities/pwrk-cnsltn-mng"; +import { + findCompletedPwrkDraft, + type PwrkCnsltnCnsltnResponseDto, + usePwrkCnsltnMngByUpDocNo, +} from "@/entities/pwrk-cnsltn-mng"; import { SplmntDmndDialog, type SplmntDmndDialogConfirmPayload } from "@/widgets/pbadms-sprt"; import { Button } from "@common/components"; @@ -41,6 +45,7 @@ import { import { computeReviewStepChecklistProgress } from "../../lib/checklist-utils"; import { buildFileSplmntReqPayload, + buildRvwRsltSplmntReqPayload, buildSavePayload, buildSupplementReqPayload, buildTechReviewReplyPayload, @@ -51,6 +56,7 @@ import type { TechReviewOpinionSectionHandle } from "../sections/tech-review-opi import { FileVrfcSplmntDialog } from "./file-vrfc-splmnt-dialog"; import { PwrkCnsltnDocMngDialog } from "./pwrk-cnsltn-doc-mng-dialog"; +import { PwrkCnsltnRvwRsltInqDialog } from "./pwrk-cnsltn-rvw-rslt-inq-dialog"; interface FloatingActionButtonsProps { data: FnrsCnsltnMngDetResponseDto; @@ -73,6 +79,7 @@ type ActiveDialog = | "fileSplmnt" | "elaprApproval" | "pwrkCnsltnDocMng" + | "rvwRsltInq" | null; function FloatingActionButtons({ @@ -311,7 +318,7 @@ function FloatingActionButtons({ data, checklistFormState, "ENV", - "EP", + "RM", "CP", getReviewOpinionFormData(), opnrNm, @@ -320,7 +327,7 @@ function FloatingActionButtons({ await executeAction({ action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply, request: payload, - successMessage: "검토결과 회신이 완료되었습니다.", + successMessage: "재원협의 결과 회신이 완료되었습니다.", }); techReviewOpinionRef?.current?.resetForm(); } @@ -435,6 +442,16 @@ function FloatingActionButtons({ }); } + async function handleRvwRsltSplmntConfirm(selected: PwrkCnsltnCnsltnResponseDto[]) { + setActiveDialog(null); + const requestPayload = buildRvwRsltSplmntReqPayload(data, selected, checklistFormState); + await executeAction({ + action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt, + request: requestPayload, + successMessage: "보완요청이 처리되었습니다.", + }); + } + return ( <>
@@ -486,7 +503,7 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRply) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && ( @@ -518,6 +535,15 @@ function FloatingActionButtons({ 실무협의서 관리 )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && ( + + )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && ( + + + + + ); +} + +function CnsltnListTable({ + rows, + selectedRowKeys, + onToggleAll, + onToggleRow, +}: CnsltnListTableProps) { + const selectedCount = rows.filter((row, index) => + selectedRowKeys.has(getCnsltnRowKey(row, index)), + ).length; + const isAllSelected = rows.length > 0 && selectedCount === rows.length; + const isSomeSelected = selectedCount > 0 && !isAllSelected; + + return ( + + + + + + + + + + onToggleAll(checked === true)} + aria-label="전체 선택" + /> + + 분류 + 검토의견 + + + + {rows.map((row, index) => { + const rowKey = getCnsltnRowKey(row, index); + const isSelected = selectedRowKeys.has(rowKey); + + return ( + + + onToggleRow(rowKey, checked === true)} + aria-label="행 선택" + /> + + +

{formatOptional(row.rmrk)}

+
+ +

{formatOptional(row.rvwOpnn)}

+
+
+ ); + })} +
+
+ ); +} + +function getCnsltnRowKey(row: PwrkCnsltnCnsltnResponseDto, index: number) { + return [row.pwrkCnsltnDocNo, row.verSn, row.dtlSn, row.rvwOpnnSn, String(index)] + .filter((value) => value !== undefined && value !== null && value !== "") + .join("::"); +} + +function formatOptional(value: string | number | undefined | null) { + if (value === undefined || value === null) return "-"; + const text = String(value).trim(); + return text ? text : "-"; +} + +export { PwrkCnsltnRvwRsltInqDialog }; +export type { PwrkCnsltnRvwRsltInqDialogProps }; diff --git a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx index adf563c65..a52c9776d 100644 --- a/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx +++ b/apps/as/src/pages/instl-sncn/ui/instl-form-page.tsx @@ -225,7 +225,9 @@ function InstlFormPage({ mode, initialData, createDefaults }: InstlFormPageProps - +
+ +
{initialData?.detDto.instlSncnDocNo && ( - +
+ +
{initialData?.detDto?.sewerBscPlanDocNo && ( void; onDownload: () => void; onFullscreen: () => void; @@ -13,16 +14,19 @@ interface HangulGiangiToolbarProps { function HangulGiangiToolbar({ isBusy = false, + canSave = true, onSave, onDownload, onFullscreen, }: HangulGiangiToolbarProps) { return (
- + {canSave && ( + + )}
-
- {uploadedFiles.length > 0 ? ( -
- {uploadedFiles.map((item, index) => { - const uploading = isUploading[item.fileKey] ?? false; - - return ( -
- - - {item.fileName} - - -
- ); - })} -
- ) : null} - -
+ ({ + fileKey: item.fileKey, + fileName: item.fileName, + }))} + inputId={fileInputId} + isUploadingByKey={isUploading} + onAdd={(fileList) => void handleFileUpload(fileList)} + onRemove={(index) => void handleFileRemove(index)} + />
diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx index 59ae0fe47..be1694755 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx @@ -4,15 +4,10 @@ import { useEffect, useState } from "react"; import dynamic from "next/dynamic"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Paperclip, X } from "lucide-react"; import { getDefaultFileKey, useAlertStore, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; -import { - attachService, - parseFilenameFromResponse, - useDownloadAttachmentByAtflId, -} from "@/entities/attach"; +import { AttachmentFileField } from "@/shared/ui"; +import { attachService, parseFilenameFromResponse } from "@/entities/attach"; import { pwrkCnsltnDocCrtRsltSaveRequestSchema, pwrkCnsltnDocCrtRsltService, @@ -70,7 +65,6 @@ function PwrkCnsltnDocMngDialog({ }: PwrkCnsltnDocMngDialogProps) { const queryClient = useQueryClient(); const alert = useAlertStore((state) => state.setMessage); - const downloadByAtflId = useDownloadAttachmentByAtflId(); const [atchFileGroupId, setAtchFileGroupId] = useState(""); const [uploadedFiles, setUploadedFiles] = useState([]); @@ -237,11 +231,6 @@ function PwrkCnsltnDocMngDialog({ } } - function handleAtchFileDownload(atflId?: string) { - if (!atflId) return; - downloadByAtflId.mutate(atflId); - } - const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]); const canSave = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code)); @@ -281,69 +270,21 @@ function PwrkCnsltnDocMngDialog({ {completedDraft ? (
-
-

첨부파일

- -
- {uploadedFiles.length > 0 ? ( -
- {uploadedFiles.map((item, index) => { - const uploading = isUploading[item.fileKey] ?? false; - - return ( -
- - - -
- ); - })} -
- ) : ( +

첨부파일

+ {uploadedFiles.length === 0 ? (

등록된 첨부파일이 없습니다.

- )} + ) : null} + ({ + fileKey: item.fileKey, + fileName: item.fileName, + }))} + inputId="pwrk-cnsltn-doc-atch-file-input" + isUploadingByKey={isUploading} + addDisabled={isAnyAtchUploading} + onAdd={(files) => void handleAtchFileUpload(files)} + onRemove={(index) => void handleAtchFileRemove(index)} + />
) : null} diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx index bdbd4e622..08f2651f4 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/sections/tech-review-opinion-section.tsx @@ -6,13 +6,11 @@ import { Paperclip } from "lucide-react"; import { PROXY_HOST } from "@/shared/config"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; -import { SectionCollapsible } from "@/shared/ui"; +import { AttachmentFileField, SectionCollapsible } from "@/shared/ui"; import { Field, FieldLabel, FieldSet } from "@/shared/ui/field"; import { attachService } from "@/entities/attach"; import type { FnrsCnsltnRvwOpnnResponseDto } from "@/entities/fnrs-cnsltn"; import { - Button, RadioGroup, RadioGroupItem, Table, @@ -246,38 +244,20 @@ const TechReviewOpinionSection = forwardRef< 첨부파일 - {uploadedFile ? ( -
- - {uploadedFile.fileName} - - -
- ) : ( - - )} + void handleFileUpload(fileList)} + onRemove={() => void handleFileRemove()} + />
diff --git a/apps/as/src/shared/ui/attachment-file-field.tsx b/apps/as/src/shared/ui/attachment-file-field.tsx new file mode 100644 index 000000000..6867a8323 --- /dev/null +++ b/apps/as/src/shared/ui/attachment-file-field.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { FileAddButton } from "./file-add-button"; +import { UploadedFileChip } from "./uploaded-file-chip"; + +interface AttachmentFileFieldItem { + fileKey: string; + fileName: string; +} + +interface AttachmentFileFieldProps { + files: AttachmentFileFieldItem[]; + inputId: string; + isUploadingByKey: Record; + onAdd: (files: FileList | null) => void; + onRemove?: (index: number) => void; + accept?: string; + multiple?: boolean; + addDisabled?: boolean; + showAddButton?: boolean; +} + +function AttachmentFileField({ + files, + inputId, + isUploadingByKey, + onAdd, + onRemove, + accept, + multiple = true, + addDisabled = false, + showAddButton = true, +}: AttachmentFileFieldProps) { + return ( +
+ {files.length > 0 ? ( +
+ {files.map((file, index) => ( + onRemove(index) : undefined} + /> + ))} +
+ ) : null} + {showAddButton ? ( + + ) : null} +
+ ); +} + +export { AttachmentFileField }; +export type { AttachmentFileFieldItem, AttachmentFileFieldProps }; diff --git a/apps/as/src/shared/ui/file-add-button.tsx b/apps/as/src/shared/ui/file-add-button.tsx new file mode 100644 index 000000000..24bfd6ecf --- /dev/null +++ b/apps/as/src/shared/ui/file-add-button.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { Button, cn } from "@common/components"; + +interface FileAddButtonProps { + inputId: string; + onFilesSelected: (files: FileList | null) => void; + accept?: string; + multiple?: boolean; + disabled?: boolean; + label?: string; +} + +function FileAddButton({ + inputId, + onFilesSelected, + accept, + multiple = true, + disabled = false, + label = "파일추가", +}: FileAddButtonProps) { + return ( + + ); +} + +export { FileAddButton }; +export type { FileAddButtonProps }; diff --git a/apps/as/src/shared/ui/index.ts b/apps/as/src/shared/ui/index.ts index 813bcafd5..71cd9a481 100644 --- a/apps/as/src/shared/ui/index.ts +++ b/apps/as/src/shared/ui/index.ts @@ -1,3 +1,7 @@ +export type { AttachmentFileFieldItem, AttachmentFileFieldProps } from "./attachment-file-field"; +export { AttachmentFileField } from "./attachment-file-field"; +export type { FileAddButtonProps } from "./file-add-button"; +export { FileAddButton } from "./file-add-button"; export { NoData } from "./no-data"; export { PageLayout } from "./page-layout"; export { SectionCollapsible } from "./section-collapsible"; @@ -7,3 +11,5 @@ export { } from "./section-collapsible-group"; export type { TextShimmerProps } from "./text-shimmer"; export { TEXT_SHIMMER_CLASS, TextShimmer } from "./text-shimmer"; +export type { UploadedFileChipProps } from "./uploaded-file-chip"; +export { UploadedFileChip } from "./uploaded-file-chip"; diff --git a/apps/as/src/shared/ui/uploaded-file-chip.tsx b/apps/as/src/shared/ui/uploaded-file-chip.tsx new file mode 100644 index 000000000..a7eb6fa22 --- /dev/null +++ b/apps/as/src/shared/ui/uploaded-file-chip.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { Paperclip, X } from "lucide-react"; + +import { TextShimmer } from "./text-shimmer"; + +interface UploadedFileChipProps { + fileName: string; + isUploading?: boolean; + onRemove?: () => void; +} + +function UploadedFileChip({ fileName, isUploading = false, onRemove }: UploadedFileChipProps) { + return ( +
+ + + {fileName} + + {onRemove ? ( + + ) : null} +
+ ); +} + +export { UploadedFileChip }; +export type { UploadedFileChipProps }; diff --git a/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx index ff62679b6..d3507d8ba 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/attachments-form-section.tsx @@ -4,13 +4,17 @@ import { useEffect, useRef, useState } from "react"; import { Controller, useFormContext } from "react-hook-form"; import { useQueryClient } from "@tanstack/react-query"; -import { Download, Paperclip, X } from "lucide-react"; +import { Download } from "lucide-react"; import { ENDPOINT } from "@/shared/config"; import { useTus } from "@/shared/lib"; import type { AtflMngClientResponseDto, EsntlAtchFileMngTreeResponseDto } from "@/shared/model"; -import { TextShimmer } from "@/shared/ui"; -import { SectionCollapsible } from "@/shared/ui"; +import { + AttachmentFileField, + FileAddButton, + SectionCollapsible, + UploadedFileChip, +} from "@/shared/ui"; import { attachService, useDownloadAttachmentByAtflCd } from "@/entities/attach"; import { useEsntlAtchFileMngTree } from "@/entities/esntl"; import { @@ -423,64 +427,21 @@ function AttachmentsFormSection({ {file.esntlAtchFileRmrk} - {uploadedFiles[file.esntlAtchFileId]?.length > 0 && ( -
- {uploadedFiles[file.esntlAtchFileId].map((item, idx) => { - const uploading = isUploading[item.fileKey] ?? false; - - return ( -
- - - {item.file?.name ?? item.fileName} - - -
- ); - })} -
- )} -
- -
+ ({ + fileKey: item.fileKey, + fileName: item.file?.name ?? item.fileName, + }))} + inputId={`${file.esntlAtchFileId}-file-input`} + isUploadingByKey={isUploading} + accept={toFileAccept(file.esntlAtchFileRmrk) || undefined} + onAdd={(fileList) => + void handleEssentialFileUpload(file.esntlAtchFileId, fileList) + } + onRemove={(index) => + void removeEssentialFile(file.esntlAtchFileId, index) + } + />
); @@ -495,55 +456,22 @@ function AttachmentsFormSection({

기타 첨부파일

- + void handleEtcFileUpload(files)} + />
{etcFiles.length > 0 ? (
- {etcFiles.map((item, idx) => { - const uploading = isUploading[item.fileKey] ?? false; - - return ( -
- - - {item.file?.name ?? item.fileName} - - -
- ); - })} + {etcFiles.map((item, idx) => ( + void removeEtcFile(idx)} + /> + ))}
) : (
diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx index 95aaaede2..752898721 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-dialog.tsx @@ -2,10 +2,8 @@ import { useEffect, useId, useState } from "react"; -import { Paperclip, X } from "lucide-react"; - import { getDefaultFileKey, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; +import { AttachmentFileField } from "@/shared/ui"; import { attachService } from "@/entities/attach"; import { CodeSelect } from "@/entities/code"; import { @@ -287,57 +285,17 @@ function SplmntDmndDialog({ /> - {row.uploadedFiles.length > 0 ? ( -
- {row.uploadedFiles.map((file, fileIndex) => { - const uploading = isUploading[file.fileKey] ?? false; - - return ( -
- - - {file.fileName} - - -
- ); - })} -
- ) : null} - + ({ + fileKey: file.fileKey, + fileName: file.fileName, + }))} + inputId={fileInputId} + isUploadingByKey={isUploading} + addDisabled={isRowUploading} + onAdd={(fileList) => void handleFileUpload(row.rowKey, fileList)} + onRemove={(fileIndex) => void handleFileRemove(row.rowKey, fileIndex)} + />
{rows.length > 1 ? ( diff --git a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx index a8f38ecb4..eea7fb7af 100644 --- a/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx +++ b/apps/as/src/widgets/pbadms-sprt/ui/splmnt-dmnd-form-section.tsx @@ -3,15 +3,13 @@ import { useEffect, useId, useRef, useState } from "react"; import { Controller, useFormContext, useWatch } from "react-hook-form"; -import { Paperclip, X } from "lucide-react"; +import { Paperclip } from "lucide-react"; import { dayjs, getDefaultFileKey, useTus } from "@/shared/lib"; -import { TextShimmer } from "@/shared/ui"; -import { SectionCollapsible } from "@/shared/ui"; +import { AttachmentFileField, SectionCollapsible } from "@/shared/ui"; import { attachService } from "@/entities/attach"; import { Badge, - Button, Table, TableBody, TableCell, @@ -303,63 +301,26 @@ function SplmntDmndFormSection({ readOnly = false }: SplmntDmndFormSectionProps) )} - {rowFiles.length > 0 ? ( -
- {rowFiles.map((file, fileIndex) => { - const uploading = isUploading[file.fileKey] ?? false; - - return ( -
- - - {file.fileName} - - {!isRowReadOnly ? ( - - ) : null} -
- ); - })} -
- ) : null} - {!isRowReadOnly ? ( - - ) : rowFiles.length === 0 ? ( + {isRowReadOnly && rowFiles.length === 0 ? ( - - ) : null} + ) : ( + ({ + fileKey: file.fileKey, + fileName: file.fileName, + }))} + inputId={fileInputId} + isUploadingByKey={isUploading} + addDisabled={isRowUploading} + showAddButton={!isRowReadOnly} + onAdd={(fileList) => void handleFileUpload(index, fileList)} + onRemove={ + isRowReadOnly + ? undefined + : (fileIndex) => void handleFileRemove(index, fileIndex) + } + /> + )}
From 6c4b8a52dc62d150d2221e2f58bd413d27708495 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 14 Sep 2026 10:16:16 +0900 Subject: [PATCH 4/7] =?UTF-8?q?[as]=20chore:=20=EB=B3=B4=EC=99=84=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=82=AC=ED=95=AD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sewer-bsc-plan-apply-payload-schema.ts | 2 +- .../pages/fnrs-cnsltn/lib/button-config.ts | 2 ++ .../lib/fnrs-cnsltn-regist-schema.ts | 2 +- .../ui/components/floating-action-buttons.tsx | 19 +++++++++++++++++++ .../ui/sections/business-cost-section.tsx | 2 +- .../sections/tech-review-opinion-section.tsx | 3 +++ .../pages/instl-sncn/lib/form-data-mapper.ts | 8 ++++---- .../lib/instl-sncn-regist-schema.ts | 2 +- .../ui/sections/business-cost-section.tsx | 2 +- .../ui/business-cost-form-section.tsx | 4 ++-- 10 files changed, 35 insertions(+), 11 deletions(-) diff --git a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts index 0060467b7..2980cfffb 100644 --- a/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts +++ b/apps/as/src/entities/sewer-bsc-plan/model/sewer-bsc-plan-apply-payload-schema.ts @@ -64,7 +64,7 @@ export const sewerBscPlanFnrsCstItemSchema = z.object({ /** 백엔드 SewerBscPlanApplySaveRequestDto.SewerBscPlanSplmntDmndItem */ export const sewerBscPlanSplmntDmndItemSchema = z.object({ splmntDmndSn: z.number().nullish(), - splmntArtclComCd: z.string().optional(), + splmntArtclComCd: z.string().nullish(), splmntDmndCn: z.string().optional(), splmntDmndDt: z.string().optional(), splmntDmndAtchFileGroupId: z.string().optional(), 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 6e042cf16..bf2d11767 100644 --- a/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts +++ b/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts @@ -6,6 +6,7 @@ export const FNRS_CNSLTN_VIEW_BUTTON_ACTION = { envoSbmsn: "envoSbmsn", rjct: "rjct", sncnAprv: "sncnAprv", + lclRply: "lclRply", rvwRply: "rvwRply", rqstTechRvw: "rqstTechRvw", fileSplmnt: "fileSplmnt", @@ -39,6 +40,7 @@ const ROLE_STATUS_BUTTON_ACTIONS: Record )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply) && ( + + )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && ( )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn) && ( + + )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && ( + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) && ( + + )} + {canEdit && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.del) && ( @@ -524,12 +547,12 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.envoSbmsn) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.lclRply) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct) && ( @@ -549,36 +572,27 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstTechRvw) && ( + )} + {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rqstActnRvw) && ( + )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.fileSplmnt) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dsgntRvwr) && ( - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocCrt) && ( - - )} - {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.pwrkCnsltnDocMng) && ( - )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.actnCmptn) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwRsltInq) && ( @@ -587,7 +601,7 @@ function FloatingActionButtons({ onClick={() => setActiveDialog("rvwRsltInq")} disabled={isProcessing} > - 검토결과조회 + 검토 결과 조회 )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwStrg) && ( @@ -597,12 +611,12 @@ function FloatingActionButtons({ )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.rvwCmptn) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.dmndSplmnt) && ( )} {shouldShowButton(FNRS_CNSLTN_VIEW_BUTTON_ACTION.atrzAprv) && ( @@ -661,6 +675,7 @@ function FloatingActionButtons({ open={activeDialog === "pwrkCnsltnDocMng"} fnrsCnsltnDocNo={docNo} atrzSeComCdList={atrzSeComCdList} + canSave={prgrsSttsComCd === "RA" ? false : undefined} onClose={() => setActiveDialog(null)} /> @@ -761,6 +776,19 @@ async function handleRequestExamine( } } +async function handleRequestActnReview( + context: ActionHandlerContext, +): Promise { + try { + const docNo = context.data.detDto?.fnrsCnsltnDocNo; + if (!docNo) throw new Error("문서번호가 없습니다."); + await fnrsCnsltnMngService.requestActnReview(docNo, context.request); + context.onSuccess?.(); + } catch (error) { + context.onError?.(error as Error); + } +} + async function handleActnCmptn( context: ActionHandlerContext<{ pwrkCnsltnDocNo: string; @@ -810,6 +838,7 @@ const buttonActionHandlers: Record = atrzAprv: handleAtrzAprv, atrzRply: handleAtrzRply, actnCmptn: handleActnCmptn, + rqstActnRvw: handleRequestActnReview, }; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx index be1694755..f87fbed9b 100644 --- a/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx +++ b/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx @@ -43,6 +43,7 @@ interface PwrkCnsltnDocMngDialogProps { open: boolean; fnrsCnsltnDocNo: string; atrzSeComCdList: string[]; + canSave?: boolean; onClose: () => void; } @@ -61,6 +62,7 @@ function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, atrzSeComCdList, + canSave: canSaveProp, onClose, }: PwrkCnsltnDocMngDialogProps) { const queryClient = useQueryClient(); @@ -232,7 +234,8 @@ function PwrkCnsltnDocMngDialog({ } const isAnyAtchUploading = uploadedFiles.some((item) => isUploading[item.fileKey]); - const canSave = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code)); + const canSaveByAtrz = !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code)); + const canSave = canSaveProp ?? canSaveByAtrz; return ( @@ -246,7 +249,7 @@ function PwrkCnsltnDocMngDialog({ {isLoading ? : null} {isError ? ( -

+

실무협의서 정보를 불러오지 못했습니다.

) : null} @@ -281,9 +284,10 @@ function PwrkCnsltnDocMngDialog({ }))} inputId="pwrk-cnsltn-doc-atch-file-input" isUploadingByKey={isUploading} - addDisabled={isAnyAtchUploading} + addDisabled={!canSave || isAnyAtchUploading} + showAddButton={canSave} onAdd={(files) => void handleAtchFileUpload(files)} - onRemove={(index) => void handleAtchFileRemove(index)} + onRemove={canSave ? (index) => void handleAtchFileRemove(index) : undefined} /> ) : null} diff --git a/apps/as/src/shared/ui/file-add-button.tsx b/apps/as/src/shared/ui/file-add-button.tsx index 24bfd6ecf..36c41406e 100644 --- a/apps/as/src/shared/ui/file-add-button.tsx +++ b/apps/as/src/shared/ui/file-add-button.tsx @@ -20,7 +20,10 @@ function FileAddButton({ label = "파일추가", }: FileAddButtonProps) { return ( -