diff --git a/frontend/src/lib/dualMonitor.ts b/frontend/src/lib/dualMonitor.ts index e4730bb..84503e9 100644 --- a/frontend/src/lib/dualMonitor.ts +++ b/frontend/src/lib/dualMonitor.ts @@ -35,7 +35,19 @@ interface WindowWithScreenDetails extends Window { getScreenDetails?: () => Promise; } -/** 우측 모니터 좌표·크기 계산 */ +/** 클릭 직후 동기적으로 쓸 기본 창 위치 (await 없음) */ +function buildSyncWindowFeatures(): string { + const left = window.screenX + window.outerWidth; + const top = window.screenY; + let width = window.screen.availWidth - left; + if (width < 800) { + width = 1280; + } + const height = window.screen.availHeight; + return `left=${left},top=${top},width=${width},height=${height},menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes`; +} + +/** 우측 모니터 좌표·크기 계산 (열린 뒤 위치 조정용) */ export async function getRightMonitorWindowFeatures(): Promise { let left = window.screenX + window.outerWidth; let top = window.screenY; @@ -116,59 +128,73 @@ function postTaskSelected(taskId: string) { getChannel().postMessage({ type: 'TASK_SELECTED', taskId } satisfies DualMonitorEvent); } -/** 상세 창 토글 — 열려 있으면 닫고, 닫혀 있으면 오른쪽 모니터에 열기 */ -export async function openDetailWindow(): Promise { - if (isDetailWindowOpen()) { - detailWindow!.close(); - detailWindow = null; - return null; - } +function scheduleTaskSelected(taskId: string) { + postTaskSelected(taskId); + setTimeout(() => postTaskSelected(taskId), 500); + setTimeout(() => postTaskSelected(taskId), 1500); +} +/** 상세 창 열기 — 반드시 사용자 클릭 직후 동기 호출 */ +function openDetailWindowSync(): Window | null { const detailUrl = `${window.location.origin}/detail`; - // 사용자 클릭 직후 동기적으로 열어야 팝업 차단을 피할 수 있음 - detailWindow = window.open(detailUrl, DETAIL_WINDOW_NAME, 'noopener,noreferrer,width=1280,height=900'); + const features = buildSyncWindowFeatures(); + + detailWindow = window.open(detailUrl, DETAIL_WINDOW_NAME, features); if (!detailWindow) { - alert('팝업이 차단되었습니다. 브라우저에서 이 사이트의 팝업을 허용해 주세요.'); + console.warn('상세 창을 열지 못했습니다. 브라우저 팝업 허용을 확인해 주세요.'); return null; } try { detailWindow.focus(); } catch { - // popup-blocked 등 + // ignore } - void getRightMonitorWindowFeatures().then((features) => { - const { left, top, width, height } = parseWindowFeatures(features); + // 창을 연 뒤 비동기로 위치만 보정 (팝업 차단과 무관) + void getRightMonitorWindowFeatures().then((f) => { + const { left, top, width, height } = parseWindowFeatures(f); if (detailWindow && !detailWindow.closed && left != null && top != null && width && height) { applyWindowPlacement(detailWindow, left, top, width, height); } }); - const savedTaskId = getPersistedTaskId(); - if (savedTaskId) { - postTaskSelected(savedTaskId); - setTimeout(() => postTaskSelected(savedTaskId), 500); - } - return detailWindow; } +/** 상세 창 토글 — 열려 있으면 닫고, 닫혀 있으면 오른쪽 모니터에 열기 */ +export function openDetailWindow(): Window | null { + if (isDetailWindowOpen()) { + detailWindow!.close(); + detailWindow = null; + return null; + } + + const win = openDetailWindowSync(); + const savedTaskId = getPersistedTaskId(); + if (win && savedTaskId) { + scheduleTaskSelected(savedTaskId); + } + return win; +} + /** 좌측 → 우측: 업무 선택 이벤트 전송 (창이 닫혀 있으면 열고 전송) */ -export async function sendTaskSelected(taskId: string): Promise { +export function sendTaskSelected(taskId: string): void { persistSelectedTask(taskId); if (!isDetailWindowOpen()) { - const win = await openDetailWindow(); + const win = openDetailWindowSync(); if (!win) return; - postTaskSelected(taskId); - setTimeout(() => postTaskSelected(taskId), 500); - setTimeout(() => postTaskSelected(taskId), 1500); + scheduleTaskSelected(taskId); return; } - detailWindow!.focus(); - postTaskSelected(taskId); + try { + detailWindow!.focus(); + } catch { + // ignore + } + scheduleTaskSelected(taskId); } /** 좌측 → 우측: 업무 선택 해제 */ @@ -193,14 +219,24 @@ export function closeChannel(): void { channel = null; } -/** 웹 링크를 우측 모니터 새 창에서 열기 (같은 URL이면 기존 창 포커스) */ -export async function openLinkOnRightMonitor(url: string, windowName: string): Promise { - const features = await getRightMonitorWindowFeatures(); +/** 웹 링크를 우측 모니터 새 창에서 열기 */ +export function openLinkOnRightMonitor(url: string, windowName: string): Window | null { + const features = buildSyncWindowFeatures(); const win = window.open(url, windowName, features); try { win?.focus(); } catch { - // popup-blocked 등 + // ignore } + + if (win) { + void getRightMonitorWindowFeatures().then((f) => { + const { left, top, width, height } = parseWindowFeatures(f); + if (win && !win.closed && left != null && top != null && width && height) { + applyWindowPlacement(win, left, top, width, height); + } + }); + } + return win; } diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 97bd708..05c4ede 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -216,7 +216,7 @@ export default function DashboardPage() { stats={stats} activeStatus={activeStatus} onStatusChange={setActiveStatus} - onOpenDetailWindow={() => void openDetailWindow()} + onOpenDetailWindow={() => { openDetailWindow(); }} onOpenTaskManager={() => setShowTaskManager(true)} />