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];
|
||||
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)
|
||||
.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(", ");
|
||||
|
||||
const values: any[] = Object.values(changedFields);
|
||||
|
|
|
|||
|
|
@ -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로 변경
|
||||
|
|
|
|||
Loading…
Reference in New Issue