feat: Enhance form validation and modal handling in various components
- Added `isInModal` prop to `ScreenModal` and `InteractiveScreenViewerDynamic` for improved modal context awareness. - Implemented `isFieldEmpty` and `checkAllRequiredFieldsFilled` utility functions to validate required fields in forms. - Updated `SaveModal` and `ButtonPrimaryComponent` to disable save actions when required fields are missing, enhancing user feedback. - Introduced error messages for required fields in modals to guide users in completing necessary inputs. Made-with: Cursor
This commit is contained in:
@@ -14,6 +14,7 @@ import { DynamicWebTypeRenderer } from "@/lib/registry";
|
||||
import { DynamicComponentRenderer } from "@/lib/registry/DynamicComponentRenderer";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { isFileComponent, isDataTableComponent, isButtonComponent } from "@/lib/utils/componentTypeUtils";
|
||||
import { isFieldEmpty } from "@/lib/utils/formValidation";
|
||||
import { FlowButtonGroup } from "./widgets/FlowButtonGroup";
|
||||
import { FlowVisibilityConfig } from "@/types/control-management";
|
||||
import { findAllButtonGroups } from "@/lib/utils/flowButtonGroupUtils";
|
||||
@@ -447,6 +448,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
onClose={() => {
|
||||
// buttonActions.ts가 이미 처리함
|
||||
}}
|
||||
isInModal={isInModal}
|
||||
// 탭 관련 정보 전달
|
||||
parentTabId={parentTabId}
|
||||
parentTabsComponentId={parentTabsComponentId}
|
||||
@@ -956,7 +958,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
|
||||
// 커스텀 색상이 있으면 Tailwind 클래스 대신 직접 스타일 적용
|
||||
const hasCustomColors = config?.backgroundColor || config?.textColor || comp.style?.backgroundColor || comp.style?.color;
|
||||
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
@@ -1119,6 +1121,42 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||
const labelOffset = (hasVisibleLabel && isVerticalLabel) ? (labelFontSize + labelMarginBottom + 2) : 0;
|
||||
|
||||
// 필수 입력값 검증 (모달 내부에서만 에러 표시)
|
||||
const reqCompType = component.type;
|
||||
const reqCompComponentType = (component as any).componentType || "";
|
||||
const isInputLikeComponent = reqCompType === "widget" || (
|
||||
reqCompType === "component" && (
|
||||
reqCompComponentType.startsWith("v2-input") ||
|
||||
reqCompComponentType.startsWith("v2-select") ||
|
||||
reqCompComponentType.startsWith("v2-date") ||
|
||||
reqCompComponentType.startsWith("v2-textarea") ||
|
||||
reqCompComponentType.startsWith("v2-number") ||
|
||||
reqCompComponentType === "entity-search-input" ||
|
||||
reqCompComponentType === "autocomplete-search-input"
|
||||
)
|
||||
);
|
||||
const isRequiredWidget = isInputLikeComponent && (
|
||||
(component as any).required === true ||
|
||||
(style as any)?.required === true ||
|
||||
(component as any).componentConfig?.required === true ||
|
||||
(component as any).overrides?.required === true
|
||||
);
|
||||
const isAutoInputField =
|
||||
(component as any).inputType === "auto" ||
|
||||
(component as any).componentConfig?.inputType === "auto" ||
|
||||
(component as any).overrides?.inputType === "auto";
|
||||
const isReadonlyWidget =
|
||||
(component as any).readonly === true ||
|
||||
(component as any).componentConfig?.readonly === true ||
|
||||
(component as any).overrides?.readonly === true;
|
||||
const requiredFieldName =
|
||||
(component as any).columnName ||
|
||||
(component as any).componentConfig?.columnName ||
|
||||
(component as any).overrides?.columnName ||
|
||||
component.id;
|
||||
const requiredFieldValue = formData[requiredFieldName];
|
||||
const showRequiredError = isInModal && isRequiredWidget && !isAutoInputField && !isReadonlyWidget && isFieldEmpty(requiredFieldValue);
|
||||
|
||||
const calculateCanvasSplitX = (): { x: number; w: number } => {
|
||||
const compType = (component as any).componentType || "";
|
||||
const isSplitLine = type === "component" && compType === "v2-split-line";
|
||||
@@ -1204,7 +1242,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
width: isSplitActive ? adjustedW : (size?.width || 200),
|
||||
height: isTableSearchWidget ? "auto" : size?.height || 10,
|
||||
minHeight: isTableSearchWidget ? "48px" : undefined,
|
||||
overflow: (isSplitActive && adjustedW < origW) ? "hidden" : (labelOffset > 0 ? "visible" : undefined),
|
||||
overflow: (isSplitActive && adjustedW < origW) ? "hidden" : ((labelOffset > 0 || showRequiredError) ? "visible" : undefined),
|
||||
willChange: canvasSplit.isDragging && isSplitActive ? "left, width" as const : undefined,
|
||||
transition: isSplitActive
|
||||
? (canvasSplit.isDragging ? "none" : "left 0.15s ease-out, width 0.15s ease-out")
|
||||
@@ -1317,6 +1355,11 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
) : (
|
||||
renderInteractiveWidget(componentToRender)
|
||||
)}
|
||||
{showRequiredError && (
|
||||
<p className="text-destructive pointer-events-none absolute left-0 text-[11px] leading-tight" style={{ top: "100%", marginTop: 2 }}>
|
||||
필수 입력 항목입니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 팝업 화면 렌더링 */}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { screenApi } from "@/lib/api/screen";
|
||||
import { dynamicFormApi, DynamicFormData } from "@/lib/api/dynamicForm";
|
||||
import { ComponentData } from "@/lib/types/screen";
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { checkAllRequiredFieldsFilled } from "@/lib/utils/formValidation";
|
||||
|
||||
interface SaveModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -304,6 +305,7 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||
};
|
||||
|
||||
const dynamicSize = calculateDynamicSize();
|
||||
const isRequiredFieldsMissing = !checkAllRequiredFieldsFilled(components, formData);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(open) => !isSaving && !open && onClose()}>
|
||||
@@ -320,7 +322,13 @@ export const SaveModal: React.FC<SaveModalProps> = ({
|
||||
<div className="flex items-center justify-between">
|
||||
<DialogTitle className="text-lg font-semibold">{initialData ? "데이터 수정" : "데이터 등록"}</DialogTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button onClick={handleSave} disabled={isSaving} size="sm" className="gap-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || isRequiredFieldsMissing}
|
||||
title={isRequiredFieldsMissing ? "필수 입력 항목을 모두 채워주세요" : undefined}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
{isSaving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
|
||||
Reference in New Issue
Block a user