feat(screener): useCanvasLayout hook (node positions + reset)
This commit is contained in:
45
src/pages/stock/screener/hooks/useCanvasLayout.js
Normal file
45
src/pages/stock/screener/hooks/useCanvasLayout.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'screener-canvas-layout-v1';
|
||||||
|
|
||||||
|
function readPositions(initial) {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) return initial;
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (!parsed || typeof parsed !== 'object') return initial;
|
||||||
|
// 누락 ID 보충
|
||||||
|
return { ...initial, ...filterValidEntries(parsed) };
|
||||||
|
} catch {
|
||||||
|
return initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterValidEntries(obj) {
|
||||||
|
const out = {};
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
if (v && typeof v.x === 'number' && typeof v.y === 'number') {
|
||||||
|
out[k] = { x: v.x, y: v.y };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCanvasLayout(initialPositions) {
|
||||||
|
const [positions, setPositions] = useState(() => readPositions(initialPositions));
|
||||||
|
|
||||||
|
const updateNodePosition = useCallback((nodeId, pos) => {
|
||||||
|
setPositions((prev) => {
|
||||||
|
const next = { ...prev, [nodeId]: { x: pos.x, y: pos.y } };
|
||||||
|
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(next)); } catch {}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const reset = useCallback(() => {
|
||||||
|
setPositions(initialPositions);
|
||||||
|
try { localStorage.removeItem(STORAGE_KEY); } catch {}
|
||||||
|
}, [initialPositions]);
|
||||||
|
|
||||||
|
return { positions, updateNodePosition, reset };
|
||||||
|
}
|
||||||
49
src/pages/stock/screener/hooks/useCanvasLayout.test.js
Normal file
49
src/pages/stock/screener/hooks/useCanvasLayout.test.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { useCanvasLayout } from './useCanvasLayout';
|
||||||
|
|
||||||
|
const INITIAL = {
|
||||||
|
a: { x: 0, y: 0 },
|
||||||
|
b: { x: 100, y: 100 },
|
||||||
|
c: { x: 200, y: 200 },
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useCanvasLayout', () => {
|
||||||
|
it('초기 호출 시 INITIAL 반환', () => {
|
||||||
|
const { result } = renderHook(() => useCanvasLayout(INITIAL));
|
||||||
|
expect(result.current.positions).toEqual(INITIAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updateNodePosition 호출 시 state + localStorage 모두 갱신', () => {
|
||||||
|
const { result } = renderHook(() => useCanvasLayout(INITIAL));
|
||||||
|
act(() => result.current.updateNodePosition('a', { x: 50, y: 50 }));
|
||||||
|
expect(result.current.positions.a).toEqual({ x: 50, y: 50 });
|
||||||
|
const stored = JSON.parse(localStorage.getItem('screener-canvas-layout-v1'));
|
||||||
|
expect(stored.a).toEqual({ x: 50, y: 50 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reset 호출 시 INITIAL 복원 + localStorage 삭제', () => {
|
||||||
|
const { result } = renderHook(() => useCanvasLayout(INITIAL));
|
||||||
|
act(() => result.current.updateNodePosition('a', { x: 50, y: 50 }));
|
||||||
|
act(() => result.current.reset());
|
||||||
|
expect(result.current.positions).toEqual(INITIAL);
|
||||||
|
expect(localStorage.getItem('screener-canvas-layout-v1')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('손상된 localStorage 는 INITIAL 로 fallback', () => {
|
||||||
|
localStorage.setItem('screener-canvas-layout-v1', 'NOT_JSON');
|
||||||
|
const { result } = renderHook(() => useCanvasLayout(INITIAL));
|
||||||
|
expect(result.current.positions).toEqual(INITIAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('localStorage 에 일부 ID 만 있으면 누락 ID 는 INITIAL 보충', () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'screener-canvas-layout-v1',
|
||||||
|
JSON.stringify({ a: { x: 999, y: 999 } })
|
||||||
|
);
|
||||||
|
const { result } = renderHook(() => useCanvasLayout(INITIAL));
|
||||||
|
expect(result.current.positions.a).toEqual({ x: 999, y: 999 });
|
||||||
|
expect(result.current.positions.b).toEqual({ x: 100, y: 100 });
|
||||||
|
expect(result.current.positions.c).toEqual({ x: 200, y: 200 });
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user