Files
vexplor/frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx
kjs 43523a0bba feat: Implement NOT NULL validation for form fields based on table metadata
- Added a new function `isColumnRequired` to determine if a column is required based on its NOT NULL status from the table schema.
- Updated the `SaveModal` and `InteractiveScreenViewer` components to incorporate this validation, ensuring that required fields are accurately assessed during form submission.
- Enhanced the `DynamicComponentRenderer` to reflect the NOT NULL requirement in the component's required state.
- Improved user feedback by marking required fields with an asterisk based on both manual settings and database constraints.

These changes enhance the form validation process, ensuring that users are prompted for all necessary information based on the underlying data structure.
2026-03-10 14:16:02 +09:00

157 lines
6.7 KiB
TypeScript

"use client";
import React from "react";
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
import { V2SelectDefinition } from "./index";
import { V2Select } from "@/components/v2/V2Select";
import { isColumnRequiredByMeta } from "../../DynamicComponentRenderer";
/**
* V2Select 렌더러
* 자동 등록 시스템을 사용하여 컴포넌트를 레지스트리에 등록
*/
export class V2SelectRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = V2SelectDefinition;
render(): React.ReactElement {
const { component, formData, onFormDataChange, isDesignMode, isSelected, isInteractive, allComponents, ...restProps } = this.props as any;
// 컴포넌트 설정 추출
const config = component.componentConfig || component.config || {};
const columnName = component.columnName;
const tableName = component.tableName || this.props.tableName;
// 🔧 카테고리 타입 감지 (inputType 또는 webType이 category인 경우)
const inputType = component.componentConfig?.inputType || component.inputType;
const webType = component.componentConfig?.webType || component.webType;
const isCategoryType = inputType === "category" || webType === "category";
// formData에서 현재 값 가져오기 (기본값 지원)
const defaultValue = config.defaultValue || "";
// 🔧 tagbox, check, tag, swap 모드는 본질적으로 다중 선택
const multiSelectModes = ["tagbox", "check", "checkbox", "tag", "swap"];
const isMultiple = config.multiple || multiSelectModes.includes(config.mode);
let currentValue = formData?.[columnName] ?? component.value ?? "";
// 🔧 다중 선택 시 값 정규화 (잘못된 형식 필터링)
if (isMultiple) {
// 헬퍼: 유효한 값인지 체크 (중괄호, 따옴표, 백슬래시 없어야 함)
// 숫자도 유효한 값으로 처리
const isValidValue = (v: any): boolean => {
// 숫자면 유효
if (typeof v === "number" && !isNaN(v)) return true;
if (typeof v !== "string") return false;
if (!v || v.trim() === "") return false;
if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false;
return true;
};
if (typeof currentValue === "string" && currentValue) {
// 🔧 PostgreSQL 배열 형식 또는 중첩된 잘못된 형식 감지
if (currentValue.startsWith("{") || currentValue.includes('{"') || currentValue.includes('\\"')) {
currentValue = [];
} else if (currentValue.includes(",")) {
// 쉼표 구분 문자열 파싱 후 유효한 값만 필터링
currentValue = currentValue
.split(",")
.map((v) => v.trim())
.filter(isValidValue);
} else if (isValidValue(currentValue)) {
currentValue = [currentValue];
} else {
currentValue = [];
}
} else if (Array.isArray(currentValue)) {
// 🔧 배열일 때도 잘못된 값 필터링 + 숫자→문자열 변환!
const filtered = currentValue.map((v) => (typeof v === "number" ? String(v) : v)).filter(isValidValue);
currentValue = filtered;
} else {
currentValue = [];
}
}
// 🆕 formData에 값이 없고 기본값이 설정된 경우, 기본값 적용
// 단, formData에 해당 키가 이미 존재하면(사용자가 명시적으로 초기화한 경우) 기본값을 재적용하지 않음
const hasKeyInFormData = formData !== undefined && formData !== null && columnName in (formData || {});
if (
(currentValue === "" || currentValue === undefined || currentValue === null) &&
defaultValue &&
isInteractive &&
onFormDataChange &&
columnName &&
!hasKeyInFormData // formData에 키 자체가 없을 때만 기본값 적용 (초기 렌더링)
) {
setTimeout(() => {
onFormDataChange(columnName, defaultValue);
}, 0);
currentValue = defaultValue;
}
// 값 변경 핸들러 (배열 → 쉼표 구분 문자열로 변환하여 저장)
const handleChange = (value: any) => {
if (isInteractive && onFormDataChange && columnName) {
// 🔧 배열이면 무조건 쉼표 구분 문자열로 변환 (PostgreSQL 배열 형식 방지)
if (Array.isArray(value)) {
const stringValue = value.map((v) => (typeof v === "number" ? String(v) : v)).join(",");
onFormDataChange(columnName, stringValue);
} else {
onFormDataChange(columnName, value);
}
}
};
// 🔧 DynamicComponentRenderer에서 전달한 style/size를 우선 사용 (height 포함)
// restProps.style에 mergedStyle(height 변환됨)이 있고, restProps.size에도 size가 있음
const effectiveStyle = restProps.style || component.style;
const effectiveSize = restProps.size || component.size;
// 디버깅 필요시 주석 해제
// console.log("🔍 [V2SelectRenderer]", { componentId: component.id, effectiveStyle, effectiveSize });
const { style: _style, size: _size, allComponents: _allComp, ...restPropsClean } = restProps as any;
return (
<V2Select
id={component.id}
value={currentValue}
onChange={handleChange}
onFormDataChange={isInteractive ? onFormDataChange : undefined}
allComponents={allComponents}
config={{
mode: config.mode || "dropdown",
source: isCategoryType ? "category" : (config.source || "distinct"),
multiple: config.multiple || false,
searchable: config.searchable ?? true,
placeholder: config.placeholder || "선택하세요",
options: config.options || [],
codeGroup: config.codeGroup,
entityTable: config.entityTable,
entityLabelColumn: config.entityLabelColumn,
entityValueColumn: config.entityValueColumn,
categoryTable: config.categoryTable || (isCategoryType ? tableName : undefined),
categoryColumn: config.categoryColumn || (isCategoryType ? columnName : undefined),
}}
tableName={tableName}
columnName={columnName}
formData={formData}
isDesignMode={isDesignMode}
{...restPropsClean}
style={effectiveStyle}
size={effectiveSize}
label={component.label}
required={component.required || isColumnRequiredByMeta(tableName, columnName)}
readonly={config.readonly || component.readonly}
disabled={config.disabled || component.disabled}
/>
);
}
}
// 자동 등록 실행
V2SelectRenderer.registerSelf();
// Hot Reload 지원 (개발 모드)
if (process.env.NODE_ENV === "development") {
V2SelectRenderer.enableHotReload();
}