feat: 개별 위젯 컴포넌트 입력 타입 및 자동 값 설정 기능 추가
- BaseComponent에 inputType, autoValueType 속성 추가 - DetailSettingsPanel에 입력 타입 및 자동 값 타입 선택 UI 추가 - RealtimePreview에서 자동 값 타입별 값 생성 및 표시 로직 구현 - 텍스트, 숫자, 날짜 위젯에서 7가지 자동 값 타입 지원 - 현재 날짜시간, 현재 날짜, 현재 시간 - 현재 사용자, UUID, 시퀀스, 사용자 정의 - 자동입력 모드에서 읽기 전용 스타일 적용 (회색 배경) - 백엔드 API에 input_type 처리 로직 추가 - TableTypeSelector에 입력 타입 설정 UI 추가
This commit is contained in:
@@ -206,6 +206,10 @@ export default function ScreenViewPage() {
|
||||
}));
|
||||
}}
|
||||
hideLabel={true} // 라벨 숨김 플래그 전달
|
||||
screenInfo={{
|
||||
id: screenId,
|
||||
tableName: screen?.tableName,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
ButtonTypeConfig,
|
||||
} from "@/types/screen";
|
||||
import { InteractiveDataTable } from "./InteractiveDataTable";
|
||||
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
interface InteractiveScreenViewerProps {
|
||||
component: ComponentData;
|
||||
@@ -35,6 +37,10 @@ interface InteractiveScreenViewerProps {
|
||||
formData?: Record<string, any>;
|
||||
onFormDataChange?: (fieldName: string, value: any) => void;
|
||||
hideLabel?: boolean;
|
||||
screenInfo?: {
|
||||
id: number;
|
||||
tableName?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = ({
|
||||
@@ -43,6 +49,7 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
formData: externalFormData,
|
||||
onFormDataChange,
|
||||
hideLabel = false,
|
||||
screenInfo,
|
||||
}) => {
|
||||
const [localFormData, setLocalFormData] = useState<Record<string, any>>({});
|
||||
const [dateValues, setDateValues] = useState<Record<string, Date | undefined>>({});
|
||||
@@ -683,28 +690,300 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||
const widget = comp as WidgetComponent;
|
||||
const config = widget.webTypeConfig as ButtonTypeConfig | undefined;
|
||||
|
||||
const handleButtonClick = () => {
|
||||
if (config?.actionType === "popup" && config.popupTitle) {
|
||||
alert(`${config.popupTitle}\n\n${config.popupContent || "팝업 내용이 없습니다."}`);
|
||||
} else if (config?.actionType === "navigate" && config.navigateUrl) {
|
||||
const handleButtonClick = async () => {
|
||||
const actionType = config?.actionType || "save";
|
||||
|
||||
try {
|
||||
switch (actionType) {
|
||||
case "save":
|
||||
await handleSaveAction();
|
||||
break;
|
||||
case "cancel":
|
||||
handleCancelAction();
|
||||
break;
|
||||
case "delete":
|
||||
await handleDeleteAction();
|
||||
break;
|
||||
case "edit":
|
||||
handleEditAction();
|
||||
break;
|
||||
case "add":
|
||||
handleAddAction();
|
||||
break;
|
||||
case "search":
|
||||
handleSearchAction();
|
||||
break;
|
||||
case "reset":
|
||||
handleResetAction();
|
||||
break;
|
||||
case "submit":
|
||||
await handleSubmitAction();
|
||||
break;
|
||||
case "close":
|
||||
handleCloseAction();
|
||||
break;
|
||||
case "popup":
|
||||
handlePopupAction();
|
||||
break;
|
||||
case "navigate":
|
||||
handleNavigateAction();
|
||||
break;
|
||||
case "custom":
|
||||
await handleCustomAction();
|
||||
break;
|
||||
default:
|
||||
console.log(`알 수 없는 액션 타입: ${actionType}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`버튼 액션 실행 오류 (${actionType}):`, error);
|
||||
alert(`작업 중 오류가 발생했습니다: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 저장 액션
|
||||
const handleSaveAction = async () => {
|
||||
if (!formData || Object.keys(formData).length === 0) {
|
||||
alert("저장할 데이터가 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 필수 항목 검증
|
||||
const requiredFields = allComponents.filter(c => c.required && (c.columnName || c.id));
|
||||
const missingFields = requiredFields.filter(field => {
|
||||
const fieldName = field.columnName || field.id;
|
||||
const value = formData[fieldName];
|
||||
return !value || value.toString().trim() === "";
|
||||
});
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
const fieldNames = missingFields.map(f => f.label || f.columnName || f.id).join(", ");
|
||||
alert(`다음 필수 항목을 입력해주세요: ${fieldNames}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!screenInfo?.id) {
|
||||
alert("화면 정보가 없어 저장할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 컬럼명 기반으로 데이터 매핑
|
||||
const mappedData: Record<string, any> = {};
|
||||
|
||||
// 컴포넌트에서 컬럼명이 있는 것들만 매핑
|
||||
allComponents.forEach(comp => {
|
||||
if (comp.columnName) {
|
||||
const fieldName = comp.columnName;
|
||||
const componentId = comp.id;
|
||||
|
||||
// formData에서 해당 값 찾기 (컬럼명 우선, 없으면 컴포넌트 ID)
|
||||
const value = formData[fieldName] || formData[componentId];
|
||||
|
||||
if (value !== undefined && value !== "") {
|
||||
mappedData[fieldName] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log("💾 저장할 데이터 매핑:", {
|
||||
원본데이터: formData,
|
||||
매핑된데이터: mappedData,
|
||||
화면정보: screenInfo,
|
||||
});
|
||||
|
||||
// 테이블명 결정 (화면 정보에서 가져오거나 첫 번째 컴포넌트의 테이블명 사용)
|
||||
const tableName = screenInfo.tableName ||
|
||||
allComponents.find(c => c.columnName)?.tableName ||
|
||||
"dynamic_form_data"; // 기본값
|
||||
|
||||
const saveData: DynamicFormData = {
|
||||
screenId: screenInfo.id,
|
||||
tableName: tableName,
|
||||
data: mappedData,
|
||||
};
|
||||
|
||||
console.log("🚀 API 저장 요청:", saveData);
|
||||
|
||||
const result = await dynamicFormApi.saveFormData(saveData);
|
||||
|
||||
if (result.success) {
|
||||
alert("저장되었습니다.");
|
||||
console.log("✅ 저장 성공:", result.data);
|
||||
|
||||
// 저장 후 데이터 초기화 (선택사항)
|
||||
if (onFormDataChange) {
|
||||
Object.keys(formData).forEach(key => {
|
||||
onFormDataChange(key, "");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.message || "저장에 실패했습니다.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 저장 실패:", error);
|
||||
alert(`저장 중 오류가 발생했습니다: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 취소 액션
|
||||
const handleCancelAction = () => {
|
||||
if (confirm("변경사항을 취소하시겠습니까?")) {
|
||||
// 폼 초기화 또는 이전 페이지로 이동
|
||||
if (onFormDataChange) {
|
||||
// 모든 폼 데이터 초기화
|
||||
Object.keys(formData).forEach(key => {
|
||||
onFormDataChange(key, "");
|
||||
});
|
||||
}
|
||||
console.log("❌ 작업이 취소되었습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제 액션
|
||||
const handleDeleteAction = async () => {
|
||||
const confirmMessage = config?.confirmMessage || "정말로 삭제하시겠습니까?";
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 삭제할 레코드 ID가 필요 (폼 데이터에서 id 필드 찾기)
|
||||
const recordId = formData["id"] || formData["ID"] || formData["objid"];
|
||||
|
||||
if (!recordId) {
|
||||
alert("삭제할 데이터를 찾을 수 없습니다. (ID가 없음)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 테이블명 결정
|
||||
const tableName = screenInfo?.tableName ||
|
||||
allComponents.find(c => c.columnName)?.tableName ||
|
||||
"unknown_table";
|
||||
|
||||
if (!tableName || tableName === "unknown_table") {
|
||||
alert("테이블 정보가 없어 삭제할 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log("🗑️ 삭제 실행:", { recordId, tableName, formData });
|
||||
|
||||
const result = await dynamicFormApi.deleteFormDataFromTable(recordId, tableName);
|
||||
|
||||
if (result.success) {
|
||||
alert("삭제되었습니다.");
|
||||
console.log("✅ 삭제 성공");
|
||||
|
||||
// 삭제 후 폼 초기화
|
||||
if (onFormDataChange) {
|
||||
Object.keys(formData).forEach(key => {
|
||||
onFormDataChange(key, "");
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(result.message || "삭제에 실패했습니다.");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("❌ 삭제 실패:", error);
|
||||
alert(`삭제 중 오류가 발생했습니다: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 편집 액션
|
||||
const handleEditAction = () => {
|
||||
console.log("✏️ 편집 모드 활성화");
|
||||
// 읽기 전용 모드를 편집 모드로 전환
|
||||
alert("편집 모드로 전환되었습니다.");
|
||||
};
|
||||
|
||||
// 추가 액션
|
||||
const handleAddAction = () => {
|
||||
console.log("➕ 새 항목 추가");
|
||||
// 새 항목 추가 로직
|
||||
alert("새 항목을 추가할 수 있습니다.");
|
||||
};
|
||||
|
||||
// 검색 액션
|
||||
const handleSearchAction = () => {
|
||||
console.log("🔍 검색 실행:", formData);
|
||||
// 검색 로직
|
||||
const searchTerms = Object.values(formData).filter(v => v && v.toString().trim());
|
||||
if (searchTerms.length === 0) {
|
||||
alert("검색할 내용을 입력해주세요.");
|
||||
} else {
|
||||
alert(`검색 실행: ${searchTerms.join(", ")}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 초기화 액션
|
||||
const handleResetAction = () => {
|
||||
if (confirm("모든 입력을 초기화하시겠습니까?")) {
|
||||
if (onFormDataChange) {
|
||||
Object.keys(formData).forEach(key => {
|
||||
onFormDataChange(key, "");
|
||||
});
|
||||
}
|
||||
console.log("🔄 폼 초기화 완료");
|
||||
alert("입력이 초기화되었습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 제출 액션
|
||||
const handleSubmitAction = async () => {
|
||||
console.log("📤 폼 제출:", formData);
|
||||
// 제출 로직
|
||||
alert("제출되었습니다.");
|
||||
};
|
||||
|
||||
// 닫기 액션
|
||||
const handleCloseAction = () => {
|
||||
console.log("❌ 창 닫기");
|
||||
// 창 닫기 또는 모달 닫기
|
||||
if (window.opener) {
|
||||
window.close();
|
||||
} else {
|
||||
history.back();
|
||||
}
|
||||
};
|
||||
|
||||
// 팝업 액션
|
||||
const handlePopupAction = () => {
|
||||
if (config?.popupTitle && config?.popupContent) {
|
||||
// 커스텀 모달 대신 기본 alert 사용 (향후 모달 컴포넌트로 교체 가능)
|
||||
alert(`${config.popupTitle}\n\n${config.popupContent}`);
|
||||
} else {
|
||||
alert("팝업을 표시합니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 네비게이션 액션
|
||||
const handleNavigateAction = () => {
|
||||
if (config?.navigateUrl) {
|
||||
if (config.navigateTarget === "_blank") {
|
||||
window.open(config.navigateUrl, "_blank");
|
||||
} else {
|
||||
window.location.href = config.navigateUrl;
|
||||
}
|
||||
} else if (config?.actionType === "custom" && config.customAction) {
|
||||
} else {
|
||||
console.log("🔗 네비게이션 URL이 설정되지 않았습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
// 커스텀 액션
|
||||
const handleCustomAction = async () => {
|
||||
if (config?.customAction) {
|
||||
try {
|
||||
// 간단한 JavaScript 실행 (보안상 제한적)
|
||||
eval(config.customAction);
|
||||
// 보안상 제한적인 eval 사용
|
||||
const result = eval(config.customAction);
|
||||
if (result instanceof Promise) {
|
||||
await result;
|
||||
}
|
||||
console.log("⚡ 커스텀 액션 실행 완료");
|
||||
} catch (error) {
|
||||
console.error("커스텀 액션 실행 오류:", error);
|
||||
}
|
||||
} else if (config?.actionType === "delete" && config.confirmMessage) {
|
||||
if (confirm(config.confirmMessage)) {
|
||||
console.log("삭제 확인됨");
|
||||
throw new Error(`커스텀 액션 실행 실패: ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`버튼 클릭: ${config?.actionType || "기본"} 액션`);
|
||||
console.log("⚡ 커스텀 액션이 설정되지 않았습니다.");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -85,8 +85,64 @@ const renderWidget = (component: ComponentData) => {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as TextTypeConfig | undefined;
|
||||
|
||||
// 입력 타입에 따른 처리
|
||||
const isAutoInput = widget.inputType === "auto";
|
||||
|
||||
// 자동 값 생성 함수
|
||||
const getAutoValue = (autoValueType: string) => {
|
||||
switch (autoValueType) {
|
||||
case "current_datetime":
|
||||
return new Date().toLocaleString("ko-KR");
|
||||
case "current_date":
|
||||
return new Date().toLocaleDateString("ko-KR");
|
||||
case "current_time":
|
||||
return new Date().toLocaleTimeString("ko-KR");
|
||||
case "current_user":
|
||||
return "현재사용자";
|
||||
case "uuid":
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
case "sequence":
|
||||
return "SEQ_001";
|
||||
case "user_defined":
|
||||
return "사용자정의값";
|
||||
default:
|
||||
return "자동생성값";
|
||||
}
|
||||
};
|
||||
|
||||
// 자동 값 플레이스홀더 생성 함수
|
||||
const getAutoPlaceholder = (autoValueType: string) => {
|
||||
switch (autoValueType) {
|
||||
case "current_datetime":
|
||||
return "현재 날짜시간";
|
||||
case "current_date":
|
||||
return "현재 날짜";
|
||||
case "current_time":
|
||||
return "현재 시간";
|
||||
case "current_user":
|
||||
return "현재 사용자";
|
||||
case "uuid":
|
||||
return "UUID";
|
||||
case "sequence":
|
||||
return "시퀀스";
|
||||
case "user_defined":
|
||||
return "사용자 정의";
|
||||
default:
|
||||
return "자동 생성됨";
|
||||
}
|
||||
};
|
||||
|
||||
// 플레이스홀더 처리
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "텍스트를 입력하세요";
|
||||
const finalPlaceholder = isAutoInput
|
||||
? getAutoPlaceholder(widget.autoValueType || "current_datetime")
|
||||
: config?.placeholder || placeholder || "텍스트를 입력하세요";
|
||||
|
||||
// 자동 값 처리
|
||||
const autoValue = isAutoInput ? getAutoValue(widget.autoValueType || "current_datetime") : "";
|
||||
|
||||
const inputType = widgetType === "email" ? "email" : widgetType === "tel" ? "tel" : "text";
|
||||
|
||||
@@ -139,12 +195,14 @@ const renderWidget = (component: ComponentData) => {
|
||||
const inputProps = {
|
||||
...commonProps,
|
||||
placeholder: finalPlaceholder,
|
||||
value: isAutoInput ? autoValue : undefined, // 자동입력인 경우 자동 값 표시
|
||||
minLength: config?.minLength,
|
||||
maxLength: config?.maxLength,
|
||||
pattern: getPatternByFormat(config?.format || "none"),
|
||||
onInput: handleInputChange,
|
||||
onChange: () => {}, // 읽기 전용으로 처리
|
||||
readOnly: true,
|
||||
readOnly: readonly || isAutoInput, // 자동입력인 경우 읽기 전용
|
||||
className: `w-full h-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`,
|
||||
};
|
||||
|
||||
// multiline이면 Textarea로 렌더링
|
||||
@@ -160,19 +218,69 @@ const renderWidget = (component: ComponentData) => {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as NumberTypeConfig | undefined;
|
||||
|
||||
// 입력 타입에 따른 처리
|
||||
const isAutoInput = widget.inputType === "auto";
|
||||
|
||||
// 자동 값 생성 함수 (숫자용)
|
||||
const getAutoNumberValue = (autoValueType: string) => {
|
||||
switch (autoValueType) {
|
||||
case "current_datetime":
|
||||
return Date.now().toString();
|
||||
case "current_date":
|
||||
return new Date().getDate().toString();
|
||||
case "current_time":
|
||||
return new Date().getHours().toString();
|
||||
case "sequence":
|
||||
return "1001";
|
||||
case "uuid":
|
||||
return Math.floor(Math.random() * 1000000).toString();
|
||||
case "user_defined":
|
||||
return "999";
|
||||
default:
|
||||
return "0";
|
||||
}
|
||||
};
|
||||
|
||||
// 자동 값 플레이스홀더 생성 함수 (숫자용)
|
||||
const getAutoNumberPlaceholder = (autoValueType: string) => {
|
||||
switch (autoValueType) {
|
||||
case "current_datetime":
|
||||
return "타임스탬프";
|
||||
case "current_date":
|
||||
return "현재 일";
|
||||
case "current_time":
|
||||
return "현재 시";
|
||||
case "sequence":
|
||||
return "시퀀스";
|
||||
case "uuid":
|
||||
return "랜덤 숫자";
|
||||
case "user_defined":
|
||||
return "사용자 정의";
|
||||
default:
|
||||
return "자동 생성";
|
||||
}
|
||||
};
|
||||
|
||||
// 자동 값 처리
|
||||
const autoValue = isAutoInput ? getAutoNumberValue(widget.autoValueType || "sequence") : "";
|
||||
|
||||
// 디버깅: 현재 설정값 확인
|
||||
console.log("🔢 숫자 위젯 렌더링:", {
|
||||
componentId: widget.id,
|
||||
widgetType: widget.widgetType,
|
||||
config,
|
||||
placeholder: widget.placeholder,
|
||||
inputType: widget.inputType,
|
||||
isAutoInput,
|
||||
});
|
||||
|
||||
// 단계값 결정: webTypeConfig > 기본값 (소수는 0.01, 정수는 1)
|
||||
const step = config?.step || (widgetType === "decimal" ? 0.01 : 1);
|
||||
|
||||
// 플레이스홀더 처리
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "숫자를 입력하세요";
|
||||
const finalPlaceholder = isAutoInput
|
||||
? getAutoNumberPlaceholder(widget.autoValueType || "sequence")
|
||||
: config?.placeholder || placeholder || "숫자를 입력하세요";
|
||||
|
||||
// 형식에 따른 표시값 처리
|
||||
const formatValue = (value: string) => {
|
||||
@@ -215,9 +323,10 @@ const renderWidget = (component: ComponentData) => {
|
||||
max={config?.max}
|
||||
{...commonProps}
|
||||
placeholder={finalPlaceholder}
|
||||
className={`${config?.prefix ? "rounded-l-none" : ""} ${config?.suffix ? "rounded-r-none" : ""} ${borderClass}`}
|
||||
value={isAutoInput ? autoValue : undefined} // 자동입력인 경우 자동 값 표시
|
||||
className={`${config?.prefix ? "rounded-l-none" : ""} ${config?.suffix ? "rounded-r-none" : ""} ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
readOnly={readonly || isAutoInput}
|
||||
/>
|
||||
{config.suffix && (
|
||||
<span className="rounded-r border border-l-0 bg-gray-50 px-2 py-2 text-sm text-gray-600">
|
||||
@@ -236,8 +345,10 @@ const renderWidget = (component: ComponentData) => {
|
||||
max={config?.max}
|
||||
{...commonProps}
|
||||
placeholder={finalPlaceholder}
|
||||
value={isAutoInput ? autoValue : undefined} // 자동입력인 경우 자동 값 표시
|
||||
className={`h-full w-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
readOnly={readonly || isAutoInput}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -247,6 +358,46 @@ const renderWidget = (component: ComponentData) => {
|
||||
const widget = component as WidgetComponent;
|
||||
const config = widget.webTypeConfig as DateTypeConfig | undefined;
|
||||
|
||||
// 입력 타입에 따른 처리
|
||||
const isAutoInput = widget.inputType === "auto";
|
||||
|
||||
// 자동 값 생성 함수 (날짜용)
|
||||
const getAutoDateValue = (autoValueType: string, inputType: string) => {
|
||||
const now = new Date();
|
||||
switch (autoValueType) {
|
||||
case "current_datetime":
|
||||
return inputType === "datetime-local"
|
||||
? now.toISOString().slice(0, 16) // YYYY-MM-DDTHH:mm
|
||||
: now.toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
case "current_date":
|
||||
return now.toISOString().slice(0, 10); // YYYY-MM-DD
|
||||
case "current_time":
|
||||
return inputType === "datetime-local"
|
||||
? now.toISOString().slice(0, 16) // YYYY-MM-DDTHH:mm
|
||||
: now.toTimeString().slice(0, 5); // HH:mm
|
||||
case "user_defined":
|
||||
return inputType === "datetime-local" ? "2024-01-01T09:00" : "2024-01-01";
|
||||
default:
|
||||
return inputType === "datetime-local" ? now.toISOString().slice(0, 16) : now.toISOString().slice(0, 10);
|
||||
}
|
||||
};
|
||||
|
||||
// 자동 값 플레이스홀더 생성 함수 (날짜용)
|
||||
const getAutoDatePlaceholder = (autoValueType: string) => {
|
||||
switch (autoValueType) {
|
||||
case "current_datetime":
|
||||
return "현재 날짜시간";
|
||||
case "current_date":
|
||||
return "현재 날짜";
|
||||
case "current_time":
|
||||
return "현재 시간";
|
||||
case "user_defined":
|
||||
return "사용자 정의";
|
||||
default:
|
||||
return "자동 생성";
|
||||
}
|
||||
};
|
||||
|
||||
// 웹타입 설정에 따른 input type 결정
|
||||
let inputType = "date";
|
||||
if (config?.showTime || config?.format?.includes("HH:mm")) {
|
||||
@@ -273,8 +424,13 @@ const renderWidget = (component: ComponentData) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 자동 값 처리
|
||||
const autoValue = isAutoInput ? getAutoDateValue(widget.autoValueType || "current_date", inputType) : "";
|
||||
|
||||
// 플레이스홀더 우선순위: webTypeConfig > placeholder > 기본값
|
||||
const finalPlaceholder = config?.placeholder || placeholder || "날짜를 선택하세요";
|
||||
const finalPlaceholder = isAutoInput
|
||||
? getAutoDatePlaceholder(widget.autoValueType || "current_date")
|
||||
: config?.placeholder || placeholder || "날짜를 선택하세요";
|
||||
|
||||
// 디버깅: 현재 설정값 확인
|
||||
console.log("📅 날짜 위젯 렌더링:", {
|
||||
@@ -331,9 +487,10 @@ const renderWidget = (component: ComponentData) => {
|
||||
placeholder={finalPlaceholder}
|
||||
min={config?.minDate}
|
||||
max={config?.maxDate}
|
||||
value={processedDefaultValue}
|
||||
value={isAutoInput ? autoValue : processedDefaultValue}
|
||||
className={`h-full w-full ${borderClass} ${isAutoInput ? "bg-gray-50 text-gray-600" : ""}`}
|
||||
onChange={() => {}} // 읽기 전용으로 처리
|
||||
readOnly
|
||||
readOnly={readonly || isAutoInput}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,6 +173,32 @@ export default function TableTypeSelector({
|
||||
}
|
||||
};
|
||||
|
||||
// 입력 타입 변경
|
||||
const handleInputTypeChange = async (columnName: string, inputType: "direct" | "auto") => {
|
||||
try {
|
||||
// 현재 컬럼 정보 가져오기
|
||||
const currentColumn = columns.find((col) => col.columnName === columnName);
|
||||
if (!currentColumn) return;
|
||||
|
||||
// 웹 타입과 함께 입력 타입 업데이트
|
||||
await tableTypeApi.setColumnWebType(
|
||||
selectedTable,
|
||||
columnName,
|
||||
currentColumn.webType || "text",
|
||||
undefined, // detailSettings
|
||||
inputType,
|
||||
);
|
||||
|
||||
// 로컬 상태 업데이트
|
||||
setColumns((prev) => prev.map((col) => (col.columnName === columnName ? { ...col, inputType } : col)));
|
||||
|
||||
console.log(`컬럼 ${columnName}의 입력 타입을 ${inputType}로 변경했습니다.`);
|
||||
} catch (error) {
|
||||
console.error("입력 타입 변경 실패:", error);
|
||||
alert("입력 타입 설정에 실패했습니다. 다시 시도해주세요.");
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTables = tables.filter((table) => table.displayName.toLowerCase().includes(searchTerm.toLowerCase()));
|
||||
|
||||
return (
|
||||
@@ -233,6 +259,7 @@ export default function TableTypeSelector({
|
||||
<TableHead>라벨</TableHead>
|
||||
<TableHead>데이터 타입</TableHead>
|
||||
<TableHead>웹 타입</TableHead>
|
||||
<TableHead>입력 타입</TableHead>
|
||||
<TableHead>필수</TableHead>
|
||||
<TableHead>표시</TableHead>
|
||||
<TableHead>액션</TableHead>
|
||||
@@ -267,6 +294,20 @@ export default function TableTypeSelector({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={column.inputType || "direct"}
|
||||
onValueChange={(value) => handleInputTypeChange(column.columnName, value as "direct" | "auto")}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="direct">직접입력</SelectItem>
|
||||
<SelectItem value="auto">자동입력</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={column.isNullable === "NO" ? "default" : "secondary"}>
|
||||
{column.isNullable === "NO" ? "필수" : "선택"}
|
||||
|
||||
@@ -48,23 +48,42 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||
const config = (component.webTypeConfig as ButtonTypeConfig) || {};
|
||||
|
||||
// 로컬 상태 관리
|
||||
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>({
|
||||
actionType: "custom",
|
||||
variant: "default",
|
||||
size: "sm",
|
||||
...config,
|
||||
const [localConfig, setLocalConfig] = useState<ButtonTypeConfig>(() => {
|
||||
const defaultConfig = {
|
||||
actionType: "custom" as ButtonActionType,
|
||||
variant: "default" as ButtonVariant,
|
||||
size: "sm" as ButtonSize,
|
||||
};
|
||||
|
||||
return {
|
||||
...defaultConfig,
|
||||
...config, // 저장된 값이 기본값을 덮어씀
|
||||
};
|
||||
});
|
||||
|
||||
// 컴포넌트 변경 시 로컬 상태 동기화
|
||||
useEffect(() => {
|
||||
const newConfig = (component.webTypeConfig as ButtonTypeConfig) || {};
|
||||
|
||||
// 기본값 설정 (실제 값이 있으면 덮어쓰지 않음)
|
||||
const defaultConfig = {
|
||||
actionType: "custom" as ButtonActionType,
|
||||
variant: "default" as ButtonVariant,
|
||||
size: "sm" as ButtonSize,
|
||||
};
|
||||
|
||||
// 실제 저장된 값이 우선순위를 가지도록 설정
|
||||
setLocalConfig({
|
||||
actionType: "custom",
|
||||
variant: "default",
|
||||
size: "sm",
|
||||
...newConfig,
|
||||
...defaultConfig,
|
||||
...newConfig, // 저장된 값이 기본값을 덮어씀
|
||||
});
|
||||
}, [component.webTypeConfig]);
|
||||
|
||||
console.log("🔄 ButtonConfigPanel 로컬 상태 동기화:", {
|
||||
componentId: component.id,
|
||||
savedConfig: newConfig,
|
||||
finalConfig: { ...defaultConfig, ...newConfig },
|
||||
});
|
||||
}, [component.webTypeConfig, component.id]);
|
||||
|
||||
// 설정 업데이트 함수
|
||||
const updateConfig = (updates: Partial<ButtonTypeConfig>) => {
|
||||
@@ -194,7 +213,22 @@ export const ButtonConfigPanel: React.FC<ButtonConfigPanelProps> = ({ component,
|
||||
break;
|
||||
}
|
||||
|
||||
updateConfig(updates);
|
||||
// 로컬 상태 업데이트 후 webTypeConfig도 함께 업데이트
|
||||
const newConfig = { ...localConfig, ...updates };
|
||||
setLocalConfig(newConfig);
|
||||
|
||||
// webTypeConfig를 마지막에 다시 업데이트하여 확실히 저장되도록 함
|
||||
setTimeout(() => {
|
||||
onUpdateComponent({
|
||||
webTypeConfig: newConfig,
|
||||
});
|
||||
|
||||
console.log("🎯 ButtonActionType webTypeConfig 최종 업데이트:", {
|
||||
actionType,
|
||||
newConfig,
|
||||
componentId: component.id,
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const selectedActionOption = actionTypeOptions.find((opt) => opt.value === localConfig.actionType);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import React from "react";
|
||||
import { Settings } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
ComponentData,
|
||||
WidgetComponent,
|
||||
@@ -223,6 +224,87 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({ select
|
||||
<span className="rounded bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">{widget.widgetType}</span>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-gray-500">컬럼: {widget.columnName}</div>
|
||||
|
||||
{/* 입력 타입 설정 */}
|
||||
<div className="mt-3 space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">입력 타입</label>
|
||||
<Select
|
||||
value={widget.inputType || "direct"}
|
||||
onValueChange={(value: "direct" | "auto") => {
|
||||
onUpdateProperty(widget.id, "inputType", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="direct">직접입력</SelectItem>
|
||||
<SelectItem value="auto">자동입력</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500">
|
||||
{widget.inputType === "auto"
|
||||
? "시스템에서 자동으로 값을 생성합니다 (읽기 전용)"
|
||||
: "사용자가 직접 값을 입력할 수 있습니다"}
|
||||
</p>
|
||||
|
||||
{/* 자동 값 타입 설정 (자동입력일 때만 표시) */}
|
||||
{widget.inputType === "auto" && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<label className="text-sm font-medium text-gray-700">자동 값 타입</label>
|
||||
<Select
|
||||
value={widget.autoValueType || "current_datetime"}
|
||||
onValueChange={(
|
||||
value:
|
||||
| "current_datetime"
|
||||
| "current_date"
|
||||
| "current_time"
|
||||
| "current_user"
|
||||
| "uuid"
|
||||
| "sequence"
|
||||
| "user_defined",
|
||||
) => {
|
||||
onUpdateProperty(widget.id, "autoValueType", value);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="current_datetime">현재 날짜시간</SelectItem>
|
||||
<SelectItem value="current_date">현재 날짜</SelectItem>
|
||||
<SelectItem value="current_time">현재 시간</SelectItem>
|
||||
<SelectItem value="current_user">현재 사용자</SelectItem>
|
||||
<SelectItem value="uuid">UUID</SelectItem>
|
||||
<SelectItem value="sequence">시퀀스</SelectItem>
|
||||
<SelectItem value="user_defined">사용자 정의</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-gray-500">
|
||||
{(() => {
|
||||
switch (widget.autoValueType || "current_datetime") {
|
||||
case "current_datetime":
|
||||
return "현재 날짜와 시간을 자동으로 입력합니다";
|
||||
case "current_date":
|
||||
return "현재 날짜를 자동으로 입력합니다";
|
||||
case "current_time":
|
||||
return "현재 시간을 자동으로 입력합니다";
|
||||
case "current_user":
|
||||
return "현재 로그인한 사용자 정보를 입력합니다";
|
||||
case "uuid":
|
||||
return "고유한 UUID를 생성합니다";
|
||||
case "sequence":
|
||||
return "순차적인 번호를 생성합니다";
|
||||
case "user_defined":
|
||||
return "사용자가 정의한 규칙에 따라 값을 생성합니다";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
})()}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상세 설정 영역 */}
|
||||
|
||||
319
frontend/lib/api/dynamicForm.ts
Normal file
319
frontend/lib/api/dynamicForm.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { apiClient, ApiResponse } from "./client";
|
||||
|
||||
// 동적 폼 데이터 타입
|
||||
export interface DynamicFormData {
|
||||
screenId: number;
|
||||
tableName: string;
|
||||
data: Record<string, any>;
|
||||
}
|
||||
|
||||
// 폼 데이터 저장 응답 타입
|
||||
export interface SaveFormDataResponse {
|
||||
id: number;
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 폼 데이터 조회 응답 타입
|
||||
export interface FormDataResponse {
|
||||
id: number;
|
||||
screenId: number;
|
||||
tableName: string;
|
||||
data: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
// 동적 폼 API 클래스
|
||||
export class DynamicFormApi {
|
||||
/**
|
||||
* 폼 데이터 저장
|
||||
* @param formData 저장할 폼 데이터
|
||||
* @returns 저장 결과
|
||||
*/
|
||||
static async saveFormData(formData: DynamicFormData): Promise<ApiResponse<SaveFormDataResponse>> {
|
||||
try {
|
||||
console.log("💾 폼 데이터 저장 요청:", formData);
|
||||
|
||||
const response = await apiClient.post("/dynamic-form/save", formData);
|
||||
|
||||
console.log("✅ 폼 데이터 저장 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
message: "데이터가 성공적으로 저장되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 저장 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 저장 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 업데이트
|
||||
* @param id 레코드 ID
|
||||
* @param formData 업데이트할 폼 데이터
|
||||
* @returns 업데이트 결과
|
||||
*/
|
||||
static async updateFormData(
|
||||
id: number,
|
||||
formData: Partial<DynamicFormData>,
|
||||
): Promise<ApiResponse<SaveFormDataResponse>> {
|
||||
try {
|
||||
console.log("🔄 폼 데이터 업데이트 요청:", { id, formData });
|
||||
|
||||
const response = await apiClient.put(`/dynamic-form/${id}`, formData);
|
||||
|
||||
console.log("✅ 폼 데이터 업데이트 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
message: "데이터가 성공적으로 업데이트되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 업데이트 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 업데이트 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 삭제
|
||||
* @param id 레코드 ID
|
||||
* @returns 삭제 결과
|
||||
*/
|
||||
static async deleteFormData(id: number): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
console.log("🗑️ 폼 데이터 삭제 요청:", id);
|
||||
|
||||
await apiClient.delete(`/dynamic-form/${id}`);
|
||||
|
||||
console.log("✅ 폼 데이터 삭제 성공");
|
||||
return {
|
||||
success: true,
|
||||
message: "데이터가 성공적으로 삭제되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 삭제 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 삭제 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 실제 테이블에서 폼 데이터 삭제
|
||||
* @param id 레코드 ID
|
||||
* @param tableName 테이블명
|
||||
* @returns 삭제 결과
|
||||
*/
|
||||
static async deleteFormDataFromTable(id: number, tableName: string): Promise<ApiResponse<void>> {
|
||||
try {
|
||||
console.log("🗑️ 실제 테이블에서 폼 데이터 삭제 요청:", { id, tableName });
|
||||
|
||||
await apiClient.delete(`/dynamic-form/${id}`, {
|
||||
data: { tableName },
|
||||
});
|
||||
|
||||
console.log("✅ 실제 테이블에서 폼 데이터 삭제 성공");
|
||||
return {
|
||||
success: true,
|
||||
message: "데이터가 성공적으로 삭제되었습니다.",
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 실제 테이블에서 폼 데이터 삭제 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 삭제 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 목록 조회
|
||||
* @param screenId 화면 ID
|
||||
* @param params 검색 파라미터
|
||||
* @returns 폼 데이터 목록
|
||||
*/
|
||||
static async getFormDataList(
|
||||
screenId: number,
|
||||
params?: {
|
||||
page?: number;
|
||||
size?: number;
|
||||
search?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: "asc" | "desc";
|
||||
},
|
||||
): Promise<
|
||||
ApiResponse<{
|
||||
content: FormDataResponse[];
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
size: number;
|
||||
}>
|
||||
> {
|
||||
try {
|
||||
console.log("📋 폼 데이터 목록 조회 요청:", { screenId, params });
|
||||
|
||||
const response = await apiClient.get(`/dynamic-form/screen/${screenId}`, { params });
|
||||
|
||||
console.log("✅ 폼 데이터 목록 조회 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 목록 조회 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 조회 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 특정 폼 데이터 조회
|
||||
* @param id 레코드 ID
|
||||
* @returns 폼 데이터
|
||||
*/
|
||||
static async getFormData(id: number): Promise<ApiResponse<FormDataResponse>> {
|
||||
try {
|
||||
console.log("📄 폼 데이터 단건 조회 요청:", id);
|
||||
|
||||
const response = await apiClient.get(`/dynamic-form/${id}`);
|
||||
|
||||
console.log("✅ 폼 데이터 단건 조회 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 단건 조회 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 조회 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 테이블의 컬럼 정보 조회 (폼 검증용)
|
||||
* @param tableName 테이블명
|
||||
* @returns 컬럼 정보
|
||||
*/
|
||||
static async getTableColumns(tableName: string): Promise<
|
||||
ApiResponse<{
|
||||
tableName: string;
|
||||
columns: Array<{
|
||||
columnName: string;
|
||||
dataType: string;
|
||||
nullable: boolean;
|
||||
primaryKey: boolean;
|
||||
maxLength?: number;
|
||||
defaultValue?: any;
|
||||
}>;
|
||||
}>
|
||||
> {
|
||||
try {
|
||||
console.log("📊 테이블 컬럼 정보 조회 요청:", tableName);
|
||||
|
||||
const response = await apiClient.get(`/dynamic-form/table/${tableName}/columns`);
|
||||
|
||||
console.log("✅ 테이블 컬럼 정보 조회 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 테이블 컬럼 정보 조회 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "테이블 정보 조회 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 폼 데이터 검증
|
||||
* @param tableName 테이블명
|
||||
* @param data 검증할 데이터
|
||||
* @returns 검증 결과
|
||||
*/
|
||||
static async validateFormData(
|
||||
tableName: string,
|
||||
data: Record<string, any>,
|
||||
): Promise<
|
||||
ApiResponse<{
|
||||
valid: boolean;
|
||||
errors: Array<{
|
||||
field: string;
|
||||
message: string;
|
||||
code: string;
|
||||
}>;
|
||||
}>
|
||||
> {
|
||||
try {
|
||||
console.log("✅ 폼 데이터 검증 요청:", { tableName, data });
|
||||
|
||||
const response = await apiClient.post(`/dynamic-form/validate`, {
|
||||
tableName,
|
||||
data,
|
||||
});
|
||||
|
||||
console.log("✅ 폼 데이터 검증 성공:", response.data);
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 검증 실패:", error);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 검증 중 오류가 발생했습니다.";
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: errorMessage,
|
||||
errorCode: error.response?.data?.errorCode,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 편의를 위한 기본 export
|
||||
export const dynamicFormApi = DynamicFormApi;
|
||||
@@ -152,10 +152,12 @@ export const tableTypeApi = {
|
||||
columnName: string,
|
||||
webType: string,
|
||||
detailSettings?: Record<string, any>,
|
||||
inputType?: "direct" | "auto",
|
||||
): Promise<void> => {
|
||||
await apiClient.put(`/table-management/tables/${tableName}/columns/${columnName}/web-type`, {
|
||||
webType,
|
||||
detailSettings,
|
||||
inputType,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -156,6 +156,15 @@ export interface BaseComponent {
|
||||
tableName?: string; // 테이블명 추가
|
||||
label?: string; // 라벨 추가
|
||||
gridColumns?: number; // 그리드에서 차지할 컬럼 수 (1-12)
|
||||
inputType?: "direct" | "auto"; // 입력 타입 (직접입력/자동입력)
|
||||
autoValueType?:
|
||||
| "current_datetime"
|
||||
| "current_date"
|
||||
| "current_time"
|
||||
| "current_user"
|
||||
| "uuid"
|
||||
| "sequence"
|
||||
| "user_defined"; // 자동 값 타입
|
||||
}
|
||||
|
||||
// 컨테이너 컴포넌트
|
||||
@@ -460,6 +469,7 @@ export interface ColumnInfo {
|
||||
dataType: string;
|
||||
webType?: WebType;
|
||||
widgetType?: WebType; // 프론트엔드에서 사용하는 필드 (webType과 동일)
|
||||
inputType?: "direct" | "auto"; // 입력 타입
|
||||
isNullable: string;
|
||||
required?: boolean; // isNullable에서 변환된 필드
|
||||
columnDefault?: string;
|
||||
|
||||
Reference in New Issue
Block a user