저장 버튼을 누르기 전 저장 x
This commit is contained in:
parent
e2d99aef40
commit
c5a4b0b10c
|
|
@ -153,18 +153,35 @@ export class YardLayoutService {
|
|||
`;
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// NaN 방지를 위한 안전한 변환 함수
|
||||
const safeParseInt = (
|
||||
value: any,
|
||||
defaultValue: number | null = null
|
||||
): number | null => {
|
||||
if (!value && value !== 0) return defaultValue;
|
||||
const parsed = parseInt(String(value), 10);
|
||||
return isNaN(parsed) ? defaultValue : parsed;
|
||||
};
|
||||
|
||||
const safeParseFloat = (value: any, defaultValue: number): number => {
|
||||
if (!value && value !== 0) return defaultValue;
|
||||
const parsed = parseFloat(String(value));
|
||||
return isNaN(parsed) ? defaultValue : parsed;
|
||||
};
|
||||
|
||||
const result = await pool.query(query, [
|
||||
layoutId,
|
||||
data.material_code || null,
|
||||
data.material_name || null,
|
||||
data.quantity ? parseInt(String(data.quantity), 10) : null,
|
||||
safeParseInt(data.quantity, null),
|
||||
data.unit || null,
|
||||
data.position_x ? parseFloat(String(data.position_x)) : 0,
|
||||
data.position_y ? parseFloat(String(data.position_y)) : 0,
|
||||
data.position_z ? parseFloat(String(data.position_z)) : 0,
|
||||
data.size_x ? parseFloat(String(data.size_x)) : 5,
|
||||
data.size_y ? parseFloat(String(data.size_y)) : 5,
|
||||
data.size_z ? parseFloat(String(data.size_z)) : 5,
|
||||
safeParseFloat(data.position_x, 0),
|
||||
safeParseFloat(data.position_y, 0),
|
||||
safeParseFloat(data.position_z, 0),
|
||||
safeParseFloat(data.size_x, 5),
|
||||
safeParseFloat(data.size_y, 5),
|
||||
safeParseFloat(data.size_z, 5),
|
||||
data.color || "#9ca3af", // 미설정 시 회색
|
||||
data.data_source_type || null,
|
||||
data.data_source_config ? JSON.stringify(data.data_source_config) : null,
|
||||
|
|
@ -201,23 +218,31 @@ export class YardLayoutService {
|
|||
`;
|
||||
|
||||
const pool = getPool();
|
||||
|
||||
// NaN 방지를 위한 안전한 변환 함수
|
||||
const safeParseInt = (value: any): number | null => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const parsed = parseInt(String(value), 10);
|
||||
return isNaN(parsed) ? null : parsed;
|
||||
};
|
||||
|
||||
const safeParseFloat = (value: any): number | null => {
|
||||
if (value === null || value === undefined) return null;
|
||||
const parsed = parseFloat(String(value));
|
||||
return isNaN(parsed) ? null : parsed;
|
||||
};
|
||||
|
||||
const result = await pool.query(query, [
|
||||
data.material_code !== undefined ? data.material_code : null,
|
||||
data.material_name !== undefined ? data.material_name : null,
|
||||
data.quantity !== undefined ? parseInt(String(data.quantity), 10) : null,
|
||||
data.quantity !== undefined ? safeParseInt(data.quantity) : null,
|
||||
data.unit !== undefined ? data.unit : null,
|
||||
data.position_x !== undefined
|
||||
? parseFloat(String(data.position_x))
|
||||
: null,
|
||||
data.position_y !== undefined
|
||||
? parseFloat(String(data.position_y))
|
||||
: null,
|
||||
data.position_z !== undefined
|
||||
? parseFloat(String(data.position_z))
|
||||
: null,
|
||||
data.size_x !== undefined ? parseFloat(String(data.size_x)) : null,
|
||||
data.size_y !== undefined ? parseFloat(String(data.size_y)) : null,
|
||||
data.size_z !== undefined ? parseFloat(String(data.size_z)) : null,
|
||||
data.position_x !== undefined ? safeParseFloat(data.position_x) : null,
|
||||
data.position_y !== undefined ? safeParseFloat(data.position_y) : null,
|
||||
data.position_z !== undefined ? safeParseFloat(data.position_z) : null,
|
||||
data.size_x !== undefined ? safeParseFloat(data.size_x) : null,
|
||||
data.size_y !== undefined ? safeParseFloat(data.size_y) : null,
|
||||
data.size_z !== undefined ? safeParseFloat(data.size_z) : null,
|
||||
data.color !== undefined ? data.color : null,
|
||||
data.data_source_type !== undefined ? data.data_source_type : null,
|
||||
data.data_source_config !== undefined
|
||||
|
|
|
|||
|
|
@ -31,11 +31,14 @@ interface YardEditorProps {
|
|||
|
||||
export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
||||
const [placements, setPlacements] = useState<YardPlacement[]>([]);
|
||||
const [originalPlacements, setOriginalPlacements] = useState<YardPlacement[]>([]); // 원본 데이터 보관
|
||||
const [selectedPlacement, setSelectedPlacement] = useState<YardPlacement | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [showConfigPanel, setShowConfigPanel] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); // 미저장 변경사항 추적
|
||||
const [nextPlacementId, setNextPlacementId] = useState(-1); // 임시 ID (음수 사용)
|
||||
|
||||
// 배치 목록 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -44,7 +47,9 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
setIsLoading(true);
|
||||
const response = await yardLayoutApi.getPlacementsByLayoutId(layout.id);
|
||||
if (response.success) {
|
||||
setPlacements(response.data as YardPlacement[]);
|
||||
const loadedData = response.data as YardPlacement[];
|
||||
setPlacements(loadedData);
|
||||
setOriginalPlacements(JSON.parse(JSON.stringify(loadedData))); // 깊은 복사
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("배치 목록 로드 실패:", error);
|
||||
|
|
@ -57,36 +62,34 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
loadPlacements();
|
||||
}, [layout.id]);
|
||||
|
||||
// 빈 요소 추가
|
||||
const handleAddElement = async () => {
|
||||
try {
|
||||
const newPlacementData = {
|
||||
position_x: 0,
|
||||
position_y: 2.5, // size_y의 절반 (5 / 2 = 2.5)
|
||||
position_z: 0,
|
||||
size_x: 5,
|
||||
size_y: 5,
|
||||
size_z: 5,
|
||||
color: "#9ca3af", // 회색 (미설정 상태)
|
||||
};
|
||||
// 빈 요소 추가 (로컬 상태에만 추가, 저장 시 서버에 반영)
|
||||
const handleAddElement = () => {
|
||||
const newPlacement: YardPlacement = {
|
||||
id: nextPlacementId, // 임시 음수 ID
|
||||
yard_layout_id: layout.id,
|
||||
material_code: null,
|
||||
material_name: null,
|
||||
quantity: null,
|
||||
unit: null,
|
||||
position_x: 0,
|
||||
position_y: 2.5,
|
||||
position_z: 0,
|
||||
size_x: 5,
|
||||
size_y: 5,
|
||||
size_z: 5,
|
||||
color: "#9ca3af",
|
||||
data_source_type: null,
|
||||
data_source_config: null,
|
||||
data_binding: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
console.log("요소 추가 요청:", { layoutId: layout.id, data: newPlacementData });
|
||||
const response = await yardLayoutApi.addMaterialPlacement(layout.id, newPlacementData);
|
||||
console.log("요소 추가 응답:", response);
|
||||
|
||||
if (response.success) {
|
||||
const newPlacement = response.data as YardPlacement;
|
||||
setPlacements((prev) => [...prev, newPlacement]);
|
||||
setSelectedPlacement(newPlacement);
|
||||
setShowConfigPanel(true); // 자동으로 설정 패널 표시
|
||||
} else {
|
||||
console.error("요소 추가 실패 (응답):", response);
|
||||
setError(response.message || "요소 추가에 실패했습니다.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("요소 추가 실패 (예외):", error);
|
||||
setError(`요소 추가에 실패했습니다: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
setPlacements((prev) => [...prev, newPlacement]);
|
||||
setSelectedPlacement(newPlacement);
|
||||
setShowConfigPanel(true);
|
||||
setHasUnsavedChanges(true);
|
||||
setNextPlacementId((prev) => prev - 1); // 다음 임시 ID
|
||||
};
|
||||
|
||||
// 요소 선택 (3D 캔버스 또는 목록에서)
|
||||
|
|
@ -101,28 +104,21 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
setShowConfigPanel(true);
|
||||
};
|
||||
|
||||
// 요소 삭제
|
||||
const handleDeletePlacement = async (placementId: number) => {
|
||||
// 요소 삭제 (로컬 상태에서만 삭제, 저장 시 서버에 반영)
|
||||
const handleDeletePlacement = (placementId: number) => {
|
||||
if (!confirm("이 요소를 삭제하시겠습니까?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await yardLayoutApi.removePlacement(placementId);
|
||||
if (response.success) {
|
||||
setPlacements((prev) => prev.filter((p) => p.id !== placementId));
|
||||
if (selectedPlacement?.id === placementId) {
|
||||
setSelectedPlacement(null);
|
||||
setShowConfigPanel(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("요소 삭제 실패:", error);
|
||||
setError("요소 삭제에 실패했습니다.");
|
||||
setPlacements((prev) => prev.filter((p) => p.id !== placementId));
|
||||
if (selectedPlacement?.id === placementId) {
|
||||
setSelectedPlacement(null);
|
||||
setShowConfigPanel(false);
|
||||
}
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
// 자재 드래그 (3D 캔버스에서)
|
||||
// 자재 드래그 (3D 캔버스에서, 로컬 상태에만 반영)
|
||||
const handlePlacementDrag = (id: number, position: { x: number; y: number; z: number }) => {
|
||||
const updatedPosition = {
|
||||
position_x: Math.round(position.x * 2) / 2,
|
||||
|
|
@ -135,27 +131,83 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
if (selectedPlacement?.id === id) {
|
||||
setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedPosition } : null));
|
||||
}
|
||||
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
// 저장
|
||||
// 전체 저장 (신규/수정/삭제 모두 처리)
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const response = await yardLayoutApi.batchUpdatePlacements(
|
||||
layout.id,
|
||||
placements.map((p) => ({
|
||||
id: p.id,
|
||||
position_x: p.position_x,
|
||||
position_y: p.position_y,
|
||||
position_z: p.position_z,
|
||||
size_x: p.size_x,
|
||||
size_y: p.size_y,
|
||||
size_z: p.size_z,
|
||||
color: p.color,
|
||||
})),
|
||||
);
|
||||
// 1. 삭제된 요소 처리 (원본에는 있지만 현재 state에 없는 경우)
|
||||
const deletedIds = originalPlacements
|
||||
.filter((orig) => !placements.find((p) => p.id === orig.id))
|
||||
.map((p) => p.id);
|
||||
|
||||
for (const id of deletedIds) {
|
||||
await yardLayoutApi.removePlacement(id);
|
||||
}
|
||||
|
||||
// 2. 신규 추가 요소 처리 (ID가 음수인 경우)
|
||||
const newPlacements = placements.filter((p) => p.id < 0);
|
||||
const addedPlacements: YardPlacement[] = [];
|
||||
|
||||
for (const newPlacement of newPlacements) {
|
||||
const response = await yardLayoutApi.addMaterialPlacement(layout.id, {
|
||||
material_code: newPlacement.material_code,
|
||||
material_name: newPlacement.material_name,
|
||||
quantity: newPlacement.quantity,
|
||||
unit: newPlacement.unit,
|
||||
position_x: newPlacement.position_x,
|
||||
position_y: newPlacement.position_y,
|
||||
position_z: newPlacement.position_z,
|
||||
size_x: newPlacement.size_x,
|
||||
size_y: newPlacement.size_y,
|
||||
size_z: newPlacement.size_z,
|
||||
color: newPlacement.color,
|
||||
data_source_type: newPlacement.data_source_type,
|
||||
data_source_config: newPlacement.data_source_config,
|
||||
data_binding: newPlacement.data_binding,
|
||||
memo: newPlacement.memo,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
addedPlacements.push(response.data as YardPlacement);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 기존 요소 수정 처리 (ID가 양수인 경우)
|
||||
const existingPlacements = placements.filter((p) => p.id > 0);
|
||||
|
||||
for (const placement of existingPlacements) {
|
||||
await yardLayoutApi.updatePlacement(placement.id, {
|
||||
material_code: placement.material_code,
|
||||
material_name: placement.material_name,
|
||||
quantity: placement.quantity,
|
||||
unit: placement.unit,
|
||||
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,
|
||||
color: placement.color,
|
||||
data_source_type: placement.data_source_type,
|
||||
data_source_config: placement.data_source_config,
|
||||
data_binding: placement.data_binding,
|
||||
memo: placement.memo,
|
||||
});
|
||||
}
|
||||
|
||||
// 4. 저장 성공 후 데이터 다시 로드
|
||||
const response = await yardLayoutApi.getPlacementsByLayoutId(layout.id);
|
||||
if (response.success) {
|
||||
const loadedData = response.data as YardPlacement[];
|
||||
setPlacements(loadedData);
|
||||
setOriginalPlacements(JSON.parse(JSON.stringify(loadedData)));
|
||||
setHasUnsavedChanges(false);
|
||||
setSelectedPlacement(null);
|
||||
setShowConfigPanel(false);
|
||||
alert("저장되었습니다");
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -166,45 +218,24 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
}
|
||||
};
|
||||
|
||||
// 설정 패널에서 저장
|
||||
const handleSaveConfig = async (updatedData: Partial<YardPlacement>) => {
|
||||
// 설정 패널에서 데이터 업데이트 (로컬 상태에만 반영, 서버 저장 안함)
|
||||
const handleSaveConfig = (updatedData: Partial<YardPlacement>) => {
|
||||
if (!selectedPlacement) return;
|
||||
|
||||
try {
|
||||
const response = await yardLayoutApi.updatePlacement(selectedPlacement.id, updatedData);
|
||||
if (response.success) {
|
||||
const updated = response.data as YardPlacement;
|
||||
// 로컬 상태만 업데이트
|
||||
setPlacements((prev) =>
|
||||
prev.map((p) => {
|
||||
if (p.id === selectedPlacement.id) {
|
||||
return { ...p, ...updatedData };
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
);
|
||||
|
||||
// 현재 위치 정보를 유지하면서 업데이트
|
||||
setPlacements((prev) =>
|
||||
prev.map((p) => {
|
||||
if (p.id === updated.id) {
|
||||
// 현재 프론트엔드 상태의 위치를 유지
|
||||
return {
|
||||
...updated,
|
||||
position_x: p.position_x,
|
||||
position_y: p.position_y,
|
||||
position_z: p.position_z,
|
||||
};
|
||||
}
|
||||
return p;
|
||||
}),
|
||||
);
|
||||
|
||||
// 선택된 요소도 동일하게 업데이트
|
||||
setSelectedPlacement({
|
||||
...updated,
|
||||
position_x: selectedPlacement.position_x,
|
||||
position_y: selectedPlacement.position_y,
|
||||
position_z: selectedPlacement.position_z,
|
||||
});
|
||||
|
||||
setShowConfigPanel(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("설정 저장 실패:", error);
|
||||
setError("설정 저장에 실패했습니다.");
|
||||
}
|
||||
// 선택된 요소도 업데이트
|
||||
setSelectedPlacement((prev) => (prev ? { ...prev, ...updatedData } : null));
|
||||
setShowConfigPanel(false);
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
// 요소가 설정되었는지 확인
|
||||
|
|
@ -227,19 +258,22 @@ export default function YardEditor({ layout, onBack }: YardEditorProps) {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<Button size="sm" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
저장
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{hasUnsavedChanges && <span className="text-sm font-medium text-orange-600">미저장 변경사항 있음</span>}
|
||||
<Button size="sm" onClick={handleSave} disabled={isSaving || !hasUnsavedChanges}>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
저장
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 에러 메시지 */}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|||
|
||||
interface YardElementConfigPanelProps {
|
||||
placement: YardPlacement;
|
||||
onSave: (updatedData: Partial<YardPlacement>) => Promise<void>;
|
||||
onSave: (updatedData: Partial<YardPlacement>) => void; // Promise 제거 (즉시 로컬 상태 업데이트)
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
|
|
@ -52,9 +52,8 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
const [sizeY, setSizeY] = useState(placement.size_y);
|
||||
const [sizeZ, setSizeZ] = useState(placement.size_z);
|
||||
|
||||
// 에러 및 로딩
|
||||
// 에러
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// 외부 DB 커넥션 목록 로드
|
||||
useEffect(() => {
|
||||
|
|
@ -180,8 +179,8 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
}
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
// 적용 (로컬 상태만 업데이트, 서버 저장은 나중에 일괄 처리)
|
||||
const handleApply = () => {
|
||||
// 검증
|
||||
if (!queryResult) {
|
||||
setError("먼저 데이터를 조회해주세요.");
|
||||
|
|
@ -203,49 +202,40 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
const selectedRow = queryResult.rows[selectedRowIndex];
|
||||
const materialName = selectedRow[materialNameField];
|
||||
const quantity = selectedRow[quantityField];
|
||||
|
||||
try {
|
||||
const selectedRow = queryResult.rows[selectedRowIndex];
|
||||
const materialName = selectedRow[materialNameField];
|
||||
const quantity = selectedRow[quantityField];
|
||||
const dataSourceConfig: YardDataSourceConfig = {
|
||||
type: dataSourceType,
|
||||
query: dataSourceType !== "rest_api" ? query : undefined,
|
||||
connectionId: dataSourceType === "external_db" ? parseInt(externalConnectionId) : undefined,
|
||||
url: dataSourceType === "rest_api" ? apiUrl : undefined,
|
||||
method: dataSourceType === "rest_api" ? apiMethod : undefined,
|
||||
dataPath: dataSourceType === "rest_api" && apiDataPath ? apiDataPath : undefined,
|
||||
};
|
||||
|
||||
const dataSourceConfig: YardDataSourceConfig = {
|
||||
type: dataSourceType,
|
||||
query: dataSourceType !== "rest_api" ? query : undefined,
|
||||
connectionId: dataSourceType === "external_db" ? parseInt(externalConnectionId) : undefined,
|
||||
url: dataSourceType === "rest_api" ? apiUrl : undefined,
|
||||
method: dataSourceType === "rest_api" ? apiMethod : undefined,
|
||||
dataPath: dataSourceType === "rest_api" && apiDataPath ? apiDataPath : undefined,
|
||||
};
|
||||
const dataBinding: YardDataBinding = {
|
||||
selectedRowIndex,
|
||||
materialNameField,
|
||||
quantityField,
|
||||
unit: unit.trim(),
|
||||
};
|
||||
|
||||
const dataBinding: YardDataBinding = {
|
||||
selectedRowIndex,
|
||||
materialNameField,
|
||||
quantityField,
|
||||
unit: unit.trim(),
|
||||
};
|
||||
const updatedData: Partial<YardPlacement> = {
|
||||
material_name: String(materialName),
|
||||
quantity: Number(quantity),
|
||||
unit: unit.trim(),
|
||||
color,
|
||||
size_x: sizeX,
|
||||
size_y: sizeY,
|
||||
size_z: sizeZ,
|
||||
data_source_type: dataSourceType,
|
||||
data_source_config: dataSourceConfig,
|
||||
data_binding: dataBinding,
|
||||
};
|
||||
|
||||
const updatedData: Partial<YardPlacement> = {
|
||||
material_name: String(materialName),
|
||||
quantity: Number(quantity),
|
||||
unit: unit.trim(),
|
||||
color,
|
||||
size_x: sizeX,
|
||||
size_y: sizeY,
|
||||
size_z: sizeZ,
|
||||
data_source_type: dataSourceType,
|
||||
data_source_config: dataSourceConfig,
|
||||
data_binding: dataBinding,
|
||||
};
|
||||
|
||||
await onSave(updatedData);
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "저장 중 오류가 발생했습니다.";
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
onSave(updatedData); // 동기적으로 즉시 로컬 상태 업데이트
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -537,15 +527,8 @@ export default function YardElementConfigPanel({ placement, onSave, onCancel }:
|
|||
<Button variant="outline" onClick={onCancel} className="flex-1">
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave} disabled={isSaving || !queryResult} className="flex-1">
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
저장 중...
|
||||
</>
|
||||
) : (
|
||||
"저장"
|
||||
)}
|
||||
<Button onClick={handleApply} disabled={!queryResult} className="flex-1">
|
||||
적용
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue