대시보드 에러 해결 #107
|
|
@ -162,6 +162,11 @@ export function CanvasElement({
|
|||
// 요소 선택 처리
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
// 모달이나 다이얼로그가 열려있으면 드래그 무시
|
||||
if (document.querySelector('[role="dialog"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 닫기 버튼이나 리사이즈 핸들 클릭 시 무시
|
||||
if ((e.target as HTMLElement).closest(".element-close, .resize-handle")) {
|
||||
return;
|
||||
|
|
@ -192,6 +197,11 @@ export function CanvasElement({
|
|||
// 리사이즈 핸들 마우스다운
|
||||
const handleResizeMouseDown = useCallback(
|
||||
(e: React.MouseEvent, handle: string) => {
|
||||
// 모달이나 다이얼로그가 열려있으면 리사이즈 무시
|
||||
if (document.querySelector('[role="dialog"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
setIsResizing(true);
|
||||
setResizeStart({
|
||||
|
|
@ -522,16 +532,15 @@ export function CanvasElement({
|
|||
<span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span>
|
||||
<div className="flex gap-1">
|
||||
{/* 설정 버튼 (기사관리 위젯만 자체 설정 UI 사용) */}
|
||||
{onConfigure &&
|
||||
!(element.type === "widget" && element.subtype === "driver-management") && (
|
||||
<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"
|
||||
onClick={() => onConfigure(element)}
|
||||
title="설정"
|
||||
>
|
||||
⚙️
|
||||
</button>
|
||||
)}
|
||||
{onConfigure && !(element.type === "widget" && element.subtype === "driver-management") && (
|
||||
<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"
|
||||
onClick={() => onConfigure(element)}
|
||||
title="설정"
|
||||
>
|
||||
⚙️
|
||||
</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"
|
||||
|
|
|
|||
|
|
@ -111,60 +111,44 @@ export function QueryEditor({ dataSource, onDataSourceChange, onQueryTest }: Que
|
|||
// 샘플 쿼리 삽입
|
||||
const insertSampleQuery = useCallback((sampleType: string) => {
|
||||
const samples = {
|
||||
comparison: `-- 제품별 월별 매출 비교 (다중 시리즈)
|
||||
-- 갤럭시(Galaxy) vs 아이폰(iPhone) 매출 비교
|
||||
SELECT
|
||||
DATE_TRUNC('month', order_date) as month,
|
||||
SUM(CASE WHEN product_category = '갤럭시' THEN amount ELSE 0 END) as galaxy_sales,
|
||||
SUM(CASE WHEN product_category = '아이폰' THEN amount ELSE 0 END) as iphone_sales,
|
||||
SUM(CASE WHEN product_category = '기타' THEN amount ELSE 0 END) as other_sales
|
||||
FROM orders
|
||||
WHERE order_date >= CURRENT_DATE - INTERVAL '12 months'
|
||||
GROUP BY DATE_TRUNC('month', order_date)
|
||||
ORDER BY month;`,
|
||||
users: `SELECT
|
||||
dept_name as 부서명,
|
||||
COUNT(*) as 회원수
|
||||
FROM user_info
|
||||
WHERE dept_name IS NOT NULL
|
||||
GROUP BY dept_name
|
||||
ORDER BY 회원수 DESC`,
|
||||
|
||||
sales: `-- 월별 매출 데이터
|
||||
SELECT
|
||||
DATE_TRUNC('month', order_date) as month,
|
||||
SUM(total_amount) as sales,
|
||||
COUNT(*) as order_count
|
||||
FROM orders
|
||||
WHERE order_date >= CURRENT_DATE - INTERVAL '12 months'
|
||||
GROUP BY DATE_TRUNC('month', order_date)
|
||||
ORDER BY month;`,
|
||||
dept: `SELECT
|
||||
dept_code as 부서코드,
|
||||
dept_name as 부서명,
|
||||
location_name as 위치,
|
||||
TO_CHAR(regdate, 'YYYY-MM-DD') as 등록일
|
||||
FROM dept_info
|
||||
ORDER BY dept_code`,
|
||||
|
||||
users: `-- 사용자 가입 추이
|
||||
SELECT
|
||||
DATE_TRUNC('week', created_at) as week,
|
||||
COUNT(*) as new_users
|
||||
FROM users
|
||||
WHERE created_at >= CURRENT_DATE - INTERVAL '3 months'
|
||||
GROUP BY DATE_TRUNC('week', created_at)
|
||||
ORDER BY week;`,
|
||||
usersByDate: `SELECT
|
||||
DATE_TRUNC('month', regdate)::date as 월,
|
||||
COUNT(*) as 신규사용자수
|
||||
FROM user_info
|
||||
WHERE regdate >= CURRENT_DATE - INTERVAL '12 months'
|
||||
GROUP BY DATE_TRUNC('month', regdate)
|
||||
ORDER BY 월`,
|
||||
|
||||
products: `-- 상품별 판매량
|
||||
SELECT
|
||||
product_name,
|
||||
SUM(quantity) as total_sold,
|
||||
SUM(quantity * price) as revenue
|
||||
FROM order_items oi
|
||||
JOIN products p ON oi.product_id = p.id
|
||||
WHERE oi.created_at >= CURRENT_DATE - INTERVAL '1 month'
|
||||
GROUP BY product_name
|
||||
ORDER BY total_sold DESC
|
||||
LIMIT 10;`,
|
||||
usersByPosition: `SELECT
|
||||
position_name as 직급,
|
||||
COUNT(*) as 인원수
|
||||
FROM user_info
|
||||
WHERE position_name IS NOT NULL
|
||||
GROUP BY position_name
|
||||
ORDER BY 인원수 DESC`,
|
||||
|
||||
regional: `-- 지역별 매출 비교
|
||||
SELECT
|
||||
region as 지역,
|
||||
SUM(CASE WHEN quarter = 'Q1' THEN sales ELSE 0 END) as Q1,
|
||||
SUM(CASE WHEN quarter = 'Q2' THEN sales ELSE 0 END) as Q2,
|
||||
SUM(CASE WHEN quarter = 'Q3' THEN sales ELSE 0 END) as Q3,
|
||||
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;`,
|
||||
deptHierarchy: `SELECT
|
||||
COALESCE(parent_dept_code, '최상위') as 상위부서코드,
|
||||
COUNT(*) as 하위부서수
|
||||
FROM dept_info
|
||||
GROUP BY parent_dept_code
|
||||
ORDER BY 하위부서수 DESC`,
|
||||
};
|
||||
|
||||
setQuery(samples[sampleType as keyof typeof samples] || "");
|
||||
|
|
@ -197,22 +181,22 @@ ORDER BY Q4 DESC;`,
|
|||
<Card className="p-4">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<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")}>
|
||||
사용자 추이
|
||||
<Code className="mr-2 h-3 w-3" />
|
||||
부서별 사용자
|
||||
</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>
|
||||
</div>
|
||||
</Card>
|
||||
|
|
@ -315,13 +299,6 @@ ORDER BY Q4 DESC;`,
|
|||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 키보드 단축키 안내 */}
|
||||
<Card className="p-3">
|
||||
<div className="text-xs text-gray-600">
|
||||
<strong>단축키:</strong> Ctrl+Enter (쿼리 실행), Ctrl+/ (주석 토글)
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export function DatabaseConfig({ dataSource, onChange }: DatabaseConfigProps) {
|
|||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="커넥션을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent className="z-[9999]">
|
||||
{connections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={String(conn.id)}>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -67,9 +67,9 @@ export default function YardManagement3DWidget({
|
|||
};
|
||||
|
||||
// 새 레이아웃 생성
|
||||
const handleCreateLayout = async (name: string, description: string) => {
|
||||
const handleCreateLayout = async (name: string) => {
|
||||
try {
|
||||
const response = await yardLayoutApi.createLayout({ name, description });
|
||||
const response = await yardLayoutApi.createLayout({ name });
|
||||
if (response.success) {
|
||||
await loadLayouts();
|
||||
setIsCreateModalOpen(false);
|
||||
|
|
@ -105,7 +105,7 @@ export default function YardManagement3DWidget({
|
|||
// 편집 모드: 레이아웃 선택 UI
|
||||
if (isEditMode) {
|
||||
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>
|
||||
<h3 className="text-sm font-semibold text-gray-700">야드 레이아웃 선택</h3>
|
||||
|
|
|
|||
|
|
@ -49,9 +49,6 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
useEffect(() => {
|
||||
if (placement) {
|
||||
setEditData({
|
||||
position_x: placement.position_x,
|
||||
position_y: placement.position_y,
|
||||
position_z: placement.position_z,
|
||||
size_x: placement.size_x,
|
||||
size_y: placement.size_y,
|
||||
size_z: placement.size_z,
|
||||
|
|
@ -107,52 +104,6 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
<div className="space-y-3">
|
||||
<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 크기 */}
|
||||
<div>
|
||||
<Label className="text-xs">크기</Label>
|
||||
|
|
@ -167,7 +118,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
value={editData.size_x ?? placement.size_x}
|
||||
onChange={(e) => setEditData({ ...editData, size_x: parseFloat(e.target.value) || 1 })}
|
||||
min="1"
|
||||
step="0.5"
|
||||
step="1"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -181,7 +132,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
value={editData.size_y ?? placement.size_y}
|
||||
onChange={(e) => setEditData({ ...editData, size_y: parseFloat(e.target.value) || 1 })}
|
||||
min="1"
|
||||
step="0.5"
|
||||
step="1"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -195,7 +146,7 @@ export default function MaterialEditPanel({ placement, onClose, onUpdate, onRemo
|
|||
value={editData.size_z ?? placement.size_z}
|
||||
onChange={(e) => setEditData({ ...editData, size_z: parseFloat(e.target.value) || 1 })}
|
||||
min="1"
|
||||
step="0.5"
|
||||
step="1"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -62,10 +62,15 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
const [placements, setPlacements] = useState<YardPlacement[]>([]);
|
||||
const [materials, setMaterials] = useState<TempMaterial[]>([]);
|
||||
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
|
||||
const [selectedMaterial, setSelectedMaterial] = useState<TempMaterial | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [editValues, setEditValues] = useState({
|
||||
size_x: 5,
|
||||
size_y: 5,
|
||||
size_z: 5,
|
||||
color: "#3b82f6",
|
||||
});
|
||||
|
||||
// 배치 목록 & 자재 목록 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -78,10 +83,10 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
]);
|
||||
|
||||
if (placementsRes.success) {
|
||||
setPlacements(placementsRes.data);
|
||||
setPlacements(placementsRes.data as YardPlacement[]);
|
||||
}
|
||||
if (materialsRes.success) {
|
||||
setMaterials(materialsRes.data);
|
||||
setMaterials(materialsRes.data as TempMaterial[]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("데이터 로드 실패:", error);
|
||||
|
|
@ -93,6 +98,18 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
loadData();
|
||||
}, [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) => {
|
||||
// 이미 배치되었는지 확인
|
||||
|
|
@ -102,8 +119,6 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
setSelectedMaterial(material);
|
||||
|
||||
// 기본 위치에 배치
|
||||
const placementData = {
|
||||
external_material_id: `TEMP-${material.id}`,
|
||||
|
|
@ -123,11 +138,10 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
try {
|
||||
const response = await yardLayoutApi.addMaterialPlacement(layout.id, placementData);
|
||||
if (response.success) {
|
||||
setPlacements((prev) => [...prev, response.data]);
|
||||
setSelectedPlacement(response.data);
|
||||
setSelectedMaterial(null);
|
||||
setPlacements((prev) => [...prev, response.data as YardPlacement]);
|
||||
setSelectedPlacement(response.data as YardPlacement);
|
||||
}
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
console.error("자재 배치 실패:", error);
|
||||
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 {
|
||||
const response = await yardLayoutApi.removePlacement(id);
|
||||
if (response.success) {
|
||||
|
|
@ -262,7 +276,7 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<Yard3DCanvas
|
||||
placements={placements}
|
||||
selectedPlacementId={selectedPlacement?.id || null}
|
||||
onPlacementClick={setSelectedPlacement}
|
||||
onPlacementClick={(placement) => setSelectedPlacement(placement as YardPlacement)}
|
||||
onPlacementDrag={handlePlacementDrag}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -304,60 +318,18 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<div className="space-y-3 border-t pt-4">
|
||||
<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>
|
||||
<Label className="text-xs">너비</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
value={selectedPlacement.size_x}
|
||||
onChange={(e) =>
|
||||
handlePlacementUpdate(selectedPlacement.id, {
|
||||
size_x: parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
value={editValues.size_x}
|
||||
onChange={(e) => {
|
||||
const newValue = parseFloat(e.target.value) || 1;
|
||||
setEditValues((prev) => ({ ...prev, size_x: newValue }));
|
||||
handlePlacementUpdate(selectedPlacement.id, { size_x: newValue });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -365,12 +337,12 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
value={selectedPlacement.size_y}
|
||||
onChange={(e) =>
|
||||
handlePlacementUpdate(selectedPlacement.id, {
|
||||
size_y: parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
value={editValues.size_y}
|
||||
onChange={(e) => {
|
||||
const newValue = parseFloat(e.target.value) || 1;
|
||||
setEditValues((prev) => ({ ...prev, size_y: newValue }));
|
||||
handlePlacementUpdate(selectedPlacement.id, { size_y: newValue });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -378,12 +350,12 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
value={selectedPlacement.size_z}
|
||||
onChange={(e) =>
|
||||
handlePlacementUpdate(selectedPlacement.id, {
|
||||
size_z: parseFloat(e.target.value),
|
||||
})
|
||||
}
|
||||
value={editValues.size_z}
|
||||
onChange={(e) => {
|
||||
const newValue = parseFloat(e.target.value) || 1;
|
||||
setEditValues((prev) => ({ ...prev, size_z: newValue }));
|
||||
handlePlacementUpdate(selectedPlacement.id, { size_z: newValue });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -392,8 +364,11 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
<Label className="text-xs">색상</Label>
|
||||
<Input
|
||||
type="color"
|
||||
value={selectedPlacement.color}
|
||||
onChange={(e) => handlePlacementUpdate(selectedPlacement.id, { color: e.target.value })}
|
||||
value={editValues.color}
|
||||
onChange={(e) => {
|
||||
setEditValues((prev) => ({ ...prev, color: e.target.value }));
|
||||
handlePlacementUpdate(selectedPlacement.id, { color: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,18 +12,17 @@ import {
|
|||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Loader2, AlertCircle } from "lucide-react";
|
||||
|
||||
interface YardLayoutCreateModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onCreate: (name: string, description: string) => Promise<void>;
|
||||
onCreate: (name: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: YardLayoutCreateModalProps) {
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
|
|
@ -38,9 +37,8 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
|
|||
setError("");
|
||||
|
||||
try {
|
||||
await onCreate(name.trim(), description.trim());
|
||||
await onCreate(name.trim());
|
||||
setName("");
|
||||
setDescription("");
|
||||
} catch (error: any) {
|
||||
console.error("야드 생성 실패:", error);
|
||||
setError(error.message || "야드 생성에 실패했습니다");
|
||||
|
|
@ -53,7 +51,6 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
|
|||
const handleClose = () => {
|
||||
if (isCreating) return;
|
||||
setName("");
|
||||
setDescription("");
|
||||
setError("");
|
||||
onClose();
|
||||
};
|
||||
|
|
@ -68,14 +65,13 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
|
|||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogContent className="sm:max-w-[500px]" onPointerDown={(e) => e.stopPropagation()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>새 야드 생성</DialogTitle>
|
||||
<DialogDescription>야드의 이름과 설명을 입력하세요</DialogDescription>
|
||||
<DialogDescription>야드 이름을 입력하세요</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* 야드 이름 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="yard-name">
|
||||
야드 이름 <span className="text-red-500">*</span>
|
||||
|
|
@ -94,21 +90,12 @@ export default function YardLayoutCreateModal({ isOpen, onClose, onCreate }: Yar
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* 설명 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="yard-description">설명</Label>
|
||||
<Textarea
|
||||
id="yard-description"
|
||||
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>}
|
||||
{error && (
|
||||
<Alert variant="destructive">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
|
|
|||
Loading…
Reference in New Issue