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:
kjs
2026-02-26 17:32:20 +09:00
parent 27be48464a
commit 17d4cc297c
22 changed files with 2001 additions and 461 deletions

View File

@@ -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}
/>
{/* 파일 관리 모달 */}