feat: Enhance BOM and UI components with improved label handling and data mapping
- Updated the BOM service to include additional fields in the BOM header retrieval, enhancing data richness. - Enhanced the EditModal to automatically map foreign key fields to dot notation, improving data handling and user experience. - Improved the rendering of labels in various components, allowing for customizable label positions and styles, enhancing UI flexibility. - Added new properties for label positioning and spacing in the V2 component styles, allowing for better layout control. - Enhanced the BomTreeComponent to support additional data mapping for entity joins, improving data accessibility and management.
This commit is contained in:
parent
385a10e2e7
commit
bfc89501ba
|
|
@ -59,7 +59,10 @@ export async function getBomHeader(bomId: string, tableName?: string) {
|
||||||
const table = safeTableName(tableName || "", "bom");
|
const table = safeTableName(tableName || "", "bom");
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT b.*,
|
SELECT b.*,
|
||||||
i.item_name, i.item_number, i.division as item_type, i.unit
|
i.item_name, i.item_number, i.division as item_type,
|
||||||
|
COALESCE(b.unit, i.unit) as unit,
|
||||||
|
i.unit as item_unit,
|
||||||
|
i.division, i.size, i.material
|
||||||
FROM ${table} b
|
FROM ${table} b
|
||||||
LEFT JOIN item_info i ON b.item_id = i.id
|
LEFT JOIN item_info i ON b.item_id = i.id
|
||||||
WHERE b.id = $1
|
WHERE b.id = $1
|
||||||
|
|
|
||||||
|
|
@ -274,7 +274,26 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// 편집 데이터로 폼 데이터 초기화
|
// 편집 데이터로 폼 데이터 초기화
|
||||||
setFormData(editData || {});
|
// entity join 필드(xxx_yyy)를 dot notation(table.column)으로도 매핑
|
||||||
|
const enriched = { ...(editData || {}) };
|
||||||
|
if (editData) {
|
||||||
|
Object.keys(editData).forEach((key) => {
|
||||||
|
// item_id_item_name → item_info.item_name 패턴 변환
|
||||||
|
const match = key.match(/^(.+?)_([a-z_]+)$/);
|
||||||
|
if (match && editData[key] != null) {
|
||||||
|
const [, fkCol, fieldName] = match;
|
||||||
|
// FK가 _id로 끝나면 참조 테이블명 추론 (item_id → item_info)
|
||||||
|
if (fkCol.endsWith("_id")) {
|
||||||
|
const refTable = fkCol.replace(/_id$/, "_info");
|
||||||
|
const dotKey = `${refTable}.${fieldName}`;
|
||||||
|
if (!(dotKey in enriched)) {
|
||||||
|
enriched[dotKey] = editData[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setFormData(enriched);
|
||||||
// originalData: changedData 계산(PATCH)에만 사용
|
// originalData: changedData 계산(PATCH)에만 사용
|
||||||
// INSERT/UPDATE 판단에는 사용하지 않음
|
// INSERT/UPDATE 판단에는 사용하지 않음
|
||||||
setOriginalData(isCreateMode ? {} : editData || {});
|
setOriginalData(isCreateMode ? {} : editData || {});
|
||||||
|
|
|
||||||
|
|
@ -245,23 +245,29 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
||||||
};
|
};
|
||||||
|
|
||||||
// 라벨 렌더링
|
// 라벨 렌더링
|
||||||
|
const labelPos = widget.style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
|
|
||||||
const renderLabel = () => {
|
const renderLabel = () => {
|
||||||
if (hideLabel) return null;
|
if (hideLabel) return null;
|
||||||
|
|
||||||
const labelStyle = widget.style || {};
|
const ls = widget.style || {};
|
||||||
const labelElement = (
|
const labelElement = (
|
||||||
<label
|
<label
|
||||||
className={`mb-2 block text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${hasError ? "text-destructive" : ""}`}
|
className={`text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70 ${hasError ? "text-destructive" : ""}`}
|
||||||
style={{
|
style={{
|
||||||
fontSize: labelStyle.labelFontSize || "14px",
|
fontSize: ls.labelFontSize || "14px",
|
||||||
color: hasError ? "hsl(var(--destructive))" : labelStyle.labelColor || undefined,
|
color: hasError ? "hsl(var(--destructive))" : ls.labelColor || undefined,
|
||||||
fontWeight: labelStyle.labelFontWeight || "500",
|
fontWeight: ls.labelFontWeight || "500",
|
||||||
fontFamily: labelStyle.labelFontFamily,
|
fontFamily: ls.labelFontFamily,
|
||||||
textAlign: labelStyle.labelTextAlign || "left",
|
textAlign: ls.labelTextAlign || "left",
|
||||||
backgroundColor: labelStyle.labelBackgroundColor,
|
backgroundColor: ls.labelBackgroundColor,
|
||||||
padding: labelStyle.labelPadding,
|
padding: ls.labelPadding,
|
||||||
borderRadius: labelStyle.labelBorderRadius,
|
borderRadius: ls.labelBorderRadius,
|
||||||
marginBottom: labelStyle.labelMarginBottom || "8px",
|
...(isHorizLabel
|
||||||
|
? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" }
|
||||||
|
: { marginBottom: labelPos === "top" ? (ls.labelMarginBottom || "8px") : undefined,
|
||||||
|
marginTop: labelPos === "bottom" ? (ls.labelMarginBottom || "8px") : undefined }),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{widget.label}
|
{widget.label}
|
||||||
|
|
@ -332,11 +338,28 @@ export const EnhancedInteractiveScreenViewer: React.FC<EnhancedInteractiveScreen
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const labelElement = renderLabel();
|
||||||
|
const widgetElement = renderByWebType();
|
||||||
|
const validationElement = renderFieldValidation();
|
||||||
|
|
||||||
|
if (isHorizLabel && labelElement) {
|
||||||
|
return (
|
||||||
|
<div key={comp.id}>
|
||||||
|
<div style={{ display: "flex", flexDirection: labelPos === "left" ? "row" : "row-reverse", alignItems: "center", gap: widget.style?.labelGap || "8px" }}>
|
||||||
|
{labelElement}
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>{widgetElement}</div>
|
||||||
|
</div>
|
||||||
|
{validationElement}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={comp.id} className="space-y-2">
|
<div key={comp.id}>
|
||||||
{renderLabel()}
|
{labelPos === "top" && labelElement}
|
||||||
{renderByWebType()}
|
{widgetElement}
|
||||||
{renderFieldValidation()}
|
{labelPos === "bottom" && labelElement}
|
||||||
|
{validationElement}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2208,15 +2208,21 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라벨 스타일 적용
|
// 라벨 위치 및 스타일
|
||||||
const labelStyle = {
|
const labelPosition = component.style?.labelPosition || "top";
|
||||||
|
const isHorizontalLabel = labelPosition === "left" || labelPosition === "right";
|
||||||
|
const labelGap = component.style?.labelGap || "8px";
|
||||||
|
|
||||||
|
const labelStyle: React.CSSProperties = {
|
||||||
fontSize: component.style?.labelFontSize || "14px",
|
fontSize: component.style?.labelFontSize || "14px",
|
||||||
color: component.style?.labelColor || "#212121",
|
color: component.style?.labelColor || "#212121",
|
||||||
fontWeight: component.style?.labelFontWeight || "500",
|
fontWeight: component.style?.labelFontWeight || "500",
|
||||||
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
backgroundColor: component.style?.labelBackgroundColor || "transparent",
|
||||||
padding: component.style?.labelPadding || "0",
|
padding: component.style?.labelPadding || "0",
|
||||||
borderRadius: component.style?.labelBorderRadius || "0",
|
borderRadius: component.style?.labelBorderRadius || "0",
|
||||||
marginBottom: component.style?.labelMarginBottom || "4px",
|
...(isHorizontalLabel
|
||||||
|
? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" }
|
||||||
|
: { marginBottom: component.style?.labelMarginBottom || "4px" }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2452,18 +2458,45 @@ export const InteractiveScreenViewer: React.FC<InteractiveScreenViewerProps> = (
|
||||||
{/* 테이블 옵션 툴바 */}
|
{/* 테이블 옵션 툴바 */}
|
||||||
<TableOptionsToolbar />
|
<TableOptionsToolbar />
|
||||||
|
|
||||||
{/* 메인 컨텐츠 */}
|
{/* 메인 컨텐츠 - 라벨 위치에 따라 flex 방향 변경 */}
|
||||||
<div className="h-full flex-1" style={{ width: '100%' }}>
|
<div
|
||||||
{/* 라벨이 있는 경우 표시 (데이터 테이블 제외) */}
|
className="h-full flex-1"
|
||||||
{shouldShowLabel && (
|
style={{
|
||||||
<label className="mb-2 block text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
width: '100%',
|
||||||
|
...(shouldShowLabel && isHorizontalLabel
|
||||||
|
? { display: 'flex', flexDirection: labelPosition === 'left' ? 'row' : 'row-reverse', alignItems: 'center', gap: labelGap }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 라벨: top 또는 left일 때 위젯보다 먼저 렌더링 */}
|
||||||
|
{shouldShowLabel && (labelPosition === "top" || labelPosition === "left") && (
|
||||||
|
<label
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
style={labelStyle}
|
||||||
|
>
|
||||||
{labelText}
|
{labelText}
|
||||||
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 실제 위젯 - 상위에서 라벨을 렌더링했으므로 자식은 라벨 숨김 */}
|
{/* 실제 위젯 */}
|
||||||
<div className="h-full" style={{ width: '100%', height: '100%' }}>{renderInteractiveWidget(componentForRendering)}</div>
|
<div className="h-full" style={{ width: '100%', height: '100%', ...(isHorizontalLabel ? { flex: 1, minWidth: 0 } : {}) }}>
|
||||||
|
{renderInteractiveWidget(componentForRendering)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 라벨: bottom 또는 right일 때 위젯 뒤에 렌더링 */}
|
||||||
|
{shouldShowLabel && (labelPosition === "bottom" || labelPosition === "right") && (
|
||||||
|
<label
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
style={{
|
||||||
|
...labelStyle,
|
||||||
|
...(labelPosition === "bottom" ? { marginBottom: 0, marginTop: component.style?.labelMarginBottom || "4px" } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelText}
|
||||||
|
{(component.required || component.componentConfig?.required) && <span className="ml-1 text-destructive">*</span>}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1078,17 +1078,21 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
// TableSearchWidget의 경우 높이를 자동으로 설정
|
// TableSearchWidget의 경우 높이를 자동으로 설정
|
||||||
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
const isTableSearchWidget = (component as any).componentId === "table-search-widget";
|
||||||
|
|
||||||
// 🆕 라벨 표시 여부 확인 (V2 입력 컴포넌트)
|
// 라벨 표시 여부 확인 (V2 입력 컴포넌트)
|
||||||
// labelDisplay가 false가 아니고, labelText 또는 label이 있으면 라벨 표시
|
const compType = (component as any).componentType || "";
|
||||||
const isV2InputComponent = type === "v2-input" || type === "v2-select" || type === "v2-date";
|
const isV2InputComponent =
|
||||||
|
type === "v2-input" || type === "v2-select" || type === "v2-date" ||
|
||||||
|
compType === "v2-input" || compType === "v2-select" || compType === "v2-date";
|
||||||
const hasVisibleLabel = isV2InputComponent &&
|
const hasVisibleLabel = isV2InputComponent &&
|
||||||
style?.labelDisplay !== false &&
|
style?.labelDisplay !== false &&
|
||||||
(style?.labelText || (component as any).label);
|
(style?.labelText || (component as any).label);
|
||||||
|
|
||||||
// 라벨이 있는 경우 상단 여백 계산 (라벨 폰트크기 + 여백)
|
// 라벨 위치에 따라 오프셋 계산 (좌/우 배치 시 세로 오프셋 불필요)
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isVerticalLabel = labelPos === "top" || labelPos === "bottom";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const labelOffset = hasVisibleLabel ? (labelFontSize + labelMarginBottom + 2) : 0;
|
const labelOffset = (hasVisibleLabel && isVerticalLabel) ? (labelFontSize + labelMarginBottom + 2) : 0;
|
||||||
|
|
||||||
const calculateCanvasSplitX = (): { x: number; w: number } => {
|
const calculateCanvasSplitX = (): { x: number; w: number } => {
|
||||||
const compType = (component as any).componentType || "";
|
const compType = (component as any).componentType || "";
|
||||||
|
|
@ -1238,10 +1242,56 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
return unsubscribe;
|
return unsubscribe;
|
||||||
}, [component.id, position?.x, size?.width, type]);
|
}, [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";
|
||||||
|
|
||||||
|
const externalLabelComponent = needsExternalLabel ? (
|
||||||
|
<label
|
||||||
|
className="text-sm font-medium leading-none"
|
||||||
|
style={{
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#212121",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
...(isHorizLabel ? { whiteSpace: "nowrap" as const, display: "flex", alignItems: "center" } : {}),
|
||||||
|
...(labelPos === "bottom" ? { marginTop: style?.labelMarginBottom || "4px" } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelText}
|
||||||
|
{((component as any).required || (component as any).componentConfig?.required) && (
|
||||||
|
<span className="ml-1 text-destructive">*</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const componentToRender = needsExternalLabel
|
||||||
|
? { ...splitAdjustedComponent, style: { ...splitAdjustedComponent.style, labelDisplay: false } }
|
||||||
|
: splitAdjustedComponent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={elRef} id={`interactive-${component.id}`} className="absolute" style={componentStyle}>
|
<div ref={elRef} id={`interactive-${component.id}`} className="absolute" style={componentStyle}>
|
||||||
{renderInteractiveWidget(splitAdjustedComponent)}
|
{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)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderInteractiveWidget(componentToRender)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 팝업 화면 렌더링 */}
|
{/* 팝업 화면 렌더링 */}
|
||||||
|
|
|
||||||
|
|
@ -839,6 +839,44 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">위치</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedComponent.style?.labelPosition || "top"}
|
||||||
|
onValueChange={(value) => handleUpdate("style.labelPosition", value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-6 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="top">위</SelectItem>
|
||||||
|
<SelectItem value="bottom">아래</SelectItem>
|
||||||
|
<SelectItem value="left">왼쪽</SelectItem>
|
||||||
|
<SelectItem value="right">오른쪽</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-xs">간격</Label>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
(selectedComponent.style?.labelPosition === "left" || selectedComponent.style?.labelPosition === "right")
|
||||||
|
? (selectedComponent.style?.labelGap || "8px")
|
||||||
|
: (selectedComponent.style?.labelMarginBottom || "4px")
|
||||||
|
}
|
||||||
|
onChange={(e) => {
|
||||||
|
const pos = selectedComponent.style?.labelPosition;
|
||||||
|
if (pos === "left" || pos === "right") {
|
||||||
|
handleUpdate("style.labelGap", e.target.value);
|
||||||
|
} else {
|
||||||
|
handleUpdate("style.labelMarginBottom", e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-6 w-full px-2 py-0 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">크기</Label>
|
<Label className="text-xs">크기</Label>
|
||||||
|
|
@ -860,12 +898,21 @@ export const V2PropertiesPanel: React.FC<V2PropertiesPanelProps> = ({
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label className="text-xs">여백</Label>
|
<Label className="text-xs">굵기</Label>
|
||||||
<Input
|
<Select
|
||||||
value={selectedComponent.style?.labelMarginBottom || "4px"}
|
value={selectedComponent.style?.labelFontWeight || "500"}
|
||||||
onChange={(e) => handleUpdate("style.labelMarginBottom", e.target.value)}
|
onValueChange={(value) => handleUpdate("style.labelFontWeight", value)}
|
||||||
className="h-6 w-full px-2 py-0 text-xs"
|
>
|
||||||
/>
|
<SelectTrigger className="h-6 text-xs">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="400">보통</SelectItem>
|
||||||
|
<SelectItem value="500">중간</SelectItem>
|
||||||
|
<SelectItem value="600">굵게</SelectItem>
|
||||||
|
<SelectItem value="700">매우 굵게</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 pt-5">
|
<div className="flex items-center space-x-2 pt-5">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
|
|
||||||
|
|
@ -466,10 +466,56 @@ export const V2Date = forwardRef<HTMLDivElement, V2DateProps>((props, ref) => {
|
||||||
const componentWidth = size?.width || style?.width;
|
const componentWidth = size?.width || style?.width;
|
||||||
const componentHeight = size?.height || style?.height;
|
const componentHeight = size?.height || style?.height;
|
||||||
|
|
||||||
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
// 라벨 위치 및 높이 계산
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||||
|
const labelGapValue = style?.labelGap || "8px";
|
||||||
|
|
||||||
|
const labelElement = showLabel ? (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
style={{
|
||||||
|
...(labelPos === "top" ? { position: "absolute" as const, top: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
...(labelPos === "bottom" ? { position: "absolute" as const, bottom: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#64748b",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
}}
|
||||||
|
className="text-sm font-medium whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const dateContent = (
|
||||||
|
<div className={isHorizLabel ? "min-w-0 flex-1" : "h-full w-full"} style={isHorizLabel ? { height: "100%" } : undefined}>
|
||||||
|
{renderDatePicker()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHorizLabel && showLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
style={{
|
||||||
|
width: componentWidth,
|
||||||
|
height: componentHeight,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: labelPos === "left" ? "row" : "row-reverse",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: labelGapValue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelElement}
|
||||||
|
{dateContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -481,27 +527,8 @@ export const V2Date = forwardRef<HTMLDivElement, V2DateProps>((props, ref) => {
|
||||||
height: componentHeight,
|
height: componentHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
|
{labelElement}
|
||||||
{showLabel && (
|
{dateContent}
|
||||||
<Label
|
|
||||||
htmlFor={id}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: `-${estimatedLabelHeight}px`,
|
|
||||||
left: 0,
|
|
||||||
fontSize: style?.labelFontSize || "14px",
|
|
||||||
color: style?.labelColor || "#64748b",
|
|
||||||
fontWeight: style?.labelFontWeight || "500",
|
|
||||||
}}
|
|
||||||
className="text-sm font-medium whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
<div className="h-full w-full">
|
|
||||||
{renderDatePicker()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -961,36 +961,83 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 라벨이 표시될 때 입력 필드가 차지할 높이 계산
|
|
||||||
// 🔧 label prop이 없어도 style.labelText에서 가져올 수 있도록 수정
|
|
||||||
const actualLabel = label || style?.labelText;
|
const actualLabel = label || style?.labelText;
|
||||||
const showLabel = actualLabel && style?.labelDisplay === true;
|
const showLabel = actualLabel && style?.labelDisplay === true;
|
||||||
// size에서 우선 가져오고, 없으면 style에서 가져옴
|
|
||||||
const componentWidth = size?.width || style?.width;
|
const componentWidth = size?.width || style?.width;
|
||||||
const componentHeight = size?.height || style?.height;
|
const componentHeight = size?.height || style?.height;
|
||||||
|
|
||||||
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
// 라벨 위치 및 높이 계산
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2; // 라벨 높이 + 여백
|
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||||
|
const labelGapValue = style?.labelGap || "8px";
|
||||||
|
|
||||||
// 🔧 커스텀 스타일 감지 (StyleEditor에서 설정한 테두리/배경/텍스트 스타일)
|
// 커스텀 스타일 감지
|
||||||
// RealtimePreview 래퍼가 외부 div에 스타일을 적용하지만,
|
|
||||||
// 내부 input/textarea가 자체 Tailwind 테두리를 가지므로 이를 제거하여 외부 스타일이 보이도록 함
|
|
||||||
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
||||||
const hasCustomBackground = !!style?.backgroundColor;
|
const hasCustomBackground = !!style?.backgroundColor;
|
||||||
const hasCustomRadius = !!style?.borderRadius;
|
const hasCustomRadius = !!style?.borderRadius;
|
||||||
|
|
||||||
// 텍스트 스타일 오버라이드 (내부 input/textarea에 직접 전달)
|
|
||||||
const customTextStyle: React.CSSProperties = {};
|
const customTextStyle: React.CSSProperties = {};
|
||||||
if (style?.color) customTextStyle.color = style.color;
|
if (style?.color) customTextStyle.color = style.color;
|
||||||
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
||||||
if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight;
|
if (style?.fontWeight) customTextStyle.fontWeight = style.fontWeight;
|
||||||
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
||||||
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
||||||
// 내부 input에 직접 적용할 텍스트 스타일 (fontSize, color, fontWeight, textAlign)
|
|
||||||
const inputTextStyle: React.CSSProperties | undefined = hasCustomText ? customTextStyle : undefined;
|
const inputTextStyle: React.CSSProperties | undefined = hasCustomText ? customTextStyle : undefined;
|
||||||
|
|
||||||
|
const labelElement = showLabel ? (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
style={{
|
||||||
|
...(labelPos === "top" ? { position: "absolute" as const, top: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
...(labelPos === "bottom" ? { position: "absolute" as const, bottom: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#64748b",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
}}
|
||||||
|
className="text-sm font-medium whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{actualLabel}
|
||||||
|
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
||||||
|
</Label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const inputContent = (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
isHorizLabel ? "min-w-0 flex-1" : "h-full w-full",
|
||||||
|
hasCustomBorder && "[&_input]:border-0! [&_textarea]:border-0! [&_.border]:border-0!",
|
||||||
|
(hasCustomBorder || hasCustomRadius) && "[&_input]:rounded-none! [&_textarea]:rounded-none! [&_.rounded-md]:rounded-none!",
|
||||||
|
hasCustomBackground && "[&_input]:bg-transparent! [&_textarea]:bg-transparent!",
|
||||||
|
)}
|
||||||
|
style={{ ...(hasCustomText ? customTextStyle : {}), ...(isHorizLabel ? { height: "100%" } : {}) }}
|
||||||
|
>
|
||||||
|
{renderInput()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHorizLabel && showLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
style={{
|
||||||
|
width: componentWidth,
|
||||||
|
height: componentHeight,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: labelPos === "left" ? "row" : "row-reverse",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: labelGapValue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelElement}
|
||||||
|
{inputContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -1001,38 +1048,8 @@ export const V2Input = forwardRef<HTMLDivElement, V2InputProps>((props, ref) =>
|
||||||
height: componentHeight,
|
height: componentHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 (높이에 포함되지 않음) */}
|
{labelElement}
|
||||||
{showLabel && (
|
{inputContent}
|
||||||
<Label
|
|
||||||
htmlFor={id}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: `-${estimatedLabelHeight}px`,
|
|
||||||
left: 0,
|
|
||||||
fontSize: style?.labelFontSize || "14px",
|
|
||||||
color: style?.labelColor || "#64748b",
|
|
||||||
fontWeight: style?.labelFontWeight || "500",
|
|
||||||
}}
|
|
||||||
className="text-sm font-medium whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{actualLabel}
|
|
||||||
{required && <span className="ml-0.5 text-orange-500">*</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-full w-full",
|
|
||||||
// 커스텀 테두리 설정 시, 내부 input/textarea의 기본 테두리 제거 (외부 래퍼 스타일이 보이도록)
|
|
||||||
hasCustomBorder && "[&_input]:border-0! [&_textarea]:border-0! [&_.border]:border-0!",
|
|
||||||
// 커스텀 모서리 설정 시, 내부 요소의 기본 모서리 제거 (외부 래퍼가 처리)
|
|
||||||
(hasCustomBorder || hasCustomRadius) && "[&_input]:rounded-none! [&_textarea]:rounded-none! [&_.rounded-md]:rounded-none!",
|
|
||||||
// 커스텀 배경 설정 시, 내부 input을 투명하게 (외부 배경이 보이도록)
|
|
||||||
hasCustomBackground && "[&_input]:bg-transparent! [&_textarea]:bg-transparent!",
|
|
||||||
)}
|
|
||||||
style={hasCustomText ? customTextStyle : undefined}
|
|
||||||
>
|
|
||||||
{renderInput()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1041,17 +1041,19 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
const componentWidth = size?.width || style?.width;
|
const componentWidth = size?.width || style?.width;
|
||||||
const componentHeight = size?.height || style?.height;
|
const componentHeight = size?.height || style?.height;
|
||||||
|
|
||||||
// 라벨 높이 계산 (기본 20px, 사용자 설정에 따라 조정)
|
// 라벨 위치 및 높이 계산
|
||||||
|
const labelPos = style?.labelPosition || "top";
|
||||||
|
const isHorizLabel = labelPos === "left" || labelPos === "right";
|
||||||
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14;
|
||||||
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4;
|
||||||
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
const estimatedLabelHeight = labelFontSize + labelMarginBottom + 2;
|
||||||
|
const labelGapValue = style?.labelGap || "8px";
|
||||||
|
|
||||||
// 🔧 커스텀 스타일 감지 (StyleEditor에서 설정한 테두리/배경/텍스트 스타일)
|
// 커스텀 스타일 감지
|
||||||
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
const hasCustomBorder = !!(style?.borderWidth || style?.borderColor || style?.borderStyle || style?.border);
|
||||||
const hasCustomBackground = !!style?.backgroundColor;
|
const hasCustomBackground = !!style?.backgroundColor;
|
||||||
const hasCustomRadius = !!style?.borderRadius;
|
const hasCustomRadius = !!style?.borderRadius;
|
||||||
|
|
||||||
// 텍스트 스타일 오버라이드 (CSS 상속)
|
|
||||||
const customTextStyle: React.CSSProperties = {};
|
const customTextStyle: React.CSSProperties = {};
|
||||||
if (style?.color) customTextStyle.color = style.color;
|
if (style?.color) customTextStyle.color = style.color;
|
||||||
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
if (style?.fontSize) customTextStyle.fontSize = style.fontSize;
|
||||||
|
|
@ -1059,6 +1061,58 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
if (style?.textAlign) customTextStyle.textAlign = style.textAlign as React.CSSProperties["textAlign"];
|
||||||
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
const hasCustomText = Object.keys(customTextStyle).length > 0;
|
||||||
|
|
||||||
|
const labelElement = showLabel ? (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
style={{
|
||||||
|
...(labelPos === "top" ? { position: "absolute" as const, top: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
...(labelPos === "bottom" ? { position: "absolute" as const, bottom: `-${estimatedLabelHeight}px`, left: 0 } : {}),
|
||||||
|
fontSize: style?.labelFontSize || "14px",
|
||||||
|
color: style?.labelColor || "#64748b",
|
||||||
|
fontWeight: style?.labelFontWeight || "500",
|
||||||
|
}}
|
||||||
|
className="text-sm font-medium whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
||||||
|
</Label>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const selectContent = (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
isHorizLabel ? "min-w-0 flex-1" : "h-full w-full",
|
||||||
|
hasCustomBorder && "[&_button]:border-0! **:data-[slot=select-trigger]:border-0! [&_.border]:border-0!",
|
||||||
|
(hasCustomBorder || hasCustomRadius) && "[&_button]:rounded-none! **:data-[slot=select-trigger]:rounded-none! [&_.rounded-md]:rounded-none!",
|
||||||
|
hasCustomBackground && "[&_button]:bg-transparent! **:data-[slot=select-trigger]:bg-transparent!",
|
||||||
|
)}
|
||||||
|
style={{ ...(hasCustomText ? customTextStyle : {}), ...(isHorizLabel ? { height: "100%" } : {}) }}
|
||||||
|
>
|
||||||
|
{renderSelect()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isHorizLabel && showLabel) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
id={id}
|
||||||
|
className={cn(isDesignMode && "pointer-events-none")}
|
||||||
|
style={{
|
||||||
|
width: componentWidth,
|
||||||
|
height: componentHeight,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: labelPos === "left" ? "row" : "row-reverse",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: labelGapValue,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{labelElement}
|
||||||
|
{selectContent}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
|
@ -1069,38 +1123,8 @@ export const V2Select = forwardRef<HTMLDivElement, V2SelectProps>(
|
||||||
height: componentHeight,
|
height: componentHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* 🔧 라벨을 absolute로 컴포넌트 위에 배치 */}
|
{labelElement}
|
||||||
{showLabel && (
|
{selectContent}
|
||||||
<Label
|
|
||||||
htmlFor={id}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
top: `-${estimatedLabelHeight}px`,
|
|
||||||
left: 0,
|
|
||||||
fontSize: style?.labelFontSize || "14px",
|
|
||||||
color: style?.labelColor || "#64748b",
|
|
||||||
fontWeight: style?.labelFontWeight || "500",
|
|
||||||
}}
|
|
||||||
className="text-sm font-medium whitespace-nowrap"
|
|
||||||
>
|
|
||||||
{label}
|
|
||||||
{required && <span className="text-orange-500 ml-0.5">*</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"h-full w-full",
|
|
||||||
// 커스텀 테두리 설정 시, 내부 select trigger의 기본 테두리 제거
|
|
||||||
hasCustomBorder && "[&_button]:border-0! **:data-[slot=select-trigger]:border-0! [&_.border]:border-0!",
|
|
||||||
// 커스텀 모서리 설정 시, 내부 요소의 기본 모서리 제거
|
|
||||||
(hasCustomBorder || hasCustomRadius) && "[&_button]:rounded-none! **:data-[slot=select-trigger]:rounded-none! [&_.rounded-md]:rounded-none!",
|
|
||||||
// 커스텀 배경 설정 시, 내부 요소를 투명하게
|
|
||||||
hasCustomBackground && "[&_button]:bg-transparent! **:data-[slot=select-trigger]:bg-transparent!",
|
|
||||||
)}
|
|
||||||
style={hasCustomText ? customTextStyle : undefined}
|
|
||||||
>
|
|
||||||
{renderSelect()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,7 @@ export function BomDetailEditModal({
|
||||||
} else {
|
} else {
|
||||||
setFormData({
|
setFormData({
|
||||||
quantity: node.quantity || "",
|
quantity: node.quantity || "",
|
||||||
unit: node.unit || node.detail_unit || "",
|
|
||||||
process_type: node.process_type || "",
|
process_type: node.process_type || "",
|
||||||
base_qty: node.base_qty || "",
|
|
||||||
loss_rate: node.loss_rate || "",
|
loss_rate: node.loss_rate || "",
|
||||||
remark: node.remark || "",
|
remark: node.remark || "",
|
||||||
});
|
});
|
||||||
|
|
@ -151,11 +149,19 @@ export function BomDetailEditModal({
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs sm:text-sm">단위</Label>
|
<Label className="text-xs sm:text-sm">단위</Label>
|
||||||
<Input
|
{isRootNode ? (
|
||||||
value={formData.unit}
|
<Input
|
||||||
onChange={(e) => handleChange("unit", e.target.value)}
|
value={formData.unit}
|
||||||
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
onChange={(e) => handleChange("unit", e.target.value)}
|
||||||
/>
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
value={node?.child_unit || node?.unit || "-"}
|
||||||
|
disabled
|
||||||
|
className="mt-1 h-8 text-xs sm:h-10 sm:text-sm"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -291,6 +291,7 @@ export function BomTreeComponent({
|
||||||
item_name: raw.item_name || "",
|
item_name: raw.item_name || "",
|
||||||
item_code: raw.item_number || raw.item_code || "",
|
item_code: raw.item_number || raw.item_code || "",
|
||||||
item_type: raw.item_type || raw.division || "",
|
item_type: raw.item_type || raw.division || "",
|
||||||
|
unit: raw.unit || raw.item_unit || "",
|
||||||
} as BomHeaderInfo;
|
} as BomHeaderInfo;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -376,6 +377,18 @@ export function BomTreeComponent({
|
||||||
detail.editData[key] = (headerInfo as any)[key];
|
detail.editData[key] = (headerInfo as any)[key];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// entity join된 필드를 dot notation으로도 매핑 (item_info.xxx 형식)
|
||||||
|
const h = headerInfo as Record<string, any>;
|
||||||
|
if (h.item_name) detail.editData["item_info.item_name"] = h.item_name;
|
||||||
|
if (h.item_type) detail.editData["item_info.division"] = h.item_type;
|
||||||
|
if (h.item_code || h.item_number) detail.editData["item_info.item_number"] = h.item_code || h.item_number;
|
||||||
|
if (h.unit) detail.editData["item_info.unit"] = h.unit;
|
||||||
|
// entity join alias 형식도 매핑
|
||||||
|
if (h.item_name) detail.editData["item_id_item_name"] = h.item_name;
|
||||||
|
if (h.item_type) detail.editData["item_id_division"] = h.item_type;
|
||||||
|
if (h.item_code || h.item_number) detail.editData["item_id_item_number"] = h.item_code || h.item_number;
|
||||||
|
if (h.unit) detail.editData["item_id_unit"] = h.unit;
|
||||||
};
|
};
|
||||||
// capture: true → EditModal 리스너(bubble)보다 반드시 먼저 실행
|
// capture: true → EditModal 리스너(bubble)보다 반드시 먼저 실행
|
||||||
window.addEventListener("openEditModal", handler, true);
|
window.addEventListener("openEditModal", handler, true);
|
||||||
|
|
|
||||||
|
|
@ -153,10 +153,12 @@ export interface CommonStyle {
|
||||||
// 라벨 스타일
|
// 라벨 스타일
|
||||||
labelDisplay?: boolean; // 라벨 표시 여부
|
labelDisplay?: boolean; // 라벨 표시 여부
|
||||||
labelText?: string; // 라벨 텍스트
|
labelText?: string; // 라벨 텍스트
|
||||||
|
labelPosition?: "top" | "left" | "right" | "bottom"; // 라벨 위치 (기본: top)
|
||||||
labelFontSize?: string;
|
labelFontSize?: string;
|
||||||
labelColor?: string;
|
labelColor?: string;
|
||||||
labelFontWeight?: string;
|
labelFontWeight?: string;
|
||||||
labelMarginBottom?: string;
|
labelMarginBottom?: string;
|
||||||
|
labelGap?: string; // 라벨-위젯 간격 (좌/우 배치 시 사용)
|
||||||
|
|
||||||
// 레이아웃
|
// 레이아웃
|
||||||
display?: string;
|
display?: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue