|
|
|
|
@@ -8,7 +8,7 @@
|
|
|
|
|
|
|
|
|
|
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
import { Package, Save, X } from 'lucide-react';
|
|
|
|
|
import { Package, Save, X, FileText, Trash2, Download } from 'lucide-react';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import { Input } from '@/components/ui/input';
|
|
|
|
|
@@ -34,9 +34,10 @@ import {
|
|
|
|
|
generateBendingItemCodeSimple,
|
|
|
|
|
generatePurchasedItemCode,
|
|
|
|
|
} from './utils/itemCodeGenerator';
|
|
|
|
|
import type { DynamicItemFormProps, DynamicFormData, DynamicSection, DynamicFieldValue, BOMLine, BOMSearchState } from './types';
|
|
|
|
|
import type { DynamicItemFormProps, DynamicFormData, DynamicSection, DynamicFieldValue, BOMLine, BOMSearchState, ItemSaveResult } from './types';
|
|
|
|
|
import type { ItemType, BendingDetail } from '@/types/item';
|
|
|
|
|
import type { ItemFieldResponse } from '@/types/item-master-api';
|
|
|
|
|
import { uploadItemFile, deleteItemFile, ItemFileType } from '@/lib/api/items';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 헤더 컴포넌트 - 기존 FormHeader와 동일한 디자인
|
|
|
|
|
@@ -220,6 +221,7 @@ function DynamicSectionRenderer({
|
|
|
|
|
export default function DynamicItemForm({
|
|
|
|
|
mode,
|
|
|
|
|
itemType: initialItemType,
|
|
|
|
|
itemId: propItemId,
|
|
|
|
|
initialData,
|
|
|
|
|
onSubmit,
|
|
|
|
|
}: DynamicItemFormProps) {
|
|
|
|
|
@@ -260,6 +262,75 @@ export default function DynamicItemForm({
|
|
|
|
|
const [specificationFile, setSpecificationFile] = useState<File | null>(null);
|
|
|
|
|
const [certificationFile, setCertificationFile] = useState<File | null>(null);
|
|
|
|
|
|
|
|
|
|
// 기존 파일 URL 상태 (edit 모드에서 사용)
|
|
|
|
|
const [existingBendingDiagram, setExistingBendingDiagram] = useState<string>('');
|
|
|
|
|
const [existingSpecificationFile, setExistingSpecificationFile] = useState<string>('');
|
|
|
|
|
const [existingSpecificationFileName, setExistingSpecificationFileName] = useState<string>('');
|
|
|
|
|
const [existingCertificationFile, setExistingCertificationFile] = useState<string>('');
|
|
|
|
|
const [existingCertificationFileName, setExistingCertificationFileName] = useState<string>('');
|
|
|
|
|
const [isDeletingFile, setIsDeletingFile] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
// initialData에서 기존 파일 정보 로드 (edit 모드)
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode === 'edit' && initialData) {
|
|
|
|
|
if (initialData.bending_diagram) {
|
|
|
|
|
setExistingBendingDiagram(initialData.bending_diagram as string);
|
|
|
|
|
}
|
|
|
|
|
if (initialData.specification_file) {
|
|
|
|
|
setExistingSpecificationFile(initialData.specification_file as string);
|
|
|
|
|
setExistingSpecificationFileName((initialData.specification_file_name as string) || '시방서');
|
|
|
|
|
}
|
|
|
|
|
if (initialData.certification_file) {
|
|
|
|
|
setExistingCertificationFile(initialData.certification_file as string);
|
|
|
|
|
setExistingCertificationFileName((initialData.certification_file_name as string) || '인정서');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}, [mode, initialData]);
|
|
|
|
|
|
|
|
|
|
// Storage 경로를 전체 URL로 변환
|
|
|
|
|
const getStorageUrl = (path: string | undefined): string | null => {
|
|
|
|
|
if (!path) return null;
|
|
|
|
|
if (path.startsWith('http://') || path.startsWith('https://')) {
|
|
|
|
|
return path;
|
|
|
|
|
}
|
|
|
|
|
const apiUrl = process.env.NEXT_PUBLIC_API_URL || '';
|
|
|
|
|
return `${apiUrl}/storage/${path}`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 파일 삭제 핸들러
|
|
|
|
|
const handleDeleteFile = async (fileType: ItemFileType) => {
|
|
|
|
|
if (!propItemId) return;
|
|
|
|
|
|
|
|
|
|
const confirmMessage = fileType === 'bending_diagram' ? '전개도 이미지를' :
|
|
|
|
|
fileType === 'specification' ? '시방서 파일을' : '인정서 파일을';
|
|
|
|
|
|
|
|
|
|
if (!confirm(`${confirmMessage} 삭제하시겠습니까?`)) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
setIsDeletingFile(fileType);
|
|
|
|
|
await deleteItemFile(propItemId, fileType);
|
|
|
|
|
|
|
|
|
|
// 상태 업데이트
|
|
|
|
|
if (fileType === 'bending_diagram') {
|
|
|
|
|
setExistingBendingDiagram('');
|
|
|
|
|
setBendingDiagram('');
|
|
|
|
|
} else if (fileType === 'specification') {
|
|
|
|
|
setExistingSpecificationFile('');
|
|
|
|
|
setExistingSpecificationFileName('');
|
|
|
|
|
} else if (fileType === 'certification') {
|
|
|
|
|
setExistingCertificationFile('');
|
|
|
|
|
setExistingCertificationFileName('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
alert('파일이 삭제되었습니다.');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[DynamicItemForm] 파일 삭제 실패:', error);
|
|
|
|
|
alert('파일 삭제에 실패했습니다.');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsDeletingFile(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// 조건부 표시 관리
|
|
|
|
|
const { shouldShowSection, shouldShowField } = useConditionalDisplay(structure, formData);
|
|
|
|
|
|
|
|
|
|
@@ -332,9 +403,18 @@ export default function DynamicItemForm({
|
|
|
|
|
const [isEditDataMapped, setIsEditDataMapped] = useState(false);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (mode !== 'edit' || !structure || !initialData || isEditDataMapped) return;
|
|
|
|
|
if (mode !== 'edit' || !structure || !initialData) return;
|
|
|
|
|
|
|
|
|
|
// console.log('[DynamicItemForm] Edit mode: mapping initialData to field_key format');
|
|
|
|
|
// 이미 매핑된 데이터가 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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('[DynamicItemForm] Edit mode: mapping initialData to field_key format');
|
|
|
|
|
console.log('[DynamicItemForm] initialData:', initialData);
|
|
|
|
|
|
|
|
|
|
// initialData의 간단한 키를 structure의 field_key로 매핑
|
|
|
|
|
// 예: { item_name: '테스트' } → { '98_item_name': '테스트' }
|
|
|
|
|
@@ -353,6 +433,17 @@ export default function DynamicItemForm({
|
|
|
|
|
// 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.sections.forEach((section) => {
|
|
|
|
|
section.fields.forEach((f) => {
|
|
|
|
|
const field = f.field;
|
|
|
|
|
@@ -378,7 +469,7 @@ export default function DynamicItemForm({
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// console.log('[DynamicItemForm] fieldKeyMap:', fieldKeyMap);
|
|
|
|
|
console.log('[DynamicItemForm] fieldKeyMap:', fieldKeyMap);
|
|
|
|
|
|
|
|
|
|
// initialData를 field_key 형식으로 변환
|
|
|
|
|
Object.entries(initialData).forEach(([key, value]) => {
|
|
|
|
|
@@ -390,13 +481,41 @@ export default function DynamicItemForm({
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// console.log('[DynamicItemForm] Mapped initialData:', mappedData);
|
|
|
|
|
// 추가: 폼 구조의 모든 필드를 순회하면서, 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);
|
|
|
|
|
@@ -1113,7 +1232,11 @@ export default function DynamicItemForm({
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// formData를 백엔드 필드명으로 변환
|
|
|
|
|
// console.log('[DynamicItemForm] formData before conversion:', 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('=========================================================');
|
|
|
|
|
const convertedData: Record<string, any> = {};
|
|
|
|
|
Object.entries(formData).forEach(([key, value]) => {
|
|
|
|
|
// "{id}_{fieldKey}" 형식 체크: 숫자로 시작하고 _가 있는 경우
|
|
|
|
|
@@ -1131,6 +1254,7 @@ export default function DynamicItemForm({
|
|
|
|
|
// "활성", 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;
|
|
|
|
|
@@ -1142,13 +1266,18 @@ export default function DynamicItemForm({
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
// console.log('[DynamicItemForm] convertedData after conversion:', convertedData);
|
|
|
|
|
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) 사용
|
|
|
|
|
@@ -1249,7 +1378,79 @@ export default function DynamicItemForm({
|
|
|
|
|
// console.log('[DynamicItemForm] 제출 데이터:', submitData);
|
|
|
|
|
|
|
|
|
|
await handleSubmit(async () => {
|
|
|
|
|
await onSubmit(submitData);
|
|
|
|
|
// 품목 저장 (ID 반환)
|
|
|
|
|
const result = await onSubmit(submitData);
|
|
|
|
|
const itemId = result?.id;
|
|
|
|
|
|
|
|
|
|
// 파일 업로드 (품목 ID가 있을 때만)
|
|
|
|
|
if (itemId) {
|
|
|
|
|
const fileUploadErrors: string[] = [];
|
|
|
|
|
|
|
|
|
|
// PT (절곡/조립) 전개도 이미지 업로드
|
|
|
|
|
if (selectedItemType === 'PT' && (isBendingPart || isAssemblyPart) && bendingDiagramFile) {
|
|
|
|
|
try {
|
|
|
|
|
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 || '',
|
|
|
|
|
})) : undefined,
|
|
|
|
|
});
|
|
|
|
|
console.log('[DynamicItemForm] 전개도 파일 업로드 성공');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[DynamicItemForm] 전개도 파일 업로드 실패:', error);
|
|
|
|
|
fileUploadErrors.push('전개도 이미지');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FG (제품) 시방서 업로드
|
|
|
|
|
if (selectedItemType === 'FG' && specificationFile) {
|
|
|
|
|
try {
|
|
|
|
|
console.log('[DynamicItemForm] 시방서 파일 업로드 시작:', specificationFile.name);
|
|
|
|
|
await uploadItemFile(itemId, specificationFile, 'specification');
|
|
|
|
|
console.log('[DynamicItemForm] 시방서 파일 업로드 성공');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[DynamicItemForm] 시방서 파일 업로드 실패:', error);
|
|
|
|
|
fileUploadErrors.push('시방서');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FG (제품) 인정서 업로드
|
|
|
|
|
if (selectedItemType === 'FG' && certificationFile) {
|
|
|
|
|
try {
|
|
|
|
|
console.log('[DynamicItemForm] 인정서 파일 업로드 시작:', certificationFile.name);
|
|
|
|
|
// formData에서 인정서 관련 필드 추출
|
|
|
|
|
const certNumber = Object.entries(formData).find(([key]) =>
|
|
|
|
|
key.includes('certification_number') || key.includes('인정번호')
|
|
|
|
|
)?.[1] as string | undefined;
|
|
|
|
|
const certStartDate = Object.entries(formData).find(([key]) =>
|
|
|
|
|
key.includes('certification_start') || key.includes('인정_유효기간_시작')
|
|
|
|
|
)?.[1] as string | undefined;
|
|
|
|
|
const certEndDate = Object.entries(formData).find(([key]) =>
|
|
|
|
|
key.includes('certification_end') || key.includes('인정_유효기간_종료')
|
|
|
|
|
)?.[1] as string | undefined;
|
|
|
|
|
|
|
|
|
|
await uploadItemFile(itemId, certificationFile, 'certification', {
|
|
|
|
|
certificationNumber: certNumber,
|
|
|
|
|
certificationStartDate: certStartDate,
|
|
|
|
|
certificationEndDate: certEndDate,
|
|
|
|
|
});
|
|
|
|
|
console.log('[DynamicItemForm] 인정서 파일 업로드 성공');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[DynamicItemForm] 인정서 파일 업로드 실패:', error);
|
|
|
|
|
fileUploadErrors.push('인정서');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파일 업로드 실패 경고 (품목은 저장됨)
|
|
|
|
|
if (fileUploadErrors.length > 0) {
|
|
|
|
|
console.warn('[DynamicItemForm] 일부 파일 업로드 실패:', fileUploadErrors.join(', '));
|
|
|
|
|
// 품목은 저장되었으므로 경고만 표시하고 진행
|
|
|
|
|
alert(`품목이 저장되었습니다.\n\n일부 파일 업로드에 실패했습니다: ${fileUploadErrors.join(', ')}\n수정 화면에서 다시 업로드해 주세요.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
router.push('/items');
|
|
|
|
|
router.refresh();
|
|
|
|
|
});
|
|
|
|
|
@@ -1484,10 +1685,36 @@ export default function DynamicItemForm({
|
|
|
|
|
{/* FG(제품) 전용: 인정 유효기간 종료일 다음에 시방서/인정서 파일 업로드 */}
|
|
|
|
|
{isCertEndDateField && selectedItemType === 'FG' && (
|
|
|
|
|
<div className="mt-4 space-y-4">
|
|
|
|
|
{/* 시방서 파일 업로드 */}
|
|
|
|
|
{/* 시방서 파일 */}
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="specification_file">시방서 (PDF)</Label>
|
|
|
|
|
<div className="mt-1.5">
|
|
|
|
|
<div className="mt-1.5 space-y-2">
|
|
|
|
|
{/* 기존 파일 표시 (edit 모드) */}
|
|
|
|
|
{mode === 'edit' && existingSpecificationFile && !specificationFile && (
|
|
|
|
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded border">
|
|
|
|
|
<FileText className="h-4 w-4 text-blue-600" />
|
|
|
|
|
<span className="text-sm flex-1 truncate">{existingSpecificationFileName}</span>
|
|
|
|
|
<a
|
|
|
|
|
href={getStorageUrl(existingSpecificationFile) || '#'}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="inline-flex items-center gap-1 text-sm text-blue-600 hover:text-blue-800"
|
|
|
|
|
>
|
|
|
|
|
<Download className="h-3.5 w-3.5" />
|
|
|
|
|
</a>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => handleDeleteFile('specification')}
|
|
|
|
|
disabled={isDeletingFile === 'specification' || isSubmitting}
|
|
|
|
|
className="h-7 w-7 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{/* 새 파일 업로드 */}
|
|
|
|
|
<Input
|
|
|
|
|
id="specification_file"
|
|
|
|
|
type="file"
|
|
|
|
|
@@ -1500,16 +1727,42 @@ export default function DynamicItemForm({
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
/>
|
|
|
|
|
{specificationFile && (
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
선택된 파일: {specificationFile.name}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* 인정서 파일 업로드 */}
|
|
|
|
|
{/* 인정서 파일 */}
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="certification_file">인정서 (PDF)</Label>
|
|
|
|
|
<div className="mt-1.5">
|
|
|
|
|
<div className="mt-1.5 space-y-2">
|
|
|
|
|
{/* 기존 파일 표시 (edit 모드) */}
|
|
|
|
|
{mode === 'edit' && existingCertificationFile && !certificationFile && (
|
|
|
|
|
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded border">
|
|
|
|
|
<FileText className="h-4 w-4 text-green-600" />
|
|
|
|
|
<span className="text-sm flex-1 truncate">{existingCertificationFileName}</span>
|
|
|
|
|
<a
|
|
|
|
|
href={getStorageUrl(existingCertificationFile) || '#'}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="inline-flex items-center gap-1 text-sm text-green-600 hover:text-green-800"
|
|
|
|
|
>
|
|
|
|
|
<Download className="h-3.5 w-3.5" />
|
|
|
|
|
</a>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => handleDeleteFile('certification')}
|
|
|
|
|
disabled={isDeletingFile === 'certification' || isSubmitting}
|
|
|
|
|
className="h-7 w-7 p-0 text-red-500 hover:text-red-700 hover:bg-red-50"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{/* 새 파일 업로드 */}
|
|
|
|
|
<Input
|
|
|
|
|
id="certification_file"
|
|
|
|
|
type="file"
|
|
|
|
|
@@ -1522,7 +1775,7 @@ export default function DynamicItemForm({
|
|
|
|
|
className="cursor-pointer"
|
|
|
|
|
/>
|
|
|
|
|
{certificationFile && (
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
선택된 파일: {certificationFile.name}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|