jskim-node #388
|
|
@ -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