feat: implement packaging unit and item management APIs
- Added CRUD operations for packaging units and their associated items in the new `packagingController.ts`. - Implemented routes for managing packaging units and items in `packagingRoutes.ts`. - Enhanced error handling and logging for better traceability. - Ensured company code filtering for data access based on user roles. Made-with: Cursor
This commit is contained in:
@@ -1,10 +1,78 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { AutoRegisteringComponentRenderer } from "../../AutoRegisteringComponentRenderer";
|
||||
import { V2InputDefinition } from "./index";
|
||||
import { V2Input } from "@/components/v2/V2Input";
|
||||
import { isColumnRequiredByMeta } from "../../DynamicComponentRenderer";
|
||||
import { v2EventBus, V2_EVENTS } from "@/lib/v2-core";
|
||||
|
||||
/**
|
||||
* dataBinding이 설정된 v2-input을 위한 wrapper
|
||||
* v2-table-list의 TABLE_DATA_CHANGE 이벤트를 구독하여
|
||||
* 선택된 행의 특정 컬럼 값을 자동으로 formData에 반영
|
||||
*/
|
||||
function DataBindingWrapper({
|
||||
dataBinding,
|
||||
columnName,
|
||||
onFormDataChange,
|
||||
isInteractive,
|
||||
children,
|
||||
}: {
|
||||
dataBinding: { sourceComponentId: string; sourceColumn: string };
|
||||
columnName: string;
|
||||
onFormDataChange?: (field: string, value: any) => void;
|
||||
isInteractive?: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const lastBoundValueRef = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataBinding?.sourceComponentId || !dataBinding?.sourceColumn) return;
|
||||
|
||||
console.log("[DataBinding] 구독 시작:", {
|
||||
sourceComponentId: dataBinding.sourceComponentId,
|
||||
sourceColumn: dataBinding.sourceColumn,
|
||||
targetColumn: columnName,
|
||||
isInteractive,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
});
|
||||
|
||||
const unsubscribe = v2EventBus.subscribe(V2_EVENTS.TABLE_DATA_CHANGE, (payload: any) => {
|
||||
console.log("[DataBinding] TABLE_DATA_CHANGE 수신:", {
|
||||
payloadSource: payload.source,
|
||||
expectedSource: dataBinding.sourceComponentId,
|
||||
dataLength: payload.data?.length,
|
||||
match: payload.source === dataBinding.sourceComponentId,
|
||||
});
|
||||
|
||||
if (payload.source !== dataBinding.sourceComponentId) return;
|
||||
|
||||
const selectedData = payload.data;
|
||||
if (selectedData && selectedData.length > 0) {
|
||||
const value = selectedData[0][dataBinding.sourceColumn];
|
||||
console.log("[DataBinding] 바인딩 값:", { column: dataBinding.sourceColumn, value, columnName });
|
||||
if (value !== lastBoundValueRef.current) {
|
||||
lastBoundValueRef.current = value;
|
||||
if (onFormDataChange && columnName) {
|
||||
onFormDataChange(columnName, value ?? "");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (lastBoundValueRef.current !== null) {
|
||||
lastBoundValueRef.current = null;
|
||||
if (onFormDataChange && columnName) {
|
||||
onFormDataChange(columnName, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return () => unsubscribe();
|
||||
}, [dataBinding?.sourceComponentId, dataBinding?.sourceColumn, columnName, onFormDataChange, isInteractive]);
|
||||
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
/**
|
||||
* V2Input 렌더러
|
||||
@@ -16,41 +84,37 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer {
|
||||
render(): React.ReactElement {
|
||||
const { component, formData, onFormDataChange, isDesignMode, isSelected, isInteractive, ...restProps } = this.props;
|
||||
|
||||
// 컴포넌트 설정 추출
|
||||
const config = component.componentConfig || component.config || {};
|
||||
const columnName = component.columnName;
|
||||
const tableName = component.tableName || this.props.tableName;
|
||||
|
||||
// formData에서 현재 값 가져오기
|
||||
const currentValue = formData?.[columnName] ?? component.value ?? "";
|
||||
|
||||
// 값 변경 핸들러
|
||||
const handleChange = (value: any) => {
|
||||
console.log("🔄 [V2InputRenderer] handleChange 호출:", {
|
||||
columnName,
|
||||
value,
|
||||
isInteractive,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
});
|
||||
if (isInteractive && onFormDataChange && columnName) {
|
||||
onFormDataChange(columnName, value);
|
||||
} else {
|
||||
console.warn("⚠️ [V2InputRenderer] onFormDataChange 호출 스킵:", {
|
||||
isInteractive,
|
||||
hasOnFormDataChange: !!onFormDataChange,
|
||||
columnName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 라벨: style.labelText 우선, 없으면 component.label 사용
|
||||
// 🔧 style.labelDisplay를 먼저 체크 (속성 패널에서 style 객체로 업데이트하므로)
|
||||
const style = component.style || {};
|
||||
const labelDisplay = style.labelDisplay ?? (component as any).labelDisplay;
|
||||
// labelDisplay: true → 라벨 표시, false → 숨김, undefined → 기존 동작 유지(숨김)
|
||||
const effectiveLabel = labelDisplay === true ? style.labelText || component.label : undefined;
|
||||
|
||||
return (
|
||||
const dataBinding = config.dataBinding || (component as any).dataBinding || config.componentConfig?.dataBinding;
|
||||
|
||||
if (dataBinding || (config as any).dataBinding || (component as any).dataBinding) {
|
||||
console.log("[V2InputRenderer] dataBinding 탐색:", {
|
||||
componentId: component.id,
|
||||
columnName,
|
||||
configKeys: Object.keys(config),
|
||||
configDataBinding: config.dataBinding,
|
||||
componentDataBinding: (component as any).dataBinding,
|
||||
nestedDataBinding: config.componentConfig?.dataBinding,
|
||||
finalDataBinding: dataBinding,
|
||||
});
|
||||
}
|
||||
|
||||
const inputElement = (
|
||||
<V2Input
|
||||
id={component.id}
|
||||
value={currentValue}
|
||||
@@ -77,10 +141,26 @@ export class V2InputRenderer extends AutoRegisteringComponentRenderer {
|
||||
{...restProps}
|
||||
label={effectiveLabel}
|
||||
required={component.required || isColumnRequiredByMeta(tableName, columnName)}
|
||||
readonly={config.readonly || component.readonly}
|
||||
readonly={config.readonly || component.readonly || !!dataBinding?.sourceComponentId}
|
||||
disabled={config.disabled || component.disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
// dataBinding이 있으면 wrapper로 감싸서 이벤트 구독
|
||||
if (dataBinding?.sourceComponentId && dataBinding?.sourceColumn) {
|
||||
return (
|
||||
<DataBindingWrapper
|
||||
dataBinding={dataBinding}
|
||||
columnName={columnName}
|
||||
onFormDataChange={onFormDataChange}
|
||||
isInteractive={isInteractive}
|
||||
>
|
||||
{inputElement}
|
||||
</DataBindingWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return inputElement;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5103,6 +5103,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -118,9 +118,9 @@ export interface AdditionalTabConfig {
|
||||
// 추가 버튼 설정 (모달 화면 연결 지원)
|
||||
addButton?: {
|
||||
enabled: boolean;
|
||||
mode: "auto" | "modal"; // auto: 내장 폼, modal: 커스텀 모달 화면
|
||||
modalScreenId?: number; // 모달로 열 화면 ID (mode: "modal"일 때)
|
||||
buttonLabel?: string; // 버튼 라벨 (기본: "추가")
|
||||
mode: "auto" | "modal";
|
||||
modalScreenId?: number;
|
||||
buttonLabel?: string;
|
||||
};
|
||||
|
||||
deleteButton?: {
|
||||
@@ -161,9 +161,9 @@ export interface SplitPanelLayoutConfig {
|
||||
// 추가 버튼 설정 (모달 화면 연결 지원)
|
||||
addButton?: {
|
||||
enabled: boolean;
|
||||
mode: "auto" | "modal"; // auto: 내장 폼, modal: 커스텀 모달 화면
|
||||
modalScreenId?: number; // 모달로 열 화면 ID (mode: "modal"일 때)
|
||||
buttonLabel?: string; // 버튼 라벨 (기본: "추가")
|
||||
mode: "auto" | "modal";
|
||||
modalScreenId?: number;
|
||||
buttonLabel?: string;
|
||||
};
|
||||
|
||||
columns?: Array<{
|
||||
@@ -334,10 +334,10 @@ export interface SplitPanelLayoutConfig {
|
||||
|
||||
// 🆕 추가 버튼 설정 (모달 화면 연결 지원)
|
||||
addButton?: {
|
||||
enabled: boolean; // 추가 버튼 표시 여부 (기본: true)
|
||||
mode: "auto" | "modal"; // auto: 내장 폼, modal: 커스텀 모달 화면
|
||||
modalScreenId?: number; // 모달로 열 화면 ID (mode: "modal"일 때)
|
||||
buttonLabel?: string; // 버튼 라벨 (기본: "추가")
|
||||
enabled: boolean;
|
||||
mode: "auto" | "modal";
|
||||
modalScreenId?: number;
|
||||
buttonLabel?: string;
|
||||
};
|
||||
|
||||
// 🆕 삭제 버튼 설정
|
||||
|
||||
@@ -2080,11 +2080,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
};
|
||||
|
||||
const handleRowSelection = (rowKey: string, checked: boolean) => {
|
||||
const newSelectedRows = new Set(selectedRows);
|
||||
if (checked) {
|
||||
newSelectedRows.add(rowKey);
|
||||
const isMultiSelect = tableConfig.checkbox?.multiple !== false;
|
||||
let newSelectedRows: Set<string>;
|
||||
|
||||
if (isMultiSelect) {
|
||||
newSelectedRows = new Set(selectedRows);
|
||||
if (checked) {
|
||||
newSelectedRows.add(rowKey);
|
||||
} else {
|
||||
newSelectedRows.delete(rowKey);
|
||||
}
|
||||
} else {
|
||||
newSelectedRows.delete(rowKey);
|
||||
// 단일 선택: 기존 선택 해제 후 새 항목만 선택
|
||||
newSelectedRows = checked ? new Set([rowKey]) : new Set();
|
||||
}
|
||||
setSelectedRows(newSelectedRows);
|
||||
|
||||
@@ -4154,6 +4162,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||
|
||||
const renderCheckboxHeader = () => {
|
||||
if (!tableConfig.checkbox?.selectAll) return null;
|
||||
if (tableConfig.checkbox?.multiple === false) return null;
|
||||
|
||||
return <Checkbox checked={isAllSelected} onCheckedChange={handleSelectAll} aria-label="전체 선택" />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user