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 ? (
+
+ );
+}
+
export function DevToolbar() {
const pathname = usePathname();
const router = useRouter();
@@ -120,6 +161,67 @@ export function DevToolbar() {
const [isExpanded, setIsExpanded] = useState(true);
const [isLoading, setIsLoading] = useState(null);
+ // ===== 드래그 & 위치 관리 =====
+ const containerRef = useRef(null);
+ const [collapsedPos, setCollapsedPos] = useState<{x: number; y: number} | null>(null);
+ const [expandedPos, setExpandedPos] = useState<{x: number; y: number} | null>(null);
+ const dragRef = useRef({ active: false, moved: false, sx: 0, sy: 0, px: 0, py: 0, target: '' as 'mini' | 'full' });
+
+ useEffect(() => {
+ setCollapsedPos(loadPos(POS_KEY_MINI) || { x: window.innerWidth - 100, y: window.innerHeight - 96 });
+ setExpandedPos(loadPos(POS_KEY_FULL) || { x: Math.max(8, (window.innerWidth - 700) / 2), y: window.innerHeight - 250 });
+ }, []);
+
+ const startDrag = useCallback((e: React.MouseEvent, target: 'mini' | 'full', pos: {x: number; y: number} | null) => {
+ if (e.button !== 0) return;
+ e.preventDefault();
+ const d = dragRef.current;
+ d.active = true; d.moved = false;
+ d.sx = e.clientX; d.sy = e.clientY;
+ d.px = pos?.x || 0; d.py = pos?.y || 0;
+ d.target = target;
+ }, []);
+
+ useEffect(() => {
+ const onMove = (e: MouseEvent) => {
+ const d = dragRef.current;
+ if (!d.active) return;
+ const dx = e.clientX - d.sx, dy = e.clientY - d.sy;
+ if (Math.abs(dx) > 3 || Math.abs(dy) > 3) d.moved = true;
+ if (!d.moved) return;
+ const p = { x: d.px + dx, y: d.py + dy };
+ if (d.target === 'mini') setCollapsedPos(p); else setExpandedPos(p);
+ };
+ const onUp = (e: MouseEvent) => {
+ const d = dragRef.current;
+ if (!d.active) return;
+ d.active = false;
+ if (d.moved) {
+ const p = { x: d.px + (e.clientX - d.sx), y: d.py + (e.clientY - d.sy) };
+ savePos(d.target === 'mini' ? POS_KEY_MINI : POS_KEY_FULL, p);
+ }
+ };
+ window.addEventListener('mousemove', onMove);
+ window.addEventListener('mouseup', onUp);
+ return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
+ }, []);
+
+ const snapTo = useCallback((preset: string) => {
+ const el = containerRef.current;
+ const w = el?.offsetWidth || 700, h = el?.offsetHeight || 200;
+ const vw = window.innerWidth, vh = window.innerHeight, m = 16;
+ let p: {x: number; y: number};
+ switch (preset) {
+ case 'tl': p = { x: m, y: m }; break;
+ case 'tr': p = { x: vw - w - m, y: m }; break;
+ case 'c': p = { x: (vw - w) / 2, y: (vh - h) / 2 }; break;
+ case 'bl': p = { x: m, y: vh - h - m }; break;
+ default: p = { x: vw - w - m, y: vh - h - m }; break;
+ }
+ setExpandedPos(p);
+ savePos(POS_KEY_FULL, p);
+ }, []);
+
// 현재 locale 추출 (유효한 locale만 인식)
const VALID_LOCALES = ['ko', 'en'];
const firstSegment = pathname.split('/')[1];
@@ -128,18 +230,19 @@ export function DevToolbar() {
// mode=new 쿼리 파라미터 확인
const isNewMode = searchParams.get('mode') === 'new';
- // 비활성화 시 렌더링하지 않음
- if (!isEnabled) return null;
+ // 비활성화 또는 위치 미초기화 시 렌더링하지 않음
+ if (!isEnabled || !collapsedPos || !expandedPos) return null;
- // 숨김 상태일 때 작은 버튼만 표시
+ // 숨김 상태일 때 작은 버튼만 표시 (드래그 이동 가능)
if (!isVisible) {
return (
-
+