Merge branch 'jskim-node' of http://39.117.244.52:3000/kjs/ERP-node into mhkim-node
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { ComponentRendererProps } from "@/types/component";
|
||||
import { AggregationWidgetConfig, AggregationItem, AggregationResult, AggregationType } from "./types";
|
||||
import { formatNumber } from "@/lib/formatting";
|
||||
import { Calculator, TrendingUp, Hash, ArrowUp, ArrowDown } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
@@ -136,11 +137,11 @@ export function AggregationWidgetComponent({
|
||||
let formattedValue = value.toFixed(item.decimalPlaces ?? 0);
|
||||
|
||||
if (item.format === "currency") {
|
||||
formattedValue = new Intl.NumberFormat("ko-KR").format(value);
|
||||
formattedValue = formatNumber(value);
|
||||
} else if (item.format === "percent") {
|
||||
formattedValue = `${(value * 100).toFixed(item.decimalPlaces ?? 1)}%`;
|
||||
} else if (item.format === "number") {
|
||||
formattedValue = new Intl.NumberFormat("ko-KR").format(value);
|
||||
formattedValue = formatNumber(value);
|
||||
}
|
||||
|
||||
if (item.prefix) {
|
||||
|
||||
@@ -28,7 +28,6 @@ import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelContext";
|
||||
import { applyMappingRules } from "@/lib/utils/dataMapping";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
|
||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||
config?: ButtonPrimaryConfig;
|
||||
// 추가 props
|
||||
@@ -1248,7 +1247,6 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화 + 행 선택 필수)
|
||||
const finalDisabled =
|
||||
componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading;
|
||||
|
||||
|
||||
@@ -112,13 +112,13 @@ import "./v2-input/V2InputRenderer"; // V2 통합 입력 컴포넌트
|
||||
import "./v2-select/V2SelectRenderer"; // V2 통합 선택 컴포넌트
|
||||
import "./v2-date/V2DateRenderer"; // V2 통합 날짜 컴포넌트
|
||||
import "./v2-file-upload/V2FileUploadRenderer"; // V2 파일 업로드 컴포넌트
|
||||
import "./v2-process-work-standard/ProcessWorkStandardRenderer"; // 공정 작업기준
|
||||
import "./v2-item-routing/ItemRoutingRenderer"; // 품목별 라우팅
|
||||
import "./v2-split-line/SplitLineRenderer"; // V2 캔버스 분할선
|
||||
import "./v2-bom-tree/BomTreeRenderer"; // BOM 트리 뷰
|
||||
import "./v2-bom-item-editor/BomItemEditorRenderer"; // BOM 하위품목 편집기
|
||||
import "./v2-approval-step/ApprovalStepRenderer"; // 결재 단계 시각화
|
||||
import "./v2-status-count/StatusCountRenderer"; // 상태별 카운트 카드
|
||||
import "./v2-process-work-standard/ProcessWorkStandardRenderer"; // 공정 작업기준
|
||||
import "./v2-item-routing/ItemRoutingRenderer"; // 품목별 라우팅
|
||||
|
||||
/**
|
||||
* 컴포넌트 초기화 함수
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* 다양한 집계 연산을 수행합니다.
|
||||
*/
|
||||
|
||||
import { getFormatRules } from "@/lib/formatting";
|
||||
|
||||
import { AggregationType, PivotFieldFormat } from "../types";
|
||||
|
||||
// ==================== 집계 함수 ====================
|
||||
@@ -102,16 +104,18 @@ export function formatNumber(
|
||||
|
||||
let formatted: string;
|
||||
|
||||
const locale = getFormatRules().number.locale;
|
||||
|
||||
switch (type) {
|
||||
case "currency":
|
||||
formatted = value.toLocaleString("ko-KR", {
|
||||
formatted = value.toLocaleString(locale, {
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision,
|
||||
});
|
||||
break;
|
||||
|
||||
case "percent":
|
||||
formatted = (value * 100).toLocaleString("ko-KR", {
|
||||
formatted = (value * 100).toLocaleString(locale, {
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision,
|
||||
});
|
||||
@@ -120,7 +124,7 @@ export function formatNumber(
|
||||
case "number":
|
||||
default:
|
||||
if (thousandSeparator) {
|
||||
formatted = value.toLocaleString("ko-KR", {
|
||||
formatted = value.toLocaleString(locale, {
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision,
|
||||
});
|
||||
@@ -138,7 +142,7 @@ export function formatNumber(
|
||||
*/
|
||||
export function formatDate(
|
||||
value: Date | string | null | undefined,
|
||||
format: string = "YYYY-MM-DD"
|
||||
format: string = getFormatRules().date.display
|
||||
): string {
|
||||
if (!value) return "-";
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ function getFieldValue(
|
||||
const weekNum = getWeekNumber(date);
|
||||
return `${date.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
|
||||
case "day":
|
||||
return formatDate(date, "YYYY-MM-DD");
|
||||
return formatDate(date);
|
||||
default:
|
||||
return String(rawValue);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AggregationWidgetConfig, AggregationItem, AggregationResult, Aggregatio
|
||||
import { Calculator, TrendingUp, Hash, ArrowUp, ArrowDown, Loader2 } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useScreenMultiLang } from "@/contexts/ScreenMultiLangContext";
|
||||
import { formatNumber } from "@/lib/formatting";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { v2EventBus, V2_EVENTS, V2ErrorBoundary } from "@/lib/v2-core";
|
||||
|
||||
@@ -566,11 +567,11 @@ export function AggregationWidgetComponent({
|
||||
let formattedValue = value.toFixed(item.decimalPlaces ?? 0);
|
||||
|
||||
if (item.format === "currency") {
|
||||
formattedValue = new Intl.NumberFormat("ko-KR").format(value);
|
||||
formattedValue = formatNumber(value);
|
||||
} else if (item.format === "percent") {
|
||||
formattedValue = `${(value * 100).toFixed(item.decimalPlaces ?? 1)}%`;
|
||||
} else if (item.format === "number") {
|
||||
formattedValue = new Intl.NumberFormat("ko-KR").format(value);
|
||||
formattedValue = formatNumber(value);
|
||||
}
|
||||
|
||||
if (item.prefix) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import { showErrorToast } from "@/lib/utils/toastUtils";
|
||||
import { filterDOMProps } from "@/lib/utils/domPropsFilter";
|
||||
import { ButtonIconRenderer } from "@/lib/button-icon-map";
|
||||
import { useCurrentFlowStep } from "@/stores/flowStepStore";
|
||||
import { useScreenPreview } from "@/contexts/ScreenPreviewContext";
|
||||
import { useScreenContextOptional } from "@/contexts/ScreenContext";
|
||||
@@ -29,7 +30,6 @@ import { useSplitPanelContext, SplitPanelPosition } from "@/contexts/SplitPanelC
|
||||
import { applyMappingRules } from "@/lib/utils/dataMapping";
|
||||
import { apiClient } from "@/lib/api/client";
|
||||
import { V2ErrorBoundary, v2EventBus, V2_EVENTS } from "@/lib/v2-core";
|
||||
|
||||
export interface ButtonPrimaryComponentProps extends ComponentRendererProps {
|
||||
config?: ButtonPrimaryConfig;
|
||||
// 추가 props
|
||||
@@ -556,13 +556,23 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
}
|
||||
|
||||
// 스타일 계산
|
||||
// 🔧 사용자가 설정한 크기가 있으면 그대로 사용
|
||||
const componentStyle: React.CSSProperties = {
|
||||
// 외부 wrapper는 부모 컨테이너(RealtimePreviewDynamic)에 맞춰 100% 채움
|
||||
// border는 내부 버튼에서만 적용 (wrapper에 적용되면 이중 테두리 발생)
|
||||
const {
|
||||
border: _border, borderWidth: _bw, borderStyle: _bs, borderColor: _bc, borderRadius: _br,
|
||||
...restComponentStyle
|
||||
} = {
|
||||
...component.style,
|
||||
...style,
|
||||
} as React.CSSProperties & Record<string, any>;
|
||||
|
||||
const componentStyle: React.CSSProperties = {
|
||||
...restComponentStyle,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
};
|
||||
|
||||
// 디자인 모드 스타일 (border 속성 분리하여 충돌 방지)
|
||||
// 디자인 모드 스타일
|
||||
if (isDesignMode) {
|
||||
componentStyle.borderWidth = "1px";
|
||||
componentStyle.borderStyle = "dashed";
|
||||
@@ -1217,15 +1227,6 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
effectiveFormData = { ...splitPanelParentData };
|
||||
}
|
||||
|
||||
console.log("🔴 [ButtonPrimary] 저장 시 formData 디버그:", {
|
||||
propsFormDataKeys: Object.keys(propsFormData),
|
||||
screenContextFormDataKeys: Object.keys(screenContextFormData),
|
||||
effectiveFormDataKeys: Object.keys(effectiveFormData),
|
||||
process_code: effectiveFormData.process_code,
|
||||
equipment_code: effectiveFormData.equipment_code,
|
||||
fullData: JSON.stringify(effectiveFormData),
|
||||
});
|
||||
|
||||
const context: ButtonActionContext = {
|
||||
formData: effectiveFormData,
|
||||
originalData: originalData, // 🔧 빈 객체 대신 undefined 유지 (UPDATE 판단에 사용)
|
||||
@@ -1382,31 +1383,29 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 최종 비활성화 상태 (설정 + 조건부 비활성화 + 행 선택 필수)
|
||||
const finalDisabled =
|
||||
componentConfig.disabled || isOperationButtonDisabled || isRowSelectionDisabled || statusLoading;
|
||||
|
||||
// 공통 버튼 스타일
|
||||
// 🔧 component.style에서 background/backgroundColor 충돌 방지 (width/height는 허용)
|
||||
// 크기는 부모 컨테이너(RealtimePreviewDynamic)에서 관리하므로 width/height 제외
|
||||
const userStyle = component.style
|
||||
? Object.fromEntries(
|
||||
Object.entries(component.style).filter(([key]) => !["background", "backgroundColor"].includes(key)),
|
||||
Object.entries(component.style).filter(([key]) => !["background", "backgroundColor", "width", "height"].includes(key)),
|
||||
)
|
||||
: {};
|
||||
|
||||
// 🔧 사용자가 설정한 크기 우선 사용, 없으면 100%
|
||||
const buttonWidth = component.size?.width ? `${component.size.width}px` : style?.width || "100%";
|
||||
const buttonHeight = component.size?.height ? `${component.size.height}px` : style?.height || "100%";
|
||||
// 버튼은 부모 컨테이너를 꽉 채움 (크기는 RealtimePreviewDynamic에서 관리)
|
||||
const buttonWidth = "100%";
|
||||
const buttonHeight = "100%";
|
||||
|
||||
const buttonElementStyle: React.CSSProperties = {
|
||||
width: buttonWidth,
|
||||
height: buttonHeight,
|
||||
minHeight: "32px", // 🔧 최소 높이를 32px로 줄임
|
||||
// 🔧 커스텀 테두리 스타일 (StyleEditor에서 설정한 값 우선)
|
||||
border: style?.border || (style?.borderWidth ? undefined : "none"),
|
||||
borderWidth: style?.borderWidth || undefined,
|
||||
borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || undefined,
|
||||
borderColor: style?.borderColor || undefined,
|
||||
// 커스텀 테두리 스타일 (StyleEditor 설정 우선, shorthand 사용 안 함)
|
||||
borderWidth: style?.borderWidth || "0",
|
||||
borderStyle: (style?.borderStyle as React.CSSProperties["borderStyle"]) || (style?.borderWidth ? "solid" : "none"),
|
||||
borderColor: style?.borderColor || "transparent",
|
||||
borderRadius: style?.borderRadius || "0.5rem",
|
||||
backgroundColor: finalDisabled ? "#e5e7eb" : buttonColor,
|
||||
color: finalDisabled ? "#9ca3af" : (style?.color || buttonTextColor), // 🔧 StyleEditor 텍스트 색상도 지원
|
||||
@@ -1444,7 +1443,7 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
cancel: "취소",
|
||||
};
|
||||
|
||||
const buttonContent =
|
||||
const buttonTextContent =
|
||||
processedConfig.text ||
|
||||
component.webTypeConfig?.text ||
|
||||
component.componentConfig?.text ||
|
||||
@@ -1458,16 +1457,17 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
<>
|
||||
<div style={componentStyle} className={className} {...safeDomProps}>
|
||||
{isDesignMode ? (
|
||||
// 디자인 모드: div로 렌더링하여 선택 가능하게 함
|
||||
<div
|
||||
className="transition-colors duration-150 hover:opacity-90"
|
||||
style={buttonElementStyle}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{buttonContent}
|
||||
<ButtonIconRenderer
|
||||
componentConfig={componentConfig}
|
||||
fallbackLabel={buttonTextContent as string}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// 일반 모드: button으로 렌더링
|
||||
<button
|
||||
type={componentConfig.actionType || "button"}
|
||||
disabled={finalDisabled}
|
||||
@@ -1476,8 +1476,12 @@ export const ButtonPrimaryComponent: React.FC<ButtonPrimaryComponentProps> = ({
|
||||
onClick={handleClick}
|
||||
onDragStart={onDragStart}
|
||||
onDragEnd={onDragEnd}
|
||||
{...(actionType ? { "data-action-type": actionType } : {})}
|
||||
>
|
||||
{buttonContent}
|
||||
<ButtonIconRenderer
|
||||
componentConfig={componentConfig}
|
||||
fallbackLabel={buttonTextContent as string}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponent
|
||||
import { V2DateDefinition } from "./index";
|
||||
import { V2Date } from "@/components/v2/V2Date";
|
||||
import { isColumnRequiredByMeta } from "../../DynamicComponentRenderer";
|
||||
import { getFormatRules } from "@/lib/formatting";
|
||||
|
||||
/**
|
||||
* V2Date 렌더러
|
||||
@@ -34,7 +35,7 @@ export class V2DateRenderer extends AutoRegisteringComponentRenderer {
|
||||
// 라벨: style.labelText 우선, 없으면 component.label 사용
|
||||
// style.labelDisplay가 false면 라벨 숨김
|
||||
const style = component.style || {};
|
||||
const effectiveLabel = style.labelDisplay === false ? undefined : (style.labelText || component.label);
|
||||
const effectiveLabel = style.labelDisplay === false ? undefined : style.labelText || component.label;
|
||||
|
||||
return (
|
||||
<V2Date
|
||||
@@ -43,7 +44,7 @@ export class V2DateRenderer extends AutoRegisteringComponentRenderer {
|
||||
onChange={handleChange}
|
||||
config={{
|
||||
dateType: config.dateType || config.webType || "date",
|
||||
format: config.format || "YYYY-MM-DD",
|
||||
format: config.format || getFormatRules().date.display,
|
||||
placeholder: config.placeholder || style.placeholder || "날짜 선택",
|
||||
showTime: config.showTime || false,
|
||||
use24Hours: config.use24Hours ?? true,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* 다양한 집계 연산을 수행합니다.
|
||||
*/
|
||||
|
||||
import { getFormatRules } from "@/lib/formatting";
|
||||
|
||||
import { AggregationType, PivotFieldFormat } from "../types";
|
||||
|
||||
// ==================== 집계 함수 ====================
|
||||
@@ -102,16 +104,18 @@ export function formatNumber(
|
||||
|
||||
let formatted: string;
|
||||
|
||||
const locale = getFormatRules().number.locale;
|
||||
|
||||
switch (type) {
|
||||
case "currency":
|
||||
formatted = value.toLocaleString("ko-KR", {
|
||||
formatted = value.toLocaleString(locale, {
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision,
|
||||
});
|
||||
break;
|
||||
|
||||
case "percent":
|
||||
formatted = (value * 100).toLocaleString("ko-KR", {
|
||||
formatted = (value * 100).toLocaleString(locale, {
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision,
|
||||
});
|
||||
@@ -120,7 +124,7 @@ export function formatNumber(
|
||||
case "number":
|
||||
default:
|
||||
if (thousandSeparator) {
|
||||
formatted = value.toLocaleString("ko-KR", {
|
||||
formatted = value.toLocaleString(locale, {
|
||||
minimumFractionDigits: precision,
|
||||
maximumFractionDigits: precision,
|
||||
});
|
||||
@@ -138,7 +142,7 @@ export function formatNumber(
|
||||
*/
|
||||
export function formatDate(
|
||||
value: Date | string | null | undefined,
|
||||
format: string = "YYYY-MM-DD"
|
||||
format: string = getFormatRules().date.display
|
||||
): string {
|
||||
if (!value) return "-";
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ function getFieldValue(
|
||||
const weekNum = getWeekNumber(date);
|
||||
return `${date.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
|
||||
case "day":
|
||||
return formatDate(date, "YYYY-MM-DD");
|
||||
return formatDate(date);
|
||||
default:
|
||||
return String(rawValue);
|
||||
}
|
||||
|
||||
@@ -468,7 +468,11 @@ export function TableSearchWidget({ component, screenId, onHeightChange }: Table
|
||||
}
|
||||
|
||||
if (!cancelled && hasNewOptions) {
|
||||
setSelectOptions((prev) => ({ ...prev, ...loadedOptions }));
|
||||
setSelectOptions((prev) => {
|
||||
// 새로 로드된 옵션으로 항상 갱신 (카테고리 label 정보가 나중에 로드될 수 있으므로)
|
||||
// 로드 실패한 컬럼의 기존 옵션은 유지
|
||||
return { ...prev, ...loadedOptions };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user