[as fr] 한글기안기 변경이력 관리 추가

This commit is contained in:
2026-10-21 15:46:10 +09:00
parent b74f2820da
commit b5eceadee8
5 changed files with 175 additions and 16 deletions
@@ -22,6 +22,28 @@ async function findLatestByCrtDmndUuid(
return response.data;
}
async function findListByCrtDmndUuid(
crtDmndUuid: string,
branchSeCd?: string,
): Promise<PwrkCnsltnDocCrtRsltResponseDto[]> {
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<Blob>(`${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<Blob>(`${BASE}/${crtDmndUuid}/${verSn}/webhwp/open`, {
params: branchSeCd ? { branchSeCd } : undefined,
responseType: "blob",
});
}
async function download(crtDmndUuid: string, verSn: number, branchSeCd?: string) {
return client.get<Blob>(`${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,
@@ -25,7 +25,16 @@ export interface PwrkCnsltnDocCrtRsltService {
crtDmndUuid: string,
branchSeCd?: string,
): Promise<PwrkCnsltnDocCrtRsltResponseDto>;
findListByCrtDmndUuid(
crtDmndUuid: string,
branchSeCd?: string,
): Promise<PwrkCnsltnDocCrtRsltResponseDto[]>;
openWebHwpLatest(crtDmndUuid: string, branchSeCd?: string): Promise<AxiosResponse<Blob>>;
openWebHwpByVer(
crtDmndUuid: string,
verSn: number,
branchSeCd?: string,
): Promise<AxiosResponse<Blob>>;
download(
crtDmndUuid: string,
verSn: number,
@@ -165,6 +165,7 @@ function PwrkCnsltnDocMngDialog({
const [uploadedFiles, setUploadedFiles] = useState<UploadedAtchFile[]>([]);
const [isPjtcoAjmtTableOpen, setIsPjtcoAjmtTableOpen] = useState(false);
const [isEvlDialogOpen, setIsEvlDialogOpen] = useState(false);
const [selectedVerSn, setSelectedVerSn] = useState<number | null>(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 = (
<div className="flex items-center gap-2">
<label htmlFor="pwrk-version-history-select" className="text-sm font-medium text-muted-foreground whitespace-nowrap">
변경이력:
</label>
<select
id="pwrk-version-history-select"
className="h-8 rounded-md border border-input bg-background px-2 py-1 text-sm shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
value={selectedVerSn ?? ""}
onChange={(e) => {
const val = e.target.value;
if (!val) {
setSelectedVerSn(null);
return;
}
const num = Number(val);
const item = historyList.find((h) => h.verSn === num);
if (item) {
const targetBranch = item.branchSeCd || "MAIN";
setCurrentBranchSeCd(targetBranch);
setSelectedVerSn(item.verSn);
}
}}
aria-label="변경이력 선택"
>
<option value="">선택</option>
{historyList.map((item) => {
const branchText =
item.branchSeCd === "SF"
? "[시설]"
: item.branchSeCd === "SP"
? "[관로]"
: "[통합]";
return (
<option key={`${item.branchSeCd}_${item.verSn}`} value={item.verSn}>
{branchText} 버전 {item.verSn}
</option>
);
})}
</select>
</div>
);
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)
</Button>
@@ -650,7 +750,10 @@ function PwrkCnsltnDocMngDialog({
type="button"
size="sm"
color={currentBranchSeCd === "SF" ? "primary" : "white"}
onClick={() => setCurrentBranchSeCd("SF")}
onClick={() => {
setCurrentBranchSeCd("SF");
setSelectedVerSn(null);
}}
>
시설 초안 (SF)
</Button>
@@ -660,7 +763,10 @@ function PwrkCnsltnDocMngDialog({
type="button"
size="sm"
color={currentBranchSeCd === "MAIN" ? "primary" : "white"}
onClick={() => setCurrentBranchSeCd("MAIN")}
onClick={() => {
setCurrentBranchSeCd("MAIN");
setSelectedVerSn(null);
}}
>
종합 실무협의서 (MAIN)
</Button>
@@ -717,13 +823,14 @@ function PwrkCnsltnDocMngDialog({
<div className="min-w-0 flex-1">
<HangulGiangi
documentBlob={documentBlob}
documentKey={`${completedDraft.crtDmndUuid}_${currentBranchSeCd}`}
documentKey={`${completedDraft.crtDmndUuid}_${currentBranchSeCd}_${selectedVerSn ?? "latest"}`}
documentFileName={documentFileName}
canSave={canSaveCurrentBranch}
canSave={canSaveInCurrentView}
iframeClassName={editorIframeHeight}
onSaveDocument={handleSaveDocument}
onDownload={handleDownload}
onDmndSplmnt={onDmndSplmnt}
rightSlot={versionHistorySelect}
/>
</div>
{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}
/>
</section>
) : null}
@@ -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 (
<div className="flex w-full flex-nowrap items-center gap-2 border-b border-border bg-background p-3">
<div className="flex w-full flex-wrap items-center justify-between gap-2 border-b border-border bg-background p-3">
<div className="flex flex-wrap items-center gap-2">
{WEBHYP_DEV_MODE && (
<label
htmlFor={openInputId}
@@ -87,6 +90,8 @@ function HangulGiangiToolbar({
보완요청
</Button>
)}
</div>
{rightSlot && <div className="flex items-center gap-2">{rightSlot}</div>}
</div>
);
}
@@ -28,6 +28,7 @@ function HangulGiangi({
className,
iframeClassName,
onReady,
rightSlot,
}: HangulGiangiProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const openedDocumentKeyRef = useRef<string | null>(null);
@@ -209,6 +210,7 @@ function HangulGiangi({
onDownload={handleDownload}
onFullscreen={handleFullscreen}
onDmndSplmnt={onDmndSplmnt}
rightSlot={rightSlot}
/>
<iframe
@@ -329,6 +331,7 @@ interface HangulGiangiProps {
className?: string;
iframeClassName?: string;
onReady?: () => void;
rightSlot?: React.ReactNode;
}
export { HangulGiangi };