테스트 프로젝트 테이블 생성 및 오류들 수정

This commit is contained in:
kjs
2025-09-19 12:19:34 +09:00
parent eb6fa71cf4
commit d1e1c7964b
28 changed files with 2221 additions and 94 deletions

View File

@@ -185,6 +185,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
currentValue,
hasFormData: !!formData,
formDataKeys: formData ? Object.keys(formData) : [],
autoGeneration: component.autoGeneration,
hidden: component.hidden,
isInteractive,
});
return (
@@ -200,6 +203,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
config={component.componentConfig}
componentConfig={component.componentConfig}
value={currentValue} // formData에서 추출한 현재 값 전달
// 새로운 기능들 전달
autoGeneration={component.autoGeneration}
hidden={component.hidden}
// React 전용 props들은 직접 전달 (DOM에 전달되지 않음)
isInteractive={isInteractive}
formData={formData}

View File

@@ -1,6 +1,6 @@
"use client";
import React, { useState } from "react";
import React, { useState, useRef, useEffect } from "react";
import { ComponentRendererProps } from "@/types/component";
import { ButtonPrimaryConfig } from "./types";
import {
@@ -71,6 +71,21 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
config: any;
context: ButtonActionContext;
} | null>(null);
// 토스트 정리를 위한 ref
const currentLoadingToastRef = useRef<string | number | undefined>();
// 컴포넌트 언마운트 시 토스트 정리
useEffect(() => {
return () => {
if (currentLoadingToastRef.current !== undefined) {
console.log("🧹 컴포넌트 언마운트 시 토스트 정리");
toast.dismiss(currentLoadingToastRef.current);
currentLoadingToastRef.current = undefined;
}
};
}, []);
// 컴포넌트 설정
const componentConfig = {
...config,
@@ -84,13 +99,26 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
processedConfig.action = {
...DEFAULT_BUTTON_ACTIONS[actionType],
type: actionType,
// 🔥 제어관리 설정 추가 (webTypeConfig에서 가져옴)
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
dataflowConfig: component.webTypeConfig?.dataflowConfig,
};
} else if (componentConfig.action && typeof componentConfig.action === "object") {
// 🔥 이미 객체인 경우에도 제어관리 설정 추가
processedConfig.action = {
...componentConfig.action,
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
dataflowConfig: component.webTypeConfig?.dataflowConfig,
};
}
console.log("🔧 버튼 컴포넌트 설정:", {
originalConfig: componentConfig,
processedConfig,
component: component,
actionConfig: processedConfig.action,
webTypeConfig: component.webTypeConfig,
enableDataflowControl: component.webTypeConfig?.enableDataflowControl,
dataflowConfig: component.webTypeConfig?.dataflowConfig,
screenId,
tableName,
onRefresh,
@@ -119,13 +147,22 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
// 실제 액션 실행 함수
const executeAction = async (actionConfig: any, context: ButtonActionContext) => {
console.log("🚀 executeAction 시작:", { actionConfig, context });
let loadingToast: string | number | undefined;
try {
// 기존 토스트가 있다면 먼저 제거
if (currentLoadingToastRef.current !== undefined) {
console.log("📱 기존 토스트 제거");
toast.dismiss(currentLoadingToastRef.current);
currentLoadingToastRef.current = undefined;
}
// 추가 안전장치: 모든 로딩 토스트 제거
toast.dismiss();
// edit 액션을 제외하고만 로딩 토스트 표시
if (actionConfig.type !== "edit") {
console.log("📱 로딩 토스트 표시 시작");
loadingToast = toast.loading(
currentLoadingToastRef.current = toast.loading(
actionConfig.type === "save"
? "저장 중..."
: actionConfig.type === "delete"
@@ -133,8 +170,11 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
: actionConfig.type === "submit"
? "제출 중..."
: "처리 중...",
{
duration: Infinity, // 명시적으로 무한대로 설정
},
);
console.log("📱 로딩 토스트 ID:", loadingToast);
console.log("📱 로딩 토스트 ID:", currentLoadingToastRef.current);
}
console.log("⚡ ButtonActionExecutor.executeAction 호출 시작");
@@ -142,9 +182,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
console.log("⚡ ButtonActionExecutor.executeAction 완료, success:", success);
// 로딩 토스트 제거 (있는 경우에만)
if (loadingToast) {
console.log("📱 로딩 토스트 제거");
toast.dismiss(loadingToast);
if (currentLoadingToastRef.current !== undefined) {
console.log("📱 로딩 토스트 제거 시도, ID:", currentLoadingToastRef.current);
toast.dismiss(currentLoadingToastRef.current);
currentLoadingToastRef.current = undefined;
}
// edit 액션은 조용히 처리 (모달 열기만 하므로 토스트 불필요)
@@ -170,9 +211,10 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
console.log("❌ executeAction catch 블록 진입:", error);
// 로딩 토스트 제거
if (loadingToast) {
console.log("📱 오류 시 로딩 토스트 제거");
toast.dismiss(loadingToast);
if (currentLoadingToastRef.current !== undefined) {
console.log("📱 오류 시 로딩 토스트 제거, ID:", currentLoadingToastRef.current);
toast.dismiss(currentLoadingToastRef.current);
currentLoadingToastRef.current = undefined;
}
console.error("❌ 버튼 액션 실행 오류:", error);

View File

@@ -1,13 +1,17 @@
"use client";
import React from "react";
import React, { useEffect, useState } from "react";
import { ComponentRendererProps } from "@/types/component";
import { DateInputConfig } from "./types";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
import { AutoGenerationConfig } from "@/types/screen";
export interface DateInputComponentProps extends ComponentRendererProps {
config?: DateInputConfig;
value?: any; // 외부에서 전달받는 값
autoGeneration?: AutoGenerationConfig;
hidden?: boolean;
}
/**
@@ -28,6 +32,8 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
formData,
onFormDataChange,
value: externalValue, // 외부에서 전달받은 값
autoGeneration,
hidden,
...props
}) => {
// 컴포넌트 설정
@@ -36,14 +42,130 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
...component.config,
} as DateInputConfig;
// 🎯 자동생성 상태 관리
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
// 🚨 컴포넌트 마운트 확인용 로그
console.log("🚨 DateInputComponent 마운트됨!", {
componentId: component.id,
isInteractive,
isDesignMode,
autoGeneration,
componentAutoGeneration: component.autoGeneration,
externalValue,
formDataValue: formData?.[component.columnName || ""],
timestamp: new Date().toISOString(),
});
// 🧪 무조건 실행되는 테스트
useEffect(() => {
console.log("🧪 DateInputComponent 무조건 실행 테스트!");
const testDate = "2025-01-19"; // 고정된 테스트 날짜
setAutoGeneratedValue(testDate);
console.log("🧪 autoGeneratedValue 설정 완료:", testDate);
}, []); // 빈 의존성 배열로 한 번만 실행
// 자동생성 설정 (props 우선, 컴포넌트 설정 폴백)
const finalAutoGeneration = autoGeneration || component.autoGeneration;
const finalHidden = hidden !== undefined ? hidden : component.hidden;
// 🧪 테스트용 간단한 자동생성 로직
useEffect(() => {
console.log("🔍 DateInputComponent useEffect 실행:", {
componentId: component.id,
finalAutoGeneration,
enabled: finalAutoGeneration?.enabled,
type: finalAutoGeneration?.type,
isInteractive,
isDesignMode,
hasOnFormDataChange: !!onFormDataChange,
columnName: component.columnName,
currentFormValue: formData?.[component.columnName || ""],
});
// 🧪 테스트: 자동생성이 활성화되어 있으면 무조건 현재 날짜 설정
if (finalAutoGeneration?.enabled) {
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
console.log("🧪 테스트용 날짜 생성:", today);
setAutoGeneratedValue(today);
// 인터랙티브 모드에서 폼 데이터에도 설정
if (isInteractive && onFormDataChange && component.columnName) {
console.log("📤 테스트용 폼 데이터 업데이트:", component.columnName, today);
onFormDataChange(component.columnName, today);
}
}
// 원래 자동생성 로직 (주석 처리)
/*
if (finalAutoGeneration?.enabled && finalAutoGeneration.type !== "none") {
const fieldName = component.columnName || component.id;
const generatedValue = AutoGenerationUtils.generateValue(finalAutoGeneration, fieldName);
console.log("🎯 DateInputComponent 자동생성 시도:", {
componentId: component.id,
fieldName,
type: finalAutoGeneration.type,
options: finalAutoGeneration.options,
generatedValue,
isInteractive,
isDesignMode,
});
if (generatedValue) {
console.log("✅ DateInputComponent 자동생성 성공:", generatedValue);
setAutoGeneratedValue(generatedValue);
// 인터랙티브 모드에서 폼 데이터 업데이트
if (isInteractive && onFormDataChange && component.columnName) {
const currentValue = formData?.[component.columnName];
if (!currentValue) {
console.log("📤 DateInputComponent -> onFormDataChange 호출:", component.columnName, generatedValue);
onFormDataChange(component.columnName, generatedValue);
} else {
console.log("⚠️ DateInputComponent 기존 값이 있어서 자동생성 스킵:", currentValue);
}
} else {
console.log("⚠️ DateInputComponent 자동생성 조건 불만족:", {
isInteractive,
hasOnFormDataChange: !!onFormDataChange,
hasColumnName: !!component.columnName,
});
}
} else {
console.log("❌ DateInputComponent 자동생성 실패: generatedValue가 null");
}
} else {
console.log("⚠️ DateInputComponent 자동생성 비활성화:", {
enabled: finalAutoGeneration?.enabled,
type: finalAutoGeneration?.type,
});
}
*/
}, [
finalAutoGeneration?.enabled,
finalAutoGeneration?.type,
finalAutoGeneration?.options,
component.id,
component.columnName,
isInteractive,
]);
// 날짜 값 계산 및 디버깅
const fieldName = component.columnName || component.id;
const rawValue =
externalValue !== undefined
? externalValue
: isInteractive && formData && component.columnName
? formData[component.columnName]
: component.value;
// 값 우선순위: externalValue > formData > autoGeneratedValue > component.value
let rawValue: any;
if (externalValue !== undefined) {
rawValue = externalValue;
} else if (isInteractive && formData && component.columnName && formData[component.columnName]) {
rawValue = formData[component.columnName];
} else if (autoGeneratedValue) {
rawValue = autoGeneratedValue;
} else {
rawValue = component.value;
}
console.log("🔍 DateInputComponent 값 디버깅:", {
componentId: component.id,
@@ -196,10 +318,14 @@ export const DateInputComponent: React.FC<DateInputComponentProps> = ({
<input
type="date"
value={formattedValue}
placeholder={componentConfig.placeholder || ""}
placeholder={
finalAutoGeneration?.enabled
? `자동생성: ${AutoGenerationUtils.getTypeDescription(finalAutoGeneration.type)}`
: componentConfig.placeholder || ""
}
disabled={componentConfig.disabled || false}
required={componentConfig.required || false}
readOnly={componentConfig.readonly || false}
readOnly={componentConfig.readonly || finalAutoGeneration?.enabled || false}
style={{
width: "100%",
height: "100%",

View File

@@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { DateInputConfig } from "./types";
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
export interface DateInputConfigPanelProps {
config: DateInputConfig;
@@ -16,21 +18,17 @@ export interface DateInputConfigPanelProps {
* DateInput 설정 패널
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
*/
export const DateInputConfigPanel: React.FC<DateInputConfigPanelProps> = ({
config,
onChange,
}) => {
export const DateInputConfigPanel: React.FC<DateInputConfigPanelProps> = ({ config, onChange }) => {
const handleChange = (key: keyof DateInputConfig, value: any) => {
console.log("🔧 DateInputConfigPanel.handleChange:", { key, value });
onChange({ [key]: value });
};
return (
<div className="space-y-4">
<div className="text-sm font-medium">
date-input
</div>
<div className="text-sm font-medium">date-input </div>
{/* date 관련 설정 */}
{/* date 관련 설정 */}
<div className="space-y-2">
<Label htmlFor="placeholder"></Label>
<Input
@@ -67,6 +65,194 @@ export const DateInputConfigPanel: React.FC<DateInputConfigPanelProps> = ({
onCheckedChange={(checked) => handleChange("readonly", checked)}
/>
</div>
{/* 숨김 기능 */}
<div className="space-y-2">
<Label htmlFor="hidden"></Label>
<Checkbox
id="hidden"
checked={config.hidden || false}
onCheckedChange={(checked) => handleChange("hidden", checked)}
/>
<p className="text-xs text-gray-500"> </p>
</div>
{/* 자동생성 기능 */}
<div className="space-y-3 border-t pt-3">
<div className="space-y-2">
<Label htmlFor="autoGeneration"></Label>
<Checkbox
id="autoGeneration"
checked={config.autoGeneration?.enabled || false}
onCheckedChange={(checked) => {
const newAutoGeneration: AutoGenerationConfig = {
...config.autoGeneration,
enabled: checked as boolean,
type: config.autoGeneration?.type || "current_time",
};
handleChange("autoGeneration", newAutoGeneration);
}}
/>
</div>
{config.autoGeneration?.enabled && (
<>
<div className="space-y-2">
<Label htmlFor="autoGenerationType"> </Label>
<Select
value={config.autoGeneration?.type || "current_time"}
onValueChange={(value: AutoGenerationType) => {
const newAutoGeneration: AutoGenerationConfig = {
...config.autoGeneration,
type: value,
options: value === "current_time" ? { format: "date" } : {},
};
handleChange("autoGeneration", newAutoGeneration);
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="current_time"> /</SelectItem>
<SelectItem value="uuid">UUID</SelectItem>
<SelectItem value="current_user"> </SelectItem>
<SelectItem value="sequence"> </SelectItem>
<SelectItem value="random_string"> </SelectItem>
<SelectItem value="random_number"> </SelectItem>
<SelectItem value="company_code"> </SelectItem>
<SelectItem value="department"> </SelectItem>
</SelectContent>
</Select>
<p className="text-xs text-gray-500">
{AutoGenerationUtils.getTypeDescription(config.autoGeneration?.type || "current_time")}
</p>
</div>
{config.autoGeneration?.type === "current_time" && (
<div className="space-y-2">
<Label htmlFor="dateFormat"> </Label>
<Select
value={config.autoGeneration?.options?.format || "date"}
onValueChange={(value) => {
const newAutoGeneration: AutoGenerationConfig = {
...config.autoGeneration!,
options: {
...config.autoGeneration?.options,
format: value,
},
};
handleChange("autoGeneration", newAutoGeneration);
}}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="date"> (YYYY-MM-DD)</SelectItem>
<SelectItem value="datetime">+ (YYYY-MM-DD HH:mm:ss)</SelectItem>
<SelectItem value="time"> (HH:mm:ss)</SelectItem>
<SelectItem value="timestamp"></SelectItem>
</SelectContent>
</Select>
</div>
)}
{(config.autoGeneration?.type === "sequence" ||
config.autoGeneration?.type === "random_string" ||
config.autoGeneration?.type === "random_number") && (
<>
{config.autoGeneration?.type === "sequence" && (
<div className="space-y-2">
<Label htmlFor="startValue"></Label>
<Input
id="startValue"
type="number"
value={config.autoGeneration?.options?.startValue || 1}
onChange={(e) => {
const newAutoGeneration: AutoGenerationConfig = {
...config.autoGeneration!,
options: {
...config.autoGeneration?.options,
startValue: parseInt(e.target.value) || 1,
},
};
handleChange("autoGeneration", newAutoGeneration);
}}
/>
</div>
)}
{(config.autoGeneration?.type === "random_string" ||
config.autoGeneration?.type === "random_number") && (
<div className="space-y-2">
<Label htmlFor="length"></Label>
<Input
id="length"
type="number"
value={
config.autoGeneration?.options?.length ||
(config.autoGeneration?.type === "random_string" ? 8 : 6)
}
onChange={(e) => {
const newAutoGeneration: AutoGenerationConfig = {
...config.autoGeneration!,
options: {
...config.autoGeneration?.options,
length: parseInt(e.target.value) || 8,
},
};
handleChange("autoGeneration", newAutoGeneration);
}}
/>
</div>
)}
<div className="grid grid-cols-2 gap-2">
<div className="space-y-2">
<Label htmlFor="prefix"></Label>
<Input
id="prefix"
value={config.autoGeneration?.options?.prefix || ""}
onChange={(e) => {
const newAutoGeneration: AutoGenerationConfig = {
...config.autoGeneration!,
options: {
...config.autoGeneration?.options,
prefix: e.target.value,
},
};
handleChange("autoGeneration", newAutoGeneration);
}}
/>
</div>
<div className="space-y-2">
<Label htmlFor="suffix"></Label>
<Input
id="suffix"
value={config.autoGeneration?.options?.suffix || ""}
onChange={(e) => {
const newAutoGeneration: AutoGenerationConfig = {
...config.autoGeneration!,
options: {
...config.autoGeneration?.options,
suffix: e.target.value,
},
};
handleChange("autoGeneration", newAutoGeneration);
}}
/>
</div>
</div>
</>
)}
<div className="rounded bg-gray-50 p-2 text-xs">
<strong>:</strong> {AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
</div>
</>
)}
</div>
</div>
);
};

View File

@@ -13,17 +13,24 @@ export class DateInputRenderer extends AutoRegisteringComponentRenderer {
static componentDefinition = DateInputDefinition;
render(): React.ReactElement {
console.log("🎯 DateInputRenderer.render() 호출:", {
componentId: this.props.component?.id,
autoGeneration: this.props.autoGeneration,
componentAutoGeneration: this.props.component?.autoGeneration,
allProps: Object.keys(this.props),
});
return <DateInputComponent {...this.props} renderer={this} />;
}
/**
* 컴포넌트별 특화 메서드들
*/
// date 타입 특화 속성 처리
protected getDateInputProps() {
const baseProps = this.getWebTypeProps();
// date 타입에 특화된 추가 속성들
return {
...baseProps,

View File

@@ -4,7 +4,7 @@ import React from "react";
import { createComponentDefinition } from "../../utils/createComponentDefinition";
import { ComponentCategory } from "@/types/component";
import type { WebType } from "@/types/screen";
import { DateInputWrapper } from "./DateInputComponent";
import { DateInputComponent } from "./DateInputComponent";
import { DateInputConfigPanel } from "./DateInputConfigPanel";
import { DateInputConfig } from "./types";
@@ -19,7 +19,7 @@ export const DateInputDefinition = createComponentDefinition({
description: "날짜 선택을 위한 날짜 선택기 컴포넌트",
category: ComponentCategory.INPUT,
webType: "date",
component: DateInputWrapper,
component: DateInputComponent,
defaultConfig: {
placeholder: "입력하세요",
},

View File

@@ -1,25 +1,29 @@
"use client";
import { ComponentConfig } from "@/types/component";
import { AutoGenerationConfig } from "@/types/screen";
/**
* DateInput 컴포넌트 설정 타입
*/
export interface DateInputConfig extends ComponentConfig {
// date 관련 설정
// date 관련 설정
placeholder?: string;
// 공통 설정
disabled?: boolean;
required?: boolean;
readonly?: boolean;
placeholder?: string;
helperText?: string;
// 자동생성 및 숨김 기능
autoGeneration?: AutoGenerationConfig;
hidden?: boolean;
// 스타일 관련
variant?: "default" | "outlined" | "filled";
size?: "sm" | "md" | "lg";
// 이벤트 관련
onChange?: (value: any) => void;
onFocus?: () => void;
@@ -37,7 +41,7 @@ export interface DateInputProps {
config?: DateInputConfig;
className?: string;
style?: React.CSSProperties;
// 이벤트 핸들러
onChange?: (value: any) => void;
onFocus?: () => void;

View File

@@ -1,9 +1,10 @@
"use client";
import React from "react";
import { ComponentRendererProps } from "@/types/component";
import React, { useEffect, useState } from "react";
import { ComponentRendererProps, AutoGenerationConfig } from "@/types/component";
import { NumberInputConfig } from "./types";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
export interface NumberInputComponentProps extends ComponentRendererProps {
config?: NumberInputConfig;

View File

@@ -539,7 +539,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
}, [refreshKey]);
// 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가)
// 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능)
const visibleColumns = useMemo(() => {
// 기본값 처리: checkbox 설정이 없으면 기본값 사용
const checkboxConfig = tableConfig.checkbox || {
@@ -554,9 +554,27 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (!displayColumns || displayColumns.length === 0) {
// displayColumns가 아직 설정되지 않은 경우 기본 컬럼 사용
if (!tableConfig.columns) return [];
columns = tableConfig.columns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
columns = tableConfig.columns
.filter((col) => {
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
if (isDesignMode) {
return col.visible; // 디자인 모드에서는 visible만 체크
} else {
return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만
}
})
.sort((a, b) => a.order - b.order);
} else {
columns = displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
columns = displayColumns
.filter((col) => {
// 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김
if (isDesignMode) {
return col.visible; // 디자인 모드에서는 visible만 체크
} else {
return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만
}
})
.sort((a, b) => a.order - b.order);
}
// 체크박스가 활성화된 경우 체크박스 컬럼을 추가
@@ -876,6 +894,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
: "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
`text-${column.align}`,
column.sortable && "hover:bg-gray-50",
// 숨김 컬럼 스타일 (디자인 모드에서만)
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
)}
style={{
minWidth: `${getColumnWidth(column)}px`,
@@ -963,13 +983,22 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
{columnsByPosition.normal.map((column) => (
<th
key={`normal-${column.columnName}`}
style={{ minWidth: `${getColumnWidth(column)}px` }}
style={{
minWidth: `${getColumnWidth(column)}px`,
minHeight: "48px",
height: "48px",
verticalAlign: "middle",
lineHeight: "1",
boxSizing: "border-box",
}}
className={cn(
column.columnName === "__checkbox__"
? "h-12 border-b px-4 py-3 text-center"
: "cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
? "h-12 border-b px-4 py-3 text-center align-middle"
: "cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
`text-${column.align}`,
column.sortable && "hover:bg-gray-50",
// 숨김 컬럼 스타일 (디자인 모드에서만)
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
)}
onClick={() => column.sortable && handleSort(column.columnName)}
>
@@ -1056,6 +1085,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
: "h-12 cursor-pointer border-b px-4 py-3 text-left align-middle font-medium whitespace-nowrap text-gray-900 select-none",
`text-${column.align}`,
column.sortable && "hover:bg-gray-50",
// 숨김 컬럼 스타일 (디자인 모드에서만)
isDesignMode && column.hidden && "bg-gray-100/50 opacity-40",
)}
style={{
minWidth: `${getColumnWidth(column)}px`,

View File

@@ -11,6 +11,35 @@ export interface EntityJoinInfo {
joinAlias: string;
}
/**
* 자동생성 타입 정의
*/
export type AutoGenerationType =
| "uuid" // UUID 생성
| "current_user" // 현재 사용자 ID
| "current_time" // 현재 시간
| "sequence" // 시퀀스 번호
| "random_string" // 랜덤 문자열
| "random_number" // 랜덤 숫자
| "company_code" // 회사 코드
| "department" // 부서 코드
| "none"; // 자동생성 없음
/**
* 자동생성 설정
*/
export interface AutoGenerationConfig {
type: AutoGenerationType;
enabled: boolean;
options?: {
length?: number; // 랜덤 문자열/숫자 길이
prefix?: string; // 접두사
suffix?: string; // 접미사
format?: string; // 시간 형식 (current_time용)
startValue?: number; // 시퀀스 시작값
};
}
/**
* 테이블 컬럼 설정
*/
@@ -31,6 +60,10 @@ export interface ColumnConfig {
// 컬럼 고정 관련 속성
fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함)
fixedOrder?: number; // 고정된 컬럼들 내에서의 순서
// 새로운 기능들
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
}
/**

View File

@@ -1,9 +1,11 @@
"use client";
import React from "react";
import React, { useEffect, useState } from "react";
import { ComponentRendererProps } from "@/types/component";
import { AutoGenerationConfig } from "@/types/screen";
import { TextInputConfig } from "./types";
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
export interface TextInputComponentProps extends ComponentRendererProps {
config?: TextInputConfig;
@@ -34,18 +36,112 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
...component.config,
} as TextInputConfig;
// 자동생성 설정 (props에서 전달받은 값 우선 사용)
const autoGeneration: AutoGenerationConfig = props.autoGeneration ||
component.autoGeneration ||
componentConfig.autoGeneration || {
type: "none",
enabled: false,
};
// 숨김 상태 (props에서 전달받은 값 우선 사용)
const isHidden = props.hidden !== undefined ? props.hidden : component.hidden || componentConfig.hidden || false;
// 자동생성된 값 상태
const [autoGeneratedValue, setAutoGeneratedValue] = useState<string>("");
// 테스트용: 컴포넌트 라벨에 "test"가 포함되면 강제로 UUID 자동생성 활성화
const testAutoGeneration = component.label?.toLowerCase().includes("test")
? {
type: "uuid" as AutoGenerationType,
enabled: true,
}
: autoGeneration;
console.log("🔧 텍스트 입력 컴포넌트 설정:", {
config,
componentConfig,
component: component,
autoGeneration,
testAutoGeneration,
isTestMode: component.label?.toLowerCase().includes("test"),
isHidden,
isInteractive,
formData,
columnName: component.columnName,
currentFormValue: formData?.[component.columnName],
componentValue: component.value,
autoGeneratedValue,
});
// 자동생성 값 생성 (컴포넌트 마운트 시 또는 폼 데이터 변경 시)
useEffect(() => {
console.log("🔄 자동생성 useEffect 실행:", {
enabled: testAutoGeneration.enabled,
type: testAutoGeneration.type,
isInteractive,
columnName: component.columnName,
hasFormData: !!formData,
hasOnFormDataChange: !!onFormDataChange,
});
if (testAutoGeneration.enabled && testAutoGeneration.type !== "none") {
// 폼 데이터에 이미 값이 있으면 자동생성하지 않음
const currentFormValue = formData?.[component.columnName];
const currentComponentValue = component.value;
console.log("🔍 자동생성 조건 확인:", {
currentFormValue,
currentComponentValue,
hasCurrentValue: !!(currentFormValue || currentComponentValue),
autoGeneratedValue,
});
// 자동생성된 값이 없고, 현재 값도 없을 때만 생성
if (!autoGeneratedValue && !currentFormValue && !currentComponentValue) {
const generatedValue = AutoGenerationUtils.generateValue(testAutoGeneration, component.columnName);
console.log("✨ 자동생성된 값:", generatedValue);
if (generatedValue) {
setAutoGeneratedValue(generatedValue);
// 폼 데이터에 자동생성된 값 설정 (인터랙티브 모드에서만)
if (isInteractive && onFormDataChange && component.columnName) {
console.log("📝 폼 데이터에 자동생성 값 설정:", {
columnName: component.columnName,
value: generatedValue,
});
onFormDataChange(component.columnName, generatedValue);
}
}
} else if (!autoGeneratedValue && testAutoGeneration.type !== "none") {
// 디자인 모드에서도 미리보기용 자동생성 값 표시
const previewValue = AutoGenerationUtils.generatePreviewValue(testAutoGeneration);
console.log("🎨 디자인 모드 미리보기 값:", previewValue);
setAutoGeneratedValue(previewValue);
} else {
console.log("⏭️ 이미 값이 있어서 자동생성 건너뜀:", {
hasAutoGenerated: !!autoGeneratedValue,
hasFormValue: !!currentFormValue,
hasComponentValue: !!currentComponentValue,
});
}
}
}, [testAutoGeneration, isInteractive, component.columnName, component.value, formData, onFormDataChange]);
// 스타일 계산 (위치는 RealtimePreviewDynamic에서 처리하므로 제외)
const componentStyle: React.CSSProperties = {
width: "100%",
height: "100%",
...component.style,
...style,
// 숨김 기능: 디자인 모드에서는 연하게, 실제 화면에서는 완전히 숨김
...(isHidden && {
opacity: isDesignMode ? 0.4 : 0,
backgroundColor: isDesignMode ? "#f3f4f6" : "transparent",
pointerEvents: isDesignMode ? "auto" : "none",
display: isDesignMode ? "block" : "none",
}),
};
// 디자인 모드 스타일
@@ -105,15 +201,37 @@ export const TextInputComponent: React.FC<TextInputComponentProps> = ({
<input
type="text"
value={
isInteractive && formData && component.columnName
? formData[component.columnName] || ""
: component.value || ""
value={(() => {
let displayValue = "";
if (isInteractive && formData && component.columnName) {
// 인터랙티브 모드: formData 우선, 없으면 자동생성 값
displayValue = formData[component.columnName] || autoGeneratedValue || "";
} else {
// 디자인 모드: component.value 우선, 없으면 자동생성 값
displayValue = component.value || autoGeneratedValue || "";
}
console.log("📄 Input 값 계산:", {
isInteractive,
hasFormData: !!formData,
columnName: component.columnName,
formDataValue: formData?.[component.columnName],
componentValue: component.value,
autoGeneratedValue,
finalDisplayValue: displayValue,
});
return displayValue;
})()}
placeholder={
testAutoGeneration.enabled && testAutoGeneration.type !== "none"
? `자동생성: ${AutoGenerationUtils.getTypeDescription(testAutoGeneration.type)}`
: componentConfig.placeholder || ""
}
placeholder={componentConfig.placeholder || ""}
disabled={componentConfig.disabled || false}
required={componentConfig.required || false}
readOnly={componentConfig.readonly || false}
readOnly={componentConfig.readonly || (testAutoGeneration.enabled && testAutoGeneration.type !== "none")}
style={{
width: "100%",
height: "100%",

View File

@@ -6,6 +6,8 @@ import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { TextInputConfig } from "./types";
import { AutoGenerationType, AutoGenerationConfig } from "@/types/screen";
import { AutoGenerationUtils } from "@/lib/utils/autoGeneration";
export interface TextInputConfigPanelProps {
config: TextInputConfig;
@@ -16,21 +18,16 @@ export interface TextInputConfigPanelProps {
* TextInput 설정 패널
* 컴포넌트의 설정값들을 편집할 수 있는 UI 제공
*/
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({
config,
onChange,
}) => {
export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({ config, onChange }) => {
const handleChange = (key: keyof TextInputConfig, value: any) => {
onChange({ [key]: value });
};
return (
<div className="space-y-4">
<div className="text-sm font-medium">
text-input
</div>
<div className="text-sm font-medium">text-input </div>
{/* 텍스트 관련 설정 */}
{/* 텍스트 관련 설정 */}
<div className="space-y-2">
<Label htmlFor="placeholder"></Label>
<Input
@@ -77,6 +74,163 @@ export const TextInputConfigPanel: React.FC<TextInputConfigPanelProps> = ({
onCheckedChange={(checked) => handleChange("readonly", checked)}
/>
</div>
{/* 구분선 */}
<div className="border-t pt-4">
<div className="mb-3 text-sm font-medium"> </div>
{/* 숨김 기능 */}
<div className="space-y-2">
<Label htmlFor="hidden"> ( , )</Label>
<Checkbox
id="hidden"
checked={config.hidden || false}
onCheckedChange={(checked) => handleChange("hidden", checked)}
/>
</div>
{/* 자동생성 기능 */}
<div className="space-y-2">
<Label htmlFor="autoGeneration"> </Label>
<Checkbox
id="autoGeneration"
checked={config.autoGeneration?.enabled || false}
onCheckedChange={(checked) => {
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
handleChange("autoGeneration", {
...currentConfig,
enabled: checked as boolean,
});
}}
/>
</div>
{/* 자동생성 타입 선택 */}
{config.autoGeneration?.enabled && (
<div className="space-y-2">
<Label htmlFor="autoGenerationType"> </Label>
<Select
value={config.autoGeneration?.type || "none"}
onValueChange={(value: AutoGenerationType) => {
const currentConfig = config.autoGeneration || { type: "none", enabled: false };
handleChange("autoGeneration", {
...currentConfig,
type: value,
});
}}
>
<SelectTrigger>
<SelectValue placeholder="자동생성 타입 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
<SelectItem value="uuid">UUID </SelectItem>
<SelectItem value="current_user"> ID</SelectItem>
<SelectItem value="current_time"> </SelectItem>
<SelectItem value="sequence"> </SelectItem>
<SelectItem value="random_string"> </SelectItem>
<SelectItem value="random_number"> </SelectItem>
<SelectItem value="company_code"> </SelectItem>
<SelectItem value="department"> </SelectItem>
</SelectContent>
</Select>
{/* 선택된 타입 설명 */}
{config.autoGeneration?.type && config.autoGeneration.type !== "none" && (
<div className="text-xs text-gray-500">
{AutoGenerationUtils.getTypeDescription(config.autoGeneration.type)}
</div>
)}
</div>
)}
{/* 자동생성 옵션 */}
{config.autoGeneration?.enabled &&
config.autoGeneration?.type &&
["random_string", "random_number", "sequence"].includes(config.autoGeneration.type) && (
<div className="space-y-2">
<Label> </Label>
{/* 길이 설정 (랜덤 문자열/숫자용) */}
{["random_string", "random_number"].includes(config.autoGeneration.type) && (
<div className="space-y-1">
<Label htmlFor="autoGenLength" className="text-xs">
</Label>
<Input
id="autoGenLength"
type="number"
min="1"
max="50"
value={
config.autoGeneration?.options?.length || (config.autoGeneration.type === "random_string" ? 8 : 6)
}
onChange={(e) => {
const currentConfig = config.autoGeneration!;
handleChange("autoGeneration", {
...currentConfig,
options: {
...currentConfig.options,
length: parseInt(e.target.value) || 8,
},
});
}}
/>
</div>
)}
{/* 접두사 */}
<div className="space-y-1">
<Label htmlFor="autoGenPrefix" className="text-xs">
</Label>
<Input
id="autoGenPrefix"
value={config.autoGeneration?.options?.prefix || ""}
onChange={(e) => {
const currentConfig = config.autoGeneration!;
handleChange("autoGeneration", {
...currentConfig,
options: {
...currentConfig.options,
prefix: e.target.value,
},
});
}}
/>
</div>
{/* 접미사 */}
<div className="space-y-1">
<Label htmlFor="autoGenSuffix" className="text-xs">
</Label>
<Input
id="autoGenSuffix"
value={config.autoGeneration?.options?.suffix || ""}
onChange={(e) => {
const currentConfig = config.autoGeneration!;
handleChange("autoGeneration", {
...currentConfig,
options: {
...currentConfig.options,
suffix: e.target.value,
},
});
}}
/>
</div>
{/* 미리보기 */}
<div className="space-y-1">
<Label className="text-xs"></Label>
<div className="rounded border bg-gray-100 p-2 text-xs">
{AutoGenerationUtils.generatePreviewValue(config.autoGeneration)}
</div>
</div>
</div>
)}
</div>
</div>
);
};

View File

@@ -1,32 +1,37 @@
"use client";
import { ComponentConfig } from "@/types/component";
import { AutoGenerationConfig } from "@/types/screen";
/**
* TextInput 컴포넌트 설정 타입
*/
export interface TextInputConfig extends ComponentConfig {
// 텍스트 관련 설정
// 텍스트 관련 설정
placeholder?: string;
maxLength?: number;
minLength?: number;
// 공통 설정
disabled?: boolean;
required?: boolean;
readonly?: boolean;
placeholder?: string;
helperText?: string;
// 스타일 관련
variant?: "default" | "outlined" | "filled";
size?: "sm" | "md" | "lg";
// 이벤트 관련
onChange?: (value: any) => void;
onFocus?: () => void;
onBlur?: () => void;
onClick?: () => void;
// 새로운 기능들
hidden?: boolean; // 숨김 기능 (편집기에서는 연하게, 실제 화면에서는 숨김)
autoGeneration?: AutoGenerationConfig; // 자동생성 설정
}
/**
@@ -39,7 +44,7 @@ export interface TextInputProps {
config?: TextInputConfig;
className?: string;
style?: React.CSSProperties;
// 이벤트 핸들러
onChange?: (value: any) => void;
onFocus?: () => void;