Merge conflict resolved: V2Media.tsx
원격 버전(7ec5a43) 채택 - 완전한 인라인 UI 코드 사용
This commit is contained in:
@@ -139,12 +139,20 @@ export async function previewNumberingCode(
|
||||
/**
|
||||
* 코드 할당 (저장 시점에 실제 순번 증가)
|
||||
* 실제 저장할 때만 호출
|
||||
* @param ruleId 채번 규칙 ID
|
||||
* @param userInputCode 사용자가 편집한 최종 코드 (수동 입력 부분 추출용)
|
||||
* @param formData 폼 데이터 (카테고리/날짜 기반 채번용)
|
||||
*/
|
||||
export async function allocateNumberingCode(
|
||||
ruleId: string
|
||||
ruleId: string,
|
||||
userInputCode?: string,
|
||||
formData?: Record<string, any>
|
||||
): Promise<ApiResponse<{ generatedCode: string }>> {
|
||||
try {
|
||||
const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`);
|
||||
const response = await apiClient.post(`/numbering-rules/${ruleId}/allocate`, {
|
||||
userInputCode,
|
||||
formData,
|
||||
});
|
||||
return response.data;
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message || "코드 할당 실패" };
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -856,8 +856,10 @@ export function RepeatScreenModalComponent({
|
||||
});
|
||||
|
||||
// 채번 API 호출 (allocate: 실제 시퀀스 증가)
|
||||
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
|
||||
const { allocateNumberingCode } = await import("@/lib/api/numberingRule");
|
||||
const response = await allocateNumberingCode(rowNumbering.numberingRuleId);
|
||||
const userInputCode = newRowData[rowNumbering.targetColumn] as string;
|
||||
const response = await allocateNumberingCode(rowNumbering.numberingRuleId, userInputCode, newRowData);
|
||||
|
||||
if (response.success && response.data) {
|
||||
newRowData[rowNumbering.targetColumn] = response.data.generatedCode;
|
||||
|
||||
+3
-2
@@ -1443,8 +1443,9 @@ export function UniversalFormModalComponent({
|
||||
|
||||
if (isNewRecord || hasNoValue) {
|
||||
try {
|
||||
// allocateNumberingCode로 실제 순번 증가
|
||||
const response = await allocateNumberingCode(field.numberingRule.ruleId);
|
||||
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
|
||||
const userInputCode = mainData[field.columnName] as string;
|
||||
const response = await allocateNumberingCode(field.numberingRule.ruleId, userInputCode, mainData);
|
||||
if (response.success && response.data?.generatedCode) {
|
||||
mainData[field.columnName] = response.data.generatedCode;
|
||||
}
|
||||
|
||||
@@ -1325,7 +1325,31 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
...userStyle,
|
||||
};
|
||||
|
||||
const buttonContent = processedConfig.text !== undefined ? processedConfig.text : component.label || "버튼";
|
||||
// 버튼 텍스트 결정 (다양한 소스에서 가져옴)
|
||||
// "기본 버튼"은 컴포넌트 생성 시 기본값이므로 무시
|
||||
const labelValue = component.label === "기본 버튼" ? undefined : component.label;
|
||||
|
||||
// 액션 타입에 따른 기본 텍스트 (modal 액션과 동일하게)
|
||||
const actionType = processedConfig.action?.type || component.componentConfig?.action?.type;
|
||||
const actionDefaultText: Record<string, string> = {
|
||||
save: "저장",
|
||||
delete: "삭제",
|
||||
modal: "등록",
|
||||
edit: "수정",
|
||||
copy: "복사",
|
||||
close: "닫기",
|
||||
cancel: "취소",
|
||||
};
|
||||
|
||||
const buttonContent =
|
||||
processedConfig.text ||
|
||||
component.webTypeConfig?.text ||
|
||||
component.componentConfig?.text ||
|
||||
component.config?.text ||
|
||||
component.style?.labelText ||
|
||||
labelValue ||
|
||||
actionDefaultText[actionType as string] ||
|
||||
"버튼";
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -25,8 +25,20 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer {
|
||||
|
||||
// 값 변경 핸들러
|
||||
const handleChange = (value: any) => {
|
||||
console.log("🔄 [V2InputRenderer] handleChange 호출:", {
|
||||
columnName,
|
||||
value,
|
||||
isInteractive,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
});
|
||||
if (isInteractive && onFormDataChange && columnName) {
|
||||
onFormDataChange(columnName, value);
|
||||
} else {
|
||||
console.warn("⚠️ [V2InputRenderer] onFormDataChange 호출 스킵:", {
|
||||
isInteractive,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -459,6 +459,9 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
|
||||
// 🆕 Filter Builder (고급 필터) 관련 상태 - filteredData보다 먼저 정의해야 함
|
||||
const [filterGroups, setFilterGroups] = useState<FilterGroup[]>([]);
|
||||
|
||||
// 🆕 joinColumnMapping - filteredData에서 사용하므로 먼저 정의해야 함
|
||||
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
|
||||
|
||||
// 🆕 분할 패널에서 우측에 이미 추가된 항목 필터링 (좌측 테이블에만 적용) + 헤더 필터
|
||||
const filteredData = useMemo(() => {
|
||||
@@ -473,14 +476,17 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 헤더 필터 적용 (joinColumnMapping 사용 안 함 - 직접 컬럼명 사용)
|
||||
// 2. 헤더 필터 적용 (joinColumnMapping 사용 - 조인된 컬럼과 일치해야 함)
|
||||
if (Object.keys(headerFilters).length > 0) {
|
||||
result = result.filter((row) => {
|
||||
return Object.entries(headerFilters).every(([columnName, values]) => {
|
||||
if (values.size === 0) return true;
|
||||
|
||||
// 여러 가능한 컬럼명 시도
|
||||
const cellValue = row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
|
||||
// joinColumnMapping을 사용하여 조인된 컬럼명 확인
|
||||
const mappedColumnName = joinColumnMapping[columnName] || columnName;
|
||||
|
||||
// 여러 가능한 컬럼명 시도 (mappedColumnName 우선)
|
||||
const cellValue = row[mappedColumnName] ?? row[columnName] ?? row[columnName.toLowerCase()] ?? row[columnName.toUpperCase()];
|
||||
const cellStr = cellValue !== null && cellValue !== undefined ? String(cellValue) : "";
|
||||
|
||||
return values.has(cellStr);
|
||||
@@ -541,7 +547,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups]);
|
||||
}, [data, splitPanelPosition, splitPanelContext?.addedItemIds, headerFilters, filterGroups, joinColumnMapping]);
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
@@ -554,7 +560,6 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
const [tableLabel, setTableLabel] = useState<string>("");
|
||||
const [localPageSize, setLocalPageSize] = useState<number>(tableConfig.pagination?.pageSize || 20);
|
||||
const [displayColumns, setDisplayColumns] = useState<ColumnConfig[]>([]);
|
||||
const [joinColumnMapping, setJoinColumnMapping] = useState<Record<string, string>>({});
|
||||
const [columnMeta, setColumnMeta] = useState<
|
||||
Record<string, { webType?: string; codeCategory?: string; inputType?: string }>
|
||||
>({});
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -737,7 +737,9 @@ export class ButtonActionExecutor {
|
||||
|
||||
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumberingRepeater)) {
|
||||
try {
|
||||
const allocateResult = await allocateNumberingCode(ruleId);
|
||||
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
|
||||
const userInputCode = context.formData[fieldName] as string;
|
||||
const allocateResult = await allocateNumberingCode(ruleId, userInputCode, context.formData);
|
||||
|
||||
if (allocateResult.success && allocateResult.data?.generatedCode) {
|
||||
const newCode = allocateResult.data.generatedCode;
|
||||
@@ -1030,7 +1032,9 @@ export class ButtonActionExecutor {
|
||||
|
||||
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
|
||||
try {
|
||||
const allocateResult = await allocateNumberingCode(ruleId);
|
||||
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
|
||||
const userInputCode = formData[fieldName] as string;
|
||||
const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData);
|
||||
|
||||
if (allocateResult.success && allocateResult.data?.generatedCode) {
|
||||
const newCode = allocateResult.data.generatedCode;
|
||||
@@ -2063,7 +2067,9 @@ export class ButtonActionExecutor {
|
||||
|
||||
for (const [fieldName, ruleId] of Object.entries(fieldsWithNumbering)) {
|
||||
try {
|
||||
const allocateResult = await allocateNumberingCode(ruleId);
|
||||
// 🆕 사용자가 편집한 값을 전달 (수동 입력 부분 추출용)
|
||||
const userInputCode = commonFieldsData[fieldName] as string;
|
||||
const allocateResult = await allocateNumberingCode(ruleId, userInputCode, formData);
|
||||
|
||||
if (allocateResult.success && allocateResult.data?.generatedCode) {
|
||||
const newCode = allocateResult.data.generatedCode;
|
||||
@@ -3494,10 +3500,13 @@ export class ButtonActionExecutor {
|
||||
const screenModalEvent = new CustomEvent("openScreenModal", {
|
||||
detail: {
|
||||
screenId: config.targetScreenId,
|
||||
title: config.editModalTitle || "데이터 수정",
|
||||
title: isCreateMode ? config.editModalTitle || "데이터 복사" : config.editModalTitle || "데이터 수정",
|
||||
description: description,
|
||||
size: config.modalSize || "lg",
|
||||
editData: rowData, // 🆕 수정 데이터 전달
|
||||
// 🔧 복사 모드에서는 editData 대신 splitPanelParentData로 전달하여 채번이 생성되도록 함
|
||||
editData: isCreateMode ? undefined : rowData,
|
||||
splitPanelParentData: isCreateMode ? rowData : undefined,
|
||||
isCreateMode: isCreateMode, // 🆕 복사 모드 플래그 전달
|
||||
},
|
||||
});
|
||||
window.dispatchEvent(screenModalEvent);
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user