컴포넌트 리뉴얼 1.0

This commit is contained in:
kjs
2025-12-19 15:44:38 +09:00
parent 2487c79a61
commit 91d00aa784
61 changed files with 11678 additions and 175 deletions

View File

@@ -20,6 +20,84 @@ import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
import { useSplitPanelContext } from "@/contexts/SplitPanelContext";
// 조건부 표시 평가 함수
function evaluateConditional(
conditional: ComponentData["conditional"],
formData: Record<string, any>,
allComponents: ComponentData[],
): { visible: boolean; disabled: boolean } {
if (!conditional || !conditional.enabled) {
return { visible: true, disabled: false };
}
const { field, operator, value, action } = conditional;
// 참조 필드의 현재 값 가져오기
// 필드 ID로 컴포넌트를 찾아 columnName 또는 id로 formData에서 값 조회
const refComponent = allComponents.find((c) => c.id === field);
const fieldName = (refComponent as any)?.columnName || field;
const fieldValue = formData[fieldName];
// 조건 평가
let conditionMet = false;
switch (operator) {
case "=":
conditionMet = fieldValue === value || String(fieldValue) === String(value);
break;
case "!=":
conditionMet = fieldValue !== value && String(fieldValue) !== String(value);
break;
case ">":
conditionMet = Number(fieldValue) > Number(value);
break;
case "<":
conditionMet = Number(fieldValue) < Number(value);
break;
case "in":
if (Array.isArray(value)) {
conditionMet = value.includes(fieldValue) || value.map(String).includes(String(fieldValue));
}
break;
case "notIn":
if (Array.isArray(value)) {
conditionMet = !value.includes(fieldValue) && !value.map(String).includes(String(fieldValue));
} else {
conditionMet = true;
}
break;
case "isEmpty":
conditionMet =
fieldValue === null ||
fieldValue === undefined ||
fieldValue === "" ||
(Array.isArray(fieldValue) && fieldValue.length === 0);
break;
case "isNotEmpty":
conditionMet =
fieldValue !== null &&
fieldValue !== undefined &&
fieldValue !== "" &&
!(Array.isArray(fieldValue) && fieldValue.length === 0);
break;
default:
conditionMet = true;
}
// 액션에 따른 결과 반환
switch (action) {
case "show":
return { visible: conditionMet, disabled: false };
case "hide":
return { visible: !conditionMet, disabled: false };
case "enable":
return { visible: true, disabled: !conditionMet };
case "disable":
return { visible: true, disabled: conditionMet };
default:
return { visible: true, disabled: false };
}
}
// 컴포넌트 렌더러들을 강제로 로드하여 레지스트리에 등록
import "@/lib/registry/components/ButtonRenderer";
import "@/lib/registry/components/CardRenderer";
@@ -56,7 +134,7 @@ interface InteractiveScreenViewerProps {
// 원본 데이터 (수정 모드에서 UPDATE 판단용)
originalData?: Record<string, any> | null;
// 탭 관련 정보 (탭 내부의 컴포넌트에서 사용)
parentTabId?: string; // 부모 탭 ID
parentTabId?: string; // 부모 탭 ID
parentTabsComponentId?: string; // 부모 탭 컴포넌트 ID
}
@@ -334,6 +412,14 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
// 동적 대화형 위젯 렌더링
const renderInteractiveWidget = (comp: ComponentData) => {
// 조건부 표시 평가
const conditionalResult = evaluateConditional(comp.conditional, formData, allComponents);
// 조건에 따라 숨김 처리
if (!conditionalResult.visible) {
return null;
}
// 데이터 테이블 컴포넌트 처리
if (isDataTableComponent(comp)) {
return (
@@ -431,6 +517,9 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
});
};
// 조건부 비활성화 적용
const isConditionallyDisabled = conditionalResult.disabled;
// 동적 웹타입 렌더링 사용
if (widgetType) {
try {
@@ -444,7 +533,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
onFormDataChange: handleFormDataChange,
formData: formData, // 🆕 전체 formData 전달
isInteractive: true,
readonly: readonly,
readonly: readonly || isConditionallyDisabled, // 조건부 비활성화 적용
disabled: isConditionallyDisabled, // 조건부 비활성화 전달
required: required,
placeholder: placeholder,
className: "w-full h-full",
@@ -470,7 +560,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
value={currentValue}
onChange={(e) => handleFormDataChange(fieldName, e.target.value)}
placeholder={`${widgetType} (렌더링 오류)`}
disabled={readonly}
disabled={readonly || isConditionallyDisabled}
required={required}
className="h-full w-full"
/>
@@ -486,7 +576,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
value={currentValue}
onChange={(e) => handleFormDataChange(fieldName, e.target.value)}
placeholder={placeholder || "입력하세요"}
disabled={readonly}
disabled={readonly || isConditionallyDisabled}
required={required}
className="h-full w-full"
/>
@@ -593,7 +683,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
const handleQuickInsertAction = async () => {
// componentConfig에서 quickInsertConfig 가져오기
const quickInsertConfig = (comp as any).componentConfig?.action?.quickInsertConfig;
if (!quickInsertConfig?.targetTable) {
toast.error("대상 테이블이 설정되지 않았습니다.");
return;
@@ -604,7 +694,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
try {
const { default: apiClient } = await import("@/lib/api/client");
const columnsResponse = await apiClient.get(
`/table-management/tables/${quickInsertConfig.targetTable}/columns`
`/table-management/tables/${quickInsertConfig.targetTable}/columns`,
);
if (columnsResponse.data?.success && columnsResponse.data?.data) {
const columnsData = columnsResponse.data.data.columns || columnsResponse.data.data;
@@ -618,7 +708,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
// 2. 컬럼 매핑에서 값 수집
const insertData: Record<string, any> = {};
const columnMappings = quickInsertConfig.columnMappings || [];
for (const mapping of columnMappings) {
let value: any;
@@ -681,31 +771,31 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
if (splitPanelContext?.selectedLeftData && targetTableColumns.length > 0) {
const leftData = splitPanelContext.selectedLeftData;
console.log("📍 좌측 패널 자동 매핑 시작:", leftData);
for (const [key, val] of Object.entries(leftData)) {
// 이미 매핑된 컬럼은 스킵
if (insertData[key] !== undefined) {
continue;
}
// 대상 테이블에 해당 컬럼이 없으면 스킵
if (!targetTableColumns.includes(key)) {
continue;
}
// 시스템 컬럼 제외
const systemColumns = ['id', 'created_date', 'updated_date', 'writer', 'writer_name'];
const systemColumns = ["id", "created_date", "updated_date", "writer", "writer_name"];
if (systemColumns.includes(key)) {
continue;
}
// _label, _name 으로 끝나는 표시용 컬럼 제외
if (key.endsWith('_label') || key.endsWith('_name')) {
if (key.endsWith("_label") || key.endsWith("_name")) {
continue;
}
// 값이 있으면 자동 추가
if (val !== undefined && val !== null && val !== '') {
if (val !== undefined && val !== null && val !== "") {
insertData[key] = val;
console.log(`📍 자동 매핑 추가: ${key} = ${val}`);
}
@@ -724,7 +814,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
if (quickInsertConfig.duplicateCheck?.enabled && quickInsertConfig.duplicateCheck?.columns?.length > 0) {
try {
const { default: apiClient } = await import("@/lib/api/client");
// 중복 체크를 위한 검색 조건 구성
const searchConditions: Record<string, any> = {};
for (const col of quickInsertConfig.duplicateCheck.columns) {
@@ -736,14 +826,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
console.log("📍 중복 체크 조건:", searchConditions);
// 기존 데이터 조회
const checkResponse = await apiClient.post(
`/table-management/tables/${quickInsertConfig.targetTable}/data`,
{
page: 1,
pageSize: 1,
search: searchConditions,
}
);
const checkResponse = await apiClient.post(`/table-management/tables/${quickInsertConfig.targetTable}/data`, {
page: 1,
pageSize: 1,
search: searchConditions,
});
console.log("📍 중복 체크 응답:", checkResponse.data);
@@ -765,7 +852,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
const response = await apiClient.post(
`/table-management/tables/${quickInsertConfig.targetTable}/add`,
insertData
insertData,
);
if (response.data?.success) {
@@ -1000,7 +1087,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
{popupScreen && (
<Dialog open={!!popupScreen} onOpenChange={() => setPopupScreen(null)}>
<DialogContent
className="overflow-hidden p-0 max-w-none"
className="max-w-none overflow-hidden p-0"
style={{
width: popupScreen.size === "small" ? "600px" : popupScreen.size === "large" ? "1400px" : "1000px",
height: "800px",