앱 위젯에서 구분선 리사이즈 안되는 문제 수정

This commit is contained in:
Macbook
2026-06-14 17:54:26 +09:00
parent 7f5e7b22d2
commit ead2ba6b3a
9 changed files with 229 additions and 65 deletions
+4 -3
View File
@@ -71,10 +71,11 @@
'<p class="muted" style="margin-top:12px">' + (info.version ? 'v' + info.version + ' · ' : '') +
(info.sizeBytes ? (info.sizeBytes / 1048576).toFixed(1) + ' MB' : '') + '</p>' +
'<a class="btn" href="' + url + '" download>APK 다운로드</a>' +
'<ol style="margin-top:20px"><li>다운로드 후 APK 파일을 열어 설치</li>' +
'<ol style="margin-top:20px"><li>다운로드 후 APK 파일을 열어 설치를 <strong>완료</strong>하세요</li>' +
'<li>「알 수 없는 출처」 허용 필요 시 설정에서 활성화</li>' +
'<li>앱은 서버(exdev.co.kr)에 자동 연결됩니다</li>' +
'<li>새 버전은 앱 시작·설정 → 업데이트 확인</li></ol>';
'<li>패키지: <code>com.goldenchart.app</code> (다른 GoldenChart 앱과 구분)</li>' +
'<li>「Unable to load script」 오류 시 예전 앱을 삭제 후 재설치</li>' +
'<li>앱은 서버(exdev.co.kr)에 자동 연결됩니다</li></ol>';
QRCode.toCanvas(document.getElementById('qr'), url, { width: 200, margin: 1 });
} catch (e) {
el.innerHTML = '<p class="err">앱 정보를 불러오지 못했습니다.</p>';
@@ -37,9 +37,27 @@ const FloatingWidgetWindow: React.FC<Props> = ({
nativeShell = false,
}) => {
const [pickSlotId, setPickSlotId] = useState<string | null>(null);
const bodyWrapRef = useRef<HTMLDivElement>(null);
const bodyRef = useRef<HTMLDivElement>(null);
const [bodySize, setBodySize] = useState({ width: 0, height: 0 });
const resolveBodySize = useCallback(() => {
const wrap = bodyWrapRef.current;
const body = bodyRef.current;
const width = wrap?.clientWidth ?? body?.clientWidth ?? bodySize.width;
const height = wrap?.clientHeight ?? body?.clientHeight ?? bodySize.height;
if (width > 0 && height > 0) {
return { width, height };
}
if (nativeShell) {
return {
width: width > 0 ? width : instance.width,
height: height > 0 ? height : instance.height,
};
}
return { width, height };
}, [bodySize.height, bodySize.width, instance.height, instance.width, nativeShell]);
const rowFr = instance.rowFr?.length === instance.rows
? instance.rowFr
: defaultGridFr(instance.rows);
@@ -92,15 +110,17 @@ const FloatingWidgetWindow: React.FC<Props> = ({
onUpdateGridFr(next, colFrRef.current);
}, [onUpdateGridFr]);
const notifyLayoutSync = useCallback(() => {
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
}, []);
const { onColSplitPointerDown, onRowSplitPointerDown } = useFloatingWidgetGridSplit({
onColFrChange: handleColFrChange,
onRowFrChange: handleRowFrChange,
getColFr: () => colFrRef.current,
getRowFr: () => rowFrRef.current,
getBodySize: () => ({
width: bodyRef.current?.clientWidth ?? bodySize.width,
height: bodyRef.current?.clientHeight ?? bodySize.height,
}),
getBodySize: resolveBodySize,
onSplitEnd: notifyLayoutSync,
});
const updateSlot = useCallback((slotId: string, updater: (s: WidgetSlot) => WidgetSlot) => {
@@ -119,24 +139,30 @@ const FloatingWidgetWindow: React.FC<Props> = ({
/* 창·그리드 크기 변경 시 내부 차트·위젯 레이아웃 재동기화 */
useEffect(() => {
const el = bodyRef.current;
const el = bodyWrapRef.current ?? bodyRef.current;
if (!el) return undefined;
let timer: ReturnType<typeof setTimeout> | null = null;
const scheduleLayoutSync = () => {
setBodySize({ width: el.clientWidth, height: el.clientHeight });
const wrap = bodyWrapRef.current;
const body = bodyRef.current;
setBodySize({
width: wrap?.clientWidth ?? body?.clientWidth ?? 0,
height: wrap?.clientHeight ?? body?.clientHeight ?? 0,
});
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
window.dispatchEvent(new CustomEvent('gc-widget-layout-sync'));
}, 80);
timer = setTimeout(notifyLayoutSync, 80);
};
scheduleLayoutSync();
const ro = new ResizeObserver(() => scheduleLayoutSync());
ro.observe(el);
if (bodyRef.current && bodyRef.current !== el) {
ro.observe(bodyRef.current);
}
return () => {
if (timer) clearTimeout(timer);
ro.disconnect();
};
}, [instance.rows, instance.cols]);
}, [instance.rows, instance.cols, notifyLayoutSync]);
const colSplitPositions = useMemo(
() => gridSplitPositions(colFr, bodySize.width, 1),
@@ -193,7 +219,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
</div>
)}
<div className="fw-window-body-wrap">
<div ref={bodyWrapRef} className="fw-window-body-wrap">
<div
ref={bodyRef}
className="fw-window-body"
@@ -221,6 +247,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
key={`col-${index}`}
className="fw-grid-splitter fw-grid-splitter--v"
style={{ left }}
data-no-chart-pan="true"
role="separator"
aria-orientation="vertical"
aria-label={`${index + 1}·${index + 2} 경계 조절`}
@@ -232,6 +259,7 @@ const FloatingWidgetWindow: React.FC<Props> = ({
key={`row-${index}`}
className="fw-grid-splitter fw-grid-splitter--h"
style={{ top }}
data-no-chart-pan="true"
role="separator"
aria-orientation="horizontal"
aria-label={`${index + 1}·${index + 2} 경계 조절`}
@@ -5,7 +5,7 @@ import {
FLOATING_WIDGET_CELL_MIN_W,
FLOATING_WIDGET_GRID_GAP,
} from '../types/floatingWidget';
import { bindWindowDrag, isPrimaryPointerButton } from '../utils/pointerDrag';
import { isPrimaryPointerButton } from '../utils/pointerDrag';
type SplitAxis = 'col' | 'row';
@@ -15,6 +15,7 @@ interface Options {
getColFr: () => number[];
getRowFr: () => number[];
getBodySize: () => { width: number; height: number };
onSplitEnd?: () => void;
}
export function useFloatingWidgetGridSplit({
@@ -23,18 +24,12 @@ export function useFloatingWidgetGridSplit({
getColFr,
getRowFr,
getBodySize,
onSplitEnd,
}: Options) {
const unbindDragRef = useRef<(() => void) | null>(null);
const endDrag = useCallback(() => {
unbindDragRef.current?.();
unbindDragRef.current = null;
document.body.style.cursor = '';
document.body.style.userSelect = '';
}, []);
const activeDragRef = useRef<(() => void) | null>(null);
useEffect(() => () => {
unbindDragRef.current?.();
activeDragRef.current?.();
document.body.style.cursor = '';
document.body.style.userSelect = '';
}, []);
@@ -60,43 +55,67 @@ export function useFloatingWidgetGridSplit({
const startY = e.clientY;
const startColFr = [...getColFr()];
const startRowFr = [...getRowFr()];
const startSize = getBodySize();
document.body.style.cursor = axis === 'col' ? 'col-resize' : 'row-resize';
document.body.style.userSelect = 'none';
unbindDragRef.current?.();
unbindDragRef.current = bindWindowDrag(
(xy) => {
if (axis === 'col') {
const delta = xy.x - startX;
onColFrChange(adjustAdjacentGridFr(
startColFr,
index,
delta,
startSize.width,
FLOATING_WIDGET_GRID_GAP,
FLOATING_WIDGET_CELL_MIN_W,
));
return;
}
const delta = xy.y - startY;
onRowFrChange(adjustAdjacentGridFr(
startRowFr,
activeDragRef.current?.();
const onMove = (ev: PointerEvent) => {
const size = getBodySize();
if (axis === 'col') {
if (size.width <= 0) return;
const delta = ev.clientX - startX;
onColFrChange(adjustAdjacentGridFr(
startColFr,
index,
delta,
startSize.height,
size.width,
FLOATING_WIDGET_GRID_GAP,
FLOATING_WIDGET_CELL_MIN_H,
FLOATING_WIDGET_CELL_MIN_W,
));
},
() => {
handle.classList.remove('fw-grid-splitter--active');
endDrag();
},
{ preventTouchScroll: true },
);
}, [endDrag, getBodySize, getColFr, getRowFr, onColFrChange, onRowFrChange]);
return;
}
if (size.height <= 0) return;
const delta = ev.clientY - startY;
onRowFrChange(adjustAdjacentGridFr(
startRowFr,
index,
delta,
size.height,
FLOATING_WIDGET_GRID_GAP,
FLOATING_WIDGET_CELL_MIN_H,
));
};
const onUp = (ev: PointerEvent) => {
handle.classList.remove('fw-grid-splitter--active');
document.body.style.cursor = '';
document.body.style.userSelect = '';
try {
if (handle.hasPointerCapture(ev.pointerId)) {
handle.releasePointerCapture(ev.pointerId);
}
} catch {
/* ignore */
}
document.removeEventListener('pointermove', onMove, true);
document.removeEventListener('pointerup', onUp, true);
document.removeEventListener('pointercancel', onUp, true);
activeDragRef.current = null;
onSplitEnd?.();
};
document.addEventListener('pointermove', onMove, { capture: true });
document.addEventListener('pointerup', onUp, { capture: true });
document.addEventListener('pointercancel', onUp, { capture: true });
activeDragRef.current = () => {
document.removeEventListener('pointermove', onMove, true);
document.removeEventListener('pointerup', onUp, true);
document.removeEventListener('pointercancel', onUp, true);
};
}, [getBodySize, getColFr, getRowFr, onColFrChange, onRowFrChange, onSplitEnd]);
const onColSplitPointerDown = useCallback(
(index: number) => (e: React.PointerEvent) => startSplit(e, 'col', index),
+14
View File
@@ -648,8 +648,22 @@ html.fw-native-widget #root {
.fw-window--native .fw-window-body-wrap {
flex: 1;
min-height: 0;
min-width: 0;
position: relative;
}
.fw-window--native .fw-window-body {
flex: 1;
min-height: 0;
min-width: 0;
width: 100%;
height: 100%;
}
.fw-window--native .fw-grid-split-layer {
z-index: 1000;
}
.fw-window--native .fw-grid-splitter {
z-index: 1001;
}