From 21d00bbe54ebe037547ee0ce3cdf8e1ced929f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=B4=EA=B3=A4?= Date: Mon, 23 Mar 2026 09:33:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20[dev]=20DevToolbar=20=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EA=B7=B8=20=EC=9D=B4=EB=8F=99=20+=20=EC=9C=84=EC=B9=98=20?= =?UTF-8?q?=ED=94=84=EB=A6=AC=EC=85=8B=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 접힌 Dev 버튼: 마우스 드래그로 자유 이동 - 펼친 툴바: 좌측 그립 아이콘으로 드래그 이동 - 3x3 위치 프리셋 그리드 (좌상/우상/중앙/좌하/우하) - localStorage에 위치 저장 (새로고침 후에도 유지) --- src/components/dev/DevToolbar.tsx | 128 ++++++++++++++++++++++++++++-- 1 file changed, 120 insertions(+), 8 deletions(-) diff --git a/src/components/dev/DevToolbar.tsx b/src/components/dev/DevToolbar.tsx index 6ac60fb1..a84f2585 100644 --- a/src/components/dev/DevToolbar.tsx +++ b/src/components/dev/DevToolbar.tsx @@ -14,7 +14,7 @@ * NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=true */ -import { useState } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { FileText, // 견적 @@ -39,6 +39,7 @@ import { PackageCheck, // 입고 // Dev 도구 아이콘 Layers, // 컴포넌트 레지스트리 + GripVertical, // 드래그 핸들 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; @@ -102,6 +103,46 @@ const MATERIAL_STEPS: { type: DevFillPageType; label: string; icon: typeof FileT { type: 'receiving', label: '입고', icon: PackageCheck, path: '/material/receiving-management/new', fillEnabled: true }, ]; +// ===== 위치 관리 헬퍼 ===== +const POS_KEY_MINI = 'dev-toolbar-pos-mini'; +const POS_KEY_FULL = 'dev-toolbar-pos-full'; + +function loadPos(key: string): { x: number; y: number } | null { + if (typeof window === 'undefined') return null; + try { + const s = localStorage.getItem(key); + return s ? JSON.parse(s) : null; + } catch { return null; } +} + +function savePos(key: string, p: { x: number; y: number }) { + try { localStorage.setItem(key, JSON.stringify(p)); } catch { /* noop */ } +} + +function PositionGrid({ onSelect }: { onSelect: (id: string) => void }) { + const spots: [string, boolean, string][] = [ + ['tl', true, '좌상단'], ['', false, ''], ['tr', true, '우상단'], + ['', false, ''], ['c', true, '중앙'], ['', false, ''], + ['bl', true, '좌하단'], ['', false, ''], ['br', true, '우하단'], + ]; + return ( +
+ {spots.map(([id, active, title], i) => + active ? ( +