[as fr] 웹 기안기 버전선택시 새창 열기 새창열기에 전체 화면 추가

This commit is contained in:
2026-10-28 08:49:49 +09:00
parent 8b34a5d7a9
commit b5f0092818
@@ -15,6 +15,7 @@ import {
import {
pwrkCnsltnDocCrtRsltSaveRequestSchema,
pwrkCnsltnDocCrtRsltService,
type PwrkCnsltnDocCrtRsltResponseDto,
} from "@/entities/pwrk-cnsltn-doc-crt-rslt";
import { usePwrkCnsltnDocPjtcoAjmtMngPage } from "@/entities/pwrk-cnsltn-doc-pjtco-ajmt-mng";
import { useElaprBranchesByDocNo, useElaprDocPrgrsByDocNo } from "@/entities/elapr";
@@ -167,7 +168,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);
const [selectedHistoryKey, setSelectedHistoryKey] = useState<string>("");
// 전자결재 브랜치 목록 직접 조회 (Single Source of Truth)
const { data: elaprBranchList = [] } = useElaprBranchesByDocNo(
@@ -315,7 +316,7 @@ function PwrkCnsltnDocMngDialog({
useEffect(() => {
if (open) {
setSelectedVerSn(null);
setSelectedHistoryKey("");
setCurrentBranchSeCd(
resolveDefaultBranchSeCd({
branchSeCd: effectiveBranchSeCd,
@@ -365,21 +366,11 @@ function PwrkCnsltnDocMngDialog({
} = useQuery({
queryKey: [
"pwrk-cnsltn-doc-crt-rslt",
selectedVerSn !== null ? "open-version" : "open-latest",
"open-latest",
crtDmndUuid,
targetBranchForApi,
selectedVerSn,
],
queryFn: () => {
if (selectedVerSn !== null) {
return pwrkCnsltnDocCrtRsltService.openWebHwpByVer(
crtDmndUuid!,
selectedVerSn,
targetBranchForApi,
);
}
return pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, targetBranchForApi);
},
queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, targetBranchForApi),
select: (response) => response.data,
enabled: open && Boolean(crtDmndUuid),
staleTime: Infinity,
@@ -394,8 +385,7 @@ function PwrkCnsltnDocMngDialog({
? "_시설"
: "_종합"
: "";
const verSuffix = selectedVerSn !== null ? `_v${selectedVerSn}` : "";
const documentFileName = `실무협의서${branchLabel}${verSuffix}_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`;
const documentFileName = `실무협의서${branchLabel}_${pwrkCnsltnData?.pwrkCnsltnDocNo ?? crtDmndUuid}.hwpx`;
const isLoading = isPwrkCnsltnLoading || isDocumentLoading;
const isError = isPwrkCnsltnError || isDocumentError;
// AI 자동화 이력 조회 (COST_VRFC 완료 여부 확인)
@@ -471,7 +461,7 @@ function PwrkCnsltnDocMngDialog({
setAtchFileGroupId("");
setUploadedFiles([]);
setIsEvlDialogOpen(false);
setSelectedVerSn(null);
setSelectedHistoryKey("");
return;
}
@@ -523,20 +513,35 @@ function PwrkCnsltnDocMngDialog({
if (currentBranchSeCd !== nextBranch) {
if (!checkDirtyAndConfirm(branchName)) return;
setCurrentBranchSeCd(nextBranch);
setSelectedHistoryKey("");
}
setSelectedVerSn(null);
}
async function handleOpenBranchInNewWindow(targetBranch: "SP" | "SF") {
async function openDocumentPopupWindow({
popupName,
pageTitle,
headerTitle,
badgeText,
badgeColor = "#2563eb",
fileName,
fetchBlob,
}: {
popupName: string;
pageTitle: string;
headerTitle: string;
badgeText?: string;
badgeColor?: string;
fileName: string;
fetchBlob: () => Promise<{ data: Blob }>;
}) {
if (!completedDraft) {
alert("열람할 실무협의서 초안 정보가 없습니다.");
return;
}
const branchName = targetBranch === "SP" ? "관로 (SP)" : "시설 (SF)";
const popup = window.open(
"",
`pwrk_viewer_${targetBranch}`,
popupName,
"width=1200,height=900,menubar=no,toolbar=no,location=no,status=no,resizable=yes",
);
if (!popup) {
@@ -551,14 +556,17 @@ function PwrkCnsltnDocMngDialog({
<html style="height:100%;margin:0;padding:0;">
<head>
<meta charset="utf-8" />
<title>실무협의서 [${branchName}] 참조 뷰어</title>
<title>${pageTitle}</title>
<style>
body { margin:0; padding:0; height:100%; display:flex; flex-direction:column; font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:#f8fafc; }
header { display:flex; justify-content:space-between; align-items:center; padding:10px 16px; background:#1e293b; color:#fff; box-shadow:0 1px 3px rgba(0,0,0,0.2); }
h2 { margin:0; font-size:15px; font-weight:600; display:flex; align-items:center; gap:8px; }
.badge { background:#2563eb; color:#fff; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:normal; }
.badge { background:${badgeColor}; color:#fff; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:normal; }
.badge-sub { background:#475569; color:#fff; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:normal; }
.actions { display:flex; align-items:center; gap:8px; }
.btn { border:0; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; font-weight:500; transition:background 0.15s; }
.btn { border:0; padding:6px 14px; border-radius:4px; cursor:pointer; font-size:13px; font-weight:500; transition:background 0.15s; display:inline-flex; align-items:center; gap:4px; }
.btn-fullscreen { background:#4f46e5; color:#fff; }
.btn-fullscreen:hover { background:#4338ca; }
.btn-download { background:#0284c7; color:#fff; }
.btn-download:hover { background:#0369a1; }
.btn-download:disabled { background:#64748b; cursor:not-allowed; opacity:0.6; }
@@ -567,19 +575,54 @@ function PwrkCnsltnDocMngDialog({
.loading { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); font-size:14px; color:#475569; background:rgba(255,255,255,0.95); padding:16px 24px; border-radius:8px; box-shadow:0 4px 6px -1px rgba(0,0,0,0.1); border:1px solid #e2e8f0; pointer-events:none; }
iframe { flex:1; width:100%; border:0; }
</style>
<script>
function toggleFullScreen() {
if (!document.fullscreenElement) {
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen().catch(function() {
try {
window.moveTo(0, 0);
window.resizeTo(screen.availWidth, screen.availHeight);
} catch(e) {}
});
} else {
try {
window.moveTo(0, 0);
window.resizeTo(screen.availWidth, screen.availHeight);
} catch(e) {}
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
document.addEventListener("fullscreenchange", function() {
var btn = document.getElementById("btn_fullscreen");
if (!btn) return;
if (document.fullscreenElement) {
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>기본 화면';
} else {
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>전체 화면';
}
});
</script>
</head>
<body>
<header>
<h2>
<span>실무협의서 ${branchName} 참조 문서</span>
<span class="badge">읽기 전용 참조 모드</span>
<span>${headerTitle}</span>
${badgeText ? `<span class="badge">${badgeText}</span>` : ""}
<span class="badge-sub">읽기 전용 참조 모드</span>
</h2>
<div class="actions">
<button id="btn_fullscreen" class="btn btn-fullscreen" onclick="toggleFullScreen()"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>전체 화면</button>
<button id="btn_download" class="btn btn-download" disabled onclick="if (window.onDownloadClick) window.onDownloadClick()">HWPX 다운로드</button>
<button class="btn btn-close" onclick="window.close()">창 닫기</button>
</div>
</header>
<div id="loading" class="loading">웹한글 기안기 및 ${branchName} 초안 문서를 불러오는 중입니다...</div>
<div id="loading" class="loading">웹한글 기안기 및 문서를 불러오는 중입니다...</div>
<iframe id="viewer_frame" src="${bridgeSrc}"></iframe>
</body>
</html>
@@ -587,12 +630,8 @@ function PwrkCnsltnDocMngDialog({
popup.document.close();
try {
const response = await pwrkCnsltnDocCrtRsltService.openWebHwpLatest(
completedDraft.crtDmndUuid,
targetBranch,
);
const response = await fetchBlob();
const blob = response.data;
const fileName = `실무협의서_${targetBranch === "SP" ? "관로" : "시설"}.hwpx`;
const documentFile = new File([blob], fileName, { type: "application/haansofthwpx" });
// 다운로드 fallback 핸들러 바인딩
@@ -633,7 +672,11 @@ function PwrkCnsltnDocMngDialog({
window.clearInterval(checkTimer);
const loadingEl = popup.document.getElementById("loading");
if (loadingEl) loadingEl.style.display = "none";
ctrl.Open(documentFile, "HWPX", "", () => {});
ctrl.Open(documentFile, "HWPX", "", () => {
try {
(ctrl as Record<string, unknown>).EditMode = 0;
} catch {}
});
}
} catch {
window.clearInterval(checkTimer);
@@ -645,13 +688,71 @@ function PwrkCnsltnDocMngDialog({
console.error("새 창 문서 로드 실패:", err);
const loadingEl = popup.document.getElementById("loading");
if (loadingEl) {
loadingEl.textContent = `${branchName} 문서를 불러오지 못했습니다.`;
loadingEl.textContent = "문서를 불러오지 못했습니다.";
loadingEl.style.color = "#ef4444";
}
alert(`${branchName} 문서를 불러오지 못했습니다.`);
alert("문서를 불러오지 못했습니다.");
}
}
async function handleOpenBranchInNewWindow(targetBranch: "SP" | "SF") {
if (!completedDraft) {
alert("열람할 실무협의서 초안 정보가 없습니다.");
return;
}
const branchName = targetBranch === "SP" ? "관로 (SP)" : "시설 (SF)";
await openDocumentPopupWindow({
popupName: `pwrk_viewer_${targetBranch}`,
pageTitle: `실무협의서 [${branchName}] 참조 뷰어`,
headerTitle: `실무협의서 ${branchName} 참조 문서`,
badgeText: targetBranch === "SP" ? "관로 초안" : "시설 초안",
badgeColor: "#2563eb",
fileName: `실무협의서_${targetBranch === "SP" ? "관로" : "시설"}.hwpx`,
fetchBlob: () =>
pwrkCnsltnDocCrtRsltService.openWebHwpLatest(
completedDraft.crtDmndUuid,
targetBranch,
),
});
}
async function handleOpenVersionInNewWindow(item: PwrkCnsltnDocCrtRsltResponseDto) {
if (!completedDraft) {
alert("열람할 실무협의서 초안 정보가 없습니다.");
return;
}
const branch = item.branchSeCd || "MAIN";
const branchName =
branch === "SP"
? "관로 (SP)"
: branch === "SF"
? "시설 (SF)"
: "종합 실무협의서 (MAIN)";
const branchShort =
branch === "SP"
? "관로"
: branch === "SF"
? "시설"
: "종합";
await openDocumentPopupWindow({
popupName: `pwrk_viewer_${completedDraft.crtDmndUuid}_${branch}_v${item.verSn}`,
pageTitle: `실무협의서 [${branchShort}] 버전 ${item.verSn} 참조 뷰어`,
headerTitle: `실무협의서 ${branchName} (버전 ${item.verSn})`,
badgeText: `버전 ${item.verSn}`,
badgeColor: "#059669",
fileName: `실무협의서_${branchShort}_v${item.verSn}.hwpx`,
fetchBlob: () =>
pwrkCnsltnDocCrtRsltService.openWebHwpByVer(
completedDraft.crtDmndUuid,
item.verSn,
branch,
),
});
}
function handleOpenChange(nextOpen: boolean) {
if (!nextOpen) {
if (currentBranchSeCd === "MAIN") {
@@ -704,7 +805,6 @@ function PwrkCnsltnDocMngDialog({
queryKey: ["pwrk-cnsltn-doc-crt-rslt", "history", completedDraft.crtDmndUuid],
}),
]);
setSelectedVerSn(null);
alert(`실무협의서를 저장했습니다. (버전 ${savedDocument.verSn})`);
if (savedDocument.verSn <= AI_DRAFT_EVL_MAX_VER_SN) {
@@ -715,17 +815,10 @@ function PwrkCnsltnDocMngDialog({
async function handleDownload() {
if (!completedDraft) throw new Error("다운로드할 실무협의서 초안이 없습니다.");
const response =
selectedVerSn !== null
? await pwrkCnsltnDocCrtRsltService.download(
completedDraft.crtDmndUuid,
selectedVerSn,
targetBranchForApi,
)
: await pwrkCnsltnDocCrtRsltService.downloadLatest(
completedDraft.crtDmndUuid,
targetBranchForApi,
);
const response = await pwrkCnsltnDocCrtRsltService.downloadLatest(
completedDraft.crtDmndUuid,
targetBranchForApi,
);
const objectUrl = window.URL.createObjectURL(response.data);
const anchor = document.createElement("a");
anchor.href = objectUrl;
@@ -859,47 +952,37 @@ 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]);
// 저장 버튼은 해당 부서 작성 권한이 있는 경우 표시 (메인 기안기는 항상 최신 버전 유지)
const canSaveInCurrentView = canSaveCurrentBranch;
// 최신 버전을 열람 중인지 여부 (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
htmlFor="pwrk-version-history-select"
className="flex items-center gap-1 text-sm font-medium text-muted-foreground whitespace-nowrap"
>
<ExternalLink className="size-3.5" />
<span>변경이력:</span>
</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 ?? ""}
value={selectedHistoryKey}
onChange={(e) => {
const val = e.target.value;
if (!val) {
setSelectedVerSn(null);
return;
}
const num = Number(val);
const item = historyList.find((h) => h.verSn === num);
setSelectedHistoryKey(val);
if (!val) return;
const [branch, verStr] = val.split("_");
const verSn = Number(verStr);
const item = historyList.find(
(h) => (h.branchSeCd || "MAIN") === branch && h.verSn === verSn,
);
if (item) {
const targetBranch = item.branchSeCd || "MAIN";
setCurrentBranchSeCd(targetBranch);
setSelectedVerSn(item.verSn);
void handleOpenVersionInNewWindow(item);
}
}}
aria-label="변경이력 선택"
aria-label="변경이력 선택 (새 창 열기)"
>
<option value="">선택</option>
{historyList.map((item) => {
@@ -908,14 +991,42 @@ function PwrkCnsltnDocMngDialog({
? "[시설]"
: item.branchSeCd === "SP"
? "[관로]"
: "[통합]";
: "[종합]";
const dateText = item.crtCmptnDt
? ` (${item.crtCmptnDt.slice(0, 10)})`
: "";
return (
<option key={`${item.branchSeCd}_${item.verSn}`} value={item.verSn}>
{branchText} 버전 {item.verSn}
<option
key={`${item.branchSeCd || "MAIN"}_${item.verSn}`}
value={`${item.branchSeCd || "MAIN"}_${item.verSn}`}
>
{branchText} 버전 {item.verSn}{dateText}
</option>
);
})}
</select>
{selectedHistoryKey && (
<Button
type="button"
size="sm"
color="white"
className="h-8 px-2 text-xs flex items-center gap-1"
onClick={() => {
const [branch, verStr] = selectedHistoryKey.split("_");
const verSn = Number(verStr);
const item = historyList.find(
(h) => (h.branchSeCd || "MAIN") === branch && h.verSn === verSn,
);
if (item) {
void handleOpenVersionInNewWindow(item);
}
}}
title="선택된 버전 새 창으로 열기"
>
<ExternalLink className="size-3.5" />
<span>새 창 열기</span>
</Button>
)}
</div>
);
@@ -1060,7 +1171,7 @@ function PwrkCnsltnDocMngDialog({
<div className="min-w-0 flex-1">
<HangulGiangi
documentBlob={documentBlob}
documentKey={`${completedDraft.crtDmndUuid}_${currentBranchSeCd}_${selectedVerSn ?? "latest"}`}
documentKey={`${completedDraft.crtDmndUuid}_${currentBranchSeCd}_latest`}
documentFileName={documentFileName}
canSave={canSaveInCurrentView}
iframeClassName={editorIframeHeight}