From dbea497cc5370d7a2ba8bde651f18f17850c5360 Mon Sep 17 00:00:00 2001 From: bicochan Date: Mon, 7 Sep 2026 15:24:06 +0900 Subject: [PATCH] =?UTF-8?q?[as]=20feat:=20=EC=9B=B9=ED=95=9C=EA=B8=80=20?= =?UTF-8?q?=EA=B8=B0=EC=95=88=EA=B8=B0=20=EB=8D=B0=EB=AA=A8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(with-base-layout)/hangulgiangi/page.tsx | 3 + apps/as/next.config.ts | 10 + apps/as/public/hangulgiangi/demo/결재_표.hwpx | Bin 0 -> 14931 bytes .../as/public/hangulgiangi/demo/결재_표2.hwpx | Bin 0 -> 15445 bytes apps/as/public/hangulgiangi/demo/보고서.hwpx | Bin 0 -> 24434 bytes apps/as/public/webhwpctrl/hwpsdkmain.html | 41 +++ apps/as/public/webhwpctrl/index.html | 65 ++++ apps/as/src/pages/hangulgiangi/index.ts | 1 + .../ui/hangul-giangi-preview-page.tsx | 32 ++ apps/as/src/shared/config/env.ts | 4 + apps/as/src/widgets/hangulgiangi/index.ts | 23 ++ .../hangulgiangi/lib/fetch-demo-blob.ts | 21 ++ .../widgets/hangulgiangi/lib/get-hwp-ctrl.ts | 29 ++ .../lib/hangul-giangi-constants.ts | 25 ++ .../widgets/hangulgiangi/model/hwp-ctrl.ts | 58 ++++ .../hangulgiangi/ui/hangul-giangi-toolbar.tsx | 116 +++++++ .../widgets/hangulgiangi/ui/hangul-giangi.tsx | 321 ++++++++++++++++++ 17 files changed, 749 insertions(+) create mode 100644 apps/as/app/(main)/(with-base-layout)/hangulgiangi/page.tsx create mode 100644 apps/as/public/hangulgiangi/demo/결재_표.hwpx create mode 100644 apps/as/public/hangulgiangi/demo/결재_표2.hwpx create mode 100644 apps/as/public/hangulgiangi/demo/보고서.hwpx create mode 100644 apps/as/public/webhwpctrl/hwpsdkmain.html create mode 100644 apps/as/public/webhwpctrl/index.html create mode 100644 apps/as/src/pages/hangulgiangi/index.ts create mode 100644 apps/as/src/pages/hangulgiangi/ui/hangul-giangi-preview-page.tsx create mode 100644 apps/as/src/widgets/hangulgiangi/index.ts create mode 100644 apps/as/src/widgets/hangulgiangi/lib/fetch-demo-blob.ts create mode 100644 apps/as/src/widgets/hangulgiangi/lib/get-hwp-ctrl.ts create mode 100644 apps/as/src/widgets/hangulgiangi/lib/hangul-giangi-constants.ts create mode 100644 apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts create mode 100644 apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx create mode 100644 apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx diff --git a/apps/as/app/(main)/(with-base-layout)/hangulgiangi/page.tsx b/apps/as/app/(main)/(with-base-layout)/hangulgiangi/page.tsx new file mode 100644 index 000000000..95c057a58 --- /dev/null +++ b/apps/as/app/(main)/(with-base-layout)/hangulgiangi/page.tsx @@ -0,0 +1,3 @@ +import { HangulGiangiPreviewPage } from "@/pages/hangulgiangi"; + +export default HangulGiangiPreviewPage; diff --git a/apps/as/next.config.ts b/apps/as/next.config.ts index 606194380..e000661fb 100644 --- a/apps/as/next.config.ts +++ b/apps/as/next.config.ts @@ -2,6 +2,12 @@ import type { NextConfig } from "next"; const subDomain = process.env.NEXT_PUBLIC_SUB_DOMAIN || "http://localhost:13000"; +const webhwpCtrlBase = ( + process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "http://hwp.onasys.co.kr/webhwpctrl" +).replace(/\/$/, ""); + +const webhwpAssetPrefixes = ["js", "hwpsdk", "dist", "commonFrame", "wasm"] as const; + const nextConfig: NextConfig = { output: "standalone", reactStrictMode: true, @@ -30,6 +36,10 @@ const nextConfig: NextConfig = { source: "/cms/:path*", destination: `${subDomain}/cms/:path*`, }, + ...webhwpAssetPrefixes.map((prefix) => ({ + source: `/webhwpctrl/${prefix}/:path*`, + destination: `${webhwpCtrlBase}/${prefix}/:path*`, + })), ], fallback: [], }; diff --git a/apps/as/public/hangulgiangi/demo/결재_표.hwpx b/apps/as/public/hangulgiangi/demo/결재_표.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..3ace9b97ccffc954cac0b2c89d9b8658a5981447 GIT binary patch literal 14931 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 literal 0 HcmV?d00001 diff --git a/apps/as/public/hangulgiangi/demo/보고서.hwpx b/apps/as/public/hangulgiangi/demo/보고서.hwpx new file mode 100644 index 0000000000000000000000000000000000000000..d6617699e6d0fc3e5190ca343b74ad3a0aecd09a GIT binary patch literal 24434 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 Jh1edv{VxWkH|YQX literal 0 HcmV?d00001 diff --git a/apps/as/public/webhwpctrl/hwpsdkmain.html b/apps/as/public/webhwpctrl/hwpsdkmain.html new file mode 100644 index 000000000..d24c7eee7 --- /dev/null +++ b/apps/as/public/webhwpctrl/hwpsdkmain.html @@ -0,0 +1,41 @@ + + + + + + + + Hwp SDK Wasm Sample + + + + + + + + +
+
+
+ 구성 중 ... 0% + +
+
+ +
+
+ + + + + + diff --git a/apps/as/public/webhwpctrl/index.html b/apps/as/public/webhwpctrl/index.html new file mode 100644 index 000000000..8dc0fc313 --- /dev/null +++ b/apps/as/public/webhwpctrl/index.html @@ -0,0 +1,65 @@ + + + + + + + 웹한글 기안기 브리지 + + + + + + +
+ + + diff --git a/apps/as/src/pages/hangulgiangi/index.ts b/apps/as/src/pages/hangulgiangi/index.ts new file mode 100644 index 000000000..f66bdbbb3 --- /dev/null +++ b/apps/as/src/pages/hangulgiangi/index.ts @@ -0,0 +1 @@ +export { HangulGiangiPreviewPage } from "./ui/hangul-giangi-preview-page"; diff --git a/apps/as/src/pages/hangulgiangi/ui/hangul-giangi-preview-page.tsx b/apps/as/src/pages/hangulgiangi/ui/hangul-giangi-preview-page.tsx new file mode 100644 index 000000000..0989b99ee --- /dev/null +++ b/apps/as/src/pages/hangulgiangi/ui/hangul-giangi-preview-page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { HangulGiangi } from "@/widgets/hangulgiangi"; + +function HangulGiangiPreviewPage() { + return ( +
+
+

웹한글 기안기 미리보기

+

+ 샘플 툴바·BuildWebHwpCtrl 브리지 연동 검증용 페이지입니다. 메뉴에는 등록하지 않습니다. +

+

+ Demo 파일( + 보고서.hwpx,{" "} + 결재_표.hwpx 등)은{" "} + public/hangulgiangi/demo/에 배치하세요. +

+

+ iframe은 로컬{" "} + /webhwpctrl/index.html 브리지를 로드하고,{" "} + BuildWebHwpCtrl로 같은 origin에서 HwpCtrl을 + 노출합니다. 초기화 스크립트는{" "} + hwp.onasys.co.kr/webhwpctrl에서 가져옵니다. +

+
+ +
+ ); +} + +export { HangulGiangiPreviewPage }; diff --git a/apps/as/src/shared/config/env.ts b/apps/as/src/shared/config/env.ts index f06c95d2b..542d546ad 100644 --- a/apps/as/src/shared/config/env.ts +++ b/apps/as/src/shared/config/env.ts @@ -10,3 +10,7 @@ export const REFRESH_TOKEN = process.env.NEXT_PUBLIC_REFRESH_TOKEN || "refresh-t export const AUTH_USER_ID = process.env.NEXT_PUBLIC_AUTH_USER_ID || "token-id"; export const COOKIE_DOMAIN = process.env.NEXT_PUBLIC_COOKIE_DOMAIN; export const AS_DASHBOARD_BBS_ID = process.env.NEXT_PUBLIC_AS_DASHBOARD_BBS_ID; + +/** 웹한글 기안기(WebHWP) 컨트롤 URL */ +export const WEBHYP_CTRL_URL = + process.env.NEXT_PUBLIC_WEBHYP_CTRL_URL || "http://hwp.onasys.co.kr/webhwpctrl"; diff --git a/apps/as/src/widgets/hangulgiangi/index.ts b/apps/as/src/widgets/hangulgiangi/index.ts new file mode 100644 index 000000000..e61f86007 --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/index.ts @@ -0,0 +1,23 @@ +export { getHwpCtrl } from "./lib/get-hwp-ctrl"; +export { + APPROVAL_FIELD_NAME, + DEFAULT_BRIDGE_URL, + DEFAULT_CTRL_URL, + DEFAULT_DEMO_BASE_URL, + DEFAULT_IFRAME_HEIGHT_CLASS, + DEMO_FILES, + HANGULGIANGI_READY_MESSAGE_TYPE, + HWP_CTRL_IFRAME_NAME, + SIGN_FIELD_NAME, +} from "./lib/hangul-giangi-constants"; +export type { + HwpCtrl, + HwpCtrlCallback, + HwpCtrlCallbackResult, + HwpCtrlFrameWindow, +} from "./model/hwp-ctrl"; +export { HangulGiangi, type HangulGiangiProps } from "./ui/hangul-giangi"; +export { + HangulGiangiToolbar, + type HangulGiangiToolbarProps, +} from "./ui/hangul-giangi-toolbar"; diff --git a/apps/as/src/widgets/hangulgiangi/lib/fetch-demo-blob.ts b/apps/as/src/widgets/hangulgiangi/lib/fetch-demo-blob.ts new file mode 100644 index 000000000..3ccbb16b8 --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/lib/fetch-demo-blob.ts @@ -0,0 +1,21 @@ +/** + * Demo 정적 파일을 부모 origin에서 fetch해 Blob으로 반환한다. + * iframe origin에서 URL을 직접 Open하면 CORS로 실패할 수 있어 Blob Open을 우선한다. + */ +async function fetchDemoBlob(demoBaseUrl: string, fileName: string): Promise { + const base = demoBaseUrl.endsWith("/") ? demoBaseUrl : `${demoBaseUrl}/`; + const url = `${base}${encodeURIComponent(fileName)}`; + const response = await fetch(url); + + if (!response.ok) + throw new Error(`Demo 파일을 불러오지 못했습니다: ${fileName} (${response.status})`); + + return response.blob(); +} + +function getFormatFromFileName(fileName: string): string { + const ext = fileName.split(".").pop()?.toUpperCase(); + return ext ?? ""; +} + +export { fetchDemoBlob, getFormatFromFileName }; diff --git a/apps/as/src/widgets/hangulgiangi/lib/get-hwp-ctrl.ts b/apps/as/src/widgets/hangulgiangi/lib/get-hwp-ctrl.ts new file mode 100644 index 000000000..58757ee70 --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/lib/get-hwp-ctrl.ts @@ -0,0 +1,29 @@ +import type { HwpCtrl, HwpCtrlFrameWindow } from "../model/hwp-ctrl"; + +import { HWP_CTRL_IFRAME_NAME } from "./hangul-giangi-constants"; + +interface WindowWithNamedHwpFrame extends Window { + Document_HwpCtrl?: Window & HwpCtrlFrameWindow; +} + +/** + * iframe ref 우선, 없으면 named frame 폴백으로 HwpCtrl을 반환한다. + * 로컬 `/webhwpctrl` 브리지는 same-origin이라 contentWindow 접근이 가능하다. + */ +function getHwpCtrl(iframe: HTMLIFrameElement | null): HwpCtrl | null { + if (typeof window === "undefined") return null; + + try { + const fromRef = iframe?.contentWindow as HwpCtrlFrameWindow | null | undefined; + if (fromRef?.HwpCtrl) return fromRef.HwpCtrl; + + const namedFrame = (window as WindowWithNamedHwpFrame)[HWP_CTRL_IFRAME_NAME]; + if (namedFrame?.HwpCtrl) return namedFrame.HwpCtrl; + + return null; + } catch { + return null; + } +} + +export { getHwpCtrl }; diff --git a/apps/as/src/widgets/hangulgiangi/lib/hangul-giangi-constants.ts b/apps/as/src/widgets/hangulgiangi/lib/hangul-giangi-constants.ts new file mode 100644 index 000000000..15a330f81 --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/lib/hangul-giangi-constants.ts @@ -0,0 +1,25 @@ +import { WEBHYP_CTRL_URL } from "@/shared/config"; + +/** iframe name — 한컴 named-iframe / 브리지 contentWindow 접근용 */ +export const HWP_CTRL_IFRAME_NAME = "Document_HwpCtrl"; + +/** 로컬 BuildWebHwpCtrl 브리지 (same-origin) */ +export const DEFAULT_BRIDGE_URL = "/webhwpctrl/index.html"; + +/** 기안기 서버 base URL (브리지 BuildWebHwpCtrl baseurl / rewrite destination) */ +export const DEFAULT_CTRL_URL = WEBHYP_CTRL_URL; + +/** Demo 정적 파일 base path (`apps/as/public/hangulgiangi/demo/`) */ +export const DEFAULT_DEMO_BASE_URL = "/hangulgiangi/demo/"; + +export const DEMO_FILES = { + report: "보고서.hwpx", + approvalTable1: "결재_표.hwpx", + approvalTable2: "결재_표2.hwpx", + signImage: "sign.png", +} as const; + +export const APPROVAL_FIELD_NAME = "결재영역"; +export const SIGN_FIELD_NAME = "결재1"; + +export const HANGULGIANGI_READY_MESSAGE_TYPE = "hangulgiangi-ready"; diff --git a/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts b/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts new file mode 100644 index 000000000..435e4fa0d --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts @@ -0,0 +1,58 @@ +/** 한컴 웹한글 기안기 HwpCtrl 콜백 결과 (문서화되지 않은 필드 허용) */ +export interface HwpCtrlCallbackResult { + result?: boolean | string | number; + message?: string; + [key: string]: unknown; +} + +export type HwpCtrlCallback = (res: HwpCtrlCallbackResult) => void; + +/** + * 샘플(v3_sample)에서 사용하는 HwpCtrl 메서드만 선언. + * 실제 런타임 객체는 iframe contentWindow에 존재한다. + */ +export interface HwpCtrl { + Open: ( + source: string | Blob | File, + format: string, + option: string, + callback?: HwpCtrlCallback, + extra?: Record, + ) => void; + Insert: ( + source: string | Blob | File, + format: string, + option: string, + callback?: HwpCtrlCallback, + ) => void; + InsertPicture: ( + source: string | Blob | File, + embedded: boolean, + sizeOption: number, + reverse: boolean, + watermark: boolean, + effect: number, + width: number, + height: number, + callback?: HwpCtrlCallback, + ) => void; + MoveToField: ( + fieldName: string, + text: boolean, + start: boolean, + select: boolean, + ) => void; + Run: (action: string) => void; + SaveAs: ( + fileName: string, + format: string, + option: string, + callback?: HwpCtrlCallback, + extra?: Record, + ) => void; + Clear: () => void; +} + +export interface HwpCtrlFrameWindow { + HwpCtrl?: HwpCtrl; +} diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx new file mode 100644 index 000000000..a28836a02 --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi-toolbar.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +import { Button, cn } from "@common/components"; + +interface HangulGiangiToolbarProps { + isApprovalMenuOpen: boolean; + isBusy?: boolean; + onOpen: () => void; + onToggleApprovalMenu: () => void; + onCloseApprovalMenu: () => void; + onApproval1: () => void; + onApproval2: () => void; + onSign: () => void; + onSave: () => void; + onClear: () => void; + onPickLocalFile: () => void; +} + +function HangulGiangiToolbar({ + isApprovalMenuOpen, + isBusy = false, + onOpen, + onToggleApprovalMenu, + onCloseApprovalMenu, + onApproval1, + onApproval2, + onSign, + onSave, + onClear, + onPickLocalFile, +}: HangulGiangiToolbarProps) { + const approvalMenuRef = useRef(null); + + useEffect(() => { + if (!isApprovalMenuOpen) return; + + function handlePointerDown(event: MouseEvent) { + const target = event.target as Node; + if (approvalMenuRef.current?.contains(target)) return; + onCloseApprovalMenu(); + } + + document.addEventListener("mousedown", handlePointerDown); + return () => document.removeEventListener("mousedown", handlePointerDown); + }, [isApprovalMenuOpen, onCloseApprovalMenu]); + + return ( +
+ + +
+ +
+ + +
+
+ + + + + +
+ ); +} + +export { HangulGiangiToolbar }; +export type { HangulGiangiToolbarProps }; diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx new file mode 100644 index 000000000..7f2a7516e --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx @@ -0,0 +1,321 @@ +"use client"; + +import { type ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; + +import { WEBHYP_CTRL_URL } from "@/shared/config"; +import { cn } from "@common/components"; + +import { fetchDemoBlob } from "../lib/fetch-demo-blob"; +import { getHwpCtrl } from "../lib/get-hwp-ctrl"; +import { + APPROVAL_FIELD_NAME, + DEFAULT_BRIDGE_URL, + DEFAULT_DEMO_BASE_URL, + DEMO_FILES, + HANGULGIANGI_READY_MESSAGE_TYPE, + HWP_CTRL_IFRAME_NAME, + SIGN_FIELD_NAME, +} from "../lib/hangul-giangi-constants"; +import type { HwpCtrl } from "../model/hwp-ctrl"; + +import { HangulGiangiToolbar } from "./hangul-giangi-toolbar"; + +interface HangulGiangiProps { + /** 로컬 BuildWebHwpCtrl 브리지 URL (기본 `/webhwpctrl/index.html`) */ + bridgeUrl?: string; + /** @deprecated bridgeUrl 사용. 하위 호환용 alias */ + ctrlUrl?: string; + /** 기안기 서버 base URL (브리지 `?baseurl=` 로 전달) */ + ctrlBaseUrl?: string; + demoBaseUrl?: string; + className?: string; + onReady?: () => void; +} + +function resolveHwpCtrl(iframe: HTMLIFrameElement | null, isCtrlReady: boolean): HwpCtrl | null { + const ctrl = getHwpCtrl(iframe); + if (!ctrl) { + alert( + isCtrlReady + ? "웹한글 기안기 컨트롤을 찾지 못했습니다. 브리지 페이지를 확인하세요." + : "웹한글 기안기가 아직 초기화되지 않았습니다. 잠시 후 다시 시도하세요.", + ); + return null; + } + return ctrl; +} + +function buildBridgeSrc(bridgeUrl: string, ctrlBaseUrl: string): string { + const base = ctrlBaseUrl.replace(/\/$/, ""); + const url = new URL( + bridgeUrl, + typeof window !== "undefined" ? window.location.origin : "http://localhost", + ); + url.searchParams.set("baseurl", base); + // URL()은 absolute path면 pathname만 쓰므로, 상대 경로 브리지는 pathname+search만 반환 + if (bridgeUrl.startsWith("/")) return `${url.pathname}${url.search}`; + return url.toString(); +} + +function HangulGiangi({ + bridgeUrl, + ctrlUrl, + ctrlBaseUrl = WEBHYP_CTRL_URL, + demoBaseUrl = DEFAULT_DEMO_BASE_URL, + className, + onReady, +}: HangulGiangiProps) { + const resolvedBridgeUrl = bridgeUrl ?? ctrlUrl ?? DEFAULT_BRIDGE_URL; + const iframeRef = useRef(null); + const fileInputRef = useRef(null); + + const [isCtrlReady, setIsCtrlReady] = useState(false); + const [isOpened, setIsOpened] = useState(false); + const [isApprovalAdded, setIsApprovalAdded] = useState(false); + const [isApprovalMenuOpen, setIsApprovalMenuOpen] = useState(false); + const [isBusy, setIsBusy] = useState(false); + const [statusMessage, setStatusMessage] = useState(null); + + const iframeSrc = useMemo( + () => buildBridgeSrc(resolvedBridgeUrl, ctrlBaseUrl), + [resolvedBridgeUrl, ctrlBaseUrl], + ); + + useEffect(() => { + function handleMessage(event: MessageEvent) { + if (event.origin !== window.location.origin) return; + if (event.data?.type !== HANGULGIANGI_READY_MESSAGE_TYPE) return; + if (iframeRef.current && event.source !== iframeRef.current.contentWindow) return; + + setIsCtrlReady(true); + setStatusMessage("기안기 초기화가 완료되었습니다."); + onReady?.(); + } + + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [onReady]); + + // postMessage 누락 대비: HwpCtrl 존재 poll + useEffect(() => { + if (isCtrlReady) return; + + const startedAt = Date.now(); + const timer = window.setInterval(() => { + if (getHwpCtrl(iframeRef.current)) { + setIsCtrlReady(true); + setStatusMessage("기안기 초기화가 완료되었습니다."); + onReady?.(); + window.clearInterval(timer); + return; + } + if (Date.now() - startedAt > 60_000) window.clearInterval(timer); + }, 500); + + return () => window.clearInterval(timer); + }, [isCtrlReady, onReady]); + + const handleOpen = useCallback(async () => { + const ctrl = resolveHwpCtrl(iframeRef.current, isCtrlReady); + if (!ctrl) return; + + setIsBusy(true); + setStatusMessage(null); + try { + const blob = await fetchDemoBlob(demoBaseUrl, DEMO_FILES.report); + ctrl.Open(blob, "HWPX", "lock:false", (res) => { + console.log("Open 보고서.hwpx:", res); + }); + setIsOpened(true); + setIsApprovalAdded(false); + setStatusMessage("문서를 열었습니다."); + } catch (error) { + console.error(error); + alert( + error instanceof Error + ? error.message + : "문서 열기에 실패했습니다. Demo 파일 경로를 확인하세요.", + ); + } finally { + setIsBusy(false); + } + }, [demoBaseUrl, isCtrlReady]); + + const insertApprovalTable = useCallback( + async (fileName: string) => { + if (!isOpened) { + alert("먼저 '열기' 버튼을 눌러주세요."); + return; + } + + const ctrl = resolveHwpCtrl(iframeRef.current, isCtrlReady); + if (!ctrl) return; + + setIsBusy(true); + setStatusMessage(null); + try { + ctrl.MoveToField(APPROVAL_FIELD_NAME, true, true, false); + ctrl.Run("SelectAll"); + ctrl.Run("Delete"); + ctrl.MoveToField(APPROVAL_FIELD_NAME, true, true, false); + + const blob = await fetchDemoBlob(demoBaseUrl, fileName); + ctrl.Insert(blob, "HWPX", "", (res) => { + console.log(`Insert ${fileName}:`, res); + }); + + setIsApprovalAdded(true); + setIsApprovalMenuOpen(false); + setStatusMessage(`${fileName} 결재 표를 삽입했습니다.`); + } catch (error) { + console.error(error); + alert( + error instanceof Error + ? error.message + : "결재 표 삽입에 실패했습니다. Demo 파일 경로를 확인하세요.", + ); + } finally { + setIsBusy(false); + } + }, + [demoBaseUrl, isCtrlReady, isOpened], + ); + + const handleSign = useCallback(async () => { + if (!isApprovalAdded) { + alert("먼저 '결재 추가' 버튼을 눌러주세요."); + return; + } + + const ctrl = resolveHwpCtrl(iframeRef.current, isCtrlReady); + if (!ctrl) return; + + setIsBusy(true); + setStatusMessage(null); + try { + ctrl.MoveToField(SIGN_FIELD_NAME, true, true, false); + const blob = await fetchDemoBlob(demoBaseUrl, DEMO_FILES.signImage); + ctrl.InsertPicture(blob, true, 1, false, false, 0, 90, 30, (res) => { + console.log("Insert sign.png:", res); + }); + setStatusMessage("결재 이미지를 삽입했습니다."); + } catch (error) { + console.error(error); + alert( + error instanceof Error + ? error.message + : "결재 이미지 삽입에 실패했습니다. Demo 파일 경로를 확인하세요.", + ); + } finally { + setIsBusy(false); + } + }, [demoBaseUrl, isApprovalAdded, isCtrlReady]); + + const handleSave = useCallback(() => { + const ctrl = resolveHwpCtrl(iframeRef.current, isCtrlReady); + if (!ctrl) return; + + ctrl.SaveAs( + "sample.hwp", + "HWP", + "download:true;", + (res) => { + console.log(res); + setStatusMessage("HWP 저장을 요청했습니다."); + }, + { tempUrl: typeof window !== "undefined" ? window.location.href : "" }, + ); + }, [isCtrlReady]); + + const handleClear = useCallback(() => { + const ctrl = resolveHwpCtrl(iframeRef.current, isCtrlReady); + if (!ctrl) return; + + ctrl.Clear(); + setIsOpened(false); + setIsApprovalAdded(false); + setIsApprovalMenuOpen(false); + setStatusMessage("문서를 클리어했습니다."); + }, [isCtrlReady]); + + const handleLocalFileChange = useCallback( + (event: ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + const ctrl = resolveHwpCtrl(iframeRef.current, isCtrlReady); + if (!ctrl) { + event.target.value = ""; + return; + } + + const blobData = new Blob([file], { type: file.type }); + ctrl.Open(blobData, "", "lock:false", (res) => { + ctrl.MoveToField(file.name, true, true, true); + console.log(res); + }); + + setIsOpened(true); + setIsApprovalAdded(false); + setStatusMessage(`${file.name} 파일을 열었습니다.`); + event.target.value = ""; + }, + [isCtrlReady], + ); + + const isToolbarBusy = isBusy || !isCtrlReady; + + return ( +
+ setIsApprovalMenuOpen((prev) => !prev)} + onCloseApprovalMenu={() => setIsApprovalMenuOpen(false)} + onApproval1={() => void insertApprovalTable(DEMO_FILES.approvalTable1)} + onApproval2={() => void insertApprovalTable(DEMO_FILES.approvalTable2)} + onSign={() => void handleSign()} + onSave={handleSave} + onClear={handleClear} + onPickLocalFile={() => fileInputRef.current?.click()} + /> + + {statusMessage ? ( +

+ {statusMessage} +

+ ) : !isCtrlReady ? ( +

+ 기안기 초기화 중… +

+ ) : null} + +