Merge remote-tracking branch 'origin/ycshin-node' into ycshin-node
Resolve conflict in InteractiveScreenViewerDynamic.tsx:
- Keep horizontal label code (fd5c61b side)
- Remove old inline required field validation (replaced by useDialogAutoValidation hook)
- Clean up checkAllRequiredFieldsFilled usage from SaveModal, ButtonPrimaryComponent
- Remove isFieldEmpty, isInputComponent, checkAllRequiredFieldsFilled from formValidation.ts
Made-with: Cursor
This commit is contained in:
@@ -14,7 +14,6 @@ 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";
|
||||
@@ -1111,7 +1110,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
type === "v2-input" || type === "v2-select" || type === "v2-date" ||
|
||||
compType === "v2-input" || compType === "v2-select" || compType === "v2-date";
|
||||
const hasVisibleLabel = isV2InputComponent &&
|
||||
style?.labelDisplay !== false &&
|
||||
style?.labelDisplay !== false && style?.labelDisplay !== "false" &&
|
||||
(style?.labelText || (component as any).label);
|
||||
|
||||
// 라벨 위치에 따라 오프셋 계산 (좌/우 배치 시 세로 오프셋 불필요)
|
||||
@@ -1121,41 +1120,11 @@ 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);
|
||||
// 수평 라벨 관련 (componentStyle 계산보다 먼저 선언)
|
||||
const needsExternalLabel = hasVisibleLabel && labelPos !== "top";
|
||||
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||
const labelText = style?.labelText || (component as any).label || "";
|
||||
const labelGapValue = style?.labelGap || "8px";
|
||||
|
||||
const calculateCanvasSplitX = (): { x: number; w: number } => {
|
||||
const compType = (component as any).componentType || "";
|
||||
@@ -1232,9 +1201,17 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
// styleWithoutSize에서 left/top 제거 (캔버스 분할 조정값 덮어쓰기 방지)
|
||||
const { left: _styleLeft, top: _styleTop, ...safeStyleWithoutSize } = styleWithoutSize as any;
|
||||
|
||||
// 수평 라벨 컴포넌트: position wrapper에서 border 제거 (내부 V2 컴포넌트가 기본 border 사용)
|
||||
const cleanedStyle = (isHorizLabel && needsExternalLabel)
|
||||
? (() => {
|
||||
const { borderWidth: _bw, borderColor: _bc, borderStyle: _bs, border: _b, borderRadius: _br, ...rest } = safeStyleWithoutSize;
|
||||
return rest;
|
||||
})()
|
||||
: safeStyleWithoutSize;
|
||||
|
||||
const componentStyle = {
|
||||
position: "absolute" as const,
|
||||
...safeStyleWithoutSize,
|
||||
...cleanedStyle,
|
||||
// left/top은 반드시 마지막에 (styleWithoutSize가 덮어쓰지 못하게)
|
||||
left: adjustedX,
|
||||
top: position?.y || 0,
|
||||
@@ -1242,7 +1219,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 || showRequiredError) ? "visible" : undefined),
|
||||
overflow: (isSplitActive && adjustedW < origW) ? "hidden" : (labelOffset > 0 ? "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")
|
||||
@@ -1305,11 +1282,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
return unsubscribe;
|
||||
}, [component.id, position?.x, size?.width, type]);
|
||||
|
||||
// 라벨 위치가 top이 아닌 경우: 외부에서 라벨을 렌더링하고 내부 라벨은 숨김
|
||||
const needsExternalLabel = hasVisibleLabel && labelPos !== "top";
|
||||
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||
const labelText = style?.labelText || (component as any).label || "";
|
||||
const labelGapValue = style?.labelGap || "8px";
|
||||
// needsExternalLabel, isHorizLabel, labelText, labelGapValue는 위에서 선언됨
|
||||
|
||||
const externalLabelComponent = needsExternalLabel ? (
|
||||
<label
|
||||
@@ -1330,36 +1303,80 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
) : null;
|
||||
|
||||
const componentToRender = needsExternalLabel
|
||||
? { ...splitAdjustedComponent, style: { ...splitAdjustedComponent.style, labelDisplay: false } }
|
||||
? {
|
||||
...splitAdjustedComponent,
|
||||
style: {
|
||||
...splitAdjustedComponent.style,
|
||||
labelDisplay: false,
|
||||
labelPosition: "top" as const,
|
||||
...(isHorizLabel ? {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderWidth: undefined,
|
||||
borderColor: undefined,
|
||||
borderStyle: undefined,
|
||||
border: undefined,
|
||||
borderRadius: undefined,
|
||||
} : {}),
|
||||
},
|
||||
...(isHorizLabel ? {
|
||||
size: {
|
||||
...splitAdjustedComponent.size,
|
||||
width: undefined as unknown as number,
|
||||
height: undefined as unknown as number,
|
||||
},
|
||||
} : {}),
|
||||
}
|
||||
: splitAdjustedComponent;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={elRef} id={`interactive-${component.id}`} className="absolute" style={componentStyle}>
|
||||
{needsExternalLabel ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: isHorizLabel ? (labelPos === "left" ? "row" : "row-reverse") : "column-reverse",
|
||||
alignItems: isHorizLabel ? "center" : undefined,
|
||||
gap: isHorizLabel ? labelGapValue : undefined,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{externalLabelComponent}
|
||||
<div style={{ flex: 1, minWidth: 0, height: isHorizLabel ? "100%" : undefined }}>
|
||||
{renderInteractiveWidget(componentToRender)}
|
||||
isHorizLabel ? (
|
||||
<div style={{ position: "relative", width: "100%", height: "100%" }}>
|
||||
<label
|
||||
className="text-sm font-medium leading-none"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
...(labelPos === "left"
|
||||
? { right: "100%", marginRight: labelGapValue }
|
||||
: { left: "100%", marginLeft: labelGapValue }),
|
||||
fontSize: style?.labelFontSize || "14px",
|
||||
color: style?.labelColor || "#212121",
|
||||
fontWeight: style?.labelFontWeight || "500",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{labelText}
|
||||
{((component as any).required || (component as any).componentConfig?.required) && (
|
||||
<span className="ml-1 text-destructive">*</span>
|
||||
)}
|
||||
</label>
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
{renderInteractiveWidget(componentToRender)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column-reverse",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
{externalLabelComponent}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
{renderInteractiveWidget(componentToRender)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
renderInteractiveWidget(componentToRender)
|
||||
)}
|
||||
{showRequiredError && (
|
||||
<p className="text-destructive pointer-events-none absolute left-0 text-[11px] leading-tight" style={{ top: "100%", marginTop: 2 }}>
|
||||
필수 입력 항목입니다
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 팝업 화면 렌더링 */}
|
||||
|
||||
Reference in New Issue
Block a user