From 0da56b505c61bba19c9bb5c922e9e8f108462bd1 Mon Sep 17 00:00:00 2001 From: EENE Dashboard Date: Fri, 5 Jun 2026 10:27:14 +0900 Subject: [PATCH] fix: open detail popup on right monitor using Window Management API Co-authored-by: Cursor --- .../components/dashboard/DashboardHeader.tsx | 2 +- frontend/src/lib/dualMonitor.ts | 81 +++++++++++++++---- 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/dashboard/DashboardHeader.tsx b/frontend/src/components/dashboard/DashboardHeader.tsx index bacd9d9..e71efd2 100644 --- a/frontend/src/components/dashboard/DashboardHeader.tsx +++ b/frontend/src/components/dashboard/DashboardHeader.tsx @@ -11,7 +11,7 @@ interface DashboardHeaderProps { stats: Stats; activeStatus: string; onStatusChange: (status: string) => void; - onOpenDetailWindow: () => void; + onOpenDetailWindow: () => void | Promise; onOpenTaskManager: () => void; } diff --git a/frontend/src/lib/dualMonitor.ts b/frontend/src/lib/dualMonitor.ts index cbe2468..0ac837a 100644 --- a/frontend/src/lib/dualMonitor.ts +++ b/frontend/src/lib/dualMonitor.ts @@ -14,6 +14,63 @@ type DualMonitorEvent = let channel: BroadcastChannel | null = null; let detailWindow: Window | null = null; +interface ScreenDetailed { + left: number; + top: number; + width: number; + height: number; + availLeft?: number; + availTop?: number; + availWidth?: number; + availHeight?: number; +} + +interface ScreenDetails { + currentScreen: ScreenDetailed; + screens: ScreenDetailed[]; +} + +interface WindowWithScreenDetails extends Window { + getScreenDetails?: () => Promise; +} + +/** 참조 대시보드와 동일: 우측 모니터 좌표·크기 계산 */ +async function getDetailWindowFeatures(): Promise { + let left = window.screenX + window.outerWidth; + let top = window.screenY; + let width = window.screen.availWidth; + let height = window.screen.availHeight; + + try { + const win = window as WindowWithScreenDetails; + if (win.getScreenDetails) { + const details = await win.getScreenDetails(); + const current = details.currentScreen; + let target = details.screens.find((s) => s.left >= current.left + current.width); + target ||= details.screens.find((s) => s !== current); + + if (target) { + left = target.availLeft ?? target.left; + top = target.availTop ?? target.top; + width = target.availWidth ?? target.width; + height = target.availHeight ?? target.height; + } else { + left = window.screenX + window.outerWidth; + width = window.screen.availWidth - left; + if (width < 800) { + width = 1920; + left = window.screen.availWidth; + } + height = window.screen.availHeight; + } + } + } catch (err) { + console.warn('Window Management API failed or denied, using fallback', err); + } + + return `left=${left},top=${top},width=${width},height=${height},menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes`; +} + function getChannel(): BroadcastChannel { if (!channel) { channel = new BroadcastChannel(CHANNEL_NAME); @@ -27,33 +84,29 @@ export function isDetailWindowOpen(): boolean { } /** 상세 창 토글 — 열려 있으면 닫고, 닫혀 있으면 오른쪽 모니터에 열기 */ -export function openDetailWindow(): Window | null { +export async function openDetailWindow(): Promise { if (isDetailWindowOpen()) { detailWindow!.close(); detailWindow = null; return null; } - // 현재 창이 왼쪽 모니터에 있다고 가정하고 - // 오른쪽 모니터의 시작 X 좌표 = 현재 창 X + 현재 화면 너비 - const screenW = window.screen.width; - const screenH = window.screen.height; - const rightMonitorLeft = window.screenX + screenW; - const detailUrl = `${window.location.origin}/detail`; + const features = await getDetailWindowFeatures(); - detailWindow = window.open( - detailUrl, - DETAIL_WINDOW_NAME, - `width=${screenW},height=${screenH},left=${rightMonitorLeft},top=0,resizable=yes,scrollbars=yes`, - ); + detailWindow = window.open(detailUrl, DETAIL_WINDOW_NAME, features); + try { + detailWindow?.focus(); + } catch { + // popup-blocked 등 + } return detailWindow; } /** 좌측 → 우측: 업무 선택 이벤트 전송 (창이 닫혀 있으면 열고 전송) */ -export function sendTaskSelected(taskId: string): void { +export async function sendTaskSelected(taskId: string): Promise { if (!isDetailWindowOpen()) { - openDetailWindow(); + await openDetailWindow(); // 창이 로드될 때까지 잠시 대기 후 전송 setTimeout(() => { getChannel().postMessage({ type: 'TASK_SELECTED', taskId } satisfies DualMonitorEvent);