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) {
|
if (options.search) {
|
||||||
for (const [key, value] of Object.entries(options.search)) {
|
for (const [key, value] of Object.entries(options.search)) {
|
||||||
// 검색값 추출 (객체 형태일 수 있음)
|
// 검색값 및 operator 추출 (객체 형태일 수 있음)
|
||||||
let searchValue = value;
|
let searchValue = value;
|
||||||
|
let operator = "contains"; // 기본값: 부분 일치
|
||||||
if (
|
if (
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
value !== null &&
|
value !== null &&
|
||||||
"value" in value
|
"value" in value
|
||||||
) {
|
) {
|
||||||
searchValue = value.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})`
|
`🎯 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 {
|
} else {
|
||||||
|
// 기본: 부분 일치 (ILIKE)
|
||||||
whereConditions.push(
|
whereConditions.push(
|
||||||
`${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
|
`${alias}.${joinConfig.displayColumn} ILIKE '%${safeValue}%'`
|
||||||
);
|
);
|
||||||
|
|
@ -3543,7 +3557,14 @@ export class TableManagementService {
|
||||||
`🔍 다중선택 컬럼 검색: ${key} → main.${key} IN (${multiValues.join(", ")})`
|
`🔍 다중선택 컬럼 검색: ${key} → main.${key} IN (${multiValues.join(", ")})`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (operator === "equals") {
|
||||||
|
// 🔧 equals 연산자: 정확히 일치
|
||||||
|
whereConditions.push(`main.${key}::text = '${safeValue}'`);
|
||||||
|
logger.info(
|
||||||
|
`🔍 정확히 일치 검색: ${key} → main.${key} = '${safeValue}'`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// 기본: 부분 일치 (ILIKE)
|
||||||
whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
|
whereConditions.push(`main.${key} ILIKE '%${safeValue}%'`);
|
||||||
logger.info(
|
logger.info(
|
||||||
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
|
`🔍 일반 컬럼 검색: ${key} → main.${key} LIKE '%${safeValue}%'`
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,8 @@ function ScreenViewPage() {
|
||||||
compType?.includes("textarea") ||
|
compType?.includes("textarea") ||
|
||||||
compType?.includes("v2-input") ||
|
compType?.includes("v2-input") ||
|
||||||
compType?.includes("v2-select") ||
|
compType?.includes("v2-select") ||
|
||||||
compType?.includes("v2-media"); // 🆕 미디어 컴포넌트 추가
|
compType?.includes("v2-media") ||
|
||||||
|
compType?.includes("file-upload"); // 🆕 레거시 파일 업로드 포함
|
||||||
const hasColumnName = !!(comp as any).columnName;
|
const hasColumnName = !!(comp as any).columnName;
|
||||||
return isInputType && hasColumnName;
|
return isInputType && hasColumnName;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -562,13 +562,18 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (V2Repeater가 별도로 저장)
|
// 🆕 리피터 데이터(배열)를 마스터 저장에서 제외 (V2Repeater가 별도로 저장)
|
||||||
// 단, v2-media 컴포넌트의 파일 배열(objid 배열)은 포함
|
// 단, 파일 업로드 컴포넌트의 파일 배열(objid 배열)은 포함
|
||||||
const masterFormData: Record<string, any> = {};
|
const masterFormData: Record<string, any> = {};
|
||||||
|
|
||||||
// v2-media 컴포넌트의 columnName 목록 수집
|
// 파일 업로드 컴포넌트의 columnName 목록 수집 (v2-media, file-upload 모두 포함)
|
||||||
const mediaColumnNames = new Set(
|
const mediaColumnNames = new Set(
|
||||||
allComponents
|
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)
|
.map((c: any) => c.columnName || c.componentConfig?.columnName)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ export function ComponentsPanel({
|
||||||
"textarea-basic",
|
"textarea-basic",
|
||||||
// V2 컴포넌트로 대체됨
|
// V2 컴포넌트로 대체됨
|
||||||
"image-widget", // → V2Media (image)
|
"image-widget", // → V2Media (image)
|
||||||
"file-upload", // → V2Media (file)
|
// "file-upload", // 🆕 레거시 컴포넌트 노출 (안정적인 파일 업로드)
|
||||||
"entity-search-input", // → V2Select (entity 모드)
|
"entity-search-input", // → V2Select (entity 모드)
|
||||||
"autocomplete-search-input", // → V2Select (autocomplete 모드)
|
"autocomplete-search-input", // → V2Select (autocomplete 모드)
|
||||||
// DataFlow 전용 (일반 화면에서 불필요)
|
// DataFlow 전용 (일반 화면에서 불필요)
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -427,9 +427,10 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
// 컴포넌트의 columnName에 해당하는 formData 값 추출
|
// 컴포넌트의 columnName에 해당하는 formData 값 추출
|
||||||
const fieldName = (component as any).columnName || (component as any).componentConfig?.columnName || component.id;
|
const fieldName = (component as any).columnName || (component as any).componentConfig?.columnName || component.id;
|
||||||
|
|
||||||
// 🔍 V2Media 디버깅
|
// 🔍 파일 업로드 컴포넌트 디버깅
|
||||||
if (componentType === "v2-media") {
|
if (componentType === "v2-media" || componentType === "file-upload") {
|
||||||
console.log("[DynamicComponentRenderer] v2-media:", {
|
console.log("[DynamicComponentRenderer] 파일 업로드:", {
|
||||||
|
componentType,
|
||||||
componentId: component.id,
|
componentId: component.id,
|
||||||
columnName: (component as any).columnName,
|
columnName: (component as any).columnName,
|
||||||
configColumnName: (component as any).componentConfig?.columnName,
|
configColumnName: (component as any).componentConfig?.columnName,
|
||||||
|
|
|
||||||
|
|
@ -3,90 +3,86 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||||
import { V2MediaDefinition } from "./index";
|
import { V2MediaDefinition } from "./index";
|
||||||
import { V2Media } from "@/components/v2/V2Media";
|
import FileUploadComponent from "../file-upload/FileUploadComponent";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V2Media 렌더러
|
* V2Media 렌더러
|
||||||
* 파일, 이미지, 비디오, 오디오 등 다양한 미디어 타입을 지원
|
* 레거시 FileUploadComponent를 사용하여 안정적인 파일 업로드 기능 제공
|
||||||
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
|
||||||
*/
|
*/
|
||||||
export class V2MediaRenderer extends AutoRegisteringComponentRenderer {
|
export class V2MediaRenderer extends AutoRegisteringComponentRenderer {
|
||||||
static componentDefinition = V2MediaDefinition;
|
static componentDefinition = V2MediaDefinition;
|
||||||
|
|
||||||
render(): React.ReactElement {
|
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 config = component.componentConfig || component.config || {};
|
||||||
const columnName = component.columnName;
|
const columnName = component.columnName;
|
||||||
const tableName = component.tableName || this.props.tableName;
|
const tableName = component.tableName || this.props.tableName;
|
||||||
|
|
||||||
// formData에서 현재 값 가져오기
|
// V1 file-upload에서 사용하는 형태로 설정 매핑
|
||||||
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에서 넘어온 설정 매핑
|
|
||||||
const mediaType = config.mediaType || config.type || this.getMediaTypeFromWebType(component.webType);
|
const mediaType = config.mediaType || config.type || this.getMediaTypeFromWebType(component.webType);
|
||||||
|
|
||||||
// maxSize: MB → bytes 변환 (V1은 bytes, V2는 MB 단위 사용)
|
// maxSize: MB → bytes 변환
|
||||||
const maxSizeBytes = config.maxSize
|
const maxSizeBytes = config.maxSize
|
||||||
? (config.maxSize > 1000 ? config.maxSize : config.maxSize * 1024 * 1024)
|
? (config.maxSize > 1000 ? config.maxSize : config.maxSize * 1024 * 1024)
|
||||||
: 10 * 1024 * 1024; // 기본 10MB
|
: 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 (
|
return (
|
||||||
<V2Media
|
<FileUploadComponent
|
||||||
id={component.id}
|
component={legacyComponent}
|
||||||
label={component.label}
|
componentConfig={legacyComponentConfig}
|
||||||
required={component.required}
|
componentStyle={component.style || {}}
|
||||||
readonly={config.readonly || component.readonly}
|
className=""
|
||||||
disabled={config.disabled || component.disabled}
|
isInteractive={isInteractive ?? true}
|
||||||
value={currentValue}
|
isDesignMode={isDesignMode ?? false}
|
||||||
onChange={handleChange}
|
formData={formData || {}}
|
||||||
config={{
|
onFormDataChange={handleFormDataChange}
|
||||||
type: mediaType,
|
onUpdate={onUpdate}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -475,9 +475,21 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||||
filterValue = filterValue.join("|");
|
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 {
|
return {
|
||||||
...filter,
|
...filter,
|
||||||
value: filterValue || "",
|
value: filterValue || "",
|
||||||
|
operator, // operator 추가
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((f) => {
|
.filter((f) => {
|
||||||
|
|
|
||||||
|
|
@ -107,18 +107,18 @@ export const WEB_TYPE_V2_MAPPING: Record<string, V2ComponentMapping> = {
|
||||||
config: { mode: "dropdown", source: "category" },
|
config: { mode: "dropdown", source: "category" },
|
||||||
},
|
},
|
||||||
|
|
||||||
// 파일/이미지 → V2Media
|
// 파일/이미지 → 레거시 file-upload (안정적인 파일 업로드)
|
||||||
file: {
|
file: {
|
||||||
componentType: "v2-media",
|
componentType: "file-upload",
|
||||||
config: { type: "file", multiple: false },
|
config: { maxFileCount: 10, accept: "*/*" },
|
||||||
},
|
},
|
||||||
image: {
|
image: {
|
||||||
componentType: "v2-media",
|
componentType: "file-upload",
|
||||||
config: { type: "image", showPreview: true },
|
config: { maxFileCount: 1, accept: "image/*" },
|
||||||
},
|
},
|
||||||
img: {
|
img: {
|
||||||
componentType: "v2-media",
|
componentType: "file-upload",
|
||||||
config: { type: "image", showPreview: true },
|
config: { maxFileCount: 1, accept: "image/*" },
|
||||||
},
|
},
|
||||||
|
|
||||||
// 버튼은 V2 컴포넌트에서 제외 (기존 버튼 시스템 사용)
|
// 버튼은 V2 컴포넌트에서 제외 (기존 버튼 시스템 사용)
|
||||||
|
|
@ -157,9 +157,9 @@ export const WEB_TYPE_COMPONENT_MAPPING: Record<string, string> = {
|
||||||
code: "v2-select",
|
code: "v2-select",
|
||||||
entity: "v2-select",
|
entity: "v2-select",
|
||||||
category: "v2-select",
|
category: "v2-select",
|
||||||
file: "v2-media",
|
file: "file-upload",
|
||||||
image: "v2-media",
|
image: "file-upload",
|
||||||
img: "v2-media",
|
img: "file-upload",
|
||||||
button: "button-primary",
|
button: "button-primary",
|
||||||
label: "v2-input",
|
label: "v2-input",
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -232,13 +232,27 @@ export interface V2MediaConfig {
|
||||||
maxSize?: number;
|
maxSize?: number;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
uploadEndpoint?: string;
|
uploadEndpoint?: string;
|
||||||
|
// 레거시 FileUpload 호환 설정
|
||||||
|
docType?: string;
|
||||||
|
docTypeName?: string;
|
||||||
|
showFileList?: boolean;
|
||||||
|
dragDrop?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface V2MediaProps extends V2BaseProps {
|
export interface V2MediaProps extends V2BaseProps {
|
||||||
v2Type: "V2Media";
|
v2Type?: "V2Media";
|
||||||
config: V2MediaConfig;
|
config?: V2MediaConfig;
|
||||||
value?: string | string[]; // 파일 URL 또는 배열
|
value?: string | string[]; // 파일 URL 또는 배열
|
||||||
onChange?: (value: string | string[]) => void;
|
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 =====
|
// ===== V2List =====
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue