From 63a40c9c75e42014414b2773ddadbe8618832b30 Mon Sep 17 00:00:00 2001 From: Macbook Date: Mon, 15 Jun 2026 16:30:44 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=B1=20=EC=9C=84=EC=A0=AF=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=ED=8C=9D=EC=97=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- desktop/src/widget-main.tsx | 16 ++- .../floatingWidgets/FloatingWidgetWindow.tsx | 53 +++++--- .../widgetDashboard/WidgetPickerModal.tsx | 115 ++++++++++++++---- frontend/src/styles/widgetDashboard.css | 89 ++++++++++++-- 4 files changed, 223 insertions(+), 50 deletions(-) diff --git a/desktop/src/widget-main.tsx b/desktop/src/widget-main.tsx index 87db751..147df9e 100644 --- a/desktop/src/widget-main.tsx +++ b/desktop/src/widget-main.tsx @@ -137,7 +137,7 @@ const WidgetApp: React.FC = () => { const sizeBeforePickerRef = useRef<{ width: number; height: number } | null>(null); - const handlePickerOpenChange = useCallback(async (open: boolean) => { + const handlePickerOpenChange = useCallback(async (open: boolean): Promise => { const win = getCurrentWindow(); if (open) { const size = await win.innerSize(); @@ -148,6 +148,18 @@ const WidgetApp: React.FC = () => { const h = Math.min(NATIVE_WIDGET_PICKER_WINDOW.height, maxH); await win.setSize(new LogicalSize(w, h)); await win.center(); + await new Promise(resolve => { + let settled = false; + const finish = () => { + if (settled) return; + settled = true; + unlisten?.(); + resolve(); + }; + let unlisten: (() => void) | undefined; + void win.onResized(finish).then(fn => { unlisten = fn; }); + window.setTimeout(finish, 150); + }); return; } const prev = sizeBeforePickerRef.current; @@ -183,7 +195,7 @@ const WidgetApp: React.FC = () => { nativeShell onClose={handleClose} onFocus={() => {}} - onPickerOpenChange={open => { void handlePickerOpenChange(open); }} + onPickerOpenChange={handlePickerOpenChange} onUpdateSlots={slots => setInstance(prev => (prev ? { ...prev, slots } : prev))} onUpdateSize={(width, height) => { setInstance(prev => (prev ? { ...prev, width, height } : prev)); diff --git a/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx b/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx index 65b027b..8354c07 100644 --- a/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx +++ b/frontend/src/components/floatingWidgets/FloatingWidgetWindow.tsx @@ -24,8 +24,8 @@ interface Props { onUpdateGridFr: (rowFr: number[], colFr: number[]) => void; /** Tauri OS 창 — 네이티브 타이틀·리사이즈 사용, 내부 플로팅 크롬 숨김 */ nativeShell?: boolean; - /** 위젯 선택 팝업 열림/닫힘 (네이티브 창 크기 조절용) */ - onPickerOpenChange?: (open: boolean) => void; + /** 위젯 선택 팝업 열림/닫힘 (네이티브 창 크기 조절 — 완료 후 picker 표시) */ + onPickerOpenChange?: (open: boolean) => void | Promise; } const FloatingWidgetWindow: React.FC = ({ @@ -40,16 +40,29 @@ const FloatingWidgetWindow: React.FC = ({ onPickerOpenChange, }) => { const [pickSlotId, setPickSlotId] = useState(null); + const [pickerOpening, setPickerOpening] = useState(false); - const openPicker = useCallback((slotId: string) => { + const openPicker = useCallback(async (slotId: string) => { + if (nativeShell && onPickerOpenChange) { + setPickerOpening(true); + try { + await onPickerOpenChange(true); + await new Promise(resolve => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + }); + } finally { + setPickerOpening(false); + } + } setPickSlotId(slotId); - onPickerOpenChange?.(true); - }, [onPickerOpenChange]); + }, [nativeShell, onPickerOpenChange]); - const closePicker = useCallback(() => { + const closePicker = useCallback(async () => { setPickSlotId(null); - onPickerOpenChange?.(false); - }, [onPickerOpenChange]); + if (nativeShell && onPickerOpenChange) { + await onPickerOpenChange(false); + } + }, [nativeShell, onPickerOpenChange]); const bodyWrapRef = useRef(null); const bodyRef = useRef(null); const [bodySize, setBodySize] = useState({ width: 0, height: 0 }); @@ -143,8 +156,7 @@ const FloatingWidgetWindow: React.FC = ({ const applyWidgetType = useCallback((widgetType: string) => { if (!pickSlotId) return; updateSlot(pickSlotId, s => ({ ...s, widgetType, config: {} })); - closePicker(); - }, [pickSlotId, updateSlot, closePicker]); + }, [pickSlotId, updateSlot]); const clearWidget = useCallback((slotId: string) => { updateSlot(slotId, s => ({ ...s, widgetType: null, config: {} })); @@ -246,7 +258,7 @@ const FloatingWidgetWindow: React.FC = ({ { void openPicker(id); }} onClearWidget={id => clearWidget(id)} /> @@ -301,13 +313,18 @@ const FloatingWidgetWindow: React.FC = ({ )} - + {(pickerOpening || pickSlotId != null) && ( + { void closePicker(); }} + onSelect={applyWidgetType} + /> + )} + {pickerOpening && pickSlotId == null && nativeShell && ( +
위젯 목록 준비 중…
+ )} ); diff --git a/frontend/src/components/widgetDashboard/WidgetPickerModal.tsx b/frontend/src/components/widgetDashboard/WidgetPickerModal.tsx index 3a91294..6b29204 100644 --- a/frontend/src/components/widgetDashboard/WidgetPickerModal.tsx +++ b/frontend/src/components/widgetDashboard/WidgetPickerModal.tsx @@ -10,20 +10,11 @@ interface Props { category: WidgetPickerCategory; onClose: () => void; onSelect: (widgetType: string) => void; - /** Tauri 네이티브 위젯 창 — 작은 viewport에 맞춘 전체 화면형 팝업 */ + /** Tauri 네이티브 위젯 창 — OS 창 전체를 쓰는 고정형 picker */ nativeCompact?: boolean; } -const WidgetPickerModal: React.FC = ({ - open, - category, - onClose, - onSelect, - nativeCompact = false, -}) => { - if (!open) return null; - - const groups = category === 'all' ? widgetsByGroupAll() : widgetsByGroup(category); +function pickerMeta(category: WidgetPickerCategory) { const title = category === 'center' ? '메인 위젯' : category === 'side' @@ -34,20 +25,19 @@ const WidgetPickerModal: React.FC = ({ : category === 'side' ? '좌·우·하단 패널에 추가할 위젯을 선택하세요. 주식 HTS/위젯 프로그램에서 흔히 쓰는 구성을 지원합니다.' : '추가할 위젯 유형을 선택하세요. 차트·시세·매매·포트폴리오 등 모든 위젯을 사용할 수 있습니다.'; + return { title, intro }; +} + +const WidgetPickerContent: React.FC<{ + category: WidgetPickerCategory; + onSelect: (widgetType: string) => void; + onClose: () => void; +}> = ({ category, onSelect, onClose }) => { + const groups = category === 'all' ? widgetsByGroupAll() : widgetsByGroup(category); + const { intro } = pickerMeta(category); return ( - + <>

{intro}

{groups.map(g => (
@@ -70,6 +60,85 @@ const WidgetPickerModal: React.FC = ({
))} + + ); +}; + +const NativeWidgetPicker: React.FC = ({ + open, + category, + onClose, + onSelect, +}) => { + if (!open) return null; + const { title } = pickerMeta(category); + + return ( +
+
+
+

+ {title} + (Widget) +

+ +
+
+ +
+
+
+ ); +}; + +const WidgetPickerModal: React.FC = ({ + open, + category, + onClose, + onSelect, + nativeCompact = false, +}) => { + if (!open) return null; + + if (nativeCompact) { + return ( + + ); + } + + const { title } = pickerMeta(category); + + return ( + + ); }; diff --git a/frontend/src/styles/widgetDashboard.css b/frontend/src/styles/widgetDashboard.css index 5f46a80..654d388 100644 --- a/frontend/src/styles/widgetDashboard.css +++ b/frontend/src/styles/widgetDashboard.css @@ -1241,17 +1241,92 @@ max-height: none; } -/* Tauri 네이티브 위젯 창 — OS 창 확대 후 팝업이 거의 전체를 채움 */ -html.fw-native-widget .app-popup-shell--widget-picker-native { - width: calc(100vw - 12px) !important; - max-width: calc(100vw - 12px) !important; - max-height: calc(100vh - 12px); - min-height: calc(100vh - 16px); +/* Tauri 네이티브 위젯 창 — 전체 창 picker (AppPopup draggable 미사용) */ +html.fw-native-widget .wd-picker-native-overlay { + position: fixed; + inset: 0; + z-index: 13200; + display: flex; + flex-direction: column; + padding: 10px; + box-sizing: border-box; + background: rgba(0, 0, 0, 0.5); } -html.fw-native-widget .app-popup-shell--widget-picker-native .wd-picker-popup-body { +html.fw-native-widget .wd-picker-native-shell { flex: 1; min-height: 0; + display: flex; + flex-direction: column; + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--accent, #3f7ef5) 45%, var(--border)); + background: var(--bg2, #1e222d); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.55); + overflow: hidden; +} + +html.fw-native-widget .wd-picker-native-header { + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 10px 14px; + border-bottom: 1px solid var(--border); + background: linear-gradient( + 180deg, + color-mix(in srgb, var(--accent, #3f7ef5) 14%, var(--bg2)) 0%, + var(--bg2, #1e222d) 100% + ); +} + +html.fw-native-widget .wd-picker-native-title { + margin: 0; + font-size: 14px; + font-weight: 700; + color: var(--text); +} + +html.fw-native-widget .wd-picker-native-title-ko { + font-size: 12px; + font-weight: 400; + color: var(--text2); +} + +html.fw-native-widget .wd-picker-native-close { + flex-shrink: 0; + width: 28px; + height: 28px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg3); + color: var(--text2); + cursor: pointer; + font-size: 14px; + line-height: 1; +} + +html.fw-native-widget .wd-picker-native-close:hover { + color: var(--text); + border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); +} + +html.fw-native-widget .wd-picker-native-body { + flex: 1; + min-height: 0; + overflow-y: auto; +} + +html.fw-native-widget .wd-picker-native-loading { + position: fixed; + inset: 0; + z-index: 13199; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.35); + color: var(--text2); + font-size: 13px; } .bps-page--wd-no-left .bps-side-wrap--left,