(null);
+
useEffect(() => {
loadHistory();
+ loadAssets();
}, [assetId]);
+ const loadAssets = async () => {
+ try {
+ const data = await assetApi.getAll();
+ setAvailableAssets(data.filter(a => a.id !== assetId));
+ } catch (error) {
+ console.error("Failed to load assets:", error);
+ }
+ };
+
const loadHistory = async () => {
try {
const data = await assetApi.getMaintenanceHistory(assetId);
@@ -614,25 +633,70 @@ function HistoryTab({ assetId }: { assetId: string }) {
}));
};
+ const handleAddPart = () => {
+ if (!partInput.part_id) return;
+ const part = availableAssets.find(a => a.id === partInput.part_id);
+ if (!part) return;
+
+ // Check if already added
+ if (selectedParts.find(p => p.part_id === partInput.part_id)) {
+ alert("이미 추가된 부품입니다.");
+ return;
+ }
+
+ setSelectedParts(prev => [...prev, {
+ part_id: part.id,
+ part_name: part.name,
+ model_name: part.model,
+ quantity: partInput.quantity
+ }]);
+
+ setPartInput({ part_id: '', quantity: 1 });
+ };
+
+ const handleRemovePart = (partId: string) => {
+ setSelectedParts(prev => prev.filter(p => p.part_id !== partId));
+ };
+
+
+
const handleSubmit = async () => {
if (!formData.content) {
alert("정비 내용을 입력해주세요.");
return;
}
try {
- await assetApi.addMaintenance(assetId, {
- maintenance_date: formData.maintenance_date,
- type: formData.type,
- content: formData.content,
- images: formData.images
- });
+ if (editingId) {
+ // Update existing record
+ await assetApi.updateMaintenance(editingId, {
+ maintenance_date: formData.maintenance_date,
+ type: formData.type,
+ content: formData.content,
+ images: formData.images,
+ parts: selectedParts
+ });
+ alert("수정되었습니다.");
+ } else {
+ // Create new record
+ await assetApi.addMaintenance(assetId, {
+ maintenance_date: formData.maintenance_date,
+ type: formData.type,
+ content: formData.content,
+ images: formData.images,
+ parts: selectedParts
+ });
+ }
+
+ // Reset Form
setIsWriting(false);
+ setEditingId(null);
setFormData({
maintenance_date: new Date().toISOString().split('T')[0],
type: '정기점검',
content: '',
images: []
});
+ setSelectedParts([]);
loadHistory();
} catch (error) {
console.error("Failed to save maintenance record:", error);
@@ -640,6 +704,32 @@ function HistoryTab({ assetId }: { assetId: string }) {
}
};
+ const handleEdit = (item: MaintenanceRecord) => {
+ setEditingId(item.id);
+ setFormData({
+ maintenance_date: new Date(item.maintenance_date).toISOString().split('T')[0],
+ type: item.type,
+ content: item.content,
+ images: item.images || []
+ });
+ setSelectedParts(item.parts || []);
+ setIsWriting(true);
+ // Scroll to form
+ document.querySelector('.maintenance-form')?.scrollIntoView({ behavior: 'smooth' });
+ };
+
+ const handleCancel = () => {
+ setIsWriting(false);
+ setEditingId(null);
+ setFormData({
+ maintenance_date: new Date().toISOString().split('T')[0],
+ type: '정기점검',
+ content: '',
+ images: []
+ });
+ setSelectedParts([]);
+ };
+
const handleDelete = async (id: number) => {
if (confirm("정비 이력을 삭제하시겠습니까?")) {
try {
@@ -665,11 +755,11 @@ function HistoryTab({ assetId }: { assetId: string }) {
onClick={handleSubmit}
className="flex items-center gap-2 px-3 py-1.5 rounded text-sm font-medium transition-colors bg-blue-600 text-white hover:bg-blue-700 shadow-sm"
>
- 저장하기
+ {editingId ? '수정완료' : '저장하기'}
)}
+
+
{isWriting && (
@@ -767,7 +859,65 @@ function HistoryTab({ assetId }: { assetId: string }) {
+ {/* Part Selection UI */}
+
+
+
+
+ setPartInput({ ...partInput, quantity: parseInt(e.target.value) || 1 })}
+ />
+
+
+ {/* Selected Parts List */}
+ {selectedParts.length > 0 && (
+
+ {selectedParts.map(part => (
+
+
+
+
+
+
{part.part_name}
+
({part.model_name})
+
+
+
+ {part.quantity}개
+
+
+
+
+ ))}
+
+ )}
+
)}
@@ -806,8 +956,19 @@ function HistoryTab({ assetId }: { assetId: string }) {
{item.type}
-
- {item.content}
+ |
+ {item.content}
+ {/* Display Used Parts */}
+ {item.parts && item.parts.length > 0 && (
+
+ {item.parts.map((part, idx) => (
+
+
+ {part.part_name} ({part.quantity})
+
+ ))}
+
+ )}
|
@@ -844,13 +1005,22 @@ function HistoryTab({ assetId }: { assetId: string }) {
|
-
+
+
+
+
|
))
@@ -883,14 +1053,13 @@ function ManualTab({ assetId }: { assetId: string }) {
if (!file) return;
try {
- // Reusing uploadImage since it returns {url} and works for files
const res = await assetApi.uploadImage(file);
await assetApi.addManual(assetId, {
file_name: file.name,
file_url: res.data.url
});
loadManuals();
- e.target.value = ''; // Reset input
+ e.target.value = '';
} catch (error) {
console.error("Failed to upload manual:", error);
alert("파일 업로드에 실패했습니다.");
diff --git a/src/shared/api/assetApi.ts b/src/shared/api/assetApi.ts
index 47cde10..852750b 100644
--- a/src/shared/api/assetApi.ts
+++ b/src/shared/api/assetApi.ts
@@ -47,6 +47,15 @@ export interface MaintenanceRecord {
image_url?: string; // Legacy
images?: string[]; // New: Multiple images
created_at?: string;
+ parts?: PartUsage[];
+}
+
+export interface PartUsage {
+ id?: number; // maintenance_parts id
+ part_id: string; // asset id
+ part_name?: string; // joined
+ model_name?: string; // joined
+ quantity: number;
}
// Manual Interface
@@ -105,6 +114,10 @@ export const assetApi = {
return apiClient.delete(`/maintenance/${id}`);
},
+ updateMaintenance: async (id: number, data: Partial) => {
+ return apiClient.put(`/maintenance/${id}`, data);
+ },
+
// Manuals / Instructions
getManuals: async (assetId: string): Promise => {
const response = await apiClient.get(`/assets/${assetId}/manuals`);
diff --git a/src/shared/auth/ModuleGuard.tsx b/src/shared/auth/ModuleGuard.tsx
new file mode 100644
index 0000000..2682c9f
--- /dev/null
+++ b/src/shared/auth/ModuleGuard.tsx
@@ -0,0 +1,25 @@
+import { type ReactNode } from 'react';
+import { Navigate } from 'react-router-dom';
+import { useSystem } from '../context/SystemContext';
+
+interface ModuleGuardProps {
+ moduleCode: string;
+ children: ReactNode;
+}
+
+export function ModuleGuard({ moduleCode, children }: ModuleGuardProps) {
+ const { modules, isLoading } = useSystem();
+
+ if (isLoading) {
+ return Loading permissions...
;
+ }
+
+ const isActive = modules[moduleCode]?.active;
+
+ if (!isActive) {
+ // Redirect to home or showing a customizable "Unauthorized" page could be better
+ return ;
+ }
+
+ return <>{children}>;
+}
diff --git a/vite.config.ts b/vite.config.ts
index f125622..6966c0a 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -26,6 +26,10 @@ export default defineConfig({
});
},
},
+ '/uploads': {
+ target: 'http://localhost:3005',
+ changeOrigin: true,
+ },
}
}
})