smart_ims/src/modules/asset/pages/AssetRegisterPage.tsx
choibk 8b2589b6fa feat: 플랫폼 보안 강화, 권한 계층 시스템 도입 및 버전 관리 통합 (v0.2.5)
- 최고관리자(Supervisor) 전용 2중 보안 잠금 시스템 및 인증 UI 적용
- 데이터베이스 인프라 및 암호화 마스터 키 자가 관리 기능 구축
- 권한 계층(Supervisor > Admin > User) 기반의 메뉴 노출 및 접근 제어 로직 강화
- 시스템 버전 정보 페이지 신규 추가 및 패키지 버전 자동 연동 (v0.2.5)
- 사용자 관리 UI 디자인 개선 및 폰트/스타일 일원화
2026-01-24 17:17:33 +09:00

341 lines
15 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card } from '../../../shared/ui/Card';
import { Button } from '../../../shared/ui/Button';
import { Input } from '../../../shared/ui/Input';
import { Select } from '../../../shared/ui/Select';
import { ArrowLeft, Save, Upload } from 'lucide-react';
import { getCategories, getLocations, getIDRule } from './AssetSettingsPage';
import { assetApi, type Asset } from '../../../shared/api/assetApi';
import './AssetRegisterPage.css';
export function AssetRegisterPage() {
const navigate = useNavigate();
// Load Settings Data
const categories = getCategories();
const locations = getLocations();
const idRule = getIDRule();
const [formData, setFormData] = useState({
id: '', // Asset ID (Auto-generated)
name: '',
categoryId: '', // Use ID for selection
model: '',
serialNo: '',
locationId: '', // Use ID for selection
manager: '',
status: 'active',
purchaseDate: new Date().toISOString().split('T')[0], // Default to today
purchasePrice: '',
image: null as File | null,
imagePreview: '' as string,
manufacturer: ''
});
// Auto-generate Asset ID
useEffect(() => {
// If category is required by rule but not selected, can't generate fully
const hasCategoryRule = idRule.some(r => r.type === 'category');
if (hasCategoryRule && !formData.categoryId) {
setFormData(prev => ({ ...prev, id: '' }));
return;
}
const category = categories.find(c => c.id === formData.categoryId);
const year = formData.purchaseDate ? new Date(formData.purchaseDate).getFullYear().toString() : new Date().getFullYear().toString();
// Build ID string based on Rule
const generatedId = idRule.map(part => {
if (part.type === 'company') return part.value; // e.g. HK
if (part.type === 'custom') return part.value;
if (part.type === 'separator') return part.value;
if (part.type === 'year') return year;
if (part.type === 'category') return category ? category.code : 'UNKNOWN';
if (part.type === 'sequence') return part.value;
return '';
}).join('');
const finalId = generatedId.replace('001', '001');
setFormData(prev => ({ ...prev, id: finalId }));
}, [formData.categoryId, formData.purchaseDate, idRule, categories]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onloadend = () => {
setFormData(prev => ({
...prev,
image: file,
imagePreview: reader.result as string
}));
};
reader.readAsDataURL(file);
}
};
const handleSubmit = async (e: React.FormEvent) => {
if (e) e.preventDefault();
// Validation
if (!formData.categoryId || !formData.name || !formData.locationId) {
alert('필수 항목(카테고리, 자산명, 설치위치)을 입력해주세요.');
return;
}
try {
const selectedCategory = categories.find(c => c.id === formData.categoryId);
const selectedLocation = locations.find(l => l.id === formData.locationId);
let imageUrl = '';
if (formData.image) {
const uploadRes = await assetApi.uploadImage(formData.image);
imageUrl = uploadRes.data.url;
}
const payload: Partial<Asset> = {
id: formData.id,
name: formData.name,
category: selectedCategory ? selectedCategory.name : '미지정',
model: formData.model,
serialNumber: formData.serialNo,
location: selectedLocation ? selectedLocation.name : '미지정',
manager: formData.manager,
status: formData.status as Asset['status'],
purchaseDate: formData.purchaseDate,
purchasePrice: formData.purchasePrice ? Number(formData.purchasePrice) : 0,
manufacturer: formData.manufacturer,
image: imageUrl
};
await assetApi.create(payload);
alert(`자산이 성공적으로 등록되었습니다.\n자산번호: ${formData.id}`);
navigate('/asset/list');
} catch (error) {
console.error('Failed to register asset:', error);
alert('자산 등록에 실패했습니다. 서버 상태를 확인해주세요.');
}
};
return (
<div className="page-container">
<div className="page-header-right">
<h1 className="page-title-text"> </h1>
<p className="page-subtitle"> .</p>
</div>
<Card className="w-full shadow-sm border border-slate-200 mb-8">
<form onSubmit={handleSubmit} className="p-2 sm:p-4 lg:p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6">
{/* Basic Info Section */}
<div className="col-span-full flex items-center gap-3 border-b border-slate-100 pb-3 mb-2">
<div className="w-1 h-6 bg-blue-500 rounded-full"></div>
<h3 className="text-xl font-bold text-slate-800"> </h3>
</div>
<Select
label="카테고리 *"
name="categoryId"
value={formData.categoryId}
onChange={handleChange}
options={categories.map(c => ({ label: c.name, value: c.id }))}
placeholder="카테고리 선택"
required
/>
<Input
label="자산 관리 번호 (자동 생성)"
name="id"
value={formData.id}
disabled
placeholder="카테고리 선택 시 자동 생성됨"
className="bg-slate-50 font-mono text-slate-600"
/>
<Input
label="자산명 *"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="예: CNC 머시닝 센터"
required
/>
<Input
label="제작사"
name="manufacturer"
value={formData.manufacturer}
onChange={handleChange}
/>
<Input
label="모델명"
name="model"
value={formData.model}
onChange={handleChange}
/>
<Input
label="시리얼 번호 / 규격"
name="serialNo"
value={formData.serialNo}
onChange={handleChange}
/>
{/* Image Upload Field */}
<div className="ui-field-container col-span-full">
<label className="ui-label"> </label>
<div className="w-full bg-white border border-slate-200 rounded-md shadow-sm p-4 flex flex-col items-start gap-4" style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '16px' }}>
<div
className="shrink-0 bg-slate-50 border border-slate-200 rounded-md overflow-hidden"
style={{
width: '400px',
height: '350px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
{formData.imagePreview ? (
<img
src={formData.imagePreview}
alt="Preview"
className="w-full h-full object-contain"
/>
) : (
<div
className="text-slate-300"
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%'
}}
>
<Upload size={32} strokeWidth={1.5} />
</div>
)}
</div>
<div className="flex flex-row items-center flex-wrap w-full max-w-[600px]" style={{ gap: '20px' }}>
<label htmlFor="image-upload" className="ui-btn ui-btn-sm ui-btn-secondary cursor-pointer shrink-0">
</label>
<span className="text-sm text-slate-600 font-medium truncate max-w-[200px]">
{formData.image ? formData.image.name : '선택된 파일 없음'}
</span>
<span className="text-sm text-slate-400 border-l border-slate-300 pl-4" style={{ paddingLeft: '20px' }}>
형식: JPG, PNG, GIF ( 5MB)
</span>
<input
id="image-upload"
type="file"
style={{ display: 'none' }}
accept="image/*"
onChange={handleImageChange}
/>
</div>
</div>
</div>
{/* Management Info Section */}
<div className="col-span-full flex items-center gap-3 border-b border-slate-100 pb-3 mb-2 mt-8">
<div className="w-1 h-6 bg-blue-500 rounded-full"></div>
<h3 className="text-xl font-bold text-slate-800"> </h3>
</div>
<Select
label="설치 위치 / 보관 장소 *"
name="locationId"
value={formData.locationId}
onChange={handleChange}
options={locations.map(l => ({ label: l.name, value: l.id }))}
placeholder="위치 선택"
required
/>
<Input
label="관리자 / 부서"
name="manager"
value={formData.manager}
onChange={handleChange}
/>
<Select
label="초기 상태"
name="status"
value={formData.status}
onChange={handleChange}
options={[
{ label: '정상 가동 (Active)', value: 'active' },
{ label: '대기 (Idle)', value: 'idle' },
{ label: '설치 중 (Installing)', value: 'installing' },
{ label: '점검 중 (Maintenance)', value: 'maintain' }
]}
/>
<div className="col-span-1"></div>
{/* Purchasing Info Section */}
<div className="col-span-full flex items-center gap-3 border-b border-slate-100 pb-3 mb-2 mt-8">
<div className="w-1 h-6 bg-blue-500 rounded-full"></div>
<h3 className="text-xl font-bold text-slate-800"> </h3>
</div>
<Input
label="도입일"
name="purchaseDate"
type="date"
value={formData.purchaseDate}
onChange={handleChange}
/>
<Input
label="구입 가격 (KRW)"
name="purchasePrice"
type="number"
value={formData.purchasePrice}
onChange={handleChange}
placeholder="0"
/>
</div>
{/* Bottom Action Buttons */}
<div className="form-actions-footer">
<Button
variant="secondary"
onClick={() => navigate(-1)}
icon={<ArrowLeft size={16} />}
>
</Button>
<Button
onClick={handleSubmit}
icon={<Save size={16} />}
>
</Button>
</div>
</form>
</Card>
</div>
);
}