feat: 수주관리 품목 CRUD 및 공통 필드 자동 복사 구현
- 품목 추가 시 공통 필드(거래처, 담당자, 메모) 자동 복사 - ModalRepeaterTable onChange 시 groupData 반영 - 백엔드 타입 캐스팅으로 PostgreSQL 에러 해결 - 타입 정규화로 불필요한 UPDATE 방지 - 수정 모달에서 거래처/수주번호 읽기 전용 처리
This commit is contained in:
parent
d04330283a
commit
5609e32daf
|
|
@ -811,9 +811,39 @@ export class DynamicFormService {
|
||||||
const primaryKeyColumn = primaryKeys[0];
|
const primaryKeyColumn = primaryKeys[0];
|
||||||
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
|
console.log(`🔑 테이블 ${tableName}의 기본키: ${primaryKeyColumn}`);
|
||||||
|
|
||||||
// 동적 UPDATE SQL 생성 (변경된 필드만)
|
// 🆕 컬럼 타입 조회 (타입 캐스팅용)
|
||||||
|
const columnTypesQuery = `
|
||||||
|
SELECT column_name, data_type
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = $1 AND table_schema = 'public'
|
||||||
|
`;
|
||||||
|
const columnTypesResult = await query<{ column_name: string; data_type: string }>(
|
||||||
|
columnTypesQuery,
|
||||||
|
[tableName]
|
||||||
|
);
|
||||||
|
const columnTypes: Record<string, string> = {};
|
||||||
|
columnTypesResult.forEach((row) => {
|
||||||
|
columnTypes[row.column_name] = row.data_type;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("📊 컬럼 타입 정보:", columnTypes);
|
||||||
|
|
||||||
|
// 🆕 동적 UPDATE SQL 생성 (타입 캐스팅 포함)
|
||||||
const setClause = Object.keys(changedFields)
|
const setClause = Object.keys(changedFields)
|
||||||
.map((key, index) => `${key} = $${index + 1}`)
|
.map((key, index) => {
|
||||||
|
const dataType = columnTypes[key];
|
||||||
|
// 숫자 타입인 경우 명시적 캐스팅
|
||||||
|
if (dataType === 'integer' || dataType === 'bigint' || dataType === 'smallint') {
|
||||||
|
return `${key} = $${index + 1}::integer`;
|
||||||
|
} else if (dataType === 'numeric' || dataType === 'decimal' || dataType === 'real' || dataType === 'double precision') {
|
||||||
|
return `${key} = $${index + 1}::numeric`;
|
||||||
|
} else if (dataType === 'boolean') {
|
||||||
|
return `${key} = $${index + 1}::boolean`;
|
||||||
|
} else {
|
||||||
|
// 문자열 타입은 캐스팅 불필요
|
||||||
|
return `${key} = $${index + 1}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
const values: any[] = Object.values(changedFields);
|
const values: any[] = Object.values(changedFields);
|
||||||
|
|
|
||||||
|
|
@ -320,43 +320,24 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
let deletedCount = 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가 없는 항목)
|
// 1️⃣ 신규 품목 추가 (id가 없는 항목)
|
||||||
for (const currentData of groupData) {
|
for (const currentData of groupData) {
|
||||||
if (!currentData.id) {
|
if (!currentData.id) {
|
||||||
console.log("➕ 신규 품목 추가:", currentData);
|
console.log("➕ 신규 품목 추가:", currentData);
|
||||||
|
console.log("📋 [신규 품목] currentData 키 목록:", Object.keys(currentData));
|
||||||
|
|
||||||
// 실제 테이블 컬럼만 추출
|
// 🆕 모든 데이터를 포함 (id 제외)
|
||||||
const insertData: Record<string, any> = {};
|
const insertData: Record<string, any> = { ...currentData };
|
||||||
Object.keys(currentData).forEach((key) => {
|
console.log("📦 [신규 품목] 복사 직후 insertData:", insertData);
|
||||||
if (salesOrderColumns.includes(key) && key !== "id") {
|
console.log("📋 [신규 품목] insertData 키 목록:", Object.keys(insertData));
|
||||||
insertData[key] = currentData[key];
|
|
||||||
}
|
delete insertData.id; // id는 자동 생성되므로 제거
|
||||||
});
|
|
||||||
|
|
||||||
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
|
// 🆕 groupByColumns의 값을 강제로 포함 (order_no 등)
|
||||||
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
if (modalState.groupByColumns && modalState.groupByColumns.length > 0) {
|
||||||
modalState.groupByColumns.forEach((colName) => {
|
modalState.groupByColumns.forEach((colName) => {
|
||||||
// 기존 품목(groupData[0])에서 groupByColumns 값 가져오기
|
// 기존 품목(originalGroupData[0])에서 groupByColumns 값 가져오기
|
||||||
const referenceData = originalGroupData[0] || groupData.find(item => item.id);
|
const referenceData = originalGroupData[0] || groupData.find((item) => item.id);
|
||||||
if (referenceData && referenceData[colName]) {
|
if (referenceData && referenceData[colName]) {
|
||||||
insertData[colName] = referenceData[colName];
|
insertData[colName] = referenceData[colName];
|
||||||
console.log(`🔑 [신규 품목] ${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:", insertData);
|
||||||
|
console.log("📋 [신규 품목] 최종 insertData 키 목록:", Object.keys(insertData));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await dynamicFormApi.saveFormData({
|
const response = await dynamicFormApi.saveFormData({
|
||||||
|
|
@ -398,16 +403,32 @@ export const EditModal: React.FC<EditModalProps> = ({ className }) => {
|
||||||
continue;
|
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> = {};
|
const changedData: Record<string, any> = {};
|
||||||
Object.keys(currentData).forEach((key) => {
|
Object.keys(currentData).forEach((key) => {
|
||||||
// sales_order_mng 테이블의 컬럼만 처리 (조인 컬럼 제외)
|
// id는 변경 불가
|
||||||
if (!salesOrderColumns.includes(key)) {
|
if (key === "id") {
|
||||||
return;
|
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}
|
isInModal={true}
|
||||||
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
// 🆕 그룹 데이터를 ModalRepeaterTable에 전달
|
||||||
groupedData={groupData.length > 0 ? groupData : undefined}
|
groupedData={groupData.length > 0 ? groupData : undefined}
|
||||||
|
// 🆕 수정 모달에서 읽기 전용 필드 지정 (수주번호, 거래처)
|
||||||
|
disabledFields={["order_no", "partner_id"]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,8 @@ interface InteractiveScreenViewerProps {
|
||||||
companyCode?: string;
|
companyCode?: string;
|
||||||
// 🆕 그룹 데이터 (EditModal에서 전달)
|
// 🆕 그룹 데이터 (EditModal에서 전달)
|
||||||
groupedData?: Record<string, any>[];
|
groupedData?: Record<string, any>[];
|
||||||
|
// 🆕 비활성화할 필드 목록 (EditModal에서 전달)
|
||||||
|
disabledFields?: string[];
|
||||||
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
// 🆕 EditModal 내부인지 여부 (button-primary가 EditModal의 handleSave 사용하도록)
|
||||||
isInModal?: boolean;
|
isInModal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
@ -66,6 +68,7 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
userName: externalUserName,
|
userName: externalUserName,
|
||||||
companyCode: externalCompanyCode,
|
companyCode: externalCompanyCode,
|
||||||
groupedData,
|
groupedData,
|
||||||
|
disabledFields = [],
|
||||||
isInModal = false,
|
isInModal = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인
|
||||||
|
|
@ -341,6 +344,8 @@ export const InteractiveScreenViewerDynamic: React.FC<InteractiveScreenViewerPro
|
||||||
}}
|
}}
|
||||||
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
|
// 🆕 그룹 데이터 전달 (EditModal → ModalRepeaterTable)
|
||||||
groupedData={groupedData}
|
groupedData={groupedData}
|
||||||
|
// 🆕 비활성화 필드 전달 (EditModal → 각 컴포넌트)
|
||||||
|
disabledFields={disabledFields}
|
||||||
flowSelectedData={flowSelectedData}
|
flowSelectedData={flowSelectedData}
|
||||||
flowSelectedStepId={flowSelectedStepId}
|
flowSelectedStepId={flowSelectedStepId}
|
||||||
onFlowSelectedDataChange={(selectedData, stepId) => {
|
onFlowSelectedDataChange={(selectedData, stepId) => {
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,8 @@ export interface DynamicComponentRendererProps {
|
||||||
selectedRows?: any[];
|
selectedRows?: any[];
|
||||||
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
// 🆕 그룹 데이터 (EditModal → ModalRepeaterTable)
|
||||||
groupedData?: Record<string, any>[];
|
groupedData?: Record<string, any>[];
|
||||||
|
// 🆕 비활성화할 필드 목록 (EditModal → 각 컴포넌트)
|
||||||
|
disabledFields?: string[];
|
||||||
selectedRowsData?: any[];
|
selectedRowsData?: any[];
|
||||||
onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[], sortBy?: string, sortOrder?: "asc" | "desc", columnOrder?: string[], tableDisplayData?: any[]) => void;
|
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 (
|
return (
|
||||||
<CategorySelectComponent
|
<CategorySelectComponent
|
||||||
tableName={tableName}
|
tableName={tableName}
|
||||||
|
|
@ -176,7 +181,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
placeholder={component.componentConfig?.placeholder || "선택하세요"}
|
placeholder={component.componentConfig?.placeholder || "선택하세요"}
|
||||||
required={(component as any).required}
|
required={(component as any).required}
|
||||||
disabled={(component as any).readonly}
|
disabled={isFieldDisabled}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
@ -271,6 +276,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
isPreview,
|
isPreview,
|
||||||
autoGeneration,
|
autoGeneration,
|
||||||
|
disabledFields, // 🆕 비활성화 필드 목록
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
|
@ -368,7 +374,8 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
|
||||||
mode,
|
mode,
|
||||||
isInModal,
|
isInModal,
|
||||||
readonly: component.readonly,
|
readonly: component.readonly,
|
||||||
disabled: component.readonly,
|
// 🆕 disabledFields 체크 또는 기존 readonly
|
||||||
|
disabled: disabledFields?.includes(fieldName) || component.readonly,
|
||||||
originalData,
|
originalData,
|
||||||
allComponents,
|
allComponents,
|
||||||
onUpdateLayout,
|
onUpdateLayout,
|
||||||
|
|
|
||||||
|
|
@ -195,13 +195,18 @@ export function ModalRepeaterTableComponent({
|
||||||
const columnName = component?.columnName;
|
const columnName = component?.columnName;
|
||||||
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
const value = (columnName && formData?.[columnName]) || componentConfig?.value || propValue || [];
|
||||||
|
|
||||||
// ✅ onChange 래퍼 (기존 onChange 콜백만 호출, formData는 beforeFormSave에서 처리)
|
// ✅ onChange 래퍼 (기존 onChange 콜백 + onFormDataChange 호출)
|
||||||
const handleChange = (newData: any[]) => {
|
const handleChange = (newData: any[]) => {
|
||||||
// 기존 onChange 콜백 호출 (호환성)
|
// 기존 onChange 콜백 호출 (호환성)
|
||||||
const externalOnChange = componentConfig?.onChange || propOnChange;
|
const externalOnChange = componentConfig?.onChange || propOnChange;
|
||||||
if (externalOnChange) {
|
if (externalOnChange) {
|
||||||
externalOnChange(newData);
|
externalOnChange(newData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 onFormDataChange 호출하여 EditModal의 groupData 업데이트
|
||||||
|
if (onFormDataChange && columnName) {
|
||||||
|
onFormDataChange(columnName, newData);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
// uniqueField 자동 보정: order_no는 item_info 테이블에 없으므로 item_number로 변경
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue