feat: 검색 기능 개선 및 레거시 파일 업로드 통합
- 테이블 관리 서비스에서 검색 옵션에 operator를 추가하여 정확한 일치(equals) 및 부분 일치(contains) 검색을 지원하도록 개선하였습니다. - 파일 업로드 컴포넌트에서 레거시 file-upload 기능을 통합하여 안정적인 파일 업로드를 제공하며, V2Media와의 호환성을 강화하였습니다. - DynamicComponentRenderer에서 파일 업로드 컴포넌트의 디버깅 로깅을 추가하여 문제 해결을 용이하게 하였습니다. - 웹 타입 매핑에서 파일 및 이미지 타입을 레거시 file-upload로 변경하여 일관성을 유지하였습니다.
This commit is contained in:
parent
e171f5a503
commit
7ec5a438d4
|
|
@ -3403,14 +3403,16 @@ export class TableManagementService {
|
|||
|
||||
if (options.search) {
|
||||
for (const [key, value] of Object.entries(options.search)) {
|
||||
// 검색값 추출 (객체 형태일 수 있음)
|
||||
// 검색값 및 operator 추출 (객체 형태일 수 있음)
|
||||
let searchValue = value;
|
||||
let operator = "contains"; // 기본값: 부분 일치
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
"value" in value
|
||||
) {
|
||||
searchValue = value.value;
|
||||
operator = (value as any).operator || "contains";
|
||||
}
|
||||
|
||||
// 빈 값이면 스킵
|
||||
|
|
@ -3482,7 +3484,19 @@ export class TableManagementService {
|
|||
`🎯 Entity 조인 다중선택 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} IN (${multiValues.join(", ")}) (별칭: ${alias})`
|
||||
);
|
||||
}
|
||||
} else if (operator === "equals") {
|
||||
// 🔧 equals 연산자: 정확히 일치
|
||||
whereConditions.push(
|
||||
`${alias}.${joinConfig.displayColumn}::text = '${safeValue}'`
|
||||
);
|
||||
entitySearchColumns.push(
|
||||
`${key} (${joinConfig.referenceTable}.${joinConfig.displayColumn})`
|
||||
);
|
||||
logger.info(
|
||||
`🎯 Entity 조인 정확히 일치 검색: ${key} → ${joinConfig.referenceTable}.${joinConfig.displayColumn} = '${safeValue}' (별칭: ${alias})`
|
||||
);
|
||||
} else {
|
||||
// 기본: 부분 일치 (ILIKE)
|
||||
whereConditions.push(
|
||||
`${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
|
||||
);
|
||||
|
|
@ -3543,7 +3557,14 @@ export class TableManagementService {
|
|||
`🔍 다중선택 컬럼 검색: ${key} → main.${key} IN (${multiValues.join(", ")})`
|
||||
);
|
||||
}
|
||||
} else if (operator === "equals") {
|
||||
// 🔧 equals 연산자: 정확히 일치
|
||||
whereConditions.push(`main.${key}::text = '${safeValue}'`);
|
||||
logger.info(
|
||||
`🔍 정확히 일치 검색: ${key} → main.${key} = '${safeValue}'`
|
||||
);
|
||||
} else {
|
||||
// 기본: 부분 일치 (ILIKE)
|
||||
whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
|
||||
logger.info(
|
||||
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
|
||||
|
|
|
|||
|
|
@ -239,7 +239,8 @@ function ScreenViewPage() {
|
|||
compType?.includes("textarea") ||
|
||||
compType?.includes("v2-input") ||
|
||||
compType?.includes("v2-select") ||
|
||||
compType?.includes("v2-media"); // 🆕 미디어 컴포넌트 추가
|
||||
compType?.includes("v2-media") ||
|
||||
compType?.includes("file-upload"); // 🆕 레거시 파일 업로드 포함
|
||||
const hasColumnName = !!(comp as any).columnName;
|
||||
return isInputType && hasColumnName;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -562,13 +562,18 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
|||
|
||||
try {
|
||||
// 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (V2Repeater가 별도로 저장)
|
||||
// 단, v2-media 컴포넌트의 파일 배열(objid 배열)은 포함
|
||||
// 단, 파일 업로드 컴포넌트의 파일 배열(objid 배열)은 포함
|
||||
const masterFormData: Record<string, any> = {};
|
||||
|
||||
// v2-media 컴포넌트의 columnName 목록 수집
|
||||
// 파일 업로드 컴포넌트의 columnName 목록 수집 (v2-media, file-upload 모두 포함)
|
||||
const mediaColumnNames = new Set(
|
||||
allComponents
|
||||
.filter((c: any) => c.componentType === "v2-media" || c.url?.includes("v2-media"))
|
||||
.filter((c: any) =>
|
||||
c.componentType === "v2-media" ||
|
||||
c.componentType === "file-upload" ||
|
||||
c.url?.includes("v2-media") ||
|
||||
c.url?.includes("file-upload")
|
||||
)
|
||||
.map((c: any) => c.columnName || c.componentConfig?.columnName)
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function ComponentsPanel({
|
|||
"textarea-basic",
|
||||
// V2 컴포넌트로 대체됨
|
||||
"image-widget", // → V2Media (image)
|
||||
"file-upload", // → V2Media (file)
|
||||
// "file-upload", // 🆕 레거시 컴포넌트 노출 (안정적인 파일 업로드)
|
||||
"entity-search-input", // → V2Select (entity 모드)
|
||||
"autocomplete-search-input", // → V2Select (autocomplete 모드)
|
||||
// DataFlow 전용 (일반 화면에서 불필요)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -427,9 +427,10 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
|||
// 컴포넌트의 columnName에 해당하는 formData 값 추출
|
||||
const fieldName = (component as any).columnName || (component as any).componentConfig?.columnName || component.id;
|
||||
|
||||
// 🔍 V2Media 디버깅
|
||||
if (componentType === "v2-media") {
|
||||
console.log("[DynamicComponentRenderer] v2-media:", {
|
||||
// 🔍 파일 업로드 컴포넌트 디버깅
|
||||
if (componentType === "v2-media" || componentType === "file-upload") {
|
||||
console.log("[DynamicComponentRenderer] 파일 업로드:", {
|
||||
componentType,
|
||||
componentId: component.id,
|
||||
columnName: (component as any).columnName,
|
||||
configColumnName: (component as any).componentConfig?.columnName,
|
||||
|
|
|
|||
|
|
@ -3,90 +3,86 @@
|
|||
import React from "react";
|
||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||
import { V2MediaDefinition } from "./index";
|
||||
import { V2Media } from "@/components/v2/V2Media";
|
||||
import FileUploadComponent from "../file-upload/FileUploadComponent";
|
||||
|
||||
/**
|
||||
* V2Media 렌더러
|
||||
* 파일, 이미지, 비디오, 오디오 등 다양한 미디어 타입을 지원
|
||||
* 레거시 FileUploadComponent를 사용하여 안정적인 파일 업로드 기능 제공
|
||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
||||
*/
|
||||
export class V2MediaRenderer extends AutoRegisteringComponentRenderer {
|
||||
static componentDefinition = V2MediaDefinition;
|
||||
|
||||
render(): React.ReactElement {
|
||||
const { component, formData, onFormDataChange, isDesignMode, isSelected, isInteractive, ...restProps } = this.props;
|
||||
const {
|
||||
component,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
isDesignMode,
|
||||
isSelected,
|
||||
isInteractive,
|
||||
onUpdate,
|
||||
...restProps
|
||||
} = this.props;
|
||||
|
||||
// 컴포넌트 설정 추출
|
||||
const config = component.componentConfig || component.config || {};
|
||||
const columnName = component.columnName;
|
||||
const tableName = component.tableName || this.props.tableName;
|
||||
|
||||
// formData에서 현재 값 가져오기
|
||||
const rawValue = formData?.[columnName] ?? component.value ?? "";
|
||||
|
||||
// objid를 미리보기 URL로 변환하는 함수 (number/string 모두 처리)
|
||||
const convertToPreviewUrl = (val: any): string => {
|
||||
if (val === null || val === undefined || val === "") return "";
|
||||
|
||||
// number면 string으로 변환
|
||||
const strVal = String(val);
|
||||
|
||||
// 이미 URL 형태면 그대로 반환
|
||||
if (strVal.startsWith("/") || strVal.startsWith("http")) return strVal;
|
||||
|
||||
// 숫자로만 이루어진 문자열이면 objid로 간주하고 미리보기 URL 생성
|
||||
if (/^\d+$/.test(strVal)) {
|
||||
return `/api/files/preview/${strVal}`;
|
||||
}
|
||||
|
||||
return strVal;
|
||||
};
|
||||
|
||||
// 배열 또는 단일 값 처리
|
||||
const currentValue = Array.isArray(rawValue)
|
||||
? rawValue.map(convertToPreviewUrl)
|
||||
: convertToPreviewUrl(rawValue);
|
||||
|
||||
console.log("[V2Media] rawValue:", rawValue, "-> currentValue:", currentValue);
|
||||
|
||||
// 값 변경 핸들러
|
||||
const handleChange = (value: any) => {
|
||||
if (isInteractive && onFormDataChange && columnName) {
|
||||
onFormDataChange(columnName, value);
|
||||
}
|
||||
};
|
||||
|
||||
// V1 file-upload, image-widget에서 넘어온 설정 매핑
|
||||
// V1 file-upload에서 사용하는 형태로 설정 매핑
|
||||
const mediaType = config.mediaType || config.type || this.getMediaTypeFromWebType(component.webType);
|
||||
|
||||
// maxSize: MB → bytes 변환 (V1은 bytes, V2는 MB 단위 사용)
|
||||
// maxSize: MB → bytes 변환
|
||||
const maxSizeBytes = config.maxSize
|
||||
? (config.maxSize > 1000 ? config.maxSize : config.maxSize * 1024 * 1024)
|
||||
: 10 * 1024 * 1024; // 기본 10MB
|
||||
|
||||
// 레거시 컴포넌트 설정 형태로 변환
|
||||
const legacyComponentConfig = {
|
||||
maxFileCount: config.multiple ? 10 : 1,
|
||||
maxFileSize: maxSizeBytes,
|
||||
accept: config.accept || this.getDefaultAccept(mediaType),
|
||||
docType: config.docType || "DOCUMENT",
|
||||
docTypeName: config.docTypeName || "일반 문서",
|
||||
showFileList: config.showFileList ?? true,
|
||||
dragDrop: config.dragDrop ?? true,
|
||||
};
|
||||
|
||||
// 레거시 컴포넌트 형태로 변환
|
||||
const legacyComponent = {
|
||||
...component,
|
||||
id: component.id,
|
||||
columnName: columnName,
|
||||
tableName: tableName,
|
||||
componentConfig: legacyComponentConfig,
|
||||
};
|
||||
|
||||
// onFormDataChange 래퍼: 레거시 컴포넌트는 객체를 전달하므로 변환 필요
|
||||
const handleFormDataChange = (data: any) => {
|
||||
if (onFormDataChange) {
|
||||
// 레거시 컴포넌트는 { [columnName]: value } 형태로 전달
|
||||
// 부모는 (fieldName, value) 형태를 기대
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// __attachmentsUpdate 같은 메타 데이터는 건너뛰기
|
||||
if (!key.startsWith("__")) {
|
||||
onFormDataChange(key, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<V2Media
|
||||
id={component.id}
|
||||
label={component.label}
|
||||
required={component.required}
|
||||
readonly={config.readonly || component.readonly}
|
||||
disabled={config.disabled || component.disabled}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
config={{
|
||||
type: mediaType,
|
||||
multiple: config.multiple ?? false,
|
||||
preview: config.preview ?? true,
|
||||
maxSize: maxSizeBytes,
|
||||
accept: config.accept || this.getDefaultAccept(mediaType),
|
||||
uploadEndpoint: config.uploadEndpoint || "/files/upload",
|
||||
}}
|
||||
style={component.style}
|
||||
size={component.size}
|
||||
formData={formData}
|
||||
columnName={columnName}
|
||||
tableName={tableName}
|
||||
{...restProps}
|
||||
<FileUploadComponent
|
||||
component={legacyComponent}
|
||||
componentConfig={legacyComponentConfig}
|
||||
componentStyle={component.style || {}}
|
||||
className=""
|
||||
isInteractive={isInteractive ?? true}
|
||||
isDesignMode={isDesignMode ?? false}
|
||||
formData={formData || {}}
|
||||
onFormDataChange={handleFormDataChange}
|
||||
onUpdate={onUpdate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -475,9 +475,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
|||
filterValue = filterValue.join("|");
|
||||
}
|
||||
|
||||
// 🔧 filterType에 따라 operator 설정
|
||||
// - "select" 유형: 정확히 일치 (equals)
|
||||
// - "text" 유형: 부분 일치 (contains)
|
||||
// - "date", "number": 각각 적절한 처리
|
||||
let operator = "contains"; // 기본값
|
||||
if (filter.filterType === "select") {
|
||||
operator = "equals"; // 선택 필터는 정확히 일치
|
||||
} else if (filter.filterType === "number") {
|
||||
operator = "equals"; // 숫자도 정확히 일치
|
||||
}
|
||||
|
||||
return {
|
||||
...filter,
|
||||
value: filterValue || "",
|
||||
operator, // operator 추가
|
||||
};
|
||||
})
|
||||
.filter((f) => {
|
||||
|
|
|
|||
|
|
@ -107,18 +107,18 @@ export const WEB_TYPE_V2_MAPPING: Record<string, V2ComponentMapping> = {
|
|||
config: { mode: "dropdown", source: "category" },
|
||||
},
|
||||
|
||||
// 파일/이미지 → V2Media
|
||||
// 파일/이미지 → 레거시 file-upload (안정적인 파일 업로드)
|
||||
file: {
|
||||
componentType: "v2-media",
|
||||
config: { type: "file", multiple: false },
|
||||
componentType: "file-upload",
|
||||
config: { maxFileCount: 10, accept: "*/*" },
|
||||
},
|
||||
image: {
|
||||
componentType: "v2-media",
|
||||
config: { type: "image", showPreview: true },
|
||||
componentType: "file-upload",
|
||||
config: { maxFileCount: 1, accept: "image/*" },
|
||||
},
|
||||
img: {
|
||||
componentType: "v2-media",
|
||||
config: { type: "image", showPreview: true },
|
||||
componentType: "file-upload",
|
||||
config: { maxFileCount: 1, accept: "image/*" },
|
||||
},
|
||||
|
||||
// 버튼은 V2 컴포넌트에서 제외 (기존 버튼 시스템 사용)
|
||||
|
|
@ -157,9 +157,9 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record<string, string> = {
|
|||
code: "v2-select",
|
||||
entity: "v2-select",
|
||||
category: "v2-select",
|
||||
file: "v2-media",
|
||||
image: "v2-media",
|
||||
img: "v2-media",
|
||||
file: "file-upload",
|
||||
image: "file-upload",
|
||||
img: "file-upload",
|
||||
button: "button-primary",
|
||||
label: "v2-input",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -232,13 +232,27 @@ export interface V2MediaConfig {
|
|||
maxSize?: number;
|
||||
preview?: boolean;
|
||||
uploadEndpoint?: string;
|
||||
// 레거시 FileUpload 호환 설정
|
||||
docType?: string;
|
||||
docTypeName?: string;
|
||||
showFileList?: boolean;
|
||||
dragDrop?: boolean;
|
||||
}
|
||||
|
||||
export interface V2MediaProps extends V2BaseProps {
|
||||
v2Type: "V2Media";
|
||||
config: V2MediaConfig;
|
||||
v2Type?: "V2Media";
|
||||
config?: V2MediaConfig;
|
||||
value?: string | string[]; // 파일 URL 또는 배열
|
||||
onChange?: (value: string | string[]) => void;
|
||||
// 레거시 FileUpload 호환 props
|
||||
formData?: Record<string, any>;
|
||||
columnName?: string;
|
||||
tableName?: string;
|
||||
// 부모 컴포넌트 시그니처: (fieldName, value) 형식
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
isDesignMode?: boolean;
|
||||
isInteractive?: boolean;
|
||||
onUpdate?: (updates: Partial<any>) => void;
|
||||
}
|
||||
|
||||
// ===== V2List =====
|
||||
|
|
|
|||
Loading…
Reference in New Issue