From e6978815817fbf8d03f3ddc0daeb3255d9c1fb0a Mon Sep 17 00:00:00 2001 From: jangwoong Date: Thu, 15 Oct 2026 17:02:26 +0900 Subject: [PATCH] =?UTF-8?q?[as=20elapr]=20=EC=9E=AC=EC=9B=90=EC=97=85?= =?UTF-8?q?=EB=AC=B4=20=ED=98=91=EC=9D=98=202=EC=B0=A8=20=EA=B2=B0?= =?UTF-8?q?=EC=9E=AC=EC=8B=9C=20=EC=9E=90=EB=8F=99=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EC=9E=90=20=EC=A7=80=EC=A0=95=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=203?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/pwrk-cnsltn-doc-crt-rslt-service.ts | 16 +- .../pwrk-cnsltn-doc-crt-rslt-interface.ts | 17 +- ...pwrk-cnsltn-doc-crt-rslt-payload-schema.ts | 1 + .../ui/components/floating-action-buttons.tsx | 3 + .../components/pwrk-cnsltn-doc-mng-dialog.tsx | 159 ++++++++++++++++-- .../pages/fnrs-cnsltn/ui/fnrs-form-page.tsx | 6 +- apps/as/src/widgets/pbadms-sprt/index.ts | 1 + .../pbadms-sprt/lib/checklist-form-utils.ts | 29 ++++ 8 files changed, 214 insertions(+), 18 deletions(-) diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/api/pwrk-cnsltn-doc-crt-rslt-service.ts b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/api/pwrk-cnsltn-doc-crt-rslt-service.ts index 82346e7c4..c4ff2581d 100644 --- a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/api/pwrk-cnsltn-doc-crt-rslt-service.ts +++ b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/api/pwrk-cnsltn-doc-crt-rslt-service.ts @@ -11,27 +11,34 @@ const BASE = ENDPOINT.AS_SERVICE.PWRK_CNSLTN_DOC_CRT_RSLT; async function findLatestByCrtDmndUuid( crtDmndUuid: string, + branchSeCd?: string, ): Promise { const response = await client.get( `${BASE}/${crtDmndUuid}/latest`, + { + params: branchSeCd ? { branchSeCd } : undefined, + }, ); return response.data; } -async function openWebHwpLatest(crtDmndUuid: string) { +async function openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string) { return client.get(`${BASE}/${crtDmndUuid}/webhwp/open`, { + params: branchSeCd ? { branchSeCd } : undefined, responseType: "blob", }); } -async function download(crtDmndUuid: string, verSn: number) { +async function download(crtDmndUuid: string, verSn: number, branchSeCd?: string) { return client.get(`${BASE}/${crtDmndUuid}/${verSn}/download`, { + params: branchSeCd ? { branchSeCd } : undefined, responseType: "blob", }); } -async function downloadLatest(crtDmndUuid: string) { +async function downloadLatest(crtDmndUuid: string, branchSeCd?: string) { return client.get(`${BASE}/${crtDmndUuid}/download`, { + params: branchSeCd ? { branchSeCd } : undefined, responseType: "blob", }); } @@ -47,6 +54,9 @@ async function saveMultipart( if (saveDto.atchFileGroupId) { formData.append("atchFileGroupId", saveDto.atchFileGroupId); } + if (saveDto.branchSeCd) { + formData.append("branchSeCd", saveDto.branchSeCd); + } if (file) { const resolvedFileName = fileName ?? (file instanceof File ? file.name : "document.hwpx"); formData.append("file", file, resolvedFileName); diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-interface.ts b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-interface.ts index 338e4ce5c..077271141 100644 --- a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-interface.ts +++ b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-interface.ts @@ -5,6 +5,7 @@ import type { PwrkCnsltnDocCrtRsltSaveRequestDto } from "./pwrk-cnsltn-doc-crt-r export interface PwrkCnsltnDocCrtRsltResponseDto { crtDmndUuid: string; verSn: number; + branchSeCd?: string; rgtrUuid?: string; rgtrNm?: string; orgnlOtptDocCn: null; @@ -14,15 +15,23 @@ export interface PwrkCnsltnDocCrtRsltResponseDto { /** 백엔드 PwrkCnsltnDocCrtRsltUpdateRequestDto (모두 optional) */ export interface PwrkCnsltnDocCrtRsltUpdateRequestDto { rgtrUuid?: string; + branchSeCd?: string; orgnlOtptDocCn?: string; crtCmptnDt?: string; } export interface PwrkCnsltnDocCrtRsltService { - findLatestByCrtDmndUuid(crtDmndUuid: string): Promise; - openWebHwpLatest(crtDmndUuid: string): Promise>; - download(crtDmndUuid: string, verSn: number): Promise>; - downloadLatest(crtDmndUuid: string): Promise>; + findLatestByCrtDmndUuid( + crtDmndUuid: string, + branchSeCd?: string, + ): Promise; + openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string): Promise>; + download( + crtDmndUuid: string, + verSn: number, + branchSeCd?: string, + ): Promise>; + downloadLatest(crtDmndUuid: string, branchSeCd?: string): Promise>; saveMultipart( saveDto: PwrkCnsltnDocCrtRsltSaveRequestDto, file?: Blob | File, diff --git a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-payload-schema.ts b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-payload-schema.ts index f2bf6ac24..1ee063565 100644 --- a/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-payload-schema.ts +++ b/apps/as/src/entities/pwrk-cnsltn-doc-crt-rslt/model/pwrk-cnsltn-doc-crt-rslt-payload-schema.ts @@ -5,6 +5,7 @@ export const pwrkCnsltnDocCrtRsltSaveRequestSchema = z.object({ crtDmndUuid: z.string().uuid(), pwrkCnsltnDocNo: z.string().min(1).max(100), atchFileGroupId: z.string().max(30).nullish(), + branchSeCd: z.string().max(20).nullish(), }); export type PwrkCnsltnDocCrtRsltSaveRequestDto = z.infer< 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 3de442ffd..7e54665fa 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 @@ -932,6 +932,9 @@ function FloatingActionButtons({ fnrsCnsltnDocNo={docNo} atrzSeComCdList={effectiveAtrzSeComCdList} prgrsSttsComCd={prgrsSttsComCd} + branchSeCd={userPendingBranchSeCd} + isForkJoin={isForkJoin} + userRole={dtlInstCd} canSave={ (needsElaprPrgrsCheck && !hasPendingApprovalRequest) || viewOnlyPwrkCnsltnDoc ? false 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 078853132..a509820f2 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 @@ -60,6 +60,10 @@ interface PwrkCnsltnDocMngDialogProps { canSave?: boolean | null; onDmndSplmnt?: () => void; onClose: () => void; + branchSeCd?: string; + isForkJoin?: boolean; + userRole?: string; + supplementDmndBranch?: string; } interface UploadedAtchFile { @@ -73,6 +77,31 @@ function createPwrkAtchUploadKey(file: File) { return `${PWRK_ATCH_FILE_KEY_PREFIX}${getDefaultFileKey(file)}`; } +function resolveDefaultBranchSeCd({ + branchSeCd, + isForkJoin, + userRole, + prgrsSttsComCd, + supplementDmndBranch, +}: { + branchSeCd?: string; + isForkJoin?: boolean; + userRole?: string; + prgrsSttsComCd?: string; + supplementDmndBranch?: string; +}): string { + if (userRole === "ROLE_USER05" && prgrsSttsComCd === "SL") { + return supplementDmndBranch || branchSeCd || "SP"; + } + if (branchSeCd === "TOTAL_AGG") { + return "MAIN"; + } + if (branchSeCd && ["SP", "SF", "MAIN"].includes(branchSeCd)) { + return branchSeCd; + } + return isForkJoin ? "SP" : "MAIN"; +} + function PwrkCnsltnDocMngDialog({ open, fnrsCnsltnDocNo, @@ -81,6 +110,10 @@ function PwrkCnsltnDocMngDialog({ canSave: canSaveProp, onDmndSplmnt, onClose, + branchSeCd, + isForkJoin, + userRole, + supplementDmndBranch, }: PwrkCnsltnDocMngDialogProps) { const queryClient = useQueryClient(); const alert = useAlertStore((state) => state.setMessage); @@ -88,6 +121,30 @@ function PwrkCnsltnDocMngDialog({ const [uploadedFiles, setUploadedFiles] = useState([]); const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false); const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false); + const [currentBranchSeCd, setCurrentBranchSeCd] = useState(() => + resolveDefaultBranchSeCd({ + branchSeCd, + isForkJoin, + userRole, + prgrsSttsComCd, + supplementDmndBranch, + }), + ); + + useEffect(() => { + if (open) { + setCurrentBranchSeCd( + resolveDefaultBranchSeCd({ + branchSeCd, + isForkJoin, + userRole, + prgrsSttsComCd, + supplementDmndBranch, + }), + ); + } + }, [open, branchSeCd, isForkJoin, userRole, prgrsSttsComCd, supplementDmndBranch]); + const { mutate: downloadByAtflId } = useDownloadAttachmentByAtflId(); const { @@ -103,13 +160,20 @@ function PwrkCnsltnDocMngDialog({ isLoading: isDocumentLoading, isError: isDocumentError, } = useQuery({ - queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", crtDmndUuid], - queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!), + queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", crtDmndUuid, currentBranchSeCd], + queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, currentBranchSeCd), select: (response) => response.data, enabled: open && Boolean(crtDmndUuid), }); - const documentFileName = `실무협의서_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`; + const branchLabel = isForkJoin + ? currentBranchSeCd === "SP" + ? "_관로" + : currentBranchSeCd === "SF" + ? "_시설" + : "_종합" + : ""; + const documentFileName = `실무협의서${branchLabel}_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`; const isLoading = isPwrkCnsltnLoading || isDocumentLoading; const isError = isPwrkCnsltnError || isDocumentError; const pjtcoAjmtSearchParams = useMemo( @@ -181,10 +245,18 @@ function PwrkCnsltnDocMngDialog({ async function handleSaveDocument(file: Blob) { if (!completedDraft) throw new Error("저장할 실무협의서 초안이 없습니다."); + const saveBranch = + branchSeCd === "TOTAL_AGG" || currentBranchSeCd === "MAIN" + ? "MAIN" + : isForkJoin + ? currentBranchSeCd + : undefined; + const saveDto = pwrkCnsltnDocCrtRsltSaveRequestSchema.parse({ crtDmndUuid: completedDraft.crtDmndUuid, pwrkCnsltnDocNo: completedDraft.pwrkCnsltnDocNo, atchFileGroupId: atchFileGroupId || undefined, + branchSeCd: saveBranch, }); const savedDocument = await pwrkCnsltnDocCrtRsltService.saveMultipart( @@ -210,7 +282,10 @@ function PwrkCnsltnDocMngDialog({ async function handleDownload() { if (!completedDraft) throw new Error("다운로드할 실무협의서 초안이 없습니다."); - const response = await pwrkCnsltnDocCrtRsltService.downloadLatest(completedDraft.crtDmndUuid); + const response = await pwrkCnsltnDocCrtRsltService.downloadLatest( + completedDraft.crtDmndUuid, + currentBranchSeCd, + ); const objectUrl = window.URL.createObjectURL(response.data); const anchor = document.createElement("a"); anchor.href = objectUrl; @@ -277,7 +352,24 @@ function PwrkCnsltnDocMngDialog({ const canSaveByAtrz = atrzSeComCdList ? !atrzSeComCdList.some((code) => APPROVER_ATRZ_SE_COM_CDS.has(code)) : true; - const canSave = !viewOnly && (canSaveProp ?? canSaveByAtrz); + const baseCanSave = !viewOnly && (canSaveProp ?? canSaveByAtrz); + + const canSaveCurrentBranch = useMemo(() => { + if (!baseCanSave) return false; + if (userRole === "ROLE_USER05") return false; + if (isForkJoin) { + if (branchSeCd === "TOTAL_AGG") { + return currentBranchSeCd === "MAIN"; + } + if (branchSeCd === "SP") { + return currentBranchSeCd === "SP"; + } + if (branchSeCd === "SF") { + return currentBranchSeCd === "SF"; + } + } + return baseCanSave; + }, [baseCanSave, userRole, isForkJoin, branchSeCd, currentBranchSeCd]); return ( <> @@ -290,6 +382,53 @@ function PwrkCnsltnDocMngDialog({ 실무협의서 관리 + {isForkJoin ? ( +
+
+ 부서별 문서 탭: + + + +
+ + {branchSeCd === "TOTAL_AGG" + ? "종합병합자: SP/SF를 참조하여 MAIN 종합본을 수동 병합 저장합니다." + : branchSeCd === "SP" + ? currentBranchSeCd === "SP" + ? "관로 검토자: 관로 초안(SP) 편집 모드" + : "관로 검토자: 타 문서 열람 모드 (읽기 전용)" + : branchSeCd === "SF" + ? currentBranchSeCd === "SF" + ? "시설 검토자: 시설 초안(SF) 편집 모드" + : "시설 검토자: 타 문서 열람 모드 (읽기 전용)" + : userRole === "ROLE_USER05" + ? "지자체 사용자: 보완요청 부서 문서 열람 전용" + : "문서 열람 모드"} + +
+ ) : null} + {isLoading ? : null} {isError ? (

@@ -306,9 +445,9 @@ function PwrkCnsltnDocMngDialog({

void handleAtchFileUpload(files)} - onRemove={canSave ? (index) => void handleAtchFileRemove(index) : undefined} + onRemove={canSaveCurrentBranch ? (index) => void handleAtchFileRemove(index) : undefined} /> ) : null} 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 5cf1ba697..15319f84c 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 @@ -39,6 +39,7 @@ import { Button } from "@common/components"; import { type FnrsCnsltnRegistFormData, fnrsCnsltnRegistSchema, + isForkJoinTask, } from "../lib/fnrs-cnsltn-regist-schema"; import { buildFormPayload, @@ -349,7 +350,10 @@ function FnrsFormPage({ mode, initialData, createDefaults }: FnrsFormPageProps) setIsPwrkCnsltnDocMngOpen(false)} /> ) : null} diff --git a/apps/as/src/widgets/pbadms-sprt/index.ts b/apps/as/src/widgets/pbadms-sprt/index.ts index c59b99732..e5105b3bb 100644 --- a/apps/as/src/widgets/pbadms-sprt/index.ts +++ b/apps/as/src/widgets/pbadms-sprt/index.ts @@ -16,6 +16,7 @@ export { buildCnstpScaleItems, recalculateBzScaleTotals } from "./lib/business-s export { canEditPbadmsDoc, type CanEditPbadmsDocParams } from "./lib/can-edit-pbadms-doc"; export { buildChecklistTabs, + isBranchChecklistComplete, isFormChecklistComplete, normalizeTaskSeComCdArray, syncCklstResultListFromTree, diff --git a/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts b/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts index bb4aae07d..057e873f8 100644 --- a/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts +++ b/apps/as/src/widgets/pbadms-sprt/lib/checklist-form-utils.ts @@ -138,3 +138,32 @@ export function isFormChecklistComplete(cklstResultList: CklstResultListItem[]): if (cklstResultList.length === 0) return true; return cklstResultList.every((item) => item.selected === "Y" || item.selected === "N"); } + +/** + * 결재자 브랜치별 체크리스트 완료 여부 검증 (병렬 결재 지원) + * - branchSeCd가 주어지면 해당 브랜치의 항목만 필수 검증 (SP 또는 SF) + * - branchSeCd가 없거나 TOTAL_AGG, MAIN 등인 경우 전체 항목 검증 + */ +export function isBranchChecklistComplete( + cklstResultList: CklstResultListItem[], + cklstTree: CklstTreeNode[], + branchSeCd?: string | null, +): boolean { + if (cklstResultList.length === 0) return true; + if (!branchSeCd || branchSeCd === "TOTAL_AGG" || branchSeCd === "MAIN") { + return isFormChecklistComplete(cklstResultList); + } + + const targetRvwSns = new Set( + cklstTree + .filter((node) => node.taskSeComCd === branchSeCd && node.bchckUseYn === "Y") + .map((node) => node.cklstRvwSn), + ); + + if (targetRvwSns.size === 0) return true; + + return cklstResultList + .filter((item) => targetRvwSns.has(item.cklstRvwSn)) + .every((item) => item.selected === "Y" || item.selected === "N"); +} +