feat: Introduce new date picker components for enhanced date selection
- Added `FormDatePicker` and `InlineCellDatePicker` components to provide flexible date selection options. - Implemented a modernized date picker interface with calendar navigation, year selection, and time input capabilities. - Enhanced `DateWidget` to support both date and datetime formats, improving user experience in date handling. - Updated `CategoryColumnList` to group columns dynamically and manage expanded states for better organization. - Improved `AlertDialog` z-index for better visibility in modal interactions. - Refactored `ScreenModal` to ensure consistent modal behavior across the application.
This commit is contained in:
@@ -105,6 +105,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
const [forceUpdate, setForceUpdate] = useState(0);
|
||||
const [representativeImageUrl, setRepresentativeImageUrl] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
// objid 기반으로 파일이 로드되었는지 추적 (다른 이펙트가 덮어쓰지 않도록 방지)
|
||||
const filesLoadedFromObjidRef = useRef(false);
|
||||
|
||||
// 🔑 레코드 모드 판단: formData에 id가 있으면 특정 행에 연결된 파일 관리
|
||||
const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_'));
|
||||
@@ -150,6 +152,7 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
if (isRecordMode || !recordId) {
|
||||
setUploadedFiles([]);
|
||||
setRepresentativeImageUrl(null);
|
||||
filesLoadedFromObjidRef.current = false;
|
||||
}
|
||||
} else if (prevIsRecordModeRef.current === null) {
|
||||
// 초기 마운트 시 모드 저장
|
||||
@@ -191,63 +194,68 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
}, [component.id, getUniqueKey, recordId, isRecordMode]); // 레코드별 고유 키 변경 시 재실행
|
||||
|
||||
// 🔑 수정 모드: formData[columnName]에 저장된 objid로 이미지 로드
|
||||
// 🆕 formData 전체가 아닌 특정 컬럼 값만 의존하도록 수정 (다른 컴포넌트 영향 방지)
|
||||
// 콤마로 구분된 다중 objid도 처리 (예: "123,456")
|
||||
const imageObjidFromFormData = formData?.[columnName];
|
||||
|
||||
useEffect(() => {
|
||||
// 이미지 objid가 있고, 숫자 문자열인 경우에만 처리
|
||||
if (imageObjidFromFormData && /^\d+$/.test(String(imageObjidFromFormData))) {
|
||||
const objidStr = String(imageObjidFromFormData);
|
||||
|
||||
// 이미 같은 objid의 파일이 로드되어 있으면 스킵
|
||||
const alreadyLoaded = uploadedFiles.some(f => String(f.objid) === objidStr);
|
||||
if (alreadyLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔑 실제 파일 정보 조회 (previewUrl 제거 - apiClient blob 다운로드 방식으로 통일)
|
||||
(async () => {
|
||||
try {
|
||||
const fileInfoResponse = await getFileInfoByObjid(objidStr);
|
||||
if (!imageObjidFromFormData) return;
|
||||
|
||||
const rawValue = String(imageObjidFromFormData);
|
||||
// 콤마 구분 다중 objid 또는 단일 objid 모두 처리
|
||||
const objids = rawValue.split(',').map(s => s.trim()).filter(s => /^\d+$/.test(s));
|
||||
|
||||
if (objids.length === 0) return;
|
||||
|
||||
// 모든 objid가 이미 로드되어 있으면 스킵
|
||||
const allLoaded = objids.every(id => uploadedFiles.some(f => String(f.objid) === id));
|
||||
if (allLoaded) return;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const loadedFiles: FileInfo[] = [];
|
||||
|
||||
for (const objid of objids) {
|
||||
// 이미 로드된 파일은 스킵
|
||||
if (uploadedFiles.some(f => String(f.objid) === objid)) continue;
|
||||
|
||||
const fileInfoResponse = await getFileInfoByObjid(objid);
|
||||
|
||||
if (fileInfoResponse.success && fileInfoResponse.data) {
|
||||
const { realFileName, fileSize, fileExt, regdate, isRepresentative } = fileInfoResponse.data;
|
||||
|
||||
const fileInfo = {
|
||||
objid: objidStr,
|
||||
realFileName: realFileName,
|
||||
fileExt: fileExt,
|
||||
fileSize: fileSize,
|
||||
filePath: getFilePreviewUrl(objidStr),
|
||||
regdate: regdate,
|
||||
loadedFiles.push({
|
||||
objid,
|
||||
realFileName,
|
||||
fileExt,
|
||||
fileSize,
|
||||
filePath: getFilePreviewUrl(objid),
|
||||
regdate,
|
||||
isImage: true,
|
||||
isRepresentative: isRepresentative,
|
||||
};
|
||||
|
||||
setUploadedFiles([fileInfo]);
|
||||
// representativeImageUrl은 loadRepresentativeImage에서 blob으로 로드됨
|
||||
isRepresentative,
|
||||
} as FileInfo);
|
||||
} else {
|
||||
// 파일 정보 조회 실패 시 최소 정보로 추가
|
||||
console.warn("🖼️ [FileUploadComponent] 파일 정보 조회 실패, 최소 정보 사용");
|
||||
const minimalFileInfo = {
|
||||
objid: objidStr,
|
||||
realFileName: `image_${objidStr}.jpg`,
|
||||
loadedFiles.push({
|
||||
objid,
|
||||
realFileName: `file_${objid}`,
|
||||
fileExt: '.jpg',
|
||||
fileSize: 0,
|
||||
filePath: getFilePreviewUrl(objidStr),
|
||||
filePath: getFilePreviewUrl(objid),
|
||||
regdate: new Date().toISOString(),
|
||||
isImage: true,
|
||||
};
|
||||
|
||||
setUploadedFiles([minimalFileInfo]);
|
||||
// representativeImageUrl은 loadRepresentativeImage에서 blob으로 로드됨
|
||||
} as FileInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🖼️ [FileUploadComponent] 파일 정보 조회 오류:", error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [imageObjidFromFormData, columnName, component.id]); // 🆕 formData 대신 특정 컬럼 값만 의존
|
||||
|
||||
if (loadedFiles.length > 0) {
|
||||
setUploadedFiles(loadedFiles);
|
||||
filesLoadedFromObjidRef.current = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("🖼️ [FileUploadComponent] 파일 정보 조회 오류:", error);
|
||||
}
|
||||
})();
|
||||
}, [imageObjidFromFormData, columnName, component.id]);
|
||||
|
||||
// 🎯 화면설계 모드에서 실제 화면으로의 실시간 동기화 이벤트 리스너
|
||||
// 🆕 columnName도 체크하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분
|
||||
@@ -365,6 +373,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
...file,
|
||||
}));
|
||||
|
||||
// 서버에서 0개 반환 + objid 기반 로딩이 이미 완료된 경우 덮어쓰지 않음
|
||||
if (formattedFiles.length === 0 && filesLoadedFromObjidRef.current) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 🔄 localStorage의 기존 파일과 서버 파일 병합 (레코드별 고유 키 사용)
|
||||
let finalFiles = formattedFiles;
|
||||
@@ -427,14 +439,19 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
return; // DB 로드 성공 시 localStorage 무시
|
||||
}
|
||||
|
||||
// 🆕 등록 모드(새 레코드)인 경우 fallback 로드도 스킵 - 항상 빈 상태 유지
|
||||
// objid 기반으로 이미 파일이 로드된 경우 빈 데이터로 덮어쓰지 않음
|
||||
if (filesLoadedFromObjidRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 등록 모드(새 레코드)인 경우 fallback 로드도 스킵 - 항상 빈 상태 유지
|
||||
if (!isRecordMode || !recordId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// DB 로드 실패 시에만 기존 로직 사용 (하위 호환성)
|
||||
|
||||
// 전역 상태에서 최신 파일 정보 가져오기 (🆕 고유 키 사용)
|
||||
// 전역 상태에서 최신 파일 정보 가져오기 (고유 키 사용)
|
||||
const globalFileState = typeof window !== "undefined" ? (window as any).globalFileState || {} : {};
|
||||
const uniqueKeyForFallback = getUniqueKey();
|
||||
const globalFiles = globalFileState[uniqueKeyForFallback] || globalFileState[component.id] || [];
|
||||
@@ -442,6 +459,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
// 최신 파일 정보 사용 (전역 상태 > 컴포넌트 속성)
|
||||
const currentFiles = globalFiles.length > 0 ? globalFiles : componentFiles;
|
||||
|
||||
// 빈 데이터로 기존 파일을 덮어쓰지 않음
|
||||
if (currentFiles.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 최신 파일과 현재 파일 비교
|
||||
if (JSON.stringify(currentFiles) !== JSON.stringify(uploadedFiles)) {
|
||||
@@ -1147,8 +1168,8 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
||||
file={viewerFile}
|
||||
isOpen={isViewerOpen}
|
||||
onClose={handleViewerClose}
|
||||
onDownload={handleFileDownload}
|
||||
onDelete={!isDesignMode ? handleFileDelete : undefined}
|
||||
onDownload={safeComponentConfig.allowDownload !== false ? handleFileDownload : undefined}
|
||||
onDelete={!isDesignMode && safeComponentConfig.allowDelete !== false ? handleFileDelete : undefined}
|
||||
/>
|
||||
|
||||
{/* 파일 관리 모달 */}
|
||||
|
||||
Reference in New Issue
Block a user