feat: 수주관리 품목 CRUD 및 공통 필드 자동 복사 구현
- 품목 추가 시 공통 필드(거래처, 담당자, 메모) 자동 복사 - ModalRepeaterTable onChange 시 groupData 반영 - 백엔드 타입 캐스팅으로 PostgreSQL 에러 해결 - 타입 정규화로 불필요한 UPDATE 방지 - 수정 모달에서 거래처/수주번호 읽기 전용 처리
This commit is contained in:
@@ -320,43 +320,24 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
let updatedCount = 0;
|
||||
let deletedCount = 0;
|
||||
|
||||
// 🆕 sales_order_mng 테이블의 실제 컬럼만 포함 (조인된 컬럼 제외)
|
||||
const salesOrderColumns = [
|
||||
"id",
|
||||
"order_no",
|
||||
"customer_code",
|
||||
"customer_name",
|
||||
"order_date",
|
||||
"delivery_date",
|
||||
"item_code",
|
||||
"quantity",
|
||||
"unit_price",
|
||||
"amount",
|
||||
"status",
|
||||
"notes",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"company_code",
|
||||
];
|
||||
|
||||
// 1️⃣ 신규 품목 추가 (id가 없는 항목)
|
||||
for (const currentData of groupData) {
|
||||
if (!currentData.id) {
|
||||
console.log("➕ 신규 품목 추가:", currentData);
|
||||
console.log("📋 [신규 품목] currentData 키 목록:", Object.keys(currentData));
|
||||
|
||||
// 실제 테이블 컬럼만 추출
|
||||
const insertData: Record<string, any> = {};
|
||||
Object.keys(currentData).forEach((key) => {
|
||||
if (salesOrderColumns.includes(key) && key !== "id") {
|
||||
insertData[key] = currentData[key];
|
||||
}
|
||||
});
|
||||
// 🆕 모든 데이터를 포함 (id 제외)
|
||||
const insertData: Record<string, any> = { ...currentData };
|
||||
console.log("📦 [신규 품목] 복사 직후 insertData:", insertData);
|
||||
console.log("📋 [신규 품목] insertData 키 목록:", Object.keys(insertData));
|
||||
|
||||
delete insertData.id; // id는 자동 생성되므로 제거
|
||||
|
||||
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
|
||||
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
||||
modalState.groupByColumns.forEach((colName) => {
|
||||
// 기존 품목(groupData[0])에서 groupByColumns 값 가져오기
|
||||
const referenceData = originalGroupData[0] || groupData.find(item => item.id);
|
||||
// 기존 품목(originalGroupData[0])에서 groupByColumns 값 가져오기
|
||||
const referenceData = originalGroupData[0] || groupData.find((item) => item.id);
|
||||
if (referenceData && referenceData[colName]) {
|
||||
insertData[colName] = referenceData[colName];
|
||||
console.log(`🔑 [신규 품목] ${colName} 값 추가:`, referenceData[colName]);
|
||||
@@ -364,7 +345,31 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
});
|
||||
}
|
||||
|
||||
// 🆕 공통 필드 추가 (거래처, 담당자, 납품처, 메모 등)
|
||||
// formData에서 품목별 필드가 아닌 공통 필드를 복사
|
||||
const commonFields = [
|
||||
'partner_id', // 거래처
|
||||
'manager_id', // 담당자
|
||||
'delivery_partner_id', // 납품처
|
||||
'delivery_address', // 납품장소
|
||||
'memo', // 메모
|
||||
'order_date', // 주문일
|
||||
'due_date', // 납기일
|
||||
'shipping_method', // 배송방법
|
||||
'status', // 상태
|
||||
'sales_type', // 영업유형
|
||||
];
|
||||
|
||||
commonFields.forEach((fieldName) => {
|
||||
// formData에 값이 있으면 추가
|
||||
if (formData[fieldName] !== undefined && formData[fieldName] !== null) {
|
||||
insertData[fieldName] = formData[fieldName];
|
||||
console.log(`🔗 [공통 필드] ${fieldName} 값 추가:`, formData[fieldName]);
|
||||
}
|
||||
});
|
||||
|
||||
console.log("📦 [신규 품목] 최종 insertData:", insertData);
|
||||
console.log("📋 [신규 품목] 최종 insertData 키 목록:", Object.keys(insertData));
|
||||
|
||||
try {
|
||||
const response = await dynamicFormApi.saveFormData({
|
||||
@@ -398,16 +403,32 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 변경된 필드만 추출
|
||||
// 🆕 값 정규화 함수 (타입 통일)
|
||||
const normalizeValue = (val: any): any => {
|
||||
if (val === null || val === undefined || val === "") return null;
|
||||
if (typeof val === "string" && !isNaN(Number(val))) {
|
||||
// 숫자로 변환 가능한 문자열은 숫자로
|
||||
return Number(val);
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
// 변경된 필드만 추출 (id 제외)
|
||||
const changedData: Record<string, any> = {};
|
||||
Object.keys(currentData).forEach((key) => {
|
||||
// sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외)
|
||||
if (!salesOrderColumns.includes(key)) {
|
||||
// id는 변경 불가
|
||||
if (key === "id") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentData[key] !== originalItemData[key]) {
|
||||
changedData[key] = currentData[key];
|
||||
// 🆕 타입 정규화 후 비교
|
||||
const currentValue = normalizeValue(currentData[key]);
|
||||
const originalValue = normalizeValue(originalItemData[key]);
|
||||
|
||||
// 값이 변경된 경우만 포함
|
||||
if (currentValue !== originalValue) {
|
||||
console.log(`🔍 [품목 수정 감지] ${key}: ${originalValue} → ${currentValue}`);
|
||||
changedData[key] = currentData[key]; // 원본 값 사용 (문자열 그대로)
|
||||
}
|
||||
});
|
||||
|
||||
@@ -677,6 +698,8 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||
isInModal={true}
|
||||
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
||||
groupedData={groupData.length > 0 ? groupData : undefined}
|
||||
// 🆕 수정 모달에서 읽기 전용 필드 지정 (수주번호, 거래처)
|
||||
disabledFields={["order_no", "partner_id"]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -48,6 +48,8 @@ interface InteractiveScreenViewerProps {
|
||||
companyCode?: string;
|
||||
// 🆕 그룹 데이터 (EditModal에서 전달)
|
||||
groupedData?: Record<string, any>[];
|
||||
// 🆕 비활성화할 필드 목록 (EditModal에서 전달)
|
||||
disabledFields?: string[];
|
||||
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
||||
isInModal?: boolean;
|
||||
}
|
||||
@@ -66,6 +68,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
userName: externalUserName,
|
||||
companyCode: externalCompanyCode,
|
||||
groupedData,
|
||||
disabledFields = [],
|
||||
isInModal = false,
|
||||
}) => {
|
||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||
@@ -341,6 +344,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||
}}
|
||||
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
|
||||
groupedData={groupedData}
|
||||
// 🆕 비활성화 필드 전달 (EditModal → 각 컴포넌트)
|
||||
disabledFields={disabledFields}
|
||||
flowSelectedData={flowSelectedData}
|
||||
flowSelectedStepId={flowSelectedStepId}
|
||||
onFlowSelectedDataChange={(selectedData, stepId) => {
|
||||
|
||||
@@ -110,6 +110,8 @@ export interface DynamicComponentRendererProps {
|
||||
selectedRows?: any[];
|
||||
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
||||
groupedData?: Record<string, any>[];
|
||||
// 🆕 비활성화할 필드 목록 (EditModal → 각 컴포넌트)
|
||||
disabledFields?: string[];
|
||||
selectedRowsData?: any[];
|
||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
|
||||
// 테이블 정렬 정보 (엑셀 다운로드용)
|
||||
@@ -168,6 +170,9 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 disabledFields 체크
|
||||
const isFieldDisabled = props.disabledFields?.includes(columnName) || (component as any).readonly;
|
||||
|
||||
return (
|
||||
<CategorySelectComponent
|
||||
tableName={tableName}
|
||||
@@ -176,7 +181,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
onChange={handleChange}
|
||||
placeholder={component.componentConfig?.placeholder || "선택하세요"}
|
||||
required={(component as any).required}
|
||||
disabled={(component as any).readonly}
|
||||
disabled={isFieldDisabled}
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
@@ -271,6 +276,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
onConfigChange,
|
||||
isPreview,
|
||||
autoGeneration,
|
||||
disabledFields, // 🆕 비활성화 필드 목록
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
@@ -368,7 +374,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||
mode,
|
||||
isInModal,
|
||||
readonly: component.readonly,
|
||||
disabled: component.readonly,
|
||||
// 🆕 disabledFields 체크 또는 기존 readonly
|
||||
disabled: disabledFields?.includes(fieldName) || component.readonly,
|
||||
originalData,
|
||||
allComponents,
|
||||
onUpdateLayout,
|
||||
|
||||
@@ -154,18 +154,18 @@ export function ConditionalSectionViewer({
|
||||
}}
|
||||
>
|
||||
<DynamicComponentRenderer
|
||||
component={component}
|
||||
component={component}
|
||||
isInteractive={true}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={user?.companyCode}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
screenId={screenInfo?.id}
|
||||
tableName={screenInfo?.tableName}
|
||||
userId={userId}
|
||||
userName={userName}
|
||||
companyCode={user?.companyCode}
|
||||
formData={formData}
|
||||
onFormDataChange={onFormDataChange}
|
||||
groupedData={groupedData}
|
||||
onSave={onSave}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -195,13 +195,18 @@ export function ModalRepeaterTableComponent({
|
||||
const columnName = component?.columnName;
|
||||
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||
|
||||
// ✅ onChange 래퍼 (기존 onChange 콜백만 호출, formData는 beforeFormSave에서 처리)
|
||||
// ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출)
|
||||
const handleChange = (newData: any[]) => {
|
||||
// 기존 onChange 콜백 호출 (호환성)
|
||||
const externalOnChange = componentConfig?.onChange || propOnChange;
|
||||
if (externalOnChange) {
|
||||
externalOnChange(newData);
|
||||
}
|
||||
|
||||
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
|
||||
if (onFormDataChange && columnName) {
|
||||
onFormDataChange(columnName, newData);
|
||||
}
|
||||
};
|
||||
|
||||
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
||||
|
||||
Reference in New Issue
Block a user