feat: Enhance input and select components with custom styling support
- Added support for custom border, background, and text styles in V2Input and V2Select components, allowing for greater flexibility in styling based on user-defined configurations. - Updated the input and select components to conditionally apply styles based on the presence of custom properties, improving the overall user experience and visual consistency. - Refactored the FileUploadComponent to handle chunked file uploads, enhancing the file upload process by allowing multiple files to be uploaded in batches, improving performance and user feedback during uploads.
This commit is contained in:
parent
308f05ca07
commit
eac2fa63b1
|
|
@ -55,7 +55,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-10 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-10 w-full min-w-0 rounded-md border bg-transparent px-3 py-2 text-sm transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
|
|
|
|||
|
|
@ -947,6 +947,21 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
|||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2; // 라벨 높이 + 여백
|
||||
|
||||
// 🔧 커스텀 스타일 감지 (StyleEditor에서 설정한 테두리/배경/텍스트 스타일)
|
||||
// RealtimePreview 래퍼가 외부 div에 스타일을 적용하지만,
|
||||
// 내부 input/textarea가 자체 Tailwind 테두리를 가지므로 이를 제거하여 외부 스타일이 보이도록 함
|
||||
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
||||
const hasCustomBackground = !!style?.backgroundColor;
|
||||
const hasCustomRadius = !!style?.borderRadius;
|
||||
|
||||
// 텍스트 스타일 오버라이드 (CSS 상속으로 내부 input에 전달)
|
||||
const customTextStyle: React.CSSProperties = {};
|
||||
if (style?.color) customTextStyle.color = style.color;
|
||||
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
||||
if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight;
|
||||
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
||||
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -975,7 +990,18 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
|||
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||
</Label>
|
||||
)}
|
||||
<div className="h-full w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-full",
|
||||
// 커스텀 테두리 설정 시, 내부 input/textarea의 기본 테두리 제거 (외부 래퍼 스타일이 보이도록)
|
||||
hasCustomBorder && "[&_input]:border-0! [&_textarea]:border-0! [&_.border]:border-0!",
|
||||
// 커스텀 모서리 설정 시, 내부 요소의 기본 모서리 제거 (외부 래퍼가 처리)
|
||||
(hasCustomBorder || hasCustomRadius) && "[&_input]:rounded-none! [&_textarea]:rounded-none! [&_.rounded-md]:rounded-none!",
|
||||
// 커스텀 배경 설정 시, 내부 input을 투명하게 (외부 배경이 보이도록)
|
||||
hasCustomBackground && "[&_input]:bg-transparent! [&_textarea]:bg-transparent!",
|
||||
)}
|
||||
style={hasCustomText ? customTextStyle : undefined}
|
||||
>
|
||||
{renderInput()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -947,6 +947,19 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
|||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||
|
||||
// 🔧 커스텀 스타일 감지 (StyleEditor에서 설정한 테두리/배경/텍스트 스타일)
|
||||
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
||||
const hasCustomBackground = !!style?.backgroundColor;
|
||||
const hasCustomRadius = !!style?.borderRadius;
|
||||
|
||||
// 텍스트 스타일 오버라이드 (CSS 상속)
|
||||
const customTextStyle: React.CSSProperties = {};
|
||||
if (style?.color) customTextStyle.color = style.color;
|
||||
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
||||
if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight;
|
||||
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
||||
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
|
|
@ -975,7 +988,18 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
|||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||
</Label>
|
||||
)}
|
||||
<div className="h-full w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"h-full w-full",
|
||||
// 커스텀 테두리 설정 시, 내부 select trigger의 기본 테두리 제거
|
||||
hasCustomBorder && "[&_button]:border-0! **:data-[slot=select-trigger]:border-0! [&_.border]:border-0!",
|
||||
// 커스텀 모서리 설정 시, 내부 요소의 기본 모서리 제거
|
||||
(hasCustomBorder || hasCustomRadius) && "[&_button]:rounded-none! **:data-[slot=select-trigger]:rounded-none! [&_.rounded-md]:rounded-none!",
|
||||
// 커스텀 배경 설정 시, 내부 요소를 투명하게
|
||||
hasCustomBackground && "[&_button]:bg-transparent! **:data-[slot=select-trigger]:bg-transparent!",
|
||||
)}
|
||||
style={hasCustomText ? customTextStyle : undefined}
|
||||
>
|
||||
{renderSelect()}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1283,13 +1283,17 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
|||
width: buttonWidth,
|
||||
height: buttonHeight,
|
||||
minHeight: "32px", // 🔧 최소 높이를 32px로 줄임
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
// 🔧 커스텀 테두리 스타일 (StyleEditor에서 설정한 값 우선)
|
||||
border: style?.border || (style?.borderWidth ? undefined : "none"),
|
||||
borderWidth: style?.borderWidth || undefined,
|
||||
borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || undefined,
|
||||
borderColor: style?.borderColor || undefined,
|
||||
borderRadius: style?.borderRadius || "0.5rem",
|
||||
backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor,
|
||||
color: finalDisabled ? "#9ca3af" : buttonTextColor, // 🔧 webTypeConfig.textColor 지원
|
||||
// 🔧 크기 설정 적용 (sm/md/lg)
|
||||
fontSize: componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem",
|
||||
fontWeight: "600",
|
||||
color: finalDisabled ? "#9ca3af" : (style?.color || buttonTextColor), // 🔧 StyleEditor 텍스트 색상도 지원
|
||||
// 🔧 크기 설정 적용 (sm/md/lg), StyleEditor fontSize 우선
|
||||
fontSize: style?.fontSize || (componentConfig.size === "sm" ? "0.75rem" : componentConfig.size === "lg" ? "1rem" : "0.875rem"),
|
||||
fontWeight: style?.fontWeight || "600",
|
||||
cursor: finalDisabled ? "not-allowed" : "pointer",
|
||||
outline: "none",
|
||||
boxSizing: "border-box",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Badge } from "@/components/ui/badge";
|
|||
import { toast } from "sonner";
|
||||
import { uploadFiles, downloadFile, deleteFile, getComponentFiles, getFileInfoByObjid, getFilePreviewUrl } from "@/lib/api/file";
|
||||
import { GlobalFileManager } from "@/lib/api/globalFile";
|
||||
import { formatFileSize } from "@/lib/utils";
|
||||
import { formatFileSize, cn } from "@/lib/utils";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { FileViewerModal } from "./FileViewerModal";
|
||||
import { FileManagerModal } from "./FileManagerModal";
|
||||
|
|
@ -513,7 +513,10 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
// 파일 업로드 처리
|
||||
// 백엔드 multer 제한에 맞춘 1회 요청당 최대 파일 수
|
||||
const CHUNK_SIZE = 10;
|
||||
|
||||
// 파일 업로드 처리 (10개 초과 시 자동 분할 업로드)
|
||||
const handleFileUpload = useCallback(
|
||||
async (files: File[]) => {
|
||||
if (!files.length) return;
|
||||
|
|
@ -548,7 +551,17 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
|
||||
const filesToUpload = uniqueFiles.length > 0 ? uniqueFiles : files;
|
||||
setUploadStatus("uploading");
|
||||
toast.loading("파일을 업로드하는 중...", { id: "file-upload" });
|
||||
|
||||
// 분할 업로드 여부 판단
|
||||
const totalFiles = filesToUpload.length;
|
||||
const totalChunks = Math.ceil(totalFiles / CHUNK_SIZE);
|
||||
const isChunked = totalChunks > 1;
|
||||
|
||||
if (isChunked) {
|
||||
toast.loading(`파일 업로드 준비 중... (총 ${totalFiles}개, ${totalChunks}회 분할)`, { id: "file-upload" });
|
||||
} else {
|
||||
toast.loading("파일을 업로드하는 중...", { id: "file-upload" });
|
||||
}
|
||||
|
||||
try {
|
||||
// 🔑 레코드 모드 우선 사용
|
||||
|
|
@ -585,13 +598,11 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode;
|
||||
|
||||
// 🔑 레코드 모드일 때는 effectiveTableName을 우선 사용
|
||||
// formData.linkedTable이 'screen_files' 같은 기본값일 수 있으므로 레코드 모드에서는 무시
|
||||
const finalLinkedTable = effectiveIsRecordMode
|
||||
? effectiveTableName
|
||||
: (formData?.linkedTable || effectiveTableName);
|
||||
|
||||
const uploadData = {
|
||||
// 🎯 formData에서 백엔드 API 설정 가져오기
|
||||
autoLink: formData?.autoLink || true,
|
||||
linkedTable: finalLinkedTable,
|
||||
recordId: effectiveRecordId || `temp_${component.id}`,
|
||||
|
|
@ -599,143 +610,163 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
isVirtualFileColumn: formData?.isVirtualFileColumn || true,
|
||||
docType: component.fileConfig?.docType || "DOCUMENT",
|
||||
docTypeName: component.fileConfig?.docTypeName || "일반 문서",
|
||||
companyCode: userCompanyCode, // 🔒 멀티테넌시: 회사 코드 명시적 전달
|
||||
// 호환성을 위한 기존 필드들
|
||||
companyCode: userCompanyCode,
|
||||
tableName: effectiveTableName,
|
||||
fieldName: effectiveColumnName,
|
||||
targetObjid: targetObjid, // InteractiveDataTable 호환을 위한 targetObjid 추가
|
||||
// 🆕 레코드 모드 플래그
|
||||
targetObjid: targetObjid,
|
||||
isRecordMode: effectiveIsRecordMode,
|
||||
};
|
||||
|
||||
const response = await uploadFiles({
|
||||
files: filesToUpload,
|
||||
...uploadData,
|
||||
});
|
||||
// 🔄 파일을 CHUNK_SIZE(10개)씩 나눠서 순차 업로드
|
||||
const allNewFiles: any[] = [];
|
||||
let failedChunks = 0;
|
||||
|
||||
if (response.success) {
|
||||
// FileUploadResponse 타입에 맞게 files 배열 사용
|
||||
const fileData = response.files || (response as any).data || [];
|
||||
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
|
||||
const start = chunkIndex * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, totalFiles);
|
||||
const chunk = filesToUpload.slice(start, end);
|
||||
|
||||
if (fileData.length === 0) {
|
||||
throw new Error("업로드된 파일 데이터를 받지 못했습니다.");
|
||||
// 분할 업로드 시 진행 상태 토스트 업데이트
|
||||
if (isChunked) {
|
||||
toast.loading(
|
||||
`업로드 중... ${chunkIndex + 1}/${totalChunks} 배치 (${start + 1}~${end}번째 파일)`,
|
||||
{ id: "file-upload" }
|
||||
);
|
||||
}
|
||||
|
||||
const newFiles = fileData.map((file: any) => ({
|
||||
objid: file.objid || file.id,
|
||||
savedFileName: file.saved_file_name || file.savedFileName,
|
||||
realFileName: file.real_file_name || file.realFileName || file.name,
|
||||
fileSize: file.file_size || file.fileSize || file.size,
|
||||
fileExt: file.file_ext || file.fileExt || file.extension,
|
||||
filePath: file.file_path || file.filePath || file.path,
|
||||
docType: file.doc_type || file.docType,
|
||||
docTypeName: file.doc_type_name || file.docTypeName,
|
||||
targetObjid: file.target_objid || file.targetObjid,
|
||||
parentTargetObjid: file.parent_target_objid || file.parentTargetObjid,
|
||||
companyCode: file.company_code || file.companyCode,
|
||||
writer: file.writer,
|
||||
regdate: file.regdate,
|
||||
status: file.status || "ACTIVE",
|
||||
uploadedAt: new Date().toISOString(),
|
||||
...file,
|
||||
}));
|
||||
|
||||
|
||||
const updatedFiles = [...uploadedFiles, ...newFiles];
|
||||
|
||||
setUploadedFiles(updatedFiles);
|
||||
setUploadStatus("success");
|
||||
|
||||
// localStorage 백업 (레코드별 고유 키 사용)
|
||||
try {
|
||||
const backupKey = getUniqueKey();
|
||||
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
|
||||
} catch (e) {
|
||||
console.warn("localStorage 백업 실패:", e);
|
||||
const response = await uploadFiles({
|
||||
files: chunk,
|
||||
...uploadData,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
const fileData = response.files || (response as any).data || [];
|
||||
const chunkFiles = fileData.map((file: any) => ({
|
||||
objid: file.objid || file.id,
|
||||
savedFileName: file.saved_file_name || file.savedFileName,
|
||||
realFileName: file.real_file_name || file.realFileName || file.name,
|
||||
fileSize: file.file_size || file.fileSize || file.size,
|
||||
fileExt: file.file_ext || file.fileExt || file.extension,
|
||||
filePath: file.file_path || file.filePath || file.path,
|
||||
docType: file.doc_type || file.docType,
|
||||
docTypeName: file.doc_type_name || file.docTypeName,
|
||||
targetObjid: file.target_objid || file.targetObjid,
|
||||
parentTargetObjid: file.parent_target_objid || file.parentTargetObjid,
|
||||
companyCode: file.company_code || file.companyCode,
|
||||
writer: file.writer,
|
||||
regdate: file.regdate,
|
||||
status: file.status || "ACTIVE",
|
||||
uploadedAt: new Date().toISOString(),
|
||||
...file,
|
||||
}));
|
||||
allNewFiles.push(...chunkFiles);
|
||||
} else {
|
||||
console.error(`❌ ${chunkIndex + 1}번째 배치 업로드 실패:`, response);
|
||||
failedChunks++;
|
||||
}
|
||||
} catch (chunkError) {
|
||||
console.error(`❌ ${chunkIndex + 1}번째 배치 업로드 오류:`, chunkError);
|
||||
failedChunks++;
|
||||
}
|
||||
}
|
||||
|
||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||
if (typeof window !== "undefined") {
|
||||
// 전역 파일 상태 업데이트 (레코드별 고유 키 사용)
|
||||
const globalFileState = (window as any).globalFileState || {};
|
||||
const uniqueKey = getUniqueKey();
|
||||
globalFileState[uniqueKey] = updatedFiles;
|
||||
(window as any).globalFileState = globalFileState;
|
||||
// 모든 배치 처리 완료 후 결과 처리
|
||||
if (allNewFiles.length === 0) {
|
||||
throw new Error("업로드된 파일 데이터를 받지 못했습니다.");
|
||||
}
|
||||
|
||||
// 🌐 전역 파일 저장소에 새 파일 등록 (페이지 간 공유용)
|
||||
GlobalFileManager.registerFiles(newFiles, {
|
||||
uploadPage: window.location.pathname,
|
||||
const updatedFiles = [...uploadedFiles, ...allNewFiles];
|
||||
|
||||
setUploadedFiles(updatedFiles);
|
||||
setUploadStatus("success");
|
||||
|
||||
// localStorage 백업 (레코드별 고유 키 사용)
|
||||
try {
|
||||
const backupKey = getUniqueKey();
|
||||
localStorage.setItem(backupKey, JSON.stringify(updatedFiles));
|
||||
} catch (e) {
|
||||
console.warn("localStorage 백업 실패:", e);
|
||||
}
|
||||
|
||||
// 전역 상태 업데이트 (모든 파일 컴포넌트 동기화)
|
||||
if (typeof window !== "undefined") {
|
||||
const globalFileState = (window as any).globalFileState || {};
|
||||
const uniqueKey = getUniqueKey();
|
||||
globalFileState[uniqueKey] = updatedFiles;
|
||||
(window as any).globalFileState = globalFileState;
|
||||
|
||||
GlobalFileManager.registerFiles(allNewFiles, {
|
||||
uploadPage: window.location.pathname,
|
||||
componentId: component.id,
|
||||
screenId: formData?.screenId,
|
||||
recordId: recordId,
|
||||
});
|
||||
|
||||
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
||||
detail: {
|
||||
componentId: component.id,
|
||||
screenId: formData?.screenId,
|
||||
recordId: recordId, // 🆕 레코드 ID 추가
|
||||
});
|
||||
eventColumnName: columnName,
|
||||
uniqueKey: uniqueKey,
|
||||
recordId: recordId,
|
||||
files: updatedFiles,
|
||||
fileCount: updatedFiles.length,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(syncEvent);
|
||||
}
|
||||
|
||||
// 모든 파일 컴포넌트에 동기화 이벤트 발생
|
||||
// 🆕 columnName 추가하여 같은 화면의 다른 파일 업로드 컴포넌트와 구분
|
||||
const syncEvent = new CustomEvent("globalFileStateChanged", {
|
||||
detail: {
|
||||
componentId: component.id,
|
||||
eventColumnName: columnName, // 🆕 컬럼명 추가
|
||||
uniqueKey: uniqueKey, // 🆕 고유 키 추가
|
||||
recordId: recordId, // 🆕 레코드 ID 추가
|
||||
files: updatedFiles,
|
||||
fileCount: updatedFiles.length,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(syncEvent);
|
||||
}
|
||||
|
||||
// 컴포넌트 업데이트
|
||||
if (onUpdate) {
|
||||
const timestamp = Date.now();
|
||||
onUpdate({
|
||||
uploadedFiles: updatedFiles,
|
||||
lastFileUpdate: timestamp,
|
||||
});
|
||||
} else {
|
||||
console.warn("⚠️ onUpdate 콜백이 없습니다!");
|
||||
}
|
||||
|
||||
// 🆕 이미지/파일 컬럼에 objid 저장 (formData 업데이트)
|
||||
if (onFormDataChange && effectiveColumnName) {
|
||||
// 🎯 이미지/파일 타입 컬럼: 첫 번째 파일의 objid를 저장 (그리드에서 표시용)
|
||||
// 단일 파일인 경우 단일 값, 복수 파일인 경우 콤마 구분 문자열
|
||||
const fileObjids = updatedFiles.map(file => file.objid);
|
||||
const columnValue = fileConfig.multiple
|
||||
? fileObjids.join(',') // 복수 파일: 콤마 구분
|
||||
: (fileObjids[0] || ''); // 단일 파일: 첫 번째 파일 ID
|
||||
|
||||
// onFormDataChange를 (fieldName, value) 형태로 호출 (SaveModal 호환)
|
||||
onFormDataChange(effectiveColumnName, columnValue);
|
||||
}
|
||||
|
||||
// 그리드 파일 상태 새로고침 이벤트 발생
|
||||
if (typeof window !== "undefined") {
|
||||
const refreshEvent = new CustomEvent("refreshFileStatus", {
|
||||
detail: {
|
||||
tableName: effectiveTableName,
|
||||
recordId: effectiveRecordId,
|
||||
columnName: effectiveColumnName,
|
||||
targetObjid: targetObjid,
|
||||
fileCount: updatedFiles.length,
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(refreshEvent);
|
||||
}
|
||||
|
||||
// 컴포넌트 설정 콜백
|
||||
if (safeComponentConfig.onFileUpload) {
|
||||
safeComponentConfig.onFileUpload(newFiles);
|
||||
}
|
||||
|
||||
// 성공 시 토스트 처리
|
||||
setUploadStatus("idle");
|
||||
toast.dismiss("file-upload");
|
||||
toast.success(`${newFiles.length}개 파일 업로드 완료`);
|
||||
// 컴포넌트 업데이트
|
||||
if (onUpdate) {
|
||||
const timestamp = Date.now();
|
||||
onUpdate({
|
||||
uploadedFiles: updatedFiles,
|
||||
lastFileUpdate: timestamp,
|
||||
});
|
||||
} else {
|
||||
console.error("❌ 파일 업로드 실패:", response);
|
||||
throw new Error(response.message || (response as any).error || "파일 업로드에 실패했습니다.");
|
||||
console.warn("⚠️ onUpdate 콜백이 없습니다!");
|
||||
}
|
||||
|
||||
// 이미지/파일 컬럼에 objid 저장 (formData 업데이트)
|
||||
if (onFormDataChange && effectiveColumnName) {
|
||||
const fileObjids = updatedFiles.map(file => file.objid);
|
||||
const columnValue = fileConfig.multiple
|
||||
? fileObjids.join(',')
|
||||
: (fileObjids[0] || '');
|
||||
onFormDataChange(effectiveColumnName, columnValue);
|
||||
}
|
||||
|
||||
// 그리드 파일 상태 새로고침 이벤트 발생
|
||||
if (typeof window !== "undefined") {
|
||||
const refreshEvent = new CustomEvent("refreshFileStatus", {
|
||||
detail: {
|
||||
tableName: effectiveTableName,
|
||||
recordId: effectiveRecordId,
|
||||
columnName: effectiveColumnName,
|
||||
targetObjid: targetObjid,
|
||||
fileCount: updatedFiles.length,
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(refreshEvent);
|
||||
}
|
||||
|
||||
// 컴포넌트 설정 콜백
|
||||
if (safeComponentConfig.onFileUpload) {
|
||||
safeComponentConfig.onFileUpload(allNewFiles);
|
||||
}
|
||||
|
||||
// 성공/부분 성공 토스트 처리
|
||||
setUploadStatus("idle");
|
||||
toast.dismiss("file-upload");
|
||||
|
||||
if (failedChunks > 0) {
|
||||
toast.warning(
|
||||
`${allNewFiles.length}개 업로드 완료, 일부 파일 실패`,
|
||||
{ description: "일부 파일이 업로드되지 않았습니다. 다시 시도해주세요." }
|
||||
);
|
||||
} else {
|
||||
toast.success(`${allNewFiles.length}개 파일 업로드 완료`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("파일 업로드 오류:", error);
|
||||
|
|
@ -991,19 +1022,26 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
[safeComponentConfig.readonly, safeComponentConfig.disabled, handleFileSelect, onClick],
|
||||
);
|
||||
|
||||
// 🔧 커스텀 스타일 감지 (StyleEditor에서 설정한 값)
|
||||
const customStyle = component.style || {};
|
||||
const hasCustomBorder = !!(customStyle.borderWidth || customStyle.borderColor || customStyle.borderStyle || customStyle.border);
|
||||
const hasCustomBackground = !!customStyle.backgroundColor;
|
||||
const hasCustomRadius = !!customStyle.borderRadius;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...componentStyle,
|
||||
width: "100%", // 🆕 부모 컨테이너 너비에 맞춤
|
||||
height: "100%", // 🆕 부모 컨테이너 높이에 맞춤
|
||||
border: "none !important",
|
||||
boxShadow: "none !important",
|
||||
outline: "none !important",
|
||||
backgroundColor: "transparent !important",
|
||||
padding: "0px !important",
|
||||
borderRadius: "0px !important",
|
||||
marginBottom: "8px !important",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
// 🔧 !important 제거 - 커스텀 스타일이 없을 때만 기본값 적용
|
||||
border: hasCustomBorder ? undefined : "none",
|
||||
boxShadow: "none",
|
||||
outline: "none",
|
||||
backgroundColor: hasCustomBackground ? undefined : "transparent",
|
||||
padding: "0px",
|
||||
borderRadius: hasCustomRadius ? undefined : "0px",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
className={`${className} file-upload-container`}
|
||||
>
|
||||
|
|
@ -1014,15 +1052,15 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
position: "absolute",
|
||||
top: "-20px",
|
||||
left: "0px",
|
||||
fontSize: "12px",
|
||||
color: "rgb(107, 114, 128)",
|
||||
fontWeight: "400",
|
||||
background: "transparent !important",
|
||||
border: "none !important",
|
||||
boxShadow: "none !important",
|
||||
outline: "none !important",
|
||||
padding: "0px !important",
|
||||
margin: "0px !important"
|
||||
fontSize: customStyle.labelFontSize || "12px",
|
||||
color: customStyle.labelColor || "rgb(107, 114, 128)",
|
||||
fontWeight: customStyle.labelFontWeight || "400",
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
outline: "none",
|
||||
padding: "0px",
|
||||
margin: "0px"
|
||||
}}
|
||||
>
|
||||
{component.label}
|
||||
|
|
@ -1033,7 +1071,13 @@ const FileUploadComponent: React.FC<FileUploadComponentProps> = ({
|
|||
)}
|
||||
|
||||
<div
|
||||
className="border-border bg-card relative flex h-full w-full flex-col rounded-lg border overflow-hidden"
|
||||
className={cn(
|
||||
"relative flex h-full w-full flex-col overflow-hidden",
|
||||
// 커스텀 테두리가 없을 때만 기본 테두리 표시
|
||||
!hasCustomBorder && "border-border rounded-lg border",
|
||||
// 커스텀 배경이 없을 때만 기본 배경 표시
|
||||
!hasCustomBackground && "bg-card",
|
||||
)}
|
||||
>
|
||||
{/* 대표 이미지 전체 화면 표시 */}
|
||||
{uploadedFiles.length > 0 ? (() => {
|
||||
|
|
|
|||
|
|
@ -56,16 +56,19 @@ export const TextDisplayComponent: React.FC<TextDisplayComponentProps> = ({
|
|||
// DOM props 필터링 (React 관련 props 제거)
|
||||
const domProps = filterDOMProps(props);
|
||||
|
||||
// 🔧 StyleEditor(component.style) 값 우선, 없으면 componentConfig 폴백
|
||||
const customStyle = component.style || {};
|
||||
|
||||
// 텍스트 스타일 계산
|
||||
const textStyle: React.CSSProperties = {
|
||||
fontSize: componentConfig.fontSize || "14px",
|
||||
fontWeight: componentConfig.fontWeight || "normal",
|
||||
color: componentConfig.color || "#212121",
|
||||
textAlign: componentConfig.textAlign || "left",
|
||||
backgroundColor: componentConfig.backgroundColor || "transparent",
|
||||
fontSize: customStyle.fontSize || componentConfig.fontSize || "14px",
|
||||
fontWeight: customStyle.fontWeight || componentConfig.fontWeight || "normal",
|
||||
color: customStyle.color || componentConfig.color || "#212121",
|
||||
textAlign: (customStyle.textAlign || componentConfig.textAlign || "left") as React.CSSProperties["textAlign"],
|
||||
backgroundColor: customStyle.backgroundColor || componentConfig.backgroundColor || "transparent",
|
||||
padding: componentConfig.padding || "0",
|
||||
borderRadius: componentConfig.borderRadius || "0",
|
||||
border: componentConfig.border || "none",
|
||||
borderRadius: customStyle.borderRadius || componentConfig.borderRadius || "0",
|
||||
border: customStyle.border || (customStyle.borderWidth ? `${customStyle.borderWidth} ${customStyle.borderStyle || "solid"} ${customStyle.borderColor || "transparent"}` : componentConfig.border || "none"),
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
|
|
|
|||
Loading…
Reference in New Issue