Merge remote-tracking branch 'upstream/main'

This commit is contained in:
dohyeons 2025-10-17 18:15:07 +09:00
commit 8871d0089d
7 changed files with 134 additions and 235 deletions

View File

@ -162,6 +162,11 @@ export function CanvasElement({
// 요소 선택 처리 // 요소 선택 처리
const handleMouseDown = useCallback( const handleMouseDown = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
// 모달이나 다이얼로그가 열려있으면 드래그 무시
if (document.querySelector('[role="dialog"]')) {
return;
}
// 닫기 버튼이나 리사이즈 핸들 클릭 시 무시 // 닫기 버튼이나 리사이즈 핸들 클릭 시 무시
if ((e.target as HTMLElement).closest(".element-close, .resize-handle")) { if ((e.target as HTMLElement).closest(".element-close, .resize-handle")) {
return; return;
@ -192,6 +197,11 @@ export function CanvasElement({
// 리사이즈 핸들 마우스다운 // 리사이즈 핸들 마우스다운
const handleResizeMouseDown = useCallback( const handleResizeMouseDown = useCallback(
(e: React.MouseEvent, handle: string) => { (e: React.MouseEvent, handle: string) => {
// 모달이나 다이얼로그가 열려있으면 리사이즈 무시
if (document.querySelector('[role="dialog"]')) {
return;
}
e.stopPropagation(); e.stopPropagation();
setIsResizing(true); setIsResizing(true);
setResizeStart({ setResizeStart({
@ -522,16 +532,15 @@ export function CanvasElement({
<span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span> <span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span>
<div className="flex gap-1"> <div className="flex gap-1">
{/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */} {/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */}
{onConfigure && {onConfigure && !(element.type === "widget" && element.subtype === "driver-management") && (
!(element.type === "widget" && element.subtype === "driver-management") && ( <button
<button className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"
className="hover:bg-accent0 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white" onClick={() => onConfigure(element)}
onClick={() => onConfigure(element)} title="설정"
title="설정" >
>
</button>
</button> )}
)}
{/* 삭제 버튼 */} {/* 삭제 버튼 */}
<button <button
className="element-close hover:bg-destructive/100 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white" className="element-close hover:bg-destructive/100 flex h-6 w-6 items-center justify-center rounded text-gray-400 transition-colors duration-200 hover:text-white"

View File

@ -111,60 +111,44 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
// 샘플 쿼리 삽입 // 샘플 쿼리 삽입
const insertSampleQuery = useCallback((sampleType: string) => { const insertSampleQuery = useCallback((sampleType: string) => {
const samples = { const samples = {
comparison: `-- 제품별 월별 매출 비교 (다중 시리즈) users: `SELECT
-- (Galaxy) vs (iPhone) dept_name as ,
SELECT COUNT(*) as
DATE_TRUNC('month', order_date) as month, FROM user_info
SUM(CASE WHEN product_category = '갤럭시' THEN amount ELSE 0 END) as galaxy_sales, WHERE dept_name IS NOT NULL
SUM(CASE WHEN product_category = '아이폰' THEN amount ELSE 0 END) as iphone_sales, GROUP BY dept_name
SUM(CASE WHEN product_category = '기타' THEN amount ELSE 0 END) as other_sales ORDER BY DESC`,
FROM orders
WHERE order_date >= CURRENT_DATE - INTERVAL '12 months'
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;`,
sales: `-- 월별 매출 데이터 dept: `SELECT
SELECT dept_code as ,
DATE_TRUNC('month', order_date) as month, dept_name as ,
SUM(total_amount) as sales, location_name as ,
COUNT(*) as order_count TO_CHAR(regdate, 'YYYY-MM-DD') as
FROM orders FROM dept_info
WHERE order_date >= CURRENT_DATE - INTERVAL '12 months' ORDER BY dept_code`,
GROUP BY DATE_TRUNC('month', order_date)
ORDER BY month;`,
users: `-- 사용자 가입 추이 usersByDate: `SELECT
SELECT DATE_TRUNC('month', regdate)::date as ,
DATE_TRUNC('week', created_at) as week, COUNT(*) as
COUNT(*) as new_users FROM user_info
FROM users WHERE regdate >= CURRENT_DATE - INTERVAL '12 months'
WHERE created_at >= CURRENT_DATE - INTERVAL '3 months' GROUP BY DATE_TRUNC('month', regdate)
GROUP BY DATE_TRUNC('week', created_at) ORDER BY `,
ORDER BY week;`,
products: `-- 상품별 판매량 usersByPosition: `SELECT
SELECT position_name as ,
product_name, COUNT(*) as
SUM(quantity) as total_sold, FROM user_info
SUM(quantity * price) as revenue WHERE position_name IS NOT NULL
FROM order_items oi GROUP BY position_name
JOIN products p ON oi.product_id = p.id ORDER BY DESC`,
WHERE oi.created_at >= CURRENT_DATE - INTERVAL '1 month'
GROUP BY product_name
ORDER BY total_sold DESC
LIMIT 10;`,
regional: `-- 지역별 매출 비교 deptHierarchy: `SELECT
SELECT COALESCE(parent_dept_code, '최상위') as ,
region as , COUNT(*) as
SUM(CASE WHEN quarter = 'Q1' THEN sales ELSE 0 END) as Q1, FROM dept_info
SUM(CASE WHEN quarter = 'Q2' THEN sales ELSE 0 END) as Q2, GROUP BY parent_dept_code
SUM(CASE WHEN quarter = 'Q3' THEN sales ELSE 0 END) as Q3, ORDER BY DESC`,
SUM(CASE WHEN quarter = 'Q4' THEN sales ELSE 0 END) as Q4
FROM regional_sales
WHERE year = EXTRACT(YEAR FROM CURRENT_DATE)
GROUP BY region
ORDER BY Q4 DESC;`,
}; };
setQuery(samples[sampleType as keyof typeof samples] || ""); setQuery(samples[sampleType as keyof typeof samples] || "");
@ -197,22 +181,22 @@ ORDER BY Q4 DESC;`,
<Card className="p-4"> <Card className="p-4">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<Label className="text-sm text-gray-600"> :</Label> <Label className="text-sm text-gray-600"> :</Label>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("comparison")}>
<Code className="mr-2 h-3 w-3" />
</Button>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("regional")}>
<Code className="mr-2 h-3 w-3" />
</Button>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("sales")}>
</Button>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("users")}> <Button variant="outline" size="sm" onClick={() => insertSampleQuery("users")}>
<Code className="mr-2 h-3 w-3" />
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("products")}> <Button variant="outline" size="sm" onClick={() => insertSampleQuery("dept")}>
<Code className="mr-2 h-3 w-3" />
</Button>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("usersByDate")}>
</Button>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("usersByPosition")}>
</Button>
<Button variant="outline" size="sm" onClick={() => insertSampleQuery("deptHierarchy")}>
</Button> </Button>
</div> </div>
</Card> </Card>
@ -315,13 +299,6 @@ ORDER BY Q4 DESC;`,
</div> </div>
</Card> </Card>
)} )}
{/* 키보드 단축키 안내 */}
<Card className="p-3">
<div className="text-xs text-gray-600">
<strong>:</strong> Ctrl+Enter ( ), Ctrl+/ ( )
</div>
</Card>
</div> </div>
); );
} }

View File

@ -150,7 +150,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
<SelectTrigger className="w-full"> <SelectTrigger className="w-full">
<SelectValue placeholder="커넥션을 선택하세요" /> <SelectValue placeholder="커넥션을 선택하세요" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent className="z-[9999]">
{connections.map((conn) => ( {connections.map((conn) => (
<SelectItem key={conn.id} value={String(conn.id)}> <SelectItem key={conn.id} value={String(conn.id)}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@ -67,9 +67,9 @@ export default function YardManagement3DWidget({
}; };
// 새 레이아웃 생성 // 새 레이아웃 생성
const handleCreateLayout = async (name: string, description: string) => { const handleCreateLayout = async (name: string) => {
try { try {
const response = await yardLayoutApi.createLayout({ name, description }); const response = await yardLayoutApi.createLayout({ name });
if (response.success) { if (response.success) {
await loadLayouts(); await loadLayouts();
setIsCreateModalOpen(false); setIsCreateModalOpen(false);
@ -105,7 +105,7 @@ export default function YardManagement3DWidget({
// 편집 모드: 레이아웃 선택 UI // 편집 모드: 레이아웃 선택 UI
if (isEditMode) { if (isEditMode) {
return ( return (
<div className="flex h-full w-full flex-col bg-white"> <div className="widget-interactive-area flex h-full w-full flex-col bg-white">
<div className="flex items-center justify-between border-b p-4"> <div className="flex items-center justify-between border-b p-4">
<div> <div>
<h3 className="text-sm font-semibold text-gray-700"> </h3> <h3 className="text-sm font-semibold text-gray-700"> </h3>

View File

@ -49,9 +49,6 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
useEffect(() => { useEffect(() => {
if (placement) { if (placement) {
setEditData({ setEditData({
position_x: placement.position_x,
position_y: placement.position_y,
position_z: placement.position_z,
size_x: placement.size_x, size_x: placement.size_x,
size_y: placement.size_y, size_y: placement.size_y,
size_z: placement.size_z, size_z: placement.size_z,
@ -107,52 +104,6 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
<div className="space-y-3"> <div className="space-y-3">
<div className="text-xs font-medium text-gray-500"> ( )</div> <div className="text-xs font-medium text-gray-500"> ( )</div>
{/* 3D 위치 */}
<div>
<Label className="text-xs"></Label>
<div className="grid grid-cols-3 gap-2">
<div>
<Label htmlFor="edit-posX" className="text-xs text-gray-600">
X
</Label>
<Input
id="edit-posX"
type="number"
value={editData.position_x ?? placement.position_x}
onChange={(e) => setEditData({ ...editData, position_x: parseFloat(e.target.value) || 0 })}
step="0.5"
className="h-8 text-xs"
/>
</div>
<div>
<Label htmlFor="edit-posY" className="text-xs text-gray-600">
Y
</Label>
<Input
id="edit-posY"
type="number"
value={editData.position_y ?? placement.position_y}
onChange={(e) => setEditData({ ...editData, position_y: parseFloat(e.target.value) || 0 })}
step="0.5"
className="h-8 text-xs"
/>
</div>
<div>
<Label htmlFor="edit-posZ" className="text-xs text-gray-600">
Z
</Label>
<Input
id="edit-posZ"
type="number"
value={editData.position_z ?? placement.position_z}
onChange={(e) => setEditData({ ...editData, position_z: parseFloat(e.target.value) || 0 })}
step="0.5"
className="h-8 text-xs"
/>
</div>
</div>
</div>
{/* 3D 크기 */} {/* 3D 크기 */}
<div> <div>
<Label className="text-xs"></Label> <Label className="text-xs"></Label>
@ -167,7 +118,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
value={editData.size_x ?? placement.size_x} value={editData.size_x ?? placement.size_x}
onChange={(e) => setEditData({ ...editData, size_x: parseFloat(e.target.value) || 1 })} onChange={(e) => setEditData({ ...editData, size_x: parseFloat(e.target.value) || 1 })}
min="1" min="1"
step="0.5" step="1"
className="h-8 text-xs" className="h-8 text-xs"
/> />
</div> </div>
@ -181,7 +132,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
value={editData.size_y ?? placement.size_y} value={editData.size_y ?? placement.size_y}
onChange={(e) => setEditData({ ...editData, size_y: parseFloat(e.target.value) || 1 })} onChange={(e) => setEditData({ ...editData, size_y: parseFloat(e.target.value) || 1 })}
min="1" min="1"
step="0.5" step="1"
className="h-8 text-xs" className="h-8 text-xs"
/> />
</div> </div>
@ -195,7 +146,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
value={editData.size_z ?? placement.size_z} value={editData.size_z ?? placement.size_z}
onChange={(e) => setEditData({ ...editData, size_z: parseFloat(e.target.value) || 1 })} onChange={(e) => setEditData({ ...editData, size_z: parseFloat(e.target.value) || 1 })}
min="1" min="1"
step="0.5" step="1"
className="h-8 text-xs" className="h-8 text-xs"
/> />
</div> </div>

View File

@ -62,10 +62,15 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
const [placements, setPlacements] = useState<YardPlacement[]>([]); const [placements, setPlacements] = useState<YardPlacement[]>([]);
const [materials, setMaterials] = useState<TempMaterial[]>([]); const [materials, setMaterials] = useState<TempMaterial[]>([]);
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null); const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
const [selectedMaterial, setSelectedMaterial] = useState<TempMaterial | null>(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const [editValues, setEditValues] = useState({
size_x: 5,
size_y: 5,
size_z: 5,
color: "#3b82f6",
});
// 배치 목록 & 자재 목록 로드 // 배치 목록 & 자재 목록 로드
useEffect(() => { useEffect(() => {
@ -78,10 +83,10 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
]); ]);
if (placementsRes.success) { if (placementsRes.success) {
setPlacements(placementsRes.data); setPlacements(placementsRes.data as YardPlacement[]);
} }
if (materialsRes.success) { if (materialsRes.success) {
setMaterials(materialsRes.data); setMaterials(materialsRes.data as TempMaterial[]);
} }
} catch (error) { } catch (error) {
console.error("데이터 로드 실패:", error); console.error("데이터 로드 실패:", error);
@ -93,6 +98,18 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
loadData(); loadData();
}, [layout.id]); }, [layout.id]);
// selectedPlacement 변경 시 editValues 업데이트
useEffect(() => {
if (selectedPlacement) {
setEditValues({
size_x: selectedPlacement.size_x,
size_y: selectedPlacement.size_y,
size_z: selectedPlacement.size_z,
color: selectedPlacement.color,
});
}
}, [selectedPlacement]);
// 자재 클릭 → 배치 추가 // 자재 클릭 → 배치 추가
const handleMaterialClick = async (material: TempMaterial) => { const handleMaterialClick = async (material: TempMaterial) => {
// 이미 배치되었는지 확인 // 이미 배치되었는지 확인
@ -102,8 +119,6 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
return; return;
} }
setSelectedMaterial(material);
// 기본 위치에 배치 // 기본 위치에 배치
const placementData = { const placementData = {
external_material_id: `TEMP-${material.id}`, external_material_id: `TEMP-${material.id}`,
@ -123,11 +138,10 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
try { try {
const response = await yardLayoutApi.addMaterialPlacement(layout.id, placementData); const response = await yardLayoutApi.addMaterialPlacement(layout.id, placementData);
if (response.success) { if (response.success) {
setPlacements((prev) => [...prev, response.data]); setPlacements((prev) => [...prev, response.data as YardPlacement]);
setSelectedPlacement(response.data); setSelectedPlacement(response.data as YardPlacement);
setSelectedMaterial(null);
} }
} catch (error: any) { } catch (error) {
console.error("자재 배치 실패:", error); console.error("자재 배치 실패:", error);
alert("자재 배치에 실패했습니다."); alert("자재 배치에 실패했습니다.");
} }
@ -166,7 +180,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
}; };
// 자재 배치 해제 // 자재 배치 해제
const handlePlacementRemove = async (id: number) => { const handlePlacementRemove = async (id: number): Promise<void> => {
try { try {
const response = await yardLayoutApi.removePlacement(id); const response = await yardLayoutApi.removePlacement(id);
if (response.success) { if (response.success) {
@ -262,7 +276,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
<Yard3DCanvas <Yard3DCanvas
placements={placements} placements={placements}
selectedPlacementId={selectedPlacement?.id || null} selectedPlacementId={selectedPlacement?.id || null}
onPlacementClick={setSelectedPlacement} onPlacementClick={(placement) => setSelectedPlacement(placement as YardPlacement)}
onPlacementDrag={handlePlacementDrag} onPlacementDrag={handlePlacementDrag}
/> />
)} )}
@ -304,60 +318,18 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
<div className="space-y-3 border-t pt-4"> <div className="space-y-3 border-t pt-4">
<Label className="text-sm font-semibold"> </Label> <Label className="text-sm font-semibold"> </Label>
<div className="grid grid-cols-3 gap-2">
<div>
<Label className="text-xs">X</Label>
<Input
type="number"
step="0.5"
value={selectedPlacement.position_x}
onChange={(e) =>
handlePlacementUpdate(selectedPlacement.id, {
position_x: parseFloat(e.target.value),
})
}
/>
</div>
<div>
<Label className="text-xs">Y</Label>
<Input
type="number"
step="0.5"
value={selectedPlacement.position_y}
onChange={(e) =>
handlePlacementUpdate(selectedPlacement.id, {
position_y: parseFloat(e.target.value),
})
}
/>
</div>
<div>
<Label className="text-xs">Z</Label>
<Input
type="number"
step="0.5"
value={selectedPlacement.position_z}
onChange={(e) =>
handlePlacementUpdate(selectedPlacement.id, {
position_z: parseFloat(e.target.value),
})
}
/>
</div>
</div>
<div className="grid grid-cols-3 gap-2"> <div className="grid grid-cols-3 gap-2">
<div> <div>
<Label className="text-xs"></Label> <Label className="text-xs"></Label>
<Input <Input
type="number" type="number"
step="1" step="1"
value={selectedPlacement.size_x} value={editValues.size_x}
onChange={(e) => onChange={(e) => {
handlePlacementUpdate(selectedPlacement.id, { const newValue = parseFloat(e.target.value) || 1;
size_x: parseFloat(e.target.value), setEditValues((prev) => ({ ...prev, size_x: newValue }));
}) handlePlacementUpdate(selectedPlacement.id, { size_x: newValue });
} }}
/> />
</div> </div>
<div> <div>
@ -365,12 +337,12 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
<Input <Input
type="number" type="number"
step="1" step="1"
value={selectedPlacement.size_y} value={editValues.size_y}
onChange={(e) => onChange={(e) => {
handlePlacementUpdate(selectedPlacement.id, { const newValue = parseFloat(e.target.value) || 1;
size_y: parseFloat(e.target.value), setEditValues((prev) => ({ ...prev, size_y: newValue }));
}) handlePlacementUpdate(selectedPlacement.id, { size_y: newValue });
} }}
/> />
</div> </div>
<div> <div>
@ -378,12 +350,12 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
<Input <Input
type="number" type="number"
step="1" step="1"
value={selectedPlacement.size_z} value={editValues.size_z}
onChange={(e) => onChange={(e) => {
handlePlacementUpdate(selectedPlacement.id, { const newValue = parseFloat(e.target.value) || 1;
size_z: parseFloat(e.target.value), setEditValues((prev) => ({ ...prev, size_z: newValue }));
}) handlePlacementUpdate(selectedPlacement.id, { size_z: newValue });
} }}
/> />
</div> </div>
</div> </div>
@ -392,8 +364,11 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
<Label className="text-xs"></Label> <Label className="text-xs"></Label>
<Input <Input
type="color" type="color"
value={selectedPlacement.color} value={editValues.color}
onChange={(e) => handlePlacementUpdate(selectedPlacement.id, { color: e.target.value })} onChange={(e) => {
setEditValues((prev) => ({ ...prev, color: e.target.value }));
handlePlacementUpdate(selectedPlacement.id, { color: e.target.value });
}}
/> />
</div> </div>
</div> </div>

View File

@ -12,18 +12,17 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Loader2 } from "lucide-react"; import { Loader2, AlertCircle } from "lucide-react";
interface YardLayoutCreateModalProps { interface YardLayoutCreateModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onCreate: (name: string, description: string) => Promise<void>; onCreate: (name: string) => Promise<void>;
} }
export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: YardLayoutCreateModalProps) { export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: YardLayoutCreateModalProps) {
const [name, setName] = useState(""); const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
@ -38,9 +37,8 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
setError(""); setError("");
try { try {
await onCreate(name.trim(), description.trim()); await onCreate(name.trim());
setName(""); setName("");
setDescription("");
} catch (error: any) { } catch (error: any) {
console.error("야드 생성 실패:", error); console.error("야드 생성 실패:", error);
setError(error.message || "야드 생성에 실패했습니다"); setError(error.message || "야드 생성에 실패했습니다");
@ -53,7 +51,6 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
const handleClose = () => { const handleClose = () => {
if (isCreating) return; if (isCreating) return;
setName(""); setName("");
setDescription("");
setError(""); setError("");
onClose(); onClose();
}; };
@ -68,14 +65,13 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
return ( return (
<Dialog open={isOpen} onOpenChange={handleClose}> <Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]"> <DialogContent className="sm:max-w-[500px]" onPointerDown={(e) => e.stopPropagation()}>
<DialogHeader> <DialogHeader>
<DialogTitle> </DialogTitle> <DialogTitle> </DialogTitle>
<DialogDescription> </DialogDescription> <DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
{/* 야드 이름 */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="yard-name"> <Label htmlFor="yard-name">
<span className="text-red-500">*</span> <span className="text-red-500">*</span>
@ -94,21 +90,12 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
/> />
</div> </div>
{/* 설명 */} {error && (
<div className="space-y-2"> <Alert variant="destructive">
<Label htmlFor="yard-description"></Label> <AlertCircle className="h-4 w-4" />
<Textarea <AlertDescription>{error}</AlertDescription>
id="yard-description" </Alert>
value={description} )}
onChange={(e) => setDescription(e.target.value)}
placeholder="야드에 대한 설명을 입력하세요 (선택사항)"
rows={3}
disabled={isCreating}
/>
</div>
{/* 에러 메시지 */}
{error && <div className="rounded-md bg-red-50 p-3 text-sm text-red-600">{error}</div>}
</div> </div>
<DialogFooter> <DialogFooter>