From 56688eb1a6decd838e1140375f6b7073a116be0b Mon Sep 17 00:00:00 2001 From: jangwoong Date: Fri, 23 Oct 2026 14:03:46 +0900 Subject: [PATCH] =?UTF-8?q?[as=20fr]=20=EB=B3=91=ED=95=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9B=B9=EA=B8=B0=EC=95=88=EA=B8=B0=20=EC=83=88=EC=B0=BD?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20SP,=20SF=20=EC=B0=B8=EC=A1=B0=20=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=ED=95=98=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...cnsltn-floating-action-button-mechanism.md | 302 ++++++++++++++++++ ...•ฉ์ž_์›น๊ธฐ์•ˆ๊ธฐ_๋ฌธ์„œ์ดˆ๊ธฐํ™”_์ด์Šˆ_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ์•ˆ.md | 183 +++++++++++ .../elapr/ui/elapr-approval-dialog.tsx | 2 +- .../components/pwrk-cnsltn-doc-mng-dialog.tsx | 215 ++++++++++++- apps/as/src/widgets/hangulgiangi/index.ts | 4 + .../hangulgiangi/lib/build-bridge-src.ts | 20 ++ .../widgets/hangulgiangi/model/hwp-ctrl.ts | 1 + .../widgets/hangulgiangi/ui/hangul-giangi.tsx | 11 +- 8 files changed, 712 insertions(+), 26 deletions(-) create mode 100644 apps/as/docs/pm2/11.fnrs-cnsltn-floating-action-button-mechanism.md create mode 100644 apps/as/docs/pwrk-cnsltn/11.๋ณ‘ํ•ฉ์ž_์›น๊ธฐ์•ˆ๊ธฐ_๋ฌธ์„œ์ดˆ๊ธฐํ™”_์ด์Šˆ_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ์•ˆ.md create mode 100644 apps/as/src/widgets/hangulgiangi/lib/build-bridge-src.ts diff --git a/apps/as/docs/pm2/11.fnrs-cnsltn-floating-action-button-mechanism.md b/apps/as/docs/pm2/11.fnrs-cnsltn-floating-action-button-mechanism.md new file mode 100644 index 000000000..63c8e9df1 --- /dev/null +++ b/apps/as/docs/pm2/11.fnrs-cnsltn-floating-action-button-mechanism.md @@ -0,0 +1,302 @@ +# ์žฌ์›ํ˜‘์˜(fnrs-cnsltn) ํ”Œ๋กœํŒ… ์•ก์…˜ ๋ฒ„ํŠผ(FloatingActionButtons) ํ˜ธ์ถœ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๋ถ„์„ + +## 1. ๊ฐœ์š” (Overview) + +๋ณธ ๋ฌธ์„œ๋Š” **๋””์ง€ํ„ธ ํ–‰์ •์ง€์›(Administrative Support, AS)** ๋ชจ๋“ˆ์˜ **์žฌ์›ํ˜‘์˜ ์ƒ์„ธ ์กฐํšŒ ํ™”๋ฉด(`FnrsViewPage`)** ํ•˜๋‹จ์— ์œ„์น˜ํ•œ [FloatingActionButtons](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx) ์ปดํฌ๋„ŒํŠธ์˜ ์•ก์…˜ ์ฒ˜๋ฆฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ƒ์„ธํžˆ ๋ถ„์„ํ•œ ๊ธฐ์ˆ  ๋ฌธ์„œ์ž…๋‹ˆ๋‹ค. + +์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด ํ•˜๋‹จ ์•ก์…˜๋ฐ”์—์„œ **๋ฐ˜๋ ค(`rjct`) ๋ฒ„ํŠผ**์„ ํด๋ฆญํ–ˆ์„ ๋•Œ ์‹คํ–‰๋˜๋Š” `handleRjctClick()` ํ•จ์ˆ˜๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ์ค‘์•™ ์ง‘์ค‘์‹ ๋””์ŠคํŒจ์ฒ˜์ธ `executeAction()`๊ณผ ๋ฃฉ์—… ํ…Œ์ด๋ธ”(`buttonActionHandlers`)์„ ๊ฑฐ์ณ ์ตœ์ข… API ํ˜ธ์ถœ ํ•จ์ˆ˜์ธ `handleRejectPrgrsStts()`๋กœ ๋ผ์šฐํŒ…๋˜๋Š” ์ „ ๊ณผ์ •์„ ์ถ”์ ํ•˜๊ณ , ์ ์šฉ๋œ ์•„ํ‚คํ…์ฒ˜ ๋””์ž์ธ ํŒจํ„ด์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 2. ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๋””์ž์ธ ํŒจํ„ด + +[FloatingActionButtons](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx)๋Š” ์ˆ˜๋งŽ์€ ํ–‰์ • ์—…๋ฌด ์•ก์…˜(์ €์žฅ, ์‹ ์ฒญ, ์‚ญ์ œ, ํ™˜๊ฒฝ์ฒญ ์ œ์ถœ, ์Šน์ธ, ๋ฐ˜๋ ค, ๊ฒ€ํ† ์˜๊ฒฌ ํšŒ์‹ , ๋ณด์™„์š”์ฒญ, ๊ธฐ์ˆ ๊ฒ€ํ† ์š”์ฒญ ๋“ฑ)์„ ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด **์ „๋žต ํŒจํ„ด(Strategy Pattern)**๊ณผ **๋””์ŠคํŒจ์ฒ˜(Dispatcher) ํŒจํ„ด**์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์„ค๊ณ„๋˜์—ˆ์Šต๋‹ˆ๋‹ค. + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ UI ์ปดํฌ๋„ŒํŠธ ์ด๋ฒคํŠธ (์˜ˆ: handleRjctClick) โ”‚ +โ”‚ - ์‚ฌ์šฉ์ž Confirm ํ™•์ธ โ”‚ +โ”‚ - ์š”์ฒญ DTO(DocPrgrsUpdateRequestDto) ์กฐ๋ฆฝ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ์ค‘์•™ ์•ก์…˜ ๋””์ŠคํŒจ์ฒ˜ (executeAction) โ”‚ +โ”‚ - ์ค‘๋ณต ํด๋ฆญ ๋ฐฉ์ง€ (isProcessing ์ƒํƒœ ๊ด€๋ฆฌ) โ”‚ +โ”‚ - buttonActionHandlers ๋ฃฉ์—… ํ…Œ์ด๋ธ”์—์„œ ์ ์ ˆํ•œ Strategy ํ•จ์ˆ˜ ๊ฒ€์ƒ‰ โ”‚ +โ”‚ - ์ผ์›ํ™”๋œ ์„ฑ๊ณต ์ฒ˜๋ฆฌ (Alert ๋ฉ”์‹œ์ง€, React Query ์บ์‹œ ๋ฌดํšจํ™”) โ”‚ +โ”‚ - ์ผ์›ํ™”๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ (Axios ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”์ถœ ๋ฐ Alert) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ action: "rjct" ํ‚ค๋กœ ์กฐํšŒ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๋ฃฉ์—… ํ…Œ์ด๋ธ” (buttonActionHandlers: Record) โ”‚ +โ”‚ - rjct: handleRejectPrgrsStts โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ–ผ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ ๊ฐœ๋ณ„ ์•ก์…˜ ํ•ธ๋“ค๋Ÿฌ (handleRejectPrgrsStts) โ”‚ +โ”‚ - fnrsCnsltnMngService.rejectPrgrsStts(payload) ๋ฐฑ์—”๋“œ API ํ˜ธ์ถœ โ”‚ +โ”‚ - context.onSuccess() / context.onError() ์ฝœ๋ฐฑ ํŠธ๋ฆฌ๊ฑฐ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2.1 ์ ์šฉ๋œ ํ•ต์‹ฌ ์›์น™ ๋ฐ ์ด์  +1. **๋‹จ์ผ ์ฑ…์ž„ ์›์น™ (SRP)**: + - ๊ฐ UI ์ด๋ฒคํŠธ ํ•จ์ˆ˜(`handleRjctClick`)๋Š” ์‚ฌ์šฉ์ž ํ™•์ธ ๋ฐ ํŽ˜์ด๋กœ๋“œ ๋นŒ๋“œ๋งŒ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + - ์ค‘์•™ ๋””์ŠคํŒจ์ฒ˜(`executeAction`)๋Š” ๋กœ๋”ฉ ์ƒํƒœ, ์บ์‹œ ๊ฐฑ์‹ , ๊ณตํ†ต ์—๋Ÿฌ ํ•ธ๋“ค๋ง์„ ์ „๋‹ดํ•ฉ๋‹ˆ๋‹ค. + - ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ํ†ต์‹ ์€ ์ˆœ์ˆ˜ ๋น„๋™๊ธฐ ํ•จ์ˆ˜(`handleRejectPrgrsStts`)๋กœ ๊ฒฉ๋ฆฌ๋ฉ๋‹ˆ๋‹ค. +2. **๊ฐœ๋ฐฉ-ํ์‡„ ์›์น™ (OCP)**: + - ์‹ ๊ทœ ์•ก์…˜ ๋ฒ„ํŠผ์ด ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ `buttonActionHandlers` ๋ ˆ์ฝ”๋“œ์— ์ƒˆ๋กœ์šด ํ‚ค-ํ•ธ๋“ค๋Ÿฌ ์Œ๋งŒ ๋“ฑ๋กํ•˜๋ฉด ๋˜๋ฉฐ, ๊ณตํ†ต ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋‚˜ ์บ์‹œ ๋ฌดํšจํ™” ๋กœ์ง์„ ์ค‘๋ณต ๊ตฌํ˜„ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. +3. **๋ฌด๊ฒฐ์„ฑ ๋ฐ ์•ˆ์ •์„ฑ**: + - `setIsProcessing(true)`๋ฅผ ํ†ตํ•ด ๋„คํŠธ์›Œํฌ ์š”์ฒญ ์ง„ํ–‰ ์ค‘ ๋‹ค์ค‘ ํด๋ฆญ(Double Submit)์„ ์›์ฒœ ์ฐจ๋‹จํ•ฉ๋‹ˆ๋‹ค. + - ์ž‘์—… ์™„๋ฃŒ ํ›„ `queryClient.invalidateQueries`๋ฅผ ํ†ตํ•ด ์ƒ์„ธ ๋ฐ์ดํ„ฐ ๋ฐ ์ „์ž๊ฒฐ์žฌ ๋ถ„๊ธฐ ์ƒํƒœ๋ฅผ ์ตœ์‹  ์ƒํƒœ๋กœ ์ฆ‰์‹œ ๋™๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 3. ์ „์ฒด ํ˜ธ์ถœ ํ๋ฆ„ ์‹œํ€€์Šค ๋‹ค์ด์–ด๊ทธ๋žจ (Sequence Diagram) + +```mermaid +sequenceDiagram + autonumber + actor User as ์‚ฌ์šฉ์ž + participant UI as FloatingActionButtons + participant Rjct as handleRjctClick + participant Exec as executeAction (๋””์ŠคํŒจ์ฒ˜) + participant Table as buttonActionHandlers (๋ฃฉ์—…) + participant Handler as handleRejectPrgrsStts + participant Service as fnrsCnsltnMngService + participant Backend as ๋ฐฑ์—”๋“œ REST API + participant Query as TanStack QueryClient + + User->>UI: [๋ฐ˜๋ ค] ๋ฒ„ํŠผ ํด๋ฆญ + UI->>Rjct: handleRjctClick() ํŠธ๋ฆฌ๊ฑฐ + Rjct->>User: confirm("ํ•ด๋‹น ์‹ ์ฒญ ๊ฑด์„ ๋ฐ˜๋ คํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?") + User-->>Rjct: ํ™•์ธ (true) + + Note over Rjct: DocPrgrsUpdateRequestDto ์ƒ์„ฑ
{ docNo, docSeComCd, prgrsSttsComCd: "SV", rgtrUuid } + + Rjct->>Exec: executeAction({ action: "rjct", request: payload, successMessage: "๋ฐ˜๋ ค๋˜์—ˆ์Šต๋‹ˆ๋‹ค." }) + + rect rgb(240, 245, 255) + Note over Exec: 1. setIsProcessing(true)
2. const handler = buttonActionHandlers["rjct"] + Exec->>Table: buttonActionHandlers["rjct"] ๊ฒ€์ƒ‰ + Table-->>Exec: handleRejectPrgrsStts ๋ฐ˜ํ™˜ + end + + Exec->>Handler: handler({ request, data, onSuccess, onError }) ์‹คํ–‰ + + Handler->>Service: rejectPrgrsStts(context.request) ํ˜ธ์ถœ + Service->>Backend: PUT /api/v1/as/fnrs-cnsltn-mng/reject-prgrs-stts + Backend-->>Service: 200 OK ์‘๋‹ต + Service-->>Handler: ์™„๋ฃŒ ๋ฐ˜ํ™˜ + + Handler->>Exec: context.onSuccess() ์ฝœ๋ฐฑ ํ˜ธ์ถœ + + rect rgb(245, 255, 240) + Note over Exec: onSuccess ํ›„์† ์ฒ˜๋ฆฌ ์‹คํ–‰ + Exec->>User: alert("๋ฐ˜๋ ค๋˜์—ˆ์Šต๋‹ˆ๋‹ค.") + Exec->>Query: invalidateQueries({ queryKey: fnrsCnsltnKeys.detail(...) }) + Exec->>Query: invalidateQueries({ queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH] }) + Note over Query: ๋ฐฑ์—”๋“œ ์ตœ์‹  ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์žฌ์กฐํšŒ ๋ฐ ํ™”๋ฉด UI ๊ฐฑ์‹  (์ƒํƒœ: ST -> SV) + end + + Note over Exec: finally { setIsProcessing(false) } +``` + +--- + +## 4. ๋‹จ๊ณ„๋ณ„ ์ƒ์„ธ ์†Œ์Šค ์ฝ”๋“œ ๋ถ„์„ + +### 4.1 Step 1: UI ์ด๋ฒคํŠธ ํŠธ๋ฆฌ๊ฑฐ ๋ฐ ํŽ˜์ด๋กœ๋“œ ์กฐ๋ฆฝ (`handleRjctClick`) + +์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด์˜ ๋ฐ˜๋ ค ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด [handleRjctClick](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L474-L489)์ด ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค. + +```typescript +// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 474~489) +async function handleRjctClick() { + // 1. ์‚ฌ์šฉ์ž ์žฌํ™•์ธ ๋Œ€ํ™”์ƒ์ž ๋…ธ์ถœ + if (!(await confirm("ํ•ด๋‹น ์‹ ์ฒญ ๊ฑด์„ ๋ฐ˜๋ คํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?"))) { + return; + } + + // 2. ๋ฐฑ์—”๋“œ ์ง„ํ–‰์ƒํƒœ ์—…๋ฐ์ดํŠธ DTO ๋นŒ๋“œ (์ž„์‹œ์ €์žฅ 'SV' ์ƒํƒœ๋กœ ๋กค๋ฐฑ) + const payload: DocPrgrsUpdateRequestDto = { + docNo, // ๋Œ€์ƒ ๋ฌธ์„œ๋ฒˆํ˜ธ + docSeComCd, // ๋ฌธ์„œ๊ตฌ๋ถ„ ๊ณตํ†ต์ฝ”๋“œ + prgrsSttsComCd: "SV", // ๋ณ€๊ฒฝํ•  ์ง„ํ–‰์ƒํƒœ: ์ž‘์„ฑ์ค‘/์ž„์‹œ์ €์žฅ(SV) + rgtrUuid: userInfo?.userUnqId, // ์ฒ˜๋ฆฌ์ž ๊ณ ์œ  ์‹๋ณ„์ž + }; + + // 3. ์ค‘์•™ ๋””์ŠคํŒจ์ฒ˜ ํ˜ธ์ถœ + await executeAction({ + action: FNRS_CNSLTN_VIEW_BUTTON_ACTION.rjct, // "rjct" ์ƒ์ˆ˜ + request: payload, + successMessage: "๋ฐ˜๋ ค๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + }); +} +``` + +* **ํ•ต์‹ฌ ํฌ์ธํŠธ**: ๋ฐ˜๋ ค ์ฒ˜๋ฆฌ๋Š” ๋ฌธ์„œ์˜ ์ง„ํ–‰ ์ƒํƒœ๋ฅผ ์‹ ์ฒญ์™„๋ฃŒ(`ST`)์—์„œ ์ž‘์„ฑ์ž(์ง€์ž์ฒด)๊ฐ€ ๋‹ค์‹œ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๋Š” ์ž„์‹œ์ €์žฅ/์ž‘์„ฑ์ค‘(`SV`) ์ƒํƒœ๋กœ ๋˜๋Œ๋ฆฌ๋Š” ๋™์ž‘์ž…๋‹ˆ๋‹ค. + +--- + +### 4.2 Step 2: ์ค‘์•™ ์ง‘์ค‘์‹ ์•ก์…˜ ๋””์ŠคํŒจ์ฒ˜ (`executeAction`) + +[executeAction](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L370-L408)์€ ์ „๋‹ฌ๋ฐ›์€ ์•ก์…˜ ์‹๋ณ„์ž์— ๋”ฐ๋ผ ๋ฃฉ์—… ํ…Œ์ด๋ธ”์—์„œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ฐพ์•„ ์‹คํ–‰ํ•˜๊ณ , ๊ณตํ†ต ๋ผ์ดํ”„์‚ฌ์ดํด(๋กœ๋”ฉ, ์„ฑ๊ณต ์•Œ๋ฆผ, ์ฟผ๋ฆฌ ์บ์‹œ ๋ฌดํšจํ™”, ์—๋Ÿฌ ์ฒ˜๋ฆฌ)์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + +```typescript +// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 370~408) +async function executeAction({ + action, + request, + successMessage, +}: { + action: FnrsCnsltnViewButtonAction; + request?: unknown; + successMessage?: string; +}) { + // 1. ๋ฃฉ์—… ํ…Œ์ด๋ธ”์—์„œ ์•ก์…˜๋ช…์— ํ•ด๋‹นํ•˜๋Š” ์‹คํ–‰ ํ•จ์ˆ˜ ์กฐํšŒ + const handler = buttonActionHandlers[action]; + if (!handler) return; + + // 2. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์ค‘ ์ค‘๋ณต ํ˜ธ์ถœ ๋ฐฉ์ง€ ํ”Œ๋ž˜๊ทธ ํ™œ์„ฑํ™” + setIsProcessing(true); + try { + // 3. ์ „๋žต ํ•ธ๋“ค๋Ÿฌ์— Context ์ฃผ์ž… ๋ฐ ์‹คํ–‰ + await handler({ + request, + data, + onSuccess: () => { + // ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ + if (successMessage) alert(successMessage); + + // ์ƒ์„ธ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ ์บ์‹œ ๋ฌดํšจํ™” -> ํ™”๋ฉด ์ž๋™ ๊ฐฑ์‹  + queryClient.invalidateQueries({ + queryKey: fnrsCnsltnKeys.detail(fnrsCnsltnDocNo), + }); + + // ์ „์ž๊ฒฐ์žฌ ๋ถ„๊ธฐ ์ƒํƒœ ์ฟผ๋ฆฌ ์บ์‹œ ๋ฌดํšจํ™” + queryClient.invalidateQueries({ + queryKey: [ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH], + }); + }, + onError: (error) => { + // ํ†ต์ผ๋œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”์ถœ ๋ฐ Alert + let errorMessage = ""; + if (error instanceof AxiosError) { + errorMessage = error.response?.data?.message ?? ""; + } else { + errorMessage = error.message ?? "์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + } + alert(errorMessage); + }, + }); + } finally { + // 4. ๋กœ๋”ฉ ํ”Œ๋ž˜๊ทธ ํ•ด์ œ + setIsProcessing(false); + } +} +``` + +--- + +### 4.3 Step 3: ์ „๋žต ๋งคํ•‘ ๋ฃฉ์—… ํ…Œ์ด๋ธ” (`buttonActionHandlers`) + +[buttonActionHandlers](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L1215-L1239)๋Š” ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ ํ•˜๋‹จ์— ์œ„์น˜ํ•˜๋ฉฐ, ๊ฐ ์•ก์…˜ ๋ฌธ์ž์—ด๊ณผ ์‹คํ–‰ํ•  ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ํ•จ์ˆ˜๋ฅผ 1:1๋กœ ๋งคํ•‘ํ•ฉ๋‹ˆ๋‹ค. + +```typescript +// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 1215~1239) +const buttonActionHandlers: Record = { + strg: handleSaveApply, + aply: handleSaveApply, + del: handleDel, + envoSbmsn: handleChangePrgrsSttsComCd, + rjct: handleRejectPrgrsStts, // <--- "rjct" ์•ก์…˜์ด handleRejectPrgrsStts ํ•จ์ˆ˜๋กœ ๋ฐ”์ธ๋”ฉ๋จ + sncnAprv: handleChangePrgrsSttsComCd, + rvwRply: handleTechReview, + rvwOpnnRply: handleTechReview, + rqstTechRvw: handleRequestExamine, + fileSplmnt: handleDmndSplmnt, + fileVrfc: () => Promise.resolve(), + dsgntRvwr: handleDsgntRvwr, + pwrkCnsltnDocCrt: () => Promise.resolve(), + pwrkCnsltnDocMng: () => Promise.resolve(), + rvwRsltInq: () => Promise.resolve(), + rvwStrg: handleTechReview, + rvwCmptn: handleTechReview, + dmndSplmnt: handleDmndSplmnt, + atrzAprv: () => Promise.resolve(), + atrzRply: () => Promise.resolve(), + actnCmptn: handleActnCmptn, + rqstActnRvw: handleRequestActnReview, + costVrfc: () => Promise.resolve(), +}; +``` + +--- + +### 4.4 Step 4: ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค API ํ˜ธ์ถœ ํ•ธ๋“ค๋Ÿฌ (`handleRejectPrgrsStts`) + +[handleRejectPrgrsStts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx#L1123-L1132) ํ•จ์ˆ˜๋Š” ์ „๋‹ฌ๋ฐ›์€ Context๋ฅผ ์–ธํŒฉํ•˜์—ฌ ๋ฐฑ์—”๋“œ REST API ์„œ๋น„์Šค๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. + +```typescript +// apps/as/src/pages/fnrs-cnsltn/ui/components/floating-action-buttons.tsx (Line 1123~1132) +async function handleRejectPrgrsStts( + context: ActionHandlerContext, +): Promise { + try { + // ๋ฐฑ์—”๋“œ ์—”ํ‹ฐํ‹ฐ ์„œ๋น„์Šค ํ˜ธ์ถœ + await fnrsCnsltnMngService.rejectPrgrsStts(context.request); + // ์„ฑ๊ณต ์‹œ executeAction์˜ onSuccess ์‹คํ–‰ + context.onSuccess?.(); + } catch (error) { + // ์‹คํŒจ ์‹œ executeAction์˜ onError ์‹คํ–‰ + context.onError?.(error as Error); + } +} +``` + +#### ๋ฐฑ์—”๋“œ ์„œ๋น„์Šค ์—ฐ๋™๋ถ€ (`fnrsCnsltnMngService.rejectPrgrsStts`) +```typescript +// apps/as/src/entities/fnrs-cnsltn/api/fnrs-cnsltn-mng-service.ts +async rejectPrgrsStts(body: DocPrgrsUpdateRequestDto): Promise { + await this.client.put>( + "/api/v1/as/fnrs-cnsltn-mng/reject-prgrs-stts", + body, + ); +} +``` + +--- + +## 5. ๋ฐ˜๋ ค ์ฒ˜๋ฆฌ ํ›„ ํ™”๋ฉด ๊ฐฑ์‹  ๋ฉ”์ปค๋‹ˆ์ฆ˜ (Post-Action Reaction) + +`handleRejectPrgrsStts`๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ข…๋ฃŒ๋œ ํ›„ ํ™”๋ฉด์ด ์ž๋™์œผ๋กœ ๊ฐฑ์‹ ๋˜๋Š” ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค: + +1. **์ฝœ๋ฐฑ ์ฒด์ธ ํŠธ๋ฆฌ๊ฑฐ**: + `handleRejectPrgrsStts` -> `context.onSuccess()` ์‹คํ–‰. +2. **Alert ์•Œ๋ฆผ ํ‘œ์‹œ**: + ์‚ฌ์šฉ์ž์—๊ฒŒ `"๋ฐ˜๋ ค๋˜์—ˆ์Šต๋‹ˆ๋‹ค."` ์•ˆ๋‚ด ๋ฉ”์‹œ์ง€ ํŒ์—… ๋…ธ์ถœ. +3. **TanStack Query ์บ์‹œ ๋ฌดํšจํ™” (`invalidateQueries`)**: + - `fnrsCnsltnKeys.detail(fnrsCnsltnDocNo)` ํ‚ค๋ฅผ ๊ฐ€์ง„ React Query ์บ์‹œ๊ฐ€ Stale ์ƒํƒœ๋กœ ๋งˆํ‚น๋˜์–ด ๋ฐฑ์—”๋“œ๋กœ๋ถ€ํ„ฐ ์ตœ์‹  ๋ฌธ์„œ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ฆ‰์‹œ ๋‹ค์‹œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + - `[ENDPOINT.AS_ELAPR_SERVICE.ELAPR_BRANCH]` ํ‚ค์˜ ๊ฒฐ์žฌ ๋ถ„๊ธฐ ์ฟผ๋ฆฌ๋„ ํ•จ๊ป˜ ๋ฌดํšจํ™”๋˜์–ด ๊ฒฐ์žฌ์„  ๋ฐ ๋ฒ„ํŠผ ์ƒํƒœ๊ฐ€ ์žฌ๊ณ„์‚ฐ๋ฉ๋‹ˆ๋‹ค. +4. **๋ฒ„ํŠผ ๊ฐ€์‹œ์„ฑ ๋ฐ ๊ถŒํ•œ ์ž๋™ ์žฌ๊ณ„์‚ฐ**: + - ์ƒ์„ธ ์‘๋‹ต DTO์˜ `detDto.prgrsSttsComCd`๊ฐ€ `"ST"`(์‹ ์ฒญ์™„๋ฃŒ)์—์„œ `"SV"`(์ž„์‹œ์ €์žฅ/์ž‘์„ฑ์ค‘)๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค. + - [button-config.ts](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/lib/button-config.ts)์˜ ๊ถŒํ•œ-์ƒํƒœ ๋งคํŠธ๋ฆญ์Šค(`ROLE_STATUS_BUTTON_ACTIONS`)์— ๋”ฐ๋ผ ํ™”๋ฉด ํ•˜๋‹จ ๋ฒ„ํŠผ์ด ๋ฐ˜๋ ค/์ ‘์ˆ˜ ๋ชจ๋“œ์—์„œ **์ˆ˜์ •/์‹ ์ฒญ/์‚ญ์ œ ๋ชจ๋“œ**๋กœ ์ž๋™ ์ „ํ™˜๋ฉ๋‹ˆ๋‹ค. + - [InstlSncnDetailGate](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/instl-sncn/ui/instl-detail-gate.tsx) ๋˜๋Š” ์ƒ์„ธ ๋ผ์šฐํŒ… ๊ทœ์น™์— ์˜ํ•ด ์ง€์ž์ฒด ์ž‘์„ฑ์ž๋Š” ์ˆ˜์ • ํŽ˜์ด์ง€(`/edit`)๋กœ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๊ฒŒ ๊ถŒํ•œ์ด ํ™œ์„ฑํ™”๋ฉ๋‹ˆ๋‹ค. + +--- + +## 6. ์š”์•ฝ ๋ฐ ๊ถŒ์žฅ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ๋ผ์ธ + +| ํ•ญ๋ชฉ | ๊ตฌํ˜„ ๋‚ด์šฉ | ์•„ํ‚คํ…์ฒ˜์  ๊ฐ€์น˜ | +| :--- | :--- | :--- | +| **์•ก์…˜ ํŠธ๋ฆฌ๊ฑฐ** | `handleRjctClick` | ์‚ฌ์šฉ์ž ์˜์‚ฌ ํ™•์ธ(`confirm`) ๋ฐ DTO ์บก์Аํ™” | +| **๊ณตํ†ต ์ฒ˜๋ฆฌ** | `executeAction` | ๋กœ๋”ฉ ์ƒํƒœ(`isProcessing`), ์—๋Ÿฌ Alert, ์บ์‹œ ๋ฌดํšจํ™” ์ผ์›ํ™” | +| **๋™์  ๋””์ŠคํŒจ์น˜** | `buttonActionHandlers[action]` | ์กฐ๊ฑด๋ฌธ(if-else/switch) ์—†๋Š” ๊น”๋”ํ•œ O(1) ๋ฃฉ์—… ํ…Œ์ด๋ธ” | +| **API ์‹คํ–‰** | `handleRejectPrgrsStts` | ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ ์„œ๋น„์Šค(`fnrsCnsltnMngService`)์— ํ†ต์‹  ์œ„์ž„ | +| **ํ™”๋ฉด ๋™๊ธฐํ™”** | `invalidateQueries` | ์ˆ˜๋™ ์ƒํƒœ ๋ณ€์ด ์—†์ด ์„ ์–ธ์  ์„œ๋ฒ„ ์ƒํƒœ ์žฌ๋™๊ธฐํ™” | + +๋‹ค๋ฅธ ์—…๋ฌด ๋ชจ๋“ˆ(`instl-sncn`, `pwrk-cnsltn`, `design-cnstn` ๋“ฑ) ๊ฐœ๋ฐœ ์‹œ์—๋„ ๋ณธ ๋ฉ”์ปค๋‹ˆ์ฆ˜๊ณผ ๋™์ผํ•˜๊ฒŒ **`[ActionHandler] -> [buttonActionHandlers] -> [executeAction]`** ๊ตฌ์กฐ๋ฅผ ์ค€์ˆ˜ํ•˜์—ฌ ๊ตฌํ˜„ํ•˜๋ฉด ์ผ๊ด€๋˜๊ณ  ๊ฒฌ๊ณ ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ UI๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. diff --git a/apps/as/docs/pwrk-cnsltn/11.๋ณ‘ํ•ฉ์ž_์›น๊ธฐ์•ˆ๊ธฐ_๋ฌธ์„œ์ดˆ๊ธฐํ™”_์ด์Šˆ_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ์•ˆ.md b/apps/as/docs/pwrk-cnsltn/11.๋ณ‘ํ•ฉ์ž_์›น๊ธฐ์•ˆ๊ธฐ_๋ฌธ์„œ์ดˆ๊ธฐํ™”_์ด์Šˆ_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ์•ˆ.md new file mode 100644 index 000000000..9119b4d8d --- /dev/null +++ b/apps/as/docs/pwrk-cnsltn/11.๋ณ‘ํ•ฉ์ž_์›น๊ธฐ์•ˆ๊ธฐ_๋ฌธ์„œ์ดˆ๊ธฐํ™”_์ด์Šˆ_๋ฐ_ํ•ด๊ฒฐ๋ฐฉ์•ˆ.md @@ -0,0 +1,183 @@ +# ๋ณ‘ํ•ฉ์ž ์‹ค๋ฌดํ˜‘์˜์„œ(MAIN) ์›น๊ธฐ์•ˆ๊ธฐ ํƒญ ์ „ํ™˜ ์‹œ ๋ฌธ์„œ ์ดˆ๊ธฐํ™” ์ด์Šˆ ๋ฐ ํ•ด๊ฒฐ๋ฐฉ์•ˆ + +> **๋ฌธ์„œ ๋Œ€์ƒ**: `apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx` +> **๊ด€๋ จ ๋ชจ๋“ˆ**: ์žฌ์›ํ˜‘์˜ยท์‹ค๋ฌดํ˜‘์˜(`fnrs-cnsltn` / `pwrk-cnsltn`), ์›นํ•œ๊ธ€๊ธฐ์•ˆ๊ธฐ(`@/widgets/hangulgiangi`), ์ „์ž๊ฒฐ์žฌ(`elapr`) +> **์ž‘์„ฑ์ผ**: 2026-10-23 +> **์ƒํƒœ**: ๊ตฌํ˜„ ์™„๋ฃŒ (`PwrkCnsltnBranchRefPanel` ์‚ฌ์ด๋“œ ํŒจ๋„ Split View ์ ์šฉ ์™„๋ฃŒ) + +--- + +## 1. ๊ฐœ์š” (Overview) + +์žฌ์›ํ˜‘์˜(FNRS)์˜ ํ•˜์œ„ ์‹ค๋ฌดํ˜‘์˜(PWRK) ํ”„๋กœ์„ธ์Šค ์ค‘ ๋ณ‘๋ ฌ ๊ฒฐ์žฌ(Fork-Join, `TE`/`RK` ์ƒํƒœ) ๋‹จ๊ณ„์—์„œ, **์ข…ํ•ฉ๋ณ‘ํ•ฉ์ž(TOTAL_AGG ๋˜๋Š” ATRZ04 ๊ถŒํ•œ์ž)**๋Š” ๊ด€๋กœ(`SP`)์™€ ์‹œ์„ค(`SF`) ๋ถ€์„œ์˜ ์ดˆ์•ˆ ๋ฐ ๊ฒ€ํ†  ์˜๊ฒฌ์„ ์ข…ํ•ฉํ•˜์—ฌ ์ตœ์ข… ์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ์ธ **`MAIN` ํƒญ ๋ฌธ์„œ๋ฅผ ํŽธ์ง‘ยท๋ฐ˜์˜ํ•˜๊ณ  ์ €์žฅ**ํ•˜๋Š” ๋ฒ•์ ยท์—…๋ฌด์  ์ฑ…์ž„์„ ์ง‘๋‹ˆ๋‹ค. + +ํ˜„์žฌ ๊ตฌํ˜„๋œ [PwrkCnsltnDocMngDialog](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx)์—์„œ๋Š” ๋ณ‘ํ•ฉ์ž๊ฐ€ `MAIN` ๋ฌธ์„œ๋ฅผ ์ˆ˜์ •ํ•˜๋˜ ์ค‘ ๋ถ€์„œ๋ณ„ ์ž‘์„ฑ ๋‚ด์šฉ์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด `[๊ด€๋กœ ์ดˆ์•ˆ (SP)]` ๋˜๋Š” `[์‹œ์„ค ์ดˆ์•ˆ (SF)]` ํƒญ ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๊ณ  ๋‹ค์‹œ `[์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ (MAIN)]` ํƒญ์œผ๋กœ ๋Œ์•„์™”์„ ๋•Œ, **๊ธฐ์•ˆ๊ธฐ ๋‚ด๋ถ€์—์„œ ํŽธ์ง‘ ์ค‘์ด๋˜ ์ˆ˜์ • ๋‚ด์šฉ์ด ๋ชจ๋‘ ์‚ฌ๋ผ์ง€๊ณ  ์„œ๋ฒ„์— ์ €์žฅ๋œ ์›๋ž˜ ๋ฒ„์ „์œผ๋กœ ์ดˆ๊ธฐํ™”(Overwrite)๋˜๋Š” ์น˜๋ช…์ ์ธ ๋ฐ์ดํ„ฐ ์œ ์‹ค ๋ฌธ์ œ**๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. + +๋ณธ ๋ฌธ์„œ๋Š” ํ•ด๋‹น ํ˜„์ƒ์˜ ๊ทผ๋ณธ ๊ธฐ์ˆ  ์›์ธ์„ ๋ถ„์„ํ•˜๊ณ , ๋ณ‘ํ•ฉ์ž์˜ ์—…๋ฌด ํ๋ฆ„๊ณผ ์‹œ์Šคํ…œ ์ œ์•ฝ์‚ฌํ•ญ์„ ๋งŒ์กฑํ•˜๋Š” ์‹ค์งˆ์  ํ•ด๊ฒฐ ๋ฐฉ์•ˆ์„ ๊ฒ€ํ† ยท์ œ์‹œํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 2. ํ˜„์ƒ ๋ฐ ์—…๋ฌด์  ๋ฐฐ๊ฒฝ (Business Context & Issue) + +### 2.1 ๋ณ‘ํ•ฉ์ž์˜ ์—ญํ•  ๋ฐ ์ฑ…์ž„ +1. **์ˆ˜์ • ๋ฐ ์ €์žฅ์˜ ๋‹จ์ผ ์ฃผ์ฒด**: + - ๋ณ‘๋ ฌ ๊ฒฐ์žฌ(`isForkJoin === true`) ํ™˜๊ฒฝ์—์„œ ๊ด€๋กœ(`SP`) ๋ฐ ์‹œ์„ค(`SF`) ๋ถ€์„œ์˜ 1์ฐจ ๊ฒ€ํ† ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ๋ณ‘ํ•ฉ์ž๊ฐ€ ์ „์ฒด ๋‚ด์šฉ์„ ์ข…ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + - ๋ณ‘ํ•ฉ์ž๋Š” ์˜ค์ง **`MAIN` ์ข…ํ•ฉ๋ณธ๋งŒ ์ˆ˜์ •ยท์ €์žฅ**ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, `SP`์™€ `SF` ๋ฌธ์„œ๋Š” ์ˆ˜์ • ๊ถŒํ•œ์ด ์—†๊ณ  **์˜ค์ง ์ฐธ์กฐ(์—ด๋žŒ)๋งŒ ๊ฐ€๋Šฅ**ํ•ฉ๋‹ˆ๋‹ค (`canSaveCurrentBranch` ์กฐ๊ฑด ์ฐธ์กฐ). +2. **๋ฐ˜์˜ ๋ฐ ๋Œ€์กฐ(Cross-Check) ์—…๋ฌด**: + - ๋ณ‘ํ•ฉ์ž๋Š” `SP`์˜ ๊ฒ€ํ†  ์˜๊ฒฌ/์‚ฌ์—…๋น„ ์กฐ์ • ๊ฒฐ๊ณผ์™€ `SF`์˜ ๊ฒ€ํ†  ์˜๊ฒฌ/์‚ฌ์—…๋น„ ์กฐ์ • ๊ฒฐ๊ณผ๋ฅผ ๋ฌธ๋‹จ๋ณ„ยทํ•ญ๋ชฉ๋ณ„๋กœ ๋Œ€์กฐํ•˜๋ฉฐ `MAIN` ๋ฌธ์„œ์˜ ๋ณธ๋ฌธ๊ณผ ํ‘œ๋ฅผ ์ง์ ‘ ์ˆ˜๊ธฐ ํŽธ์ง‘(๋ณ‘ํ•ฉ)ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +### 2.2 ๋ฐœ์ƒ ํ˜„์ƒ (Problem Scenario) +``` +[์‚ฌ์šฉ์ž ํ–‰๋™ ํ๋ฆ„] +1. ๋ณ‘ํ•ฉ์ž๊ฐ€ ๋‹ค์ด์–ผ๋กœ๊ทธ ์ง„์ž… ์‹œ ๊ธฐ๋ณธ ํ™œ์„ฑํ™”๋œ [์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ (MAIN)] ํƒญ์—์„œ ํ•œ๊ธ€ ๋ฌธ์„œ ์ˆ˜์ • ์‹œ์ž‘ +2. ๊ด€๋กœ ๋ถ€์„œ๊ฐ€ ์ˆ˜์ •ํ•œ ์ƒ์„ธ ๋‚ด์—ญ์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ƒ๋‹จ [๊ด€๋กœ ์ดˆ์•ˆ (SP)] ํƒญ ๋ฒ„ํŠผ ํด๋ฆญ +3. ๊ด€๋กœ ์ดˆ์•ˆ ํ™•์ธ ํ›„, ๋‹ค์‹œ ์ƒ๋‹จ [์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ (MAIN)] ํƒญ ๋ฒ„ํŠผ ํด๋ฆญ +4. ๊ฒฐ๊ณผ: ๋ฐฉ๊ธˆ ์ „๊นŒ์ง€ ์ž‘์„ฑ/์ˆ˜์ • ์ค‘์ด๋˜ MAIN ํ•œ๊ธ€ ๋ฌธ์„œ ๋‚ด์šฉ์ด ์ „๋ถ€ ์‚ฌ๋ผ์ง€๊ณ  ์„œ๋ฒ„ ์›๋ณธ์œผ๋กœ ๋กค๋ฐฑ๋จ +``` + +--- + +## 3. ๊ธฐ์ˆ ์  ์›์ธ ๋ถ„์„ (Root Cause Analysis) + +### 3.1 ๋‹จ์ผ ์›นํ•œ๊ธ€๊ธฐ์•ˆ๊ธฐ(iframe) ์ธ์Šคํ„ด์Šค ๊ณต์œ  ๋ฐ ๋ฌด์กฐ๊ฑด ๋ฌธ์„œ ๋กœ๋“œ (`HwpCtrl.Open`) +[PwrkCnsltnDocMngDialog.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/pages/fnrs-cnsltn/ui/components/pwrk-cnsltn-doc-mng-dialog.tsx#L357-L383)์˜ ๊ตฌ์กฐ: +```tsx +// ํƒญ ๋ณ€๊ฒฝ ์‹œ targetBranchForApi ๋ณ€๊ฒฝ -> React Query๊ฐ€ ํ•ด๋‹น ํƒญ์˜ ์ตœ์‹  HWPX Blob ์žฌ์กฐํšŒ +const targetBranchForApi = isForkJoin ? currentBranchSeCd : undefined; +const { data: documentBlob } = useQuery({ + queryKey: ["pwrk-cnsltn-doc-crt-rslt", ..., targetBranchForApi, ...], + queryFn: () => pwrkCnsltnDocCrtRsltService.openWebHwpLatest(crtDmndUuid!, targetBranchForApi), +}); +``` + +๊ธฐ์•ˆ๊ธฐ ๋ Œ๋”๋ง๋ถ€: +```tsx + +``` + +[HangulGiangi.tsx](file:///Users/changwng/workspace.ona/frontend_as/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx#L120-L130)์˜ ๋ฌธ์„œ ๋กœ๋“œ ์ดํŽ™ํŠธ: +```tsx +useEffect(() => { + if (!isCtrlReady || ...) return; + // documentKey๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ๊ธฐ์กด ๊ธฐ์•ˆ๊ธฐ ์ปจํŠธ๋กค์— ์ƒˆ Blob์„ ๋ฐ”๋กœ Openํ•ด๋ฒ„๋ฆผ! + openHwpxBlob(documentBlob, documentFileName); +}, [documentBlob, documentFileName, documentKey, isCtrlReady, openHwpxBlob]); +``` + +- ์‚ฌ์šฉ์ž๊ฐ€ ํƒญ์„ ์ „ํ™˜ํ•˜๋ฉด `currentBranchSeCd`๊ฐ€ ๋ฐ”๋€Œ๋ฉด์„œ `documentKey`๊ฐ€ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. +- ๋‹จ์ผ iframe์— ์œ„์น˜ํ•œ ๊ธฐ์•ˆ๊ธฐ ์ปจํŠธ๋กค๋Ÿฌ(`HwpCtrl`)๋Š” ์ฆ‰์‹œ `HwpCtrl.Open(documentFile)`์„ ์‹คํ–‰ํ•˜์—ฌ ์ƒˆ๋กœ์šด HWPX ๋ฌธ์„œ๋ฅผ ์บ”๋ฒ„์Šค์— ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค. +- ์ด ๊ณผ์ •์—์„œ ๊ธฐ์•ˆ๊ธฐ ๋ฉ”๋ชจ๋ฆฌ ๋ฒ„ํผ์— ์กด์žฌํ•˜๋˜ `MAIN` ๋ฌธ์„œ์˜ ํŽธ์ง‘ ๋ณ€๊ฒฝ์‚ฌํ•ญ์€ **์„œ๋ฒ„ ์ €์žฅ์ด๋‚˜ ๋กœ์ปฌ ์บ์‹ฑ ์—†์ด ์ฆ‰์‹œ ํŒŒ๊ธฐ**๋ฉ๋‹ˆ๋‹ค. +- ๋‹ค์‹œ `MAIN` ํƒญ์„ ํด๋ฆญํ•˜๋ฉด ์„œ๋ฒ„์˜ ์ตœ์‹  ์ €์žฅ๋ณธ(`openWebHwpLatest`)์ด ๋‹ค์‹œ `Open`๋˜๋ฏ€๋กœ, ์ž‘์„ฑ ์ค‘์ด๋˜ ๋‚ด์šฉ์ด ์™„์ „ํžˆ ๋‚ ์•„๊ฐ€๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. + +### 3.2 ๋ณ€๊ฒฝ ๊ฐ์ง€(Dirty Check) ๋ฐ ํ™”๋ฉด ์ดํƒˆ ๊ฐ€๋“œ ๋ถ€์žฌ +- ํ•œ์ปด ์›น๊ธฐ์•ˆ๊ธฐ๋Š” ๋ฌธ์„œ ์ˆ˜์ • ์—ฌ๋ถ€๋ฅผ ํŒ๋ณ„ํ•  ์ˆ˜ ์žˆ๋Š” ํ”„๋กœํผํ‹ฐ(`HwpCtrl.IsModified`)๋ฅผ ์ œ๊ณตํ•˜์ง€๋งŒ([ref-webhwp-api.md:L269](file:///Users/changwng/workspace.ona/frontend_as/apps/as/docs/pwrk-cnsltn/ref-webhwp-api.md#L269)), ํƒญ ์ „ํ™˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ ์ด๋ฅผ ์ „ํ˜€ ๊ฒ€์‚ฌํ•˜์ง€ ์•Š๊ณ  ์ฆ‰์‹œ state๋ฅผ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. +- ๋ฏธ์ €์žฅ ๋ณ€๊ฒฝ์‚ฌํ•ญ์ด ์žˆ์„ ๋•Œ ์‚ฌ์šฉ์ž์—๊ฒŒ ํ™•์ธ์„ ์š”์ฒญํ•˜๋Š” ์ปจํŽŒ/๋ชจ๋‹ฌ ๊ฐ€๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. + +### 3.3 UX ์„ค๊ณ„์™€ ๋ณ‘ํ•ฉ์ž ์—…๋ฌด ํŠน์„ฑ์˜ ๋ถˆ์ผ์น˜ +- ๋ณ‘ํ•ฉ์ž๋Š” `SP`/`SF`๋ฅผ ๋ณด๋ฉด์„œ **๋™์‹œ์— `MAIN`์— ์ž…๋ ฅยท๋ณต์‚ฌ**ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. +- ํ™”๋ฉด ์ „์ฒด๋ฅผ `SP` $\leftrightarrow$ `MAIN`์œผ๋กœ ๋ฒˆ๊ฐˆ์•„ ์Šค์œ„์นญํ•˜๋Š” ๋‹จ์ผ ์บ”๋ฒ„์Šค ํƒญ ๋ฐฉ์‹์€ ์ดˆ๊ธฐํ™” ๋ฌธ์ œ ์™ธ์—๋„ **"์•ž์„œ ๋ณธ ๋‚ด์šฉ์„ ์™ธ์šฐ๊ฑฐ๋‚˜ ๋ฉ”๋ชจ์žฅ์— ๋ณต์‚ฌํ•œ ๋’ค ํƒญ์„ ๋„˜๋‚˜๋“ค์–ด์•ผ ํ•˜๋Š”" ๊ทน์‹ฌํ•œ ์—…๋ฌด ๋น„ํšจ์œจ**์„ ์ดˆ๋ž˜ํ•ฉ๋‹ˆ๋‹ค. + +--- + +## 4. ํ•ด๊ฒฐ ๋ฐฉ์•ˆ ๋น„๊ต ๊ฒ€ํ†  (Solution Analysis) + +| ํ‰๊ฐ€ ํ•ญ๋ชฉ | [๋ฐฉ์•ˆ 1] ๋ณ‘ํ•ฉ์ž์šฉ ์ฐธ์กฐ ๋ฌธ์„œ ๋ถ„ํ• /๋ณ„๋„ ์ฐฝ ๋ทฐ์–ด ์ œ๊ณต (๊ฐ•๋ ฅ ๊ถŒ์žฅ โ˜…โ˜…โ˜…โ˜…โ˜…) | [๋ฐฉ์•ˆ 2] ํƒญ ์ „ํ™˜ ์‹œ ์ธ๋ฉ”๋ชจ๋ฆฌ Blob ์ž„์‹œ ์บ์‹ฑ | [๋ฐฉ์•ˆ 3] ํƒญ ์ „ํ™˜ ์‹œ Dirty Check ๋ฐ ์ €์žฅ ๊ฐ•์ œ ์ปจํŽŒ | +| :--- | :--- | :--- | :--- | +| **๊ฐœ๋…** | `MAIN` ๊ธฐ์•ˆ๊ธฐ๋Š” ํ•ญ์ƒ ํ™”๋ฉด์— ์œ ์ง€ํ•˜๊ณ , `SP`/`SF`๋Š” **๋ณ„๋„ ์ƒˆ ์ฐฝ(Window) ๋˜๋Š” ํŒ์—… ๋ชจ๋‹ฌ/์šฐ์ธก ํŒจ๋„**๋กœ ์—ด๋žŒ | ํƒญ ์ „ํ™˜ ์‹œ `HwpCtrl.SaveAs`๋กœ HWPX Blob์„ ์ถ”์ถœํ•ด React State/Ref์— ์ž„์‹œ ๋ณด๊ด€ ํ›„ ๋ณต๊ท€ ์‹œ ์žฌ์˜คํ”ˆ | ๋ฏธ์ €์žฅ ๋ณ€๊ฒฝ์‚ฌํ•ญ(`IsModified`)์ด ์žˆ์„ ๋•Œ ํƒญ ์ „ํ™˜์„ ์ฐจ๋‹จํ•˜๊ณ  ์„œ๋ฒ„์— ๋จผ์ € ์ €์žฅํ•˜๋„๋ก ์œ ๋„ | +| **์ดˆ๊ธฐํ™” ๋ฐฉ์ง€** | **์›์ฒœ ์ฐจ๋‹จ** (`MAIN` ๊ธฐ์•ˆ๊ธฐ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ ˆ๋Œ€ ์–ธ๋กœ๋“œ๋˜์ง€ ์•Š์Œ) | ํ•ด๊ฒฐ ๊ฐ€๋Šฅ (๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋ณต๊ตฌ) | ํ•ด๊ฒฐ ๊ฐ€๋Šฅ (์ €์žฅ ํ›„ ์ด๋™) | +| **๋ณ‘ํ•ฉ ์—…๋ฌด ํŽธ์˜์„ฑ** | **์ตœ์ƒ** (๋“€์–ผ ๋ชจ๋‹ˆํ„ฐ ๋˜๋Š” ๋ถ„ํ•  ์ฐฝ์œผ๋กœ ๋‘ ๋ฌธ์„œ๋ฅผ ๋‚˜๋ž€ํžˆ ๋Œ€์กฐ ๊ฐ€๋Šฅ) | ๋ณดํ†ต (์—ฌ์ „ํžˆ ํƒญ์„ ์™”๋‹ค ๊ฐ”๋‹ค ํ•˜๋ฉฐ ์™ธ์›Œ์„œ ์ž‘์—…ํ•ด์•ผ ํ•จ) | ๋‚ฎ์Œ (SP ํ•œ๋ฒˆ ๋ณด๋ ค๋ฉด MAIN ์„œ๋ฒ„ ์ €์žฅ ํ•„์ˆ˜, ๋ฒ„์ „ ํญ์ฆ) | +| **์‹œ์Šคํ…œ ์•ˆ์ •์„ฑ** | **๋งค์šฐ ๋†’์Œ** (๋ณต์žกํ•œ ๊ธฐ์•ˆ๊ธฐ ์ƒํƒœ ๊ด€๋ฆฌ ๋ถˆํ•„์š”) | ๋‚ฎ์Œ ~ ๋ณดํ†ต (๋น„๋™๊ธฐ ์ฝœ๋ฐฑ ๋ ˆ์ด์Šค ์ปจ๋””์…˜ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ์œ„ํ—˜) | ๋ณดํ†ต (๋ถˆํ•„์š”ํ•œ `verSn` ์ด๋ ฅ ์ฆ๊ฐ€) | +| **๊ตฌํ˜„ ๋ณต์žก๋„** | ๋‚ฎ์Œ ~ ๋ณดํ†ต | ๋†’์Œ | ๋‚ฎ์Œ | + +--- + +## 5. ์ƒ์„ธ ๊ถŒ์žฅ ํ•ด๊ฒฐ์ฑ… (Recommended Architecture) + +๊ฐ€์žฅ ์•ˆ์ „ํ•˜๊ณ  ๋ณ‘ํ•ฉ์ž์˜ ์—…๋ฌด ์ƒ์‚ฐ์„ฑ์„ ๊ทน๋Œ€ํ™”ํ•  ์ˆ˜ ์žˆ๋Š” **ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์ ‘๊ทผ๋ฒ•(๋‹จ๊ณ„๋ณ„ ์ ์šฉ)**์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. + +```mermaid +graph TD + subgraph ๋ณ‘ํ•ฉ์ž(TOTAL_AGG) ํ™”๋ฉด ๊ตฌ์กฐ + MAIN_VIEW["MAIN ๊ธฐ์•ˆ๊ธฐ (ํ•ญ์ƒ ์œ ์ง€, ์ˆ˜์ • ๋‚ด์šฉ ๋ณด์กด)"] + BTN_SP["[๊ด€๋กœ(SP) ์ดˆ์•ˆ ์ƒˆ ์ฐฝ ์—ด๊ธฐ / ๋Œ€์กฐ]"] + BTN_SF["[์‹œ์„ค(SF) ์ดˆ์•ˆ ์ƒˆ ์ฐฝ ์—ด๊ธฐ / ๋Œ€์กฐ]"] + GUARD{"ํƒญ ์ „ํ™˜ ์‹œ๋„ ์‹œ
HwpCtrl.IsModified ์ฒดํฌ"} + end + + subgraph ๋ณ„๋„ ์ฐธ์กฐ ๋ทฐ (Popup / Window / Split) + REF_SP["๊ด€๋กœ(SP) ์ฝ๊ธฐ ์ „์šฉ ๋ทฐ์–ด"] + REF_SF["์‹œ์„ค(SF) ์ฝ๊ธฐ ์ „์šฉ ๋ทฐ์–ด"] + end + + BTN_SP -->|๋…๋ฆฝ ์‹คํ–‰| REF_SP + BTN_SF -->|๋…๋ฆฝ ์‹คํ–‰| REF_SF + REF_SP -.๋™์‹œ ๋Œ€์กฐ ๋ฐ MAIN์— ๋ณต์‚ฌ/๋ฐ˜์˜.-> MAIN_VIEW + REF_SF -.๋™์‹œ ๋Œ€์กฐ ๋ฐ MAIN์— ๋ณต์‚ฌ/๋ฐ˜์˜.-> MAIN_VIEW + GUARD -->|๋ฏธ์ €์žฅ ๋‚ด์šฉ ์žˆ์Œ| WARN["๊ฒฝ๊ณ  ์•Œ๋ฆผ: ์ €์žฅ ํ›„ ์ด๋™ ๋˜๋Š” ์ทจ์†Œ"] +``` + +--- + +### ๋‹จ๊ณ„ 1: ๋ณ‘ํ•ฉ์ž๋ฅผ ์œ„ํ•œ "SP / SF ์ฐธ์กฐ ๋ฌธ์„œ ๋ณ„๋„ ์ฐฝ ์—ด๋žŒ" ์ œ๊ณต (๋ฐฉ์•ˆ 1 ์ ์šฉ ์™„๋ฃŒ) + +๋ณ‘ํ•ฉ์ž์—๊ฒŒ `SP`์™€ `SF`๋Š” ํŽธ์ง‘ ๋Œ€์ƒ์ด ์•„๋‹ˆ๋ผ **์ฐธ์กฐ(์—ด๋žŒ) ๋Œ€์ƒ**์ด๋ฏ€๋กœ, `MAIN` ๊ธฐ์•ˆ๊ธฐ ํ™”๋ฉด์„ ์ „ํ™˜ํ•˜์ง€ ์•Š๊ณ  ๋ณ„๋„ ์ฐฝ(Popup Window)์œผ๋กœ ๋„์›Œ ๋“€์–ผ ๋ชจ๋‹ˆํ„ฐ๋‚˜ ๋ถ„ํ•  ํ™”๋ฉด์œผ๋กœ ๋Œ€์กฐํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + +#### 1) UI ์•ก์…˜ ๋ฒ„ํŠผ ๋ถ„๊ธฐ +๋ณ‘ํ•ฉ์ž(`isMerger === true`)๊ฐ€ `MAIN` ํƒญ์„ ๋ณด๊ณ  ์žˆ์„ ๋•Œ, ์ƒ๋‹จ ํƒญ ์˜์—ญ ์šฐ์ธก์— **๋…๋ฆฝ ์ƒˆ ์ฐฝ ์—ด๊ธฐ ๋ฒ„ํŠผ**์„ ๋ฐฐ์น˜: +- `[๊ด€๋กœ(SP) ์ƒˆ ์ฐฝ ๋ณด๊ธฐ]` +- `[์‹œ์„ค(SF) ์ƒˆ ์ฐฝ ๋ณด๊ธฐ]` + +#### 2) ์ƒˆ ์ฐฝ ์—ด๊ธฐ ๋ฐ ํ•œ์ปด ๋ผ์ด์„ ์Šค ์˜ค๋ฅ˜ ํ•ด๊ฒฐ ๋ฉ”์ปค๋‹ˆ์ฆ˜ +- **๋ผ์ด์„ ์Šค ์˜ค๋ฅ˜ ์›์ธ ๋ฐ ํ•ด๊ฒฐ**: + - ๊ธฐ์กด ์ƒˆ ์ฐฝ iframe ํ˜ธ์ถœ ์‹œ `/webhwpctrl/index.html`๋กœ๋งŒ ํ˜ธ์ถœํ•˜์—ฌ `baseurl` ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ๊ณ , ๋กœ์ปฌ ์ฃผ์†Œ(`localhost:3004`)๋ฅผ ์ปจํŠธ๋กค ๊ธฐ๋ณธ URL๋กœ ์ธ์‹ํ•˜์—ฌ ํ•œ์ปด ๊ธฐ์•ˆ๊ธฐ ์Šคํฌ๋ฆฝํŠธ์—์„œ `๋ผ์ด์„ผ์Šค ์˜ค๋ฅ˜` alert์ด ๋ฐœ์ƒํ•จ. + - ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด `buildBridgeSrc(DEFAULT_BRIDGE_URL, WEBHYP_CTRL_URL)` ์œ ํ‹ธ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด `?baseurl=https://hwp.onasys.co.kr/webhwpctrl`์ด ์•ˆ์ „ํ•˜๊ฒŒ ์ฃผ์ž…๋˜๋„๋ก ๊ฐœ์„ . +- **HWPX ๋‹ค์šด๋กœ๋“œ Fallback ์ง€์›**: + - ์ƒˆ ์ฐฝ ์ƒ๋‹จ ํ—ค๋”์— `[HWPX ๋‹ค์šด๋กœ๋“œ]` ๋ฒ„ํŠผ์„ ํ•จ๊ป˜ ์ œ๊ณตํ•˜์—ฌ, ์›น๊ธฐ์•ˆ๊ธฐ ๋ทฐ์–ด ์™ธ์—๋„ ๋กœ์ปฌ ํ•œ์ปด์˜คํ”ผ์Šค ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์ฆ‰์‹œ ๋ฌธ์„œ๋ฅผ ๋‚ด๋ ค๋ฐ›์•„ ์—ด๋žŒํ•  ์ˆ˜ ์žˆ๋„๋ก ์ด์ค‘ ๋ณด์žฅ. + +--- + +### ๋‹จ๊ณ„ 2: ํƒญ ์ „ํ™˜ ์‹œ Dirty Check ๋ฐ ๋ฐ์ดํ„ฐ ์œ ์‹ค ๋ฐฉ์ง€ ๊ฐ€๋“œ (์•ˆ์ „ ๊ฐ€๋“œ ์ ์šฉ ์™„๋ฃŒ) + +๋งŒ์•ฝ ์‚ฌ์šฉ์ž๊ฐ€ ๊ธฐ์กด ํƒญ ๋ฒ„ํŠผ(`SP`, `SF`)์„ ๋ˆŒ๋Ÿฌ ํƒญ์„ ์ „ํ™˜ํ•˜๋ ค๊ณ  ์‹œ๋„ํ•  ๋•Œ, ์ž‘์„ฑ ์ค‘์ด๋˜ ๋‚ด์šฉ์ด ์†Œ์‹ค๋˜์ง€ ์•Š๋„๋ก ํ™•์ธ ๊ฐ€๋“œ๋ฅผ ๋ฐฐ์น˜ํ–ˆ์Šต๋‹ˆ๋‹ค. + +#### 1) `HwpCtrl.IsModified` ๊ฐ์ง€ ํ•จ์ˆ˜ +- `apps/as/src/widgets/hangulgiangi/lib/get-hwp-ctrl.ts`์˜ `getHwpCtrl(null)`์„ ํ†ตํ•ด ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ํ•œ์ปด ๊ธฐ์•ˆ๊ธฐ ์ปจํŠธ๋กค์˜ `IsModified` ํ”Œ๋ž˜๊ทธ๋ฅผ ์‹ค์‹œ๊ฐ„ ํ™•์ธ. + +#### 2) ํƒญ ์ „ํ™˜ ๋ฐ ์ฐฝ ๋‹ซ๊ธฐ ์•ˆ์ „ ํ™•์ธ +```typescript +function checkDirtyAndConfirm(targetDesc: string): boolean { + if (currentBranchSeCd === "MAIN") { + try { + const ctrl = getHwpCtrl(null); + if (ctrl?.IsModified) { + return window.confirm( + `์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ(MAIN)์— ์•„์ง ์ €์žฅ๋˜์ง€ ์•Š์€ ์ˆ˜์ • ๋‚ด์šฉ์ด ์žˆ์Šต๋‹ˆ๋‹ค.\n` + + `${targetDesc}(์œผ)๋กœ ์ด๋™ํ•˜๋ฉด ์ž‘์„ฑ ์ค‘์ธ ๋‚ด์šฉ์ด ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.\n\n` + + `๊ณ„์† ์ง„ํ–‰ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? (์ทจ์†Œ ํ›„ [์‹ค๋ฌดํ˜‘์˜์„œ ์ €์žฅ]์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.)` + ); + } + } catch (e) { + console.warn("๊ธฐ์•ˆ๊ธฐ ๋ณ€๊ฒฝ ๊ฐ์ง€ ์‹คํŒจ:", e); + } + } + return true; +} +``` + +--- + +## 6. ๊ฒฐ๋ก  ๋ฐ ์ ์šฉ ๊ฒฐ๊ณผ + +1. **์ดˆ๊ธฐํ™” ๋ฒ„๊ทธ ์›์ฒœ ํ•ด๊ฒฐ**: + - `MAIN` ํƒญ์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๊ณ ๋„ `[๊ด€๋กœ(SP) ์ƒˆ ์ฐฝ ๋ณด๊ธฐ]`, `[์‹œ์„ค(SF) ์ƒˆ ์ฐฝ ๋ณด๊ธฐ]`๋ฅผ ํ†ตํ•ด ๋…๋ฆฝ ํŒ์—…์œผ๋กœ ๊ฐ ๋ถ€์„œ์˜ ์›๋ณธ ๋ฌธ์„œ๋ฅผ ๋Œ€์กฐํ•  ์ˆ˜ ์žˆ์–ด `MAIN` ๊ธฐ์•ˆ๊ธฐ ์ธ์Šคํ„ด์Šค๊ฐ€ ์ ˆ๋Œ€ ์–ธ๋กœ๋“œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + - ์‹ค์ˆ˜๋กœ ํƒญ์„ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ ์ฐฝ์„ ๋‹ซ์œผ๋ ค ํ•  ๋•Œ๋„ `IsModified` ๊ธฐ๋ฐ˜ ์•ˆ์ „ ์ปจํŽŒ ์ฐฝ์ด ์ž‘๋™ํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์œ ์‹ค์„ 100% ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. +2. **์ƒˆ ์ฐฝ ์›น๊ธฐ์•ˆ๊ธฐ ๋ผ์ด์„ ์Šค ์ธ์ฆ ์ •์ƒํ™”**: + - ์ƒˆ ์ฐฝ iframe์˜ ๋ธŒ๋ฆฌ์ง€ URL์— `WEBHYP_CTRL_URL` ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ •์ƒ ๋ฐ”์ธ๋”ฉํ•˜์—ฌ "๋ผ์ด์„ผ์Šค ์˜ค๋ฅ˜" alert์„ ๊ทผ๋ณธ์ ์œผ๋กœ ์ œ๊ฑฐํ•˜์˜€์Šต๋‹ˆ๋‹ค. + - ์ƒˆ ์ฐฝ ํ—ค๋”์— `[HWPX ๋‹ค์šด๋กœ๋“œ]` ๋ฒ„ํŠผ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋„ ํŽธ๋ฆฌํ•˜๊ฒŒ ๋ฌธ์„œ๋ฅผ ์—ด์–ด๋ณผ ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. +3. **๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๊ฒ€์ฆ ์™„๋ฃŒ**: + - ์ด 19๊ฐœ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ(`pnpm --filter as exec vitest run src/pages/fnrs-cnsltn/ui/__tests__/pwrk-cnsltn-doc-mng-dialog.test.tsx`)๊ฐ€ ์ „์ฒด ํ†ต๊ณผ(`19 passed (19)`)ํ•จ์„ ๊ฒ€์ฆํ–ˆ์Šต๋‹ˆ๋‹ค. + diff --git a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx index 26d896267..9be7da1bd 100644 --- a/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx +++ b/apps/as/src/entities/elapr/ui/elapr-approval-dialog.tsx @@ -438,7 +438,7 @@ function ElaprApprovalDialog({
{isForkJoin && canReject && (
-
โš ๏ธ [๋‹จ๊ณ„๋ณ„ ๋ฐ˜๋ ค ์•ˆ๋‚ด (10์›” 12์ผ ๊ฐœ์ •)]
+
โš ๏ธ [๋‹จ๊ณ„๋ณ„ ๋ฐ˜๋ ค ์•ˆ๋‚ด]

ํ˜„์žฌ ๋ณตํ•ฉ์‚ฌ์—… ๋ณ‘๋ ฌ ๊ฒ€ํ† ๊ฐ€ ์ง„ํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค. ๋ณธ ๋‹จ๊ณ„์—์„œ ๋ฐ˜๋ ค ์ฒ˜๋ฆฌ ์‹œ:

  • ๊ท€ํ•˜(ํ˜„์žฌ ๊ฒฐ์žฌ์ž)๋Š” ๋ฐ˜๋ ค(ATRZ_PRGRS_004) ์ฒ˜๋ฆฌ๋ฉ๋‹ˆ๋‹ค.
  • 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 0513f09ce..e2f438495 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 @@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react"; import dynamic from "next/dynamic"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { PanelRightClose, PanelRightOpen } from "lucide-react"; +import { ExternalLink, PanelRightClose, PanelRightOpen } from "lucide-react"; import { getDefaultFileKey, useAlertStore, useTus, useUserStore } from "@/shared/lib"; import { AttachmentFileField } from "@/shared/ui"; import { @@ -33,6 +33,7 @@ import { Skeleton, cn, } from "@common/components"; +import { buildBridgeSrc, getHwpCtrl } from "@/widgets/hangulgiangi"; import { PwrkCnsltnDocEvlDialog } from "./pwrk-cnsltn-doc-evl-dialog"; import { PwrkCnsltnDocPjtcoAjmtTable } from "./pwrk-cnsltn-doc-pjtco-ajmt-table"; @@ -456,8 +457,174 @@ function PwrkCnsltnDocMngDialog({ }; }, [open, pwrkCnsltnData?.atchFileGroupId, fnrsCnsltnDocNo, isForkJoin]); + function checkDirtyAndConfirm(targetBranchName: string): boolean { + if (currentBranchSeCd !== "MAIN") return true; + + try { + const ctrl = getHwpCtrl(null); + if (ctrl?.IsModified) { + return window.confirm( + `์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ(MAIN)์— ์ €์žฅ๋˜์ง€ ์•Š์€ ์ˆ˜์ •์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.\n` + + `${targetBranchName}์œผ๋กœ ์ด๋™ํ•˜๋ฉด ์ž‘์„ฑ ์ค‘์ธ ๋‚ด์šฉ์ด ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค.\n\n` + + `๊ณ„์† ์ด๋™ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? (์ด๋™์„ ์ทจ์†Œํ•˜๊ณ  [์‹ค๋ฌดํ˜‘์˜์„œ ์ €์žฅ] ํ›„ ์ด๋™ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.)` + ); + } + } catch { + return true; + } + return true; + } + + function handleTabChange(nextBranch: string, branchName: string) { + if (currentBranchSeCd !== nextBranch) { + if (!checkDirtyAndConfirm(branchName)) return; + setCurrentBranchSeCd(nextBranch); + } + setSelectedVerSn(null); + } + + async function handleOpenBranchInNewWindow(targetBranch: "SP" | "SF") { + if (!completedDraft) { + alert("์—ด๋žŒํ•  ์‹ค๋ฌดํ˜‘์˜์„œ ์ดˆ์•ˆ ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + const branchName = targetBranch === "SP" ? "๊ด€๋กœ (SP)" : "์‹œ์„ค (SF)"; + const popup = window.open( + "", + `pwrk_viewer_${targetBranch}`, + "width=1200,height=900,menubar=no,toolbar=no,location=no,status=no,resizable=yes", + ); + if (!popup) { + alert("ํŒ์—…์ด ์ฐจ๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €์˜ ํŒ์—… ํ—ˆ์šฉ ์„ค์ •์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”."); + return; + } + + const bridgeSrc = buildBridgeSrc(); + + popup.document.write(` + + + + + ์‹ค๋ฌดํ˜‘์˜์„œ [${branchName}] ์ฐธ์กฐ ๋ทฐ์–ด + + + +
    +

    + ์‹ค๋ฌดํ˜‘์˜์„œ ${branchName} ์ฐธ์กฐ ๋ฌธ์„œ + ์ฝ๊ธฐ ์ „์šฉ ์ฐธ์กฐ ๋ชจ๋“œ +

    +
    + + +
    +
    +
    ์›นํ•œ๊ธ€ ๊ธฐ์•ˆ๊ธฐ ๋ฐ ${branchName} ์ดˆ์•ˆ ๋ฌธ์„œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘์ž…๋‹ˆ๋‹ค...
    + + + + `); + popup.document.close(); + + try { + const response = await pwrkCnsltnDocCrtRsltService.openWebHwpLatest( + completedDraft.crtDmndUuid, + targetBranch, + ); + const blob = response.data; + const fileName = `์‹ค๋ฌดํ˜‘์˜์„œ_${targetBranch === "SP" ? "๊ด€๋กœ" : "์‹œ์„ค"}.hwpx`; + const documentFile = new File([blob], fileName, { type: "application/haansofthwpx" }); + + // ๋‹ค์šด๋กœ๋“œ fallback ํ•ธ๋“ค๋Ÿฌ ๋ฐ”์ธ๋”ฉ + (popup as unknown as { onDownloadClick?: () => void }).onDownloadClick = () => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + const downloadBtn = popup.document.getElementById("btn_download") as HTMLButtonElement | null; + if (downloadBtn) { + downloadBtn.disabled = false; + } + + const checkTimer = window.setInterval(() => { + try { + if (popup.closed) { + window.clearInterval(checkTimer); + return; + } + const frame = popup.document.getElementById("viewer_frame") as HTMLIFrameElement | null; + const ctrl = (frame?.contentWindow as Record | null)?.HwpCtrl as { + Impl?: unknown; + Open?: ( + source: File, + format: string, + option: string, + callback?: () => void, + ) => boolean; + } | null | undefined; + + if (ctrl && ctrl.Impl && typeof ctrl.Open === "function") { + window.clearInterval(checkTimer); + const loadingEl = popup.document.getElementById("loading"); + if (loadingEl) loadingEl.style.display = "none"; + ctrl.Open(documentFile, "HWPX", "", () => {}); + } + } catch { + window.clearInterval(checkTimer); + } + }, 500); + + window.setTimeout(() => window.clearInterval(checkTimer), 30_000); + } catch (err) { + console.error("์ƒˆ ์ฐฝ ๋ฌธ์„œ ๋กœ๋“œ ์‹คํŒจ:", err); + const loadingEl = popup.document.getElementById("loading"); + if (loadingEl) { + loadingEl.textContent = `${branchName} ๋ฌธ์„œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.`; + loadingEl.style.color = "#ef4444"; + } + alert(`${branchName} ๋ฌธ์„œ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.`); + } + } + function handleOpenChange(nextOpen: boolean) { - if (!nextOpen) onClose(); + if (!nextOpen) { + if (currentBranchSeCd === "MAIN") { + try { + const ctrl = getHwpCtrl(null); + if (ctrl?.IsModified) { + const confirmed = window.confirm( + "์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ(MAIN)์— ์ €์žฅ๋˜์ง€ ์•Š์€ ์ˆ˜์ •์‚ฌํ•ญ์ด ์žˆ์Šต๋‹ˆ๋‹ค.\n์ฐฝ์„ ๋‹ซ์œผ๋ฉด ์ž‘์„ฑ ์ค‘์ธ ๋‚ด์šฉ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ๋‹ซ์œผ์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?", + ); + if (!confirmed) return; + } + } catch { + // ignore + } + } + onClose(); + } } async function handleSaveDocument(file: Blob) { @@ -739,10 +906,7 @@ function PwrkCnsltnDocMngDialog({ type="button" size="sm" color={currentBranchSeCd === "SP" ? "primary" : "white"} - onClick={() => { - setCurrentBranchSeCd("SP"); - setSelectedVerSn(null); - }} + onClick={() => handleTabChange("SP", "๊ด€๋กœ ์ดˆ์•ˆ (SP)")} > ๊ด€๋กœ ์ดˆ์•ˆ (SP) @@ -750,10 +914,7 @@ function PwrkCnsltnDocMngDialog({ type="button" size="sm" color={currentBranchSeCd === "SF" ? "primary" : "white"} - onClick={() => { - setCurrentBranchSeCd("SF"); - setSelectedVerSn(null); - }} + onClick={() => handleTabChange("SF", "์‹œ์„ค ์ดˆ์•ˆ (SF)")} > ์‹œ์„ค ์ดˆ์•ˆ (SF) @@ -763,13 +924,37 @@ function PwrkCnsltnDocMngDialog({ type="button" size="sm" color={currentBranchSeCd === "MAIN" ? "primary" : "white"} - onClick={() => { - setCurrentBranchSeCd("MAIN"); - setSelectedVerSn(null); - }} + onClick={() => handleTabChange("MAIN", "์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ (MAIN)")} > ์ข…ํ•ฉ ์‹ค๋ฌดํ˜‘์˜์„œ (MAIN) + {(isMerger || effectiveBranchSeCd === "TOTAL_AGG") && ( +
    + ์ฐธ์กฐ ์ƒˆ ์ฐฝ: + + +
    + )}
{effectiveUserRole === "ROLE_USER05" @@ -794,7 +979,7 @@ function PwrkCnsltnDocMngDialog({ ? isMergerApproved ? "์ข…ํ•ฉ๋ณ‘ํ•ฉ์ž: ๋ณ‘ํ•ฉ ๊ฒฐ์žฌ ์™„๋ฃŒ (์ฝ๊ธฐ ์ „์šฉ ๋ชจ๋“œ)" : currentBranchSeCd === "MAIN" - ? "์ข…ํ•ฉ๋ณ‘ํ•ฉ์ž: SP/SF๋ฅผ ์ฐธ์กฐํ•˜์—ฌ MAIN ์ข…ํ•ฉ๋ณธ์„ ์ˆ˜๋™ ๋ณ‘ํ•ฉ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค." + ? "์ข…ํ•ฉ๋ณ‘ํ•ฉ์ž: ์ƒ๋‹จ [์ƒˆ ์ฐฝ ๋ณด๊ธฐ]๋กœ ๊ด€๋กœ/์‹œ์„ค ๋ฌธ์„œ๋ฅผ ๋„์›Œ ๋Œ€์กฐํ•˜๋ฉด์„œ, MAIN ์ข…ํ•ฉ๋ณธ์„ ์ˆ˜๋™ ๋ณ‘ํ•ฉ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค." : "์ข…ํ•ฉ๋ณ‘ํ•ฉ์ž: ํƒ€ ๋ฌธ์„œ ์—ด๋žŒ ๋ชจ๋“œ (์ฝ๊ธฐ ์ „์šฉ)" : effectiveBranchSeCd === "SP" ? currentBranchSeCd === "SP" diff --git a/apps/as/src/widgets/hangulgiangi/index.ts b/apps/as/src/widgets/hangulgiangi/index.ts index cbce6f73a..089cad5f6 100644 --- a/apps/as/src/widgets/hangulgiangi/index.ts +++ b/apps/as/src/widgets/hangulgiangi/index.ts @@ -1 +1,5 @@ export { HangulGiangi, type HangulGiangiProps } from "./ui/hangul-giangi"; +export { getHwpCtrl } from "./lib/get-hwp-ctrl"; +export { buildBridgeSrc } from "./lib/build-bridge-src"; +export { DEFAULT_BRIDGE_URL } from "./lib/hangul-giangi-constants"; +export type { HwpCtrl } from "./model/hwp-ctrl"; diff --git a/apps/as/src/widgets/hangulgiangi/lib/build-bridge-src.ts b/apps/as/src/widgets/hangulgiangi/lib/build-bridge-src.ts new file mode 100644 index 000000000..9039c6fbb --- /dev/null +++ b/apps/as/src/widgets/hangulgiangi/lib/build-bridge-src.ts @@ -0,0 +1,20 @@ +import { WEBHYP_CTRL_URL } from "@/shared/config"; +import { DEFAULT_BRIDGE_URL } from "./hangul-giangi-constants"; + +/** + * ์›นํ•œ๊ธ€ ๊ธฐ์•ˆ๊ธฐ ๋ธŒ๋ฆฌ์ง€ HTML(/webhwpctrl/index.html)์— + * ํ•œ์ปด ์ปจํŠธ๋กค ์„œ๋ฒ„ baseurl ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•œ iframe src URL์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ +export function buildBridgeSrc( + bridgeUrl: string = DEFAULT_BRIDGE_URL, + ctrlBaseUrl: string = WEBHYP_CTRL_URL, +): string { + const base = ctrlBaseUrl.replace(/\/$/, ""); + const url = new URL( + bridgeUrl, + typeof window !== "undefined" ? window.location.origin : "http://localhost", + ); + url.searchParams.set("baseurl", base); + if (bridgeUrl.startsWith("/")) return `${url.pathname}${url.search}`; + return url.toString(); +} diff --git a/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts b/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts index bc3ac97cd..90cc8c1c0 100644 --- a/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts +++ b/apps/as/src/widgets/hangulgiangi/model/hwp-ctrl.ts @@ -59,6 +59,7 @@ export interface HwpCtrl { callbackUserData?: unknown, ) => boolean; ShowHorizontalScroll: (show: boolean) => boolean; + IsModified?: boolean; } export interface HwpCtrlFrameWindow { diff --git a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx index 4fa2b8c3e..65bcf9fd6 100644 --- a/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx +++ b/apps/as/src/widgets/hangulgiangi/ui/hangul-giangi.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { WEBHYP_CTRL_URL } from "@/shared/config"; import { cn } from "@common/components"; +import { buildBridgeSrc } from "../lib/build-bridge-src"; import { getHwpCtrl } from "../lib/get-hwp-ctrl"; import { DEFAULT_BRIDGE_URL, @@ -240,16 +241,6 @@ function resolveHwpCtrl(iframe: HTMLIFrameElement | null, isCtrlReady: boolean): return null; } -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); - if (bridgeUrl.startsWith("/")) return `${url.pathname}${url.search}`; - return url.toString(); -} async function resolveSaveAsDocumentBlob(result: HwpCtrlSaveAsCallbackResult): Promise { const downloadCandidate = result.downloadUrl ?? result.path;