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 c4ff2581d..8deea09de 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 @@ -22,6 +22,28 @@ async function findLatestByCrtDmndUuid( return response.data; } +async function findListByCrtDmndUuid( + crtDmndUuid: string, + branchSeCd?: string, +): Promise { + const response = await client.get<{ content: PwrkCnsltnDocCrtRsltResponseDto[] } | PwrkCnsltnDocCrtRsltResponseDto[]>( + BASE, + { + params: { + crtDmndUuid, + branchSeCd: branchSeCd || undefined, + page: 0, + size: 100, + sort: "verSn,desc", + }, + }, + ); + if (Array.isArray(response.data)) { + return response.data; + } + return response.data?.content ?? []; +} + async function openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string) { return client.get(`${BASE}/${crtDmndUuid}/webhwp/open`, { params: branchSeCd ? { branchSeCd } : undefined, @@ -29,6 +51,17 @@ async function openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string) { }); } +async function openWebHwpByVer( + crtDmndUuid: string, + verSn: number, + branchSeCd?: string, +) { + return client.get(`${BASE}/${crtDmndUuid}/${verSn}/webhwp/open`, { + params: branchSeCd ? { branchSeCd } : undefined, + responseType: "blob", + }); +} + async function download(crtDmndUuid: string, verSn: number, branchSeCd?: string) { return client.get(`${BASE}/${crtDmndUuid}/${verSn}/download`, { params: branchSeCd ? { branchSeCd } : undefined, @@ -83,7 +116,9 @@ async function updateMultipart( export const pwrkCnsltnDocCrtRsltService = { findLatestByCrtDmndUuid, + findListByCrtDmndUuid, openWebHwpLatest, + openWebHwpByVer, download, downloadLatest, saveMultipart, 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 077271141..e298005c5 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 @@ -25,7 +25,16 @@ export interface PwrkCnsltnDocCrtRsltService { crtDmndUuid: string, branchSeCd?: string, ): Promise; + findListByCrtDmndUuid( + crtDmndUuid: string, + branchSeCd?: string, + ): Promise; openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string): Promise>; + openWebHwpByVer( + crtDmndUuid: string, + verSn: number, + branchSeCd?: string, + ): Promise>; download( crtDmndUuid: string, verSn: number, 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 c3e002551..46ac4d478 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 @@ -165,6 +165,7 @@ function PwrkCnsltnDocMngDialog({ const [uploadedFiles, setUploadedFiles] = useState([]); const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false); const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false); + const [selectedVerSn, setSelectedVerSn] = useState(null); // 전자결재 브랜치 목록 직접 조회 (Single Source of Truth) const { data: elaprBranchList = [] } = useElaprBranchesByDocNo( @@ -312,6 +313,7 @@ function PwrkCnsltnDocMngDialog({ useEffect(() => { if (open) { + setSelectedVerSn(null); setCurrentBranchSeCd( resolveDefaultBranchSeCd({ branchSeCd: effectiveBranchSeCd, @@ -345,6 +347,13 @@ function PwrkCnsltnDocMngDialog({ const completedDraft = findCompletedPwrkDraft(pwrkCnsltnData); const crtDmndUuid = completedDraft?.crtDmndUuid; + // 전체 버전 이력 목록 조회 (verSn 역순 정렬) + const { data: historyList = [] } = useQuery({ + queryKey: ["pwrk-cnsltn-doc-crt-rslt", "history", crtDmndUuid], + queryFn: () => pwrkCnsltnDocCrtRsltService.findListByCrtDmndUuid(crtDmndUuid!), + enabled: open && Boolean(crtDmndUuid), + }); + const targetBranchForApi = isForkJoin ? currentBranchSeCd : undefined; const { @@ -352,8 +361,23 @@ function PwrkCnsltnDocMngDialog({ isLoading: isDocumentLoading, isError: isDocumentError, } = useQuery({ - queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", crtDmndUuid, targetBranchForApi], - queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, targetBranchForApi), + queryKey: [ + "pwrk-cnsltn-doc-crt-rslt", + selectedVerSn !== null ? "open-version" : "open-latest", + crtDmndUuid, + targetBranchForApi, + selectedVerSn, + ], + queryFn: () => { + if (selectedVerSn !== null) { + return pwrkCnsltnDocCrtRsltService.openWebHwpByVer( + crtDmndUuid!, + selectedVerSn, + targetBranchForApi, + ); + } + return pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, targetBranchForApi); + }, select: (response) => response.data, enabled: open && Boolean(crtDmndUuid), }); @@ -365,7 +389,8 @@ function PwrkCnsltnDocMngDialog({ ? "_시설" : "_종합" : ""; - const documentFileName = `실무협의서${branchLabel}_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`; + const verSuffix = selectedVerSn !== null ? `_v${selectedVerSn}` : ""; + const documentFileName = `실무협의서${branchLabel}${verSuffix}_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`; const isLoading = isPwrkCnsltnLoading || isDocumentLoading; const isError = isPwrkCnsltnError || isDocumentError; const pjtcoAjmtSearchParams = useMemo( @@ -401,6 +426,7 @@ function PwrkCnsltnDocMngDialog({ setAtchFileGroupId(""); setUploadedFiles([]); setIsEvlDialogOpen(false); + setSelectedVerSn(null); return; } @@ -463,7 +489,11 @@ function PwrkCnsltnDocMngDialog({ queryClient.invalidateQueries({ queryKey: ["pwrk-cnsltn-doc-crt-rslt", "open-latest", completedDraft.crtDmndUuid], }), + queryClient.invalidateQueries({ + queryKey: ["pwrk-cnsltn-doc-crt-rslt", "history", completedDraft.crtDmndUuid], + }), ]); + setSelectedVerSn(null); alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`); if (savedDocument.verSn <= AI_DRAFT_EVL_MAX_VER_SN) { @@ -474,10 +504,17 @@ function PwrkCnsltnDocMngDialog({ async function handleDownload() { if (!completedDraft) throw new Error("다운로드할 실무협의서 초안이 없습니다."); - const response = await pwrkCnsltnDocCrtRsltService.downloadLatest( - completedDraft.crtDmndUuid, - targetBranchForApi, - ); + const response = + selectedVerSn !== null + ? await pwrkCnsltnDocCrtRsltService.download( + completedDraft.crtDmndUuid, + selectedVerSn, + targetBranchForApi, + ) + : await pwrkCnsltnDocCrtRsltService.downloadLatest( + completedDraft.crtDmndUuid, + targetBranchForApi, + ); const objectUrl = window.URL.createObjectURL(response.data); const anchor = document.createElement("a"); anchor.href = objectUrl; @@ -611,6 +648,66 @@ function PwrkCnsltnDocMngDialog({ supplementBranches, ]); + // 현재 활성화된 탭(브랜치)의 최신(제일 마지막) verSn 계산 + const latestVerSnForCurrentBranch = useMemo(() => { + const branchItems = historyList.filter( + (item) => (item.branchSeCd || "MAIN") === (currentBranchSeCd || "MAIN"), + ); + if (!branchItems.length) return null; + return Math.max(...branchItems.map((item) => item.verSn)); + }, [historyList, currentBranchSeCd]); + + // 최신 버전을 열람 중인지 여부 (null이면 최신 모드이거나, 선택된 버전이 해당 탭의 최댓값과 일치) + const isCurrentBranchLatest = + selectedVerSn === null || selectedVerSn === latestVerSnForCurrentBranch; + + // 저장 버튼은 해당 탭의 제일 마지막(최신) 버전이 아니면 자동 숨김 처리 + const canSaveInCurrentView = canSaveCurrentBranch && isCurrentBranchLatest; + + // 변경이력 콤보 박스 컴포넌트 + const versionHistorySelect = ( +
+ + +
+ ); + const editorIframeHeight = isForkJoin ? "h-[50vh] min-h-[26rem]" : "h-[58vh] min-h-[32rem]"; @@ -642,7 +739,10 @@ function PwrkCnsltnDocMngDialog({ type="button" size="sm" color={currentBranchSeCd === "SP" ? "primary" : "white"} - onClick={() => setCurrentBranchSeCd("SP")} + onClick={() => { + setCurrentBranchSeCd("SP"); + setSelectedVerSn(null); + }} > 관로 초안 (SP) @@ -650,7 +750,10 @@ function PwrkCnsltnDocMngDialog({ type="button" size="sm" color={currentBranchSeCd === "SF" ? "primary" : "white"} - onClick={() => setCurrentBranchSeCd("SF")} + onClick={() => { + setCurrentBranchSeCd("SF"); + setSelectedVerSn(null); + }} > 시설 초안 (SF) @@ -660,7 +763,10 @@ function PwrkCnsltnDocMngDialog({ type="button" size="sm" color={currentBranchSeCd === "MAIN" ? "primary" : "white"} - onClick={() => setCurrentBranchSeCd("MAIN")} + onClick={() => { + setCurrentBranchSeCd("MAIN"); + setSelectedVerSn(null); + }} > 종합 실무협의서 (MAIN) @@ -717,13 +823,14 @@ function PwrkCnsltnDocMngDialog({
{pjtcoAjmtPage?.content?.length ? ( @@ -783,10 +890,10 @@ function PwrkCnsltnDocMngDialog({ }))} inputId="pwrk-cnsltn-doc-atch-file-input" isUploadingByKey={isUploading} - addDisabled={!canSaveCurrentBranch || isAnyAtchUploading} - showAddButton={canSaveCurrentBranch} + addDisabled={!canSaveInCurrentView || isAnyAtchUploading} + showAddButton={canSaveInCurrentView} onAdd={(files) => void handleAtchFileUpload(files)} - onRemove={canSaveCurrentBranch ? (index) => void handleAtchFileRemove(index) : undefined} + onRemove={canSaveInCurrentView ? (index) => void handleAtchFileRemove(index) : undefined} /> ) : null} diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx index b11182436..69571087b 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx @@ -16,6 +16,7 @@ interface HangulGiangiToolbarProps { onDownload: () => void; onFullscreen: () => void; onDmndSplmnt?: () => void; + rightSlot?: React.ReactNode; } function HangulGiangiToolbar({ @@ -26,11 +27,13 @@ function HangulGiangiToolbar({ onDownload, onFullscreen, onDmndSplmnt, + rightSlot, }: HangulGiangiToolbarProps) { const openInputId = useId(); return ( -
+
+
{WEBHYP_DEV_MODE && (
+ {rightSlot &&
{rightSlot}
}
); } diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx index bb5fe2da9..4fa2b8c3e 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx @@ -28,6 +28,7 @@ function HangulGiangi({ className, iframeClassName, onReady, + rightSlot, }: HangulGiangiProps) { const iframeRef = useRef(null); const openedDocumentKeyRef = useRef(null); @@ -209,6 +210,7 @@ function HangulGiangi({ onDownload={handleDownload} onFullscreen={handleFullscreen} onDmndSplmnt={onDmndSplmnt} + rightSlot={rightSlot} />