fix: TypeScript 타입 오류 수정 및 설정 페이지 추가
- BOMItem Omit 타입 시그니처 통일 (useTemplateManagement, SectionsTab, ItemMasterContext) - HeadersInit → Record<string, string> 타입 변경 - Zustand useShallow 마이그레이션 (zustand/react/shallow) - DataTable, ListPageTemplate 제네릭 타입 제약 추가 - 설정 관리 페이지 추가 (직급, 직책, 휴가정책, 근무일정, 권한) - HR 관리 페이지 추가 (급여, 휴가) - 단가관리 페이지 리팩토링 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -136,9 +136,12 @@ export function DrawingCanvas({
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return { x: 0, y: 0 };
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
// 실제 캔버스 크기와 표시 크기의 비율 계산
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
return {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
x: (e.clientX - rect.left) * scaleX,
|
||||
y: (e.clientY - rect.top) * scaleY,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -80,22 +80,6 @@ export function DropdownField({
|
||||
// 옵션이 없으면 드롭다운을 disabled로 표시
|
||||
const hasOptions = options.length > 0;
|
||||
|
||||
// 디버깅: 단위 필드 값 추적
|
||||
if (isUnitField) {
|
||||
console.log('[DropdownField] 단위 필드 디버깅:', {
|
||||
fieldKey,
|
||||
fieldName: field.field_name,
|
||||
rawValue: value,
|
||||
stringValue,
|
||||
isUnitField,
|
||||
unitOptionsCount: unitOptions?.length || 0,
|
||||
unitOptions: unitOptions?.slice(0, 3), // 처음 3개만
|
||||
optionsCount: options.length,
|
||||
options: options.slice(0, 3), // 처음 3개만
|
||||
valueInOptions: options.some(o => o.value === stringValue),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Label htmlFor={fieldKey}>
|
||||
|
||||
@@ -72,17 +72,6 @@ export function useConditionalDisplay(
|
||||
}
|
||||
});
|
||||
|
||||
// 디버깅: 조건부 표시 설정 확인
|
||||
console.log('[useConditionalDisplay] 트리거 필드 목록:', triggers.map(t => ({
|
||||
fieldKey: t.fieldKey,
|
||||
fieldId: t.fieldId,
|
||||
fieldConditions: t.condition.fieldConditions?.map(fc => ({
|
||||
expectedValue: fc.expectedValue,
|
||||
targetFieldIds: fc.targetFieldIds,
|
||||
targetSectionIds: fc.targetSectionIds,
|
||||
})),
|
||||
})));
|
||||
|
||||
return triggers;
|
||||
}, [structure]);
|
||||
|
||||
@@ -102,15 +91,6 @@ export function useConditionalDisplay(
|
||||
// 현재 값과 기대값이 일치하는지 확인
|
||||
const isMatch = String(currentValue) === fc.expectedValue;
|
||||
|
||||
// 디버깅: 조건 매칭 확인
|
||||
console.log('[useConditionalDisplay] 조건 매칭 체크:', {
|
||||
triggerFieldKey: trigger.fieldKey,
|
||||
currentValue: String(currentValue),
|
||||
expectedValue: fc.expectedValue,
|
||||
isMatch,
|
||||
targetFieldIds: fc.targetFieldIds,
|
||||
});
|
||||
|
||||
if (isMatch) {
|
||||
// 일치하면 타겟 섹션/필드 활성화
|
||||
if (fc.targetSectionIds) {
|
||||
@@ -124,8 +104,6 @@ export function useConditionalDisplay(
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[useConditionalDisplay] 활성화된 필드 ID:', [...activeFieldIds]);
|
||||
|
||||
return { activeSectionIds, activeFieldIds };
|
||||
}, [triggerFields, formData]);
|
||||
|
||||
|
||||
@@ -178,7 +178,6 @@ export function useDynamicFormState(
|
||||
|
||||
// 폼 초기화
|
||||
const resetForm = useCallback((newInitialData?: DynamicFormData) => {
|
||||
console.log('[useDynamicFormState] resetForm 호출됨:', newInitialData);
|
||||
setFormData(newInitialData || {});
|
||||
setErrors({});
|
||||
setIsSubmitting(false);
|
||||
|
||||
@@ -398,127 +398,38 @@ export default function DynamicItemForm({
|
||||
}
|
||||
}, [selectedItemType, structure, mode, resetForm]);
|
||||
|
||||
// Edit 모드: structure 로드 후 initialData를 field_key 형식으로 변환
|
||||
// 2025-12-04: initialData 키(item_name)와 structure의 field_key(98_item_name)가 다른 문제 해결
|
||||
// Edit 모드: initialData를 폼에 직접 로드
|
||||
// 2025-12-09: field_key 통일로 복잡한 매핑 로직 제거
|
||||
// 백엔드에서 field_key 그대로 응답하므로 직접 사용 가능
|
||||
const [isEditDataMapped, setIsEditDataMapped] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 'edit' || !structure || !initialData) return;
|
||||
console.log('[DynamicItemForm] Edit useEffect 체크:', {
|
||||
mode,
|
||||
hasStructure: !!structure,
|
||||
hasInitialData: !!initialData,
|
||||
isEditDataMapped,
|
||||
structureSections: structure?.sections?.length,
|
||||
});
|
||||
|
||||
// 이미 매핑된 데이터가 formData에 있으면 스킵 (98_unit 같은 field_key 형식)
|
||||
// StrictMode 리렌더에서도 안전하게 동작
|
||||
const hasFieldKeyData = Object.keys(formData).some(key => /^\d+_/.test(key));
|
||||
if (hasFieldKeyData) {
|
||||
console.log('[DynamicItemForm] Edit mode: 이미 field_key 형식 데이터 있음, 매핑 스킵');
|
||||
return;
|
||||
}
|
||||
if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return;
|
||||
|
||||
console.log('[DynamicItemForm] Edit mode: mapping initialData to field_key format');
|
||||
console.log('[DynamicItemForm] Edit mode: initialData 직접 로드 (field_key 통일됨)');
|
||||
console.log('[DynamicItemForm] initialData:', initialData);
|
||||
|
||||
// initialData의 간단한 키를 structure의 field_key로 매핑
|
||||
// 예: { item_name: '테스트' } → { '98_item_name': '테스트' }
|
||||
const mappedData: DynamicFormData = {};
|
||||
|
||||
// field_key에서 실제 필드명 추출하는 함수
|
||||
// 예: '98_item_name' → 'item_name', '110_품목명' → '품목명'
|
||||
const extractFieldName = (fieldKey: string): string => {
|
||||
const underscoreIndex = fieldKey.indexOf('_');
|
||||
if (underscoreIndex > 0) {
|
||||
return fieldKey.substring(underscoreIndex + 1);
|
||||
}
|
||||
return fieldKey;
|
||||
};
|
||||
|
||||
// structure에서 모든 필드의 field_key 수집
|
||||
const fieldKeyMap: Record<string, string> = {}; // 간단한 키 → field_key 매핑
|
||||
|
||||
// 영문 → 한글 필드명 별칭 (API 응답 키 → structure field_name 매핑)
|
||||
// API는 영문 키(unit, note)로 응답하지만, structure field_key는 한글(단위, 비고) 포함
|
||||
const fieldAliases: Record<string, string> = {
|
||||
'unit': '단위',
|
||||
'note': '비고',
|
||||
'remarks': '비고', // Material 모델은 remarks 사용
|
||||
'item_name': '품목명',
|
||||
'specification': '규격',
|
||||
'description': '설명',
|
||||
};
|
||||
|
||||
// structure의 field_key들 확인
|
||||
const fieldKeys: string[] = [];
|
||||
structure.sections.forEach((section) => {
|
||||
section.fields.forEach((f) => {
|
||||
const field = f.field;
|
||||
const fieldKey = field.field_key || `field_${field.id}`;
|
||||
const simpleName = extractFieldName(fieldKey);
|
||||
fieldKeyMap[simpleName] = fieldKey;
|
||||
|
||||
// field_name도 매핑에 추가 (한글 필드명 지원)
|
||||
if (field.field_name) {
|
||||
fieldKeyMap[field.field_name] = fieldKey;
|
||||
}
|
||||
fieldKeys.push(f.field.field_key || `field_${f.field.id}`);
|
||||
});
|
||||
});
|
||||
console.log('[DynamicItemForm] structure field_keys:', fieldKeys);
|
||||
console.log('[DynamicItemForm] initialData keys:', Object.keys(initialData));
|
||||
|
||||
structure.directFields.forEach((f) => {
|
||||
const field = f.field;
|
||||
const fieldKey = field.field_key || `field_${field.id}`;
|
||||
const simpleName = extractFieldName(fieldKey);
|
||||
fieldKeyMap[simpleName] = fieldKey;
|
||||
|
||||
if (field.field_name) {
|
||||
fieldKeyMap[field.field_name] = fieldKey;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[DynamicItemForm] fieldKeyMap:', fieldKeyMap);
|
||||
|
||||
// initialData를 field_key 형식으로 변환
|
||||
Object.entries(initialData).forEach(([key, value]) => {
|
||||
// 이미 field_key 형식인 경우 그대로 사용
|
||||
if (key.includes('_') && /^\d+_/.test(key)) {
|
||||
mappedData[key] = value;
|
||||
}
|
||||
// 간단한 키인 경우 field_key로 변환
|
||||
else if (fieldKeyMap[key]) {
|
||||
mappedData[fieldKeyMap[key]] = value;
|
||||
}
|
||||
// 영문 → 한글 별칭으로 시도 (API 응답 키 → structure field_name)
|
||||
else if (fieldAliases[key] && fieldKeyMap[fieldAliases[key]]) {
|
||||
mappedData[fieldKeyMap[fieldAliases[key]]] = value;
|
||||
console.log(`[DynamicItemForm] 별칭 매핑: ${key} → ${fieldAliases[key]} → ${fieldKeyMap[fieldAliases[key]]}`);
|
||||
}
|
||||
// 매핑 없는 경우 그대로 유지
|
||||
else {
|
||||
mappedData[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// 추가: 폼 구조의 모든 필드를 순회하면서, initialData에서 해당 값 직접 찾아서 설정
|
||||
// (fieldKeyMap에 매핑이 없는 경우를 위한 fallback)
|
||||
Object.entries(fieldKeyMap).forEach(([simpleName, fieldKey]) => {
|
||||
// 아직 매핑 안된 필드인데 initialData에 값이 있으면 설정
|
||||
if (mappedData[fieldKey] === undefined && initialData[simpleName] !== undefined) {
|
||||
mappedData[fieldKey] = initialData[simpleName];
|
||||
}
|
||||
});
|
||||
|
||||
// 추가: 영문 별칭을 역으로 검색하여 매핑 (한글 field_name → 영문 API 키)
|
||||
// 예: fieldKeyMap에 '단위'가 있고, initialData에 'unit'이 있으면 매핑
|
||||
Object.entries(fieldAliases).forEach(([englishKey, koreanKey]) => {
|
||||
const targetFieldKey = fieldKeyMap[koreanKey];
|
||||
if (targetFieldKey && mappedData[targetFieldKey] === undefined && initialData[englishKey] !== undefined) {
|
||||
mappedData[targetFieldKey] = initialData[englishKey];
|
||||
console.log(`[DynamicItemForm] 별칭 fallback 매핑: ${englishKey} → ${koreanKey} → ${targetFieldKey}`);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('========== [DynamicItemForm] Edit 모드 데이터 매핑 ==========');
|
||||
console.log('specification 관련 키:', Object.keys(mappedData).filter(k => k.includes('specification') || k.includes('규격')));
|
||||
console.log('is_active 관련 키:', Object.keys(mappedData).filter(k => k.includes('active') || k.includes('상태')));
|
||||
console.log('매핑된 데이터:', mappedData);
|
||||
console.log('==============================================================');
|
||||
|
||||
// 변환된 데이터로 폼 리셋
|
||||
resetForm(mappedData);
|
||||
// field_key가 통일되었으므로 initialData를 그대로 사용
|
||||
// 기존 레거시 데이터(98_unit 형식)도 그대로 동작
|
||||
resetForm(initialData);
|
||||
setIsEditDataMapped(true);
|
||||
}, [mode, structure, initialData, isEditDataMapped, resetForm]);
|
||||
|
||||
@@ -1202,82 +1113,22 @@ export default function DynamicItemForm({
|
||||
return;
|
||||
}
|
||||
|
||||
// field_key → 백엔드 필드명 매핑
|
||||
// field_key 형식: "{id}_{key}" (예: "98_item_name", "110_품목명")
|
||||
// 백엔드 필드명으로 변환 필요
|
||||
// 2025-12-03: 한글 field_key 지원 추가
|
||||
const fieldKeyToBackendKey: Record<string, string> = {
|
||||
'item_name': 'name',
|
||||
'productName': 'name', // FG(제품) 품목명 필드
|
||||
'품목명': 'name', // 한글 field_key 지원
|
||||
'specification': 'spec',
|
||||
'standard': 'spec', // 규격 대체 필드명
|
||||
'규격': 'spec', // 한글 field_key 지원
|
||||
'사양': 'spec', // 한글 대체
|
||||
'unit': 'unit',
|
||||
'단위': 'unit', // 한글 field_key 지원
|
||||
'note': 'note',
|
||||
'비고': 'note', // 한글 field_key 지원
|
||||
'description': 'description',
|
||||
'설명': 'description', // 한글 field_key 지원
|
||||
'part_type': 'part_type',
|
||||
'부품유형': 'part_type', // 한글 field_key 지원
|
||||
'부품 유형': 'part_type', // 공백 포함 한글
|
||||
'is_active': 'is_active',
|
||||
'status': 'is_active',
|
||||
'active': 'is_active',
|
||||
'품목상태': 'is_active', // 한글 field_key 지원
|
||||
'품목 상태': 'is_active', // 공백 포함 한글
|
||||
'상태': 'is_active', // 짧은 한글
|
||||
};
|
||||
// 2025-12-09: field_key 통일로 변환 로직 제거
|
||||
// formData의 field_key가 백엔드 필드명과 일치하므로 직접 사용
|
||||
console.log('[DynamicItemForm] 저장 시 formData:', formData);
|
||||
|
||||
// formData를 백엔드 필드명으로 변환
|
||||
console.log('========== [DynamicItemForm] 저장 시 formData ==========');
|
||||
console.log('specification 관련:', Object.entries(formData).filter(([k]) => k.includes('specification') || k.includes('규격')));
|
||||
console.log('is_active 관련:', Object.entries(formData).filter(([k]) => k.includes('active') || k.includes('상태')));
|
||||
console.log('전체 formData:', formData);
|
||||
console.log('=========================================================');
|
||||
// is_active 필드만 boolean 변환 (드롭다운 값 → boolean)
|
||||
const convertedData: Record<string, any> = {};
|
||||
Object.entries(formData).forEach(([key, value]) => {
|
||||
// "{id}_{fieldKey}" 형식 체크: 숫자로 시작하고 _가 있는 경우
|
||||
// 예: "98_item_name" → true, "item_name" → false
|
||||
const isFieldKeyFormat = /^\d+_/.test(key);
|
||||
|
||||
if (isFieldKeyFormat) {
|
||||
// "{id}_{fieldKey}" 형식에서 fieldKey 추출
|
||||
const underscoreIndex = key.indexOf('_');
|
||||
const fieldKey = key.substring(underscoreIndex + 1);
|
||||
const backendKey = fieldKeyToBackendKey[fieldKey] || fieldKey;
|
||||
|
||||
// is_active 필드는 boolean으로 변환
|
||||
if (backendKey === 'is_active') {
|
||||
// "활성", true, "true", "1", 1 등을 true로, 나머지는 false로
|
||||
const isActive = value === true || value === 'true' || value === '1' ||
|
||||
value === 1 || value === '활성' || value === 'active';
|
||||
console.log(`[DynamicItemForm] is_active 변환: key=${key}, value=${value}(${typeof value}) → isActive=${isActive}`);
|
||||
convertedData[backendKey] = isActive;
|
||||
} else {
|
||||
convertedData[backendKey] = value;
|
||||
}
|
||||
if (key === 'is_active' || key.endsWith('_is_active')) {
|
||||
// "활성", true, "true", "1", 1 등을 true로, 나머지는 false로
|
||||
const isActive = value === true || value === 'true' || value === '1' ||
|
||||
value === 1 || value === '활성' || value === 'active';
|
||||
convertedData[key] = isActive;
|
||||
} else {
|
||||
// field_key 형식이 아닌 경우, 매핑 테이블에서 변환 시도
|
||||
const backendKey = fieldKeyToBackendKey[key] || key;
|
||||
|
||||
if (backendKey === 'is_active') {
|
||||
const isActive = value === true || value === 'true' || value === '1' ||
|
||||
value === 1 || value === '활성' || value === 'active';
|
||||
console.log(`[DynamicItemForm] is_active 변환 (non-field_key): key=${key}, value=${value}(${typeof value}) → isActive=${isActive}`);
|
||||
convertedData[backendKey] = isActive;
|
||||
} else {
|
||||
convertedData[backendKey] = value;
|
||||
}
|
||||
convertedData[key] = value;
|
||||
}
|
||||
});
|
||||
console.log('========== [DynamicItemForm] convertedData 결과 ==========');
|
||||
console.log('is_active:', convertedData.is_active);
|
||||
console.log('specification:', convertedData.spec || convertedData.specification);
|
||||
console.log('전체:', convertedData);
|
||||
console.log('===========================================================');
|
||||
|
||||
// 품목명 값 추출 (품목코드와 품목명 모두 필요)
|
||||
// 2025-12-04: 절곡 부품은 별도 품목명 필드(bendingFieldKeys.itemName) 사용
|
||||
@@ -1333,7 +1184,8 @@ export default function DynamicItemForm({
|
||||
}
|
||||
|
||||
// 품목 유형 및 BOM 데이터 추가
|
||||
const submitData: DynamicFormData = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const submitData = {
|
||||
...convertedData,
|
||||
// 백엔드 필드명 사용
|
||||
product_type: selectedItemType, // item_type → product_type
|
||||
@@ -1373,7 +1225,7 @@ export default function DynamicItemForm({
|
||||
...(selectedItemType === 'FG' && !convertedData.unit ? {
|
||||
unit: 'EA',
|
||||
} : {}),
|
||||
};
|
||||
} as DynamicFormData;
|
||||
|
||||
// console.log('[DynamicItemForm] 제출 데이터:', submitData);
|
||||
|
||||
@@ -1392,9 +1244,9 @@ export default function DynamicItemForm({
|
||||
console.log('[DynamicItemForm] 전개도 파일 업로드 시작:', bendingDiagramFile.name);
|
||||
await uploadItemFile(itemId, bendingDiagramFile, 'bending_diagram', {
|
||||
bendingDetails: bendingDetails.length > 0 ? bendingDetails.map(d => ({
|
||||
angle: d.angle || 0,
|
||||
length: d.width || 0,
|
||||
type: d.direction || '',
|
||||
angle: d.aAngle || 0,
|
||||
length: d.input || 0,
|
||||
type: d.shaded ? 'shaded' : 'normal',
|
||||
})) : undefined,
|
||||
});
|
||||
console.log('[DynamicItemForm] 전개도 파일 업로드 성공');
|
||||
@@ -1924,6 +1776,25 @@ export default function DynamicItemForm({
|
||||
onOpenChange={setIsDrawingOpen}
|
||||
onSave={(imageData) => {
|
||||
setBendingDiagram(imageData);
|
||||
|
||||
// Base64 string을 File 객체로 변환 (업로드용)
|
||||
// 2025-12-06: 드로잉 방식에서도 파일 업로드 지원
|
||||
try {
|
||||
const byteString = atob(imageData.split(',')[1]);
|
||||
const mimeType = imageData.split(',')[0].split(':')[1].split(';')[0];
|
||||
const arrayBuffer = new ArrayBuffer(byteString.length);
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
uint8Array[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
const blob = new Blob([uint8Array], { type: mimeType });
|
||||
const file = new File([blob], `bending_diagram_${Date.now()}.png`, { type: mimeType });
|
||||
setBendingDiagramFile(file);
|
||||
console.log('[DynamicItemForm] 드로잉 캔버스 → File 변환 성공:', file.name);
|
||||
} catch (error) {
|
||||
console.error('[DynamicItemForm] 드로잉 캔버스 → File 변환 실패:', error);
|
||||
}
|
||||
|
||||
setIsDrawingOpen(false);
|
||||
}}
|
||||
initialImage={bendingDiagram}
|
||||
|
||||
@@ -118,7 +118,7 @@ export interface BOMSearchState {
|
||||
/**
|
||||
* 동적 폼 필드 값 타입
|
||||
*/
|
||||
export type DynamicFieldValue = string | number | boolean | null | undefined | Record<string, unknown>[] | Record<string, unknown>;
|
||||
export type DynamicFieldValue = string | number | boolean | string[] | null | undefined | Record<string, unknown>[] | Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 동적 폼 데이터 (field_key를 key로 사용)
|
||||
|
||||
@@ -256,23 +256,20 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{item.category1 && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">품목명</Label>
|
||||
<p className="mt-1">
|
||||
<Badge variant="outline" className="bg-indigo-50 text-indigo-700">
|
||||
{item.category1 === 'guide_rail' ? '가이드레일' :
|
||||
item.category1 === 'case' ? '케이스' :
|
||||
item.category1 === 'bottom_finish' ? '하단마감재' :
|
||||
item.category1}
|
||||
</Badge>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{item.installationType && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">설치 유형</Label>
|
||||
<p className="mt-1">
|
||||
{/* 품목명 - itemName 표시 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground">품목명</Label>
|
||||
<p className="mt-1">
|
||||
<Badge variant="outline" className="bg-indigo-50 text-indigo-700">
|
||||
{item.itemName}
|
||||
</Badge>
|
||||
</p>
|
||||
</div>
|
||||
{/* 설치 유형 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground">설치 유형</Label>
|
||||
<p className="mt-1">
|
||||
{item.installationType ? (
|
||||
<Badge variant="outline" className="bg-green-50 text-green-700">
|
||||
{item.installationType === 'wall' ? '벽면형 (R)' :
|
||||
item.installationType === 'side' ? '측면형 (S)' :
|
||||
@@ -280,19 +277,41 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
item.installationType === 'iron' ? '철재 (T)' :
|
||||
item.installationType}
|
||||
</Badge>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{item.assemblyType && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">마감</Label>
|
||||
<p className="mt-1">
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{/* 마감 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground">마감</Label>
|
||||
<p className="mt-1">
|
||||
{item.assemblyType ? (
|
||||
<code className="text-sm bg-gray-100 px-2 py-1 rounded">
|
||||
{item.assemblyType}
|
||||
</code>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
) : (
|
||||
<span className="text-muted-foreground">-</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{/* 길이 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground">길이</Label>
|
||||
<p className="mt-1 font-medium">
|
||||
{item.assemblyLength || item.length ? `${item.assemblyLength || item.length}mm` : '-'}
|
||||
</p>
|
||||
</div>
|
||||
{/* 측면 규격 */}
|
||||
<div>
|
||||
<Label className="text-muted-foreground">측면 규격</Label>
|
||||
<p className="mt-1">
|
||||
{item.sideSpecWidth && item.sideSpecHeight
|
||||
? `${item.sideSpecWidth} × ${item.sideSpecHeight}mm`
|
||||
: '-'}
|
||||
</p>
|
||||
</div>
|
||||
{/* 재질 (있으면) */}
|
||||
{item.material && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">재질</Label>
|
||||
@@ -303,20 +322,6 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{item.assemblyLength && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">길이</Label>
|
||||
<p className="mt-1 font-medium">{item.assemblyLength}mm</p>
|
||||
</div>
|
||||
)}
|
||||
{item.sideSpecWidth && item.sideSpecHeight && (
|
||||
<div>
|
||||
<Label className="text-muted-foreground">측면 규격</Label>
|
||||
<p className="mt-1">
|
||||
{item.sideSpecWidth} × {item.sideSpecHeight}mm
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -355,10 +360,9 @@ export default function ItemDetailClient({ item }: ItemDetailClientProps) {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 절곡품/조립품 전개도 정보 */}
|
||||
{/* 절곡품/조립품 전개도 정보 - 조립부품은 항상 표시 */}
|
||||
{item.itemType === 'PT' &&
|
||||
(item.partType === 'BENDING' || item.partType === 'ASSEMBLY') &&
|
||||
(item.bendingDiagram || (item.bendingDetails && item.bendingDetails.length > 0)) && (
|
||||
(item.partType === 'BENDING' || item.partType === 'ASSEMBLY') && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base md:text-lg">
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function PartForm({
|
||||
value={selectedPartType}
|
||||
onValueChange={handlePartTypeChange}
|
||||
>
|
||||
<SelectTrigger className={errors.partType ? 'border-red-500' : ''}>
|
||||
<SelectTrigger className={(errors as any).partType ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="부품 유형을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -181,12 +181,12 @@ export default function PartForm({
|
||||
<SelectItem value="PURCHASED">구매 부품 (Purchased Part)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.partType && (
|
||||
{(errors as any).partType && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.partType.message}
|
||||
{(errors as any).partType.message}
|
||||
</p>
|
||||
)}
|
||||
{!errors.partType && selectedPartType === 'BENDING' && (
|
||||
{!(errors as any).partType && selectedPartType === 'BENDING' && (
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
* 절곡 부품은 전개도(바라시)만 있으며, 부품 구성(BOM)은 사용하지 않습니다.
|
||||
</p>
|
||||
|
||||
@@ -18,6 +18,11 @@ import { X } from 'lucide-react';
|
||||
import type { UseFormRegister, UseFormSetValue, UseFormGetValues, FieldErrors } from 'react-hook-form';
|
||||
import type { CreateItemFormData } from '@/lib/utils/validation';
|
||||
|
||||
// ProductForm에서 사용하는 확장 타입 (productName 포함)
|
||||
type ProductFormErrors = FieldErrors<CreateItemFormData> & {
|
||||
productName?: { message?: string };
|
||||
};
|
||||
|
||||
interface ProductFormProps {
|
||||
productName: string;
|
||||
setProductName: (value: string) => void;
|
||||
@@ -35,7 +40,7 @@ interface ProductFormProps {
|
||||
register: UseFormRegister<CreateItemFormData>;
|
||||
setValue: UseFormSetValue<CreateItemFormData>;
|
||||
getValues: UseFormGetValues<CreateItemFormData>;
|
||||
errors: FieldErrors<CreateItemFormData>;
|
||||
errors: ProductFormErrors;
|
||||
}
|
||||
|
||||
export default function ProductForm({
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function BendingPartForm({
|
||||
setValue('material', value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={errors.material ? 'border-red-500' : ''}>
|
||||
<SelectTrigger className={(errors as any).material ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="재질을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -144,9 +144,9 @@ export default function BendingPartForm({
|
||||
<SelectItem value="SUS 1.5T">SUS 1.5T</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.material && (
|
||||
{(errors as any).material && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.material.message}
|
||||
{(errors as any).material.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -165,16 +165,16 @@ export default function BendingPartForm({
|
||||
}}
|
||||
placeholder="전개도 상세를 입력해주세요"
|
||||
readOnly={bendingDetailsLength > 0}
|
||||
className={`${bendingDetailsLength > 0 ? "bg-blue-50 font-medium" : ""} ${errors.length ? 'border-red-500' : ''}`}
|
||||
className={`${bendingDetailsLength > 0 ? "bg-blue-50 font-medium" : ""} ${(errors as any).length ? 'border-red-500' : ''}`}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">mm</span>
|
||||
</div>
|
||||
{errors.length && (
|
||||
{(errors as any).length && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.length.message}
|
||||
{(errors as any).length.message}
|
||||
</p>
|
||||
)}
|
||||
{!errors.length && bendingDetailsLength > 0 && (
|
||||
{!(errors as any).length && bendingDetailsLength > 0 && (
|
||||
<p className="text-xs text-blue-600 mt-1">
|
||||
* 전개도 상세 입력의 합계가 자동 반영됩니다
|
||||
</p>
|
||||
@@ -192,7 +192,7 @@ export default function BendingPartForm({
|
||||
setValue('bendingLength', value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={errors.bendingLength ? 'border-red-500' : ''}>
|
||||
<SelectTrigger className={(errors as any).bendingLength ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="모양&길이를 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -210,9 +210,9 @@ export default function BendingPartForm({
|
||||
<SelectItem value="4300">4300mm</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.bendingLength && (
|
||||
{(errors as any).bendingLength && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.bendingLength.message}
|
||||
{(errors as any).bendingLength.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -119,7 +119,7 @@ export default function PurchasedPartForm({
|
||||
setValue('electricOpenerPower', value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={errors.electricOpenerPower ? 'border-red-500' : ''}>
|
||||
<SelectTrigger className={(errors as any).electricOpenerPower ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="전원을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -127,9 +127,9 @@ export default function PurchasedPartForm({
|
||||
<SelectItem value="380V">380V</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.electricOpenerPower && (
|
||||
{(errors as any).electricOpenerPower && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.electricOpenerPower.message}
|
||||
{(errors as any).electricOpenerPower.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -144,7 +144,7 @@ export default function PurchasedPartForm({
|
||||
setValue('electricOpenerCapacity', value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={errors.electricOpenerCapacity ? 'border-red-500' : ''}>
|
||||
<SelectTrigger className={(errors as any).electricOpenerCapacity ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="용량을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -157,9 +157,9 @@ export default function PurchasedPartForm({
|
||||
<SelectItem value="1000">1000 KG</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.electricOpenerCapacity && (
|
||||
{(errors as any).electricOpenerCapacity && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.electricOpenerCapacity.message}
|
||||
{(errors as any).electricOpenerCapacity.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -182,7 +182,7 @@ export default function PurchasedPartForm({
|
||||
setValue('motorVoltage', value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={errors.motorVoltage ? 'border-red-500' : ''}>
|
||||
<SelectTrigger className={(errors as any).motorVoltage ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="전압을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -190,9 +190,9 @@ export default function PurchasedPartForm({
|
||||
<SelectItem value="380">380V</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.motorVoltage && (
|
||||
{(errors as any).motorVoltage && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.motorVoltage.message}
|
||||
{(errors as any).motorVoltage.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -211,7 +211,7 @@ export default function PurchasedPartForm({
|
||||
setValue('chainSpec', value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className={errors.chainSpec ? 'border-red-500' : ''}>
|
||||
<SelectTrigger className={(errors as any).chainSpec ? 'border-red-500' : ''}>
|
||||
<SelectValue placeholder="규격을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -221,9 +221,9 @@ export default function PurchasedPartForm({
|
||||
<SelectItem value="80">80</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.chainSpec && (
|
||||
{(errors as any).chainSpec && (
|
||||
<p className="text-xs text-red-500 mt-1">
|
||||
{errors.chainSpec.message}
|
||||
{(errors as any).chainSpec.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -194,7 +194,6 @@ export default function ItemListClient() {
|
||||
console.log('[Delete] 응답:', { status: response.status, result });
|
||||
|
||||
if (response.ok && result.success) {
|
||||
alert('품목이 삭제되었습니다.');
|
||||
refresh();
|
||||
} else {
|
||||
throw new Error(result.message || '삭제에 실패했습니다.');
|
||||
|
||||
Reference in New Issue
Block a user