Merge branch 'main' into feature/screen-management
This commit is contained in:
@@ -698,6 +698,7 @@ router.post(
|
||||
try {
|
||||
const { tableName } = req.params;
|
||||
const filterConditions = req.body;
|
||||
const userCompany = req.user?.companyCode;
|
||||
|
||||
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
||||
return res.status(400).json({
|
||||
@@ -706,11 +707,12 @@ router.post(
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`🗑️ 그룹 삭제:`, { tableName, filterConditions });
|
||||
console.log(`🗑️ 그룹 삭제:`, { tableName, filterConditions, userCompany });
|
||||
|
||||
const result = await dataService.deleteGroupRecords(
|
||||
tableName,
|
||||
filterConditions
|
||||
filterConditions,
|
||||
userCompany // 회사 코드 전달
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -1189,6 +1189,13 @@ class DataService {
|
||||
[tableName]
|
||||
);
|
||||
|
||||
console.log(`🔍 테이블 ${tableName}의 Primary Key 조회 결과:`, {
|
||||
pkColumns: pkResult.map((r) => r.attname),
|
||||
pkCount: pkResult.length,
|
||||
inputId: typeof id === "object" ? JSON.stringify(id).substring(0, 200) + "..." : id,
|
||||
inputIdType: typeof id,
|
||||
});
|
||||
|
||||
let whereClauses: string[] = [];
|
||||
let params: any[] = [];
|
||||
|
||||
@@ -1216,17 +1223,31 @@ class DataService {
|
||||
params.push(typeof id === "object" ? id[pkColumn] : id);
|
||||
}
|
||||
|
||||
const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(" AND ")}`;
|
||||
const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(" AND ")} RETURNING *`;
|
||||
console.log(`🗑️ 삭제 쿼리:`, queryText, params);
|
||||
|
||||
const result = await query<any>(queryText, params);
|
||||
|
||||
// 삭제된 행이 없으면 실패 처리
|
||||
if (result.length === 0) {
|
||||
console.warn(
|
||||
`⚠️ 레코드 삭제 실패: ${tableName}, 해당 조건에 맞는 레코드가 없습니다.`,
|
||||
{ whereClauses, params }
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
message: "삭제할 레코드를 찾을 수 없습니다. 이미 삭제되었거나 권한이 없습니다.",
|
||||
error: "RECORD_NOT_FOUND",
|
||||
};
|
||||
}
|
||||
|
||||
console.log(
|
||||
`✅ 레코드 삭제 완료: ${tableName}, 영향받은 행: ${result.length}`
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result[0], // 삭제된 레코드 정보 반환
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`레코드 삭제 오류 (${tableName}):`, error);
|
||||
@@ -1240,10 +1261,14 @@ class DataService {
|
||||
|
||||
/**
|
||||
* 조건에 맞는 모든 레코드 삭제 (그룹 삭제)
|
||||
* @param tableName 테이블명
|
||||
* @param filterConditions 삭제 조건
|
||||
* @param userCompany 사용자 회사 코드 (멀티테넌시 필터링)
|
||||
*/
|
||||
async deleteGroupRecords(
|
||||
tableName: string,
|
||||
filterConditions: Record<string, any>
|
||||
filterConditions: Record<string, any>,
|
||||
userCompany?: string
|
||||
): Promise<ServiceResponse<{ deleted: number }>> {
|
||||
try {
|
||||
const validation = await this.validateTableAccess(tableName);
|
||||
@@ -1255,6 +1280,7 @@ class DataService {
|
||||
const whereValues: any[] = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
// 사용자 필터 조건 추가
|
||||
for (const [key, value] of Object.entries(filterConditions)) {
|
||||
whereConditions.push(`"${key}" = $${paramIndex}`);
|
||||
whereValues.push(value);
|
||||
@@ -1269,10 +1295,24 @@ class DataService {
|
||||
};
|
||||
}
|
||||
|
||||
// 🔒 멀티테넌시: company_code 필터링 (최고 관리자 제외)
|
||||
const hasCompanyCode = await this.checkColumnExists(tableName, "company_code");
|
||||
if (hasCompanyCode && userCompany && userCompany !== "*") {
|
||||
whereConditions.push(`"company_code" = $${paramIndex}`);
|
||||
whereValues.push(userCompany);
|
||||
paramIndex++;
|
||||
console.log(`🔒 멀티테넌시 필터 적용: company_code = ${userCompany}`);
|
||||
}
|
||||
|
||||
const whereClause = whereConditions.join(" AND ");
|
||||
const deleteQuery = `DELETE FROM "${tableName}" WHERE ${whereClause} RETURNING *`;
|
||||
|
||||
console.log(`🗑️ 그룹 삭제:`, { tableName, conditions: filterConditions });
|
||||
console.log(`🗑️ 그룹 삭제:`, {
|
||||
tableName,
|
||||
conditions: filterConditions,
|
||||
userCompany,
|
||||
whereClause,
|
||||
});
|
||||
|
||||
const result = await pool.query(deleteQuery, whereValues);
|
||||
|
||||
|
||||
@@ -93,10 +93,15 @@ export class DynamicFormApi {
|
||||
): Promise<ApiResponse<SaveFormDataResponse>> {
|
||||
try {
|
||||
console.log("🔄 폼 데이터 업데이트 요청:", { id, formData });
|
||||
console.log("🌐 API URL:", `/dynamic-form/${id}`);
|
||||
console.log("📦 요청 본문:", JSON.stringify(formData, null, 2));
|
||||
|
||||
const response = await apiClient.put(`/dynamic-form/${id}`, formData);
|
||||
|
||||
console.log("✅ 폼 데이터 업데이트 성공:", response.data);
|
||||
console.log("📊 응답 상태:", response.status);
|
||||
console.log("📋 응답 헤더:", response.headers);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: response.data,
|
||||
@@ -104,6 +109,8 @@ export class DynamicFormApi {
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error("❌ 폼 데이터 업데이트 실패:", error);
|
||||
console.error("📊 에러 응답:", error.response?.data);
|
||||
console.error("📊 에러 상태:", error.response?.status);
|
||||
|
||||
const errorMessage = error.response?.data?.message || error.message || "데이터 업데이트 중 오류가 발생했습니다.";
|
||||
|
||||
|
||||
@@ -1613,47 +1613,89 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||
try {
|
||||
console.log("🗑️ 데이터 삭제:", { tableName, primaryKey });
|
||||
|
||||
// 🔍 중복 제거 설정 디버깅
|
||||
console.log("🔍 중복 제거 디버깅:", {
|
||||
// 🔍 그룹 삭제 설정 확인 (editButton.groupByColumns 또는 deduplication)
|
||||
const groupByColumns = componentConfig.rightPanel?.editButton?.groupByColumns || [];
|
||||
const deduplication = componentConfig.rightPanel?.dataFilter?.deduplication;
|
||||
|
||||
console.log("🔍 삭제 설정 디버깅:", {
|
||||
panel: deleteModalPanel,
|
||||
dataFilter: componentConfig.rightPanel?.dataFilter,
|
||||
deduplication: componentConfig.rightPanel?.dataFilter?.deduplication,
|
||||
enabled: componentConfig.rightPanel?.dataFilter?.deduplication?.enabled,
|
||||
groupByColumns,
|
||||
deduplication,
|
||||
deduplicationEnabled: deduplication?.enabled,
|
||||
});
|
||||
|
||||
let result;
|
||||
|
||||
// 🔧 중복 제거가 활성화된 경우, groupByColumn 기준으로 모든 관련 레코드 삭제
|
||||
if (deleteModalPanel === "right" && componentConfig.rightPanel?.dataFilter?.deduplication?.enabled) {
|
||||
const deduplication = componentConfig.rightPanel.dataFilter.deduplication;
|
||||
const groupByColumn = deduplication.groupByColumn;
|
||||
|
||||
if (groupByColumn && deleteModalItem[groupByColumn]) {
|
||||
const groupValue = deleteModalItem[groupByColumn];
|
||||
console.log(`🔗 중복 제거 활성화: ${groupByColumn} = ${groupValue} 기준으로 모든 레코드 삭제`);
|
||||
|
||||
// groupByColumn 값으로 필터링하여 삭제
|
||||
const filterConditions: Record<string, any> = {
|
||||
[groupByColumn]: groupValue,
|
||||
};
|
||||
|
||||
// 좌측 패널의 선택된 항목 정보도 포함 (customer_id 등)
|
||||
if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") {
|
||||
const leftColumn = componentConfig.rightPanel.join.leftColumn;
|
||||
const rightColumn = componentConfig.rightPanel.join.rightColumn;
|
||||
filterConditions[rightColumn] = selectedLeftItem[leftColumn];
|
||||
// 🔧 우측 패널 삭제 시 그룹 삭제 조건 확인
|
||||
if (deleteModalPanel === "right") {
|
||||
// 1. groupByColumns가 설정된 경우 (패널 설정에서 선택된 컬럼들)
|
||||
if (groupByColumns.length > 0) {
|
||||
const filterConditions: Record<string, any> = {};
|
||||
|
||||
// 선택된 컬럼들의 값을 필터 조건으로 추가
|
||||
for (const col of groupByColumns) {
|
||||
if (deleteModalItem[col] !== undefined && deleteModalItem[col] !== null) {
|
||||
filterConditions[col] = deleteModalItem[col];
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🗑️ 그룹 삭제 조건:", filterConditions);
|
||||
// 🔒 안전장치: 조인 모드에서 좌측 패널의 키 값도 필터 조건에 포함
|
||||
// (다른 거래처의 같은 품목이 삭제되는 것을 방지)
|
||||
if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") {
|
||||
const leftColumn = componentConfig.rightPanel.join?.leftColumn;
|
||||
const rightColumn = componentConfig.rightPanel.join?.rightColumn;
|
||||
if (leftColumn && rightColumn && selectedLeftItem[leftColumn]) {
|
||||
// rightColumn이 filterConditions에 없으면 추가
|
||||
if (!filterConditions[rightColumn]) {
|
||||
filterConditions[rightColumn] = selectedLeftItem[leftColumn];
|
||||
console.log(`🔒 안전장치: ${rightColumn} = ${selectedLeftItem[leftColumn]} 추가`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 그룹 삭제 API 호출
|
||||
result = await dataApi.deleteGroupRecords(tableName, filterConditions);
|
||||
} else {
|
||||
// 단일 레코드 삭제
|
||||
// 필터 조건이 있으면 그룹 삭제
|
||||
if (Object.keys(filterConditions).length > 0) {
|
||||
console.log(`🔗 그룹 삭제 (groupByColumns): ${groupByColumns.join(", ")} 기준`);
|
||||
console.log("🗑️ 그룹 삭제 조건:", filterConditions);
|
||||
|
||||
result = await dataApi.deleteGroupRecords(tableName, filterConditions);
|
||||
} else {
|
||||
// 필터 조건이 없으면 단일 삭제
|
||||
console.log("⚠️ groupByColumns 값이 없어 단일 삭제로 전환");
|
||||
result = await dataApi.deleteRecord(tableName, primaryKey);
|
||||
}
|
||||
}
|
||||
// 2. 중복 제거(deduplication)가 활성화된 경우
|
||||
else if (deduplication?.enabled && deduplication?.groupByColumn) {
|
||||
const groupByColumn = deduplication.groupByColumn;
|
||||
const groupValue = deleteModalItem[groupByColumn];
|
||||
|
||||
if (groupValue) {
|
||||
console.log(`🔗 중복 제거 활성화: ${groupByColumn} = ${groupValue} 기준으로 모든 레코드 삭제`);
|
||||
|
||||
const filterConditions: Record<string, any> = {
|
||||
[groupByColumn]: groupValue,
|
||||
};
|
||||
|
||||
// 좌측 패널의 선택된 항목 정보도 포함 (customer_id 등)
|
||||
if (selectedLeftItem && componentConfig.rightPanel?.mode === "join") {
|
||||
const leftColumn = componentConfig.rightPanel.join.leftColumn;
|
||||
const rightColumn = componentConfig.rightPanel.join.rightColumn;
|
||||
filterConditions[rightColumn] = selectedLeftItem[leftColumn];
|
||||
}
|
||||
|
||||
console.log("🗑️ 그룹 삭제 조건:", filterConditions);
|
||||
result = await dataApi.deleteGroupRecords(tableName, filterConditions);
|
||||
} else {
|
||||
result = await dataApi.deleteRecord(tableName, primaryKey);
|
||||
}
|
||||
}
|
||||
// 3. 그 외: 단일 레코드 삭제
|
||||
else {
|
||||
result = await dataApi.deleteRecord(tableName, primaryKey);
|
||||
}
|
||||
} else {
|
||||
// 단일 레코드 삭제
|
||||
// 좌측 패널: 단일 레코드 삭제
|
||||
result = await dataApi.deleteRecord(tableName, primaryKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -333,6 +333,14 @@ export function UniversalFormModalComponent({
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 테이블 섹션 데이터 병합 (품목 리스트 등)
|
||||
for (const [key, value] of Object.entries(formData)) {
|
||||
if (key.startsWith("_tableSection_") && Array.isArray(value)) {
|
||||
event.detail.formData[key] = value;
|
||||
console.log(`[UniversalFormModal] 테이블 섹션 병합: ${key}, ${value.length}개 항목`);
|
||||
}
|
||||
}
|
||||
|
||||
// 🆕 수정 모드: 원본 그룹 데이터 전달 (UPDATE/DELETE 추적용)
|
||||
if (originalGroupedData.length > 0) {
|
||||
event.detail.formData._originalGroupedData = originalGroupedData;
|
||||
@@ -355,15 +363,9 @@ export function UniversalFormModalComponent({
|
||||
// 테이블 타입 섹션 찾기
|
||||
const tableSection = config.sections.find((s) => s.type === "table");
|
||||
if (!tableSection) {
|
||||
// console.log("[UniversalFormModal] 테이블 섹션 없음 - _groupedData 무시");
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("[UniversalFormModal] 수정 모드 - 테이블 섹션 초기화:", {
|
||||
// sectionId: tableSection.id,
|
||||
// itemCount: _groupedData.length,
|
||||
// });
|
||||
|
||||
// 원본 데이터 저장 (수정/삭제 추적용)
|
||||
setOriginalGroupedData(JSON.parse(JSON.stringify(_groupedData)));
|
||||
|
||||
|
||||
@@ -724,11 +724,16 @@ export class ButtonActionExecutor {
|
||||
// originalData는 수정 버튼 클릭 시 editData로 전달되어 context.originalData로 설정됨
|
||||
// 빈 객체 {}도 truthy이므로 Object.keys로 실제 데이터 유무 확인
|
||||
const hasRealOriginalData = originalData && Object.keys(originalData).length > 0;
|
||||
const isUpdate = hasRealOriginalData && !!primaryKeyValue;
|
||||
|
||||
// 🆕 폴백 로직: originalData가 없어도 formData에 id가 있으면 UPDATE로 판단
|
||||
// 조건부 컨테이너 등에서 originalData 전달이 누락되는 경우를 처리
|
||||
const hasIdInFormData = formData.id !== undefined && formData.id !== null && formData.id !== "";
|
||||
const isUpdate = (hasRealOriginalData || hasIdInFormData) && !!primaryKeyValue;
|
||||
|
||||
console.log("🔍 [handleSave] INSERT/UPDATE 판단:", {
|
||||
hasOriginalData: !!originalData,
|
||||
hasRealOriginalData,
|
||||
hasIdInFormData,
|
||||
originalDataKeys: originalData ? Object.keys(originalData) : [],
|
||||
primaryKeyValue,
|
||||
isUpdate,
|
||||
@@ -741,18 +746,18 @@ export class ButtonActionExecutor {
|
||||
// UPDATE 처리 - 부분 업데이트 사용 (원본 데이터가 있는 경우)
|
||||
console.log("🔄 UPDATE 모드로 저장:", {
|
||||
primaryKeyValue,
|
||||
formData,
|
||||
originalData,
|
||||
hasOriginalData: !!originalData,
|
||||
hasIdInFormData,
|
||||
updateReason: hasRealOriginalData ? "originalData 존재" : "formData.id 존재 (폴백)",
|
||||
});
|
||||
|
||||
if (originalData) {
|
||||
if (hasRealOriginalData) {
|
||||
// 부분 업데이트: 변경된 필드만 업데이트
|
||||
console.log("📝 부분 업데이트 실행 (변경된 필드만)");
|
||||
saveResult = await DynamicFormApi.updateFormDataPartial(primaryKeyValue, originalData, formData, tableName);
|
||||
} else {
|
||||
// 전체 업데이트 (기존 방식)
|
||||
console.log("📝 전체 업데이트 실행 (모든 필드)");
|
||||
// 전체 업데이트 (originalData 없이 id로 UPDATE 판단된 경우)
|
||||
console.log("📝 전체 업데이트 실행 (originalData 없음 - 폴백 모드)");
|
||||
saveResult = await DynamicFormApi.updateFormData(primaryKeyValue, {
|
||||
tableName,
|
||||
data: formData,
|
||||
@@ -1862,37 +1867,45 @@ export class ButtonActionExecutor {
|
||||
const originalItem = originalGroupedData.find((orig) => orig.id === item.id);
|
||||
|
||||
if (!originalItem) {
|
||||
console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - INSERT로 처리: id=${item.id}`);
|
||||
// 원본이 없으면 신규로 처리
|
||||
const rowToSave = { ...commonFieldsData, ...item, ...userInfo };
|
||||
Object.keys(rowToSave).forEach((key) => {
|
||||
// 🆕 폴백 로직: 원본 데이터가 없어도 id가 있으면 UPDATE 시도
|
||||
// originalGroupedData 전달이 누락된 경우를 처리
|
||||
console.warn(`⚠️ [UPDATE] 원본 데이터 없음 - id가 있으므로 UPDATE 시도 (폴백): id=${item.id}`);
|
||||
|
||||
// ⚠️ 중요: commonFieldsData가 item보다 우선순위가 높아야 함
|
||||
// item에 있는 기존 값(예: manager_id=123)이 commonFieldsData의 새 값(manager_id=234)을 덮어쓰지 않도록
|
||||
// 순서: item(기존) → commonFieldsData(새로 입력) → userInfo(메타데이터)
|
||||
const rowToUpdate = { ...item, ...commonFieldsData, ...userInfo };
|
||||
Object.keys(rowToUpdate).forEach((key) => {
|
||||
if (key.startsWith("_")) {
|
||||
delete rowToSave[key];
|
||||
delete rowToUpdate[key];
|
||||
}
|
||||
});
|
||||
delete rowToSave.id; // id 제거하여 INSERT
|
||||
|
||||
// 🆕 메인 레코드 ID 연결 (별도 테이블에 저장하는 경우)
|
||||
if (targetTableName && mainRecordId && saveConfig.primaryKeyColumn) {
|
||||
rowToSave[saveConfig.primaryKeyColumn] = mainRecordId;
|
||||
}
|
||||
|
||||
const saveResult = await DynamicFormApi.saveFormData({
|
||||
screenId: screenId!,
|
||||
console.log("📝 [UPDATE 폴백] 저장할 데이터:", {
|
||||
id: item.id,
|
||||
tableName: saveTableName,
|
||||
data: rowToSave,
|
||||
commonFieldsData,
|
||||
itemFields: Object.keys(item).filter(k => !k.startsWith("_")),
|
||||
rowToUpdate,
|
||||
});
|
||||
|
||||
if (!saveResult.success) {
|
||||
throw new Error(saveResult.message || "품목 저장 실패");
|
||||
// id를 유지하고 UPDATE 실행
|
||||
const updateResult = await DynamicFormApi.updateFormData(item.id, {
|
||||
tableName: saveTableName,
|
||||
data: rowToUpdate,
|
||||
});
|
||||
|
||||
if (!updateResult.success) {
|
||||
throw new Error(updateResult.message || "품목 수정 실패");
|
||||
}
|
||||
|
||||
insertedCount++;
|
||||
updatedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 변경 사항 확인 (공통 필드 포함)
|
||||
const currentDataWithCommon = { ...commonFieldsData, ...item };
|
||||
// ⚠️ 중요: commonFieldsData가 item보다 우선순위가 높아야 함 (새로 입력한 값이 기존 값을 덮어씀)
|
||||
const currentDataWithCommon = { ...item, ...commonFieldsData };
|
||||
const hasChanges = this.checkForChanges(originalItem, currentDataWithCommon);
|
||||
|
||||
if (hasChanges) {
|
||||
@@ -1917,13 +1930,14 @@ export class ButtonActionExecutor {
|
||||
}
|
||||
|
||||
// 3️⃣ 삭제된 품목 DELETE (원본에는 있지만 현재에는 없는 항목)
|
||||
const currentIds = new Set(currentItems.map((item) => item.id).filter(Boolean));
|
||||
const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(orig.id));
|
||||
// ⚠️ id 타입 통일: 문자열로 변환하여 비교 (숫자 vs 문자열 불일치 방지)
|
||||
const currentIds = new Set(currentItems.map((item) => String(item.id)).filter(Boolean));
|
||||
const deletedItems = originalGroupedData.filter((orig) => orig.id && !currentIds.has(String(orig.id)));
|
||||
|
||||
for (const deletedItem of deletedItems) {
|
||||
console.log(`🗑️ [DELETE] 품목 삭제: id=${deletedItem.id}, tableName=${saveTableName}`);
|
||||
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(saveTableName, deletedItem.id);
|
||||
const deleteResult = await DynamicFormApi.deleteFormDataFromTable(deletedItem.id, saveTableName);
|
||||
|
||||
if (!deleteResult.success) {
|
||||
throw new Error(deleteResult.message || "품목 삭제 실패");
|
||||
|
||||
Reference in New Issue
Block a user