feat(repeat-screen-modal): 집계 저장 및 채번 규칙 값 저장 기능 추가
- RepeatScreenModal 집계 결과를 연관 테이블에 저장하는 기능 추가 - ButtonPrimary 저장 시 채번 규칙 값(shipment_plan_no) 함께 저장 - _repeatScreenModal_* 데이터 감지 시 메인 테이블 중복 저장 방지 - 기존 행 수정 모드(_isEditing) 지원 - AggregationSaveConfig 타입 및 ConfigPanel UI 추가
This commit is contained in:
@@ -99,6 +99,123 @@ export function RepeatScreenModalComponent({
|
||||
contentRowId: string;
|
||||
} | null>(null);
|
||||
|
||||
// 🆕 v3.9: beforeFormSave 이벤트 핸들러 - ButtonPrimary 저장 시 externalTableData를 formData에 병합
|
||||
useEffect(() => {
|
||||
const handleBeforeFormSave = (event: Event) => {
|
||||
if (!(event instanceof CustomEvent) || !event.detail?.formData) return;
|
||||
|
||||
console.log("[RepeatScreenModal] beforeFormSave 이벤트 수신");
|
||||
|
||||
// 외부 테이블 데이터에서 dirty 행만 추출하여 저장 데이터 준비
|
||||
const saveDataByTable: Record<string, any[]> = {};
|
||||
|
||||
for (const [key, rows] of Object.entries(externalTableData)) {
|
||||
// contentRow 찾기
|
||||
const contentRow = contentRows.find((r) => key.includes(r.id));
|
||||
if (!contentRow?.tableDataSource?.enabled) continue;
|
||||
|
||||
const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable;
|
||||
|
||||
// dirty 행만 필터링 (삭제된 행 제외)
|
||||
const dirtyRows = rows.filter((row) => row._isDirty && !row._isDeleted);
|
||||
|
||||
if (dirtyRows.length === 0) continue;
|
||||
|
||||
// 저장할 필드만 추출
|
||||
const editableFields = (contentRow.tableColumns || [])
|
||||
.filter((col) => col.editable)
|
||||
.map((col) => col.field);
|
||||
|
||||
const joinKeys = (contentRow.tableDataSource.joinConditions || [])
|
||||
.map((cond) => cond.sourceKey);
|
||||
|
||||
const allowedFields = [...new Set([...editableFields, ...joinKeys])];
|
||||
|
||||
if (!saveDataByTable[targetTable]) {
|
||||
saveDataByTable[targetTable] = [];
|
||||
}
|
||||
|
||||
for (const row of dirtyRows) {
|
||||
const saveData: Record<string, any> = {};
|
||||
|
||||
// 허용된 필드만 포함
|
||||
for (const field of allowedFields) {
|
||||
if (row[field] !== undefined) {
|
||||
saveData[field] = row[field];
|
||||
}
|
||||
}
|
||||
|
||||
// _isNew 플래그 유지
|
||||
saveData._isNew = row._isNew;
|
||||
saveData._targetTable = targetTable;
|
||||
|
||||
// 기존 레코드의 경우 id 포함
|
||||
if (!row._isNew && row._originalData?.id) {
|
||||
saveData.id = row._originalData.id;
|
||||
}
|
||||
|
||||
saveDataByTable[targetTable].push(saveData);
|
||||
}
|
||||
}
|
||||
|
||||
// formData에 테이블별 저장 데이터 추가
|
||||
for (const [tableName, rows] of Object.entries(saveDataByTable)) {
|
||||
const fieldKey = `_repeatScreenModal_${tableName}`;
|
||||
event.detail.formData[fieldKey] = rows;
|
||||
console.log(`[RepeatScreenModal] beforeFormSave - ${tableName} 저장 데이터:`, rows);
|
||||
}
|
||||
|
||||
// 🆕 v3.9: 집계 저장 설정 정보도 formData에 추가
|
||||
if (grouping?.aggregations && groupedCardsData.length > 0) {
|
||||
const aggregationSaveConfigs: Array<{
|
||||
resultField: string;
|
||||
aggregatedValue: number;
|
||||
targetTable: string;
|
||||
targetColumn: string;
|
||||
joinKey: { sourceField: string; targetField: string };
|
||||
sourceValue: any; // 조인 키 값
|
||||
}> = [];
|
||||
|
||||
for (const card of groupedCardsData) {
|
||||
for (const agg of grouping.aggregations) {
|
||||
if (agg.saveConfig?.enabled) {
|
||||
const { saveConfig, resultField } = agg;
|
||||
const { targetTable, targetColumn, joinKey } = saveConfig;
|
||||
|
||||
if (!targetTable || !targetColumn || !joinKey?.sourceField || !joinKey?.targetField) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const aggregatedValue = card._aggregations?.[resultField] ?? 0;
|
||||
const sourceValue = card._representativeData?.[joinKey.sourceField];
|
||||
|
||||
if (sourceValue !== undefined) {
|
||||
aggregationSaveConfigs.push({
|
||||
resultField,
|
||||
aggregatedValue,
|
||||
targetTable,
|
||||
targetColumn,
|
||||
joinKey,
|
||||
sourceValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aggregationSaveConfigs.length > 0) {
|
||||
event.detail.formData._repeatScreenModal_aggregations = aggregationSaveConfigs;
|
||||
console.log("[RepeatScreenModal] beforeFormSave - 집계 저장 설정:", aggregationSaveConfigs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
|
||||
return () => {
|
||||
window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener);
|
||||
};
|
||||
}, [externalTableData, contentRows, grouping, groupedCardsData]);
|
||||
|
||||
// 초기 데이터 로드
|
||||
useEffect(() => {
|
||||
const loadInitialData = async () => {
|
||||
@@ -795,16 +912,91 @@ export function RepeatScreenModalComponent({
|
||||
const result = await saveTableAreaData(cardId, contentRowId, contentRow);
|
||||
if (result.success) {
|
||||
console.log("[RepeatScreenModal] 테이블 영역 저장 성공:", result);
|
||||
// 성공 알림 (필요 시 toast 추가)
|
||||
|
||||
// 🆕 v3.9: 집계 저장 설정이 있는 경우 연관 테이블 동기화
|
||||
const card = groupedCardsData.find((c) => c._cardId === cardId);
|
||||
if (card && grouping?.aggregations) {
|
||||
await saveAggregationsToRelatedTables(card, contentRowId);
|
||||
}
|
||||
} else {
|
||||
console.error("[RepeatScreenModal] 테이블 영역 저장 실패:", result.message);
|
||||
// 실패 알림 (필요 시 toast 추가)
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 v3.9: 집계 결과를 연관 테이블에 저장
|
||||
const saveAggregationsToRelatedTables = async (card: GroupedCardData, contentRowId: string) => {
|
||||
if (!grouping?.aggregations) return;
|
||||
|
||||
const savePromises: Promise<any>[] = [];
|
||||
|
||||
for (const agg of grouping.aggregations) {
|
||||
const saveConfig = agg.saveConfig;
|
||||
|
||||
// 저장 설정이 없거나 비활성화된 경우 스킵
|
||||
if (!saveConfig?.enabled) continue;
|
||||
|
||||
// 자동 저장이 아닌 경우, 레이아웃에 연결되어 있는지 확인 필요
|
||||
// (현재는 자동 저장과 동일하게 처리 - 추후 레이아웃 연결 체크 추가 가능)
|
||||
|
||||
// 집계 결과 값 가져오기
|
||||
const aggregatedValue = card._aggregations[agg.resultField];
|
||||
|
||||
if (aggregatedValue === undefined) {
|
||||
console.warn(`[RepeatScreenModal] 집계 결과 없음: ${agg.resultField}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 조인 키로 대상 레코드 식별
|
||||
const sourceKeyValue = card._representativeData[saveConfig.joinKey.sourceField];
|
||||
|
||||
if (!sourceKeyValue) {
|
||||
console.warn(`[RepeatScreenModal] 조인 키 값 없음: ${saveConfig.joinKey.sourceField}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[RepeatScreenModal] 집계 저장 시작:`, {
|
||||
aggregation: agg.resultField,
|
||||
value: aggregatedValue,
|
||||
targetTable: saveConfig.targetTable,
|
||||
targetColumn: saveConfig.targetColumn,
|
||||
joinKey: `${saveConfig.joinKey.sourceField}=${sourceKeyValue} -> ${saveConfig.joinKey.targetField}`,
|
||||
});
|
||||
|
||||
// UPDATE API 호출
|
||||
const updatePayload = {
|
||||
originalData: { [saveConfig.joinKey.targetField]: sourceKeyValue },
|
||||
updatedData: {
|
||||
[saveConfig.targetColumn]: aggregatedValue,
|
||||
[saveConfig.joinKey.targetField]: sourceKeyValue,
|
||||
},
|
||||
};
|
||||
|
||||
savePromises.push(
|
||||
apiClient.put(`/table-management/tables/${saveConfig.targetTable}/edit`, updatePayload)
|
||||
.then((res) => {
|
||||
console.log(`[RepeatScreenModal] 집계 저장 성공: ${agg.resultField} -> ${saveConfig.targetTable}.${saveConfig.targetColumn}`);
|
||||
return res;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`[RepeatScreenModal] 집계 저장 실패: ${agg.resultField}`, err.response?.data || err.message);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (savePromises.length > 0) {
|
||||
try {
|
||||
await Promise.all(savePromises);
|
||||
console.log(`[RepeatScreenModal] 모든 집계 저장 완료: ${savePromises.length}건`);
|
||||
} catch (error) {
|
||||
console.error("[RepeatScreenModal] 일부 집계 저장 실패:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 v3.1: 외부 테이블 행 삭제 요청
|
||||
const handleDeleteExternalRowRequest = (cardId: string, rowId: string, contentRowId: string, contentRow: CardContentRowConfig) => {
|
||||
if (contentRow.tableCrud?.deleteConfirm?.enabled !== false) {
|
||||
@@ -1002,8 +1194,11 @@ export function RepeatScreenModalComponent({
|
||||
});
|
||||
}
|
||||
|
||||
// 안정적인 _cardId 생성 (Date.now() 대신 groupKey 사용)
|
||||
// groupKey가 없으면 대표 데이터의 id 사용
|
||||
const stableId = groupKey || representativeData.id || cardIndex;
|
||||
result.push({
|
||||
_cardId: `grouped-card-${cardIndex}-${Date.now()}`,
|
||||
_cardId: `grouped-card-${cardIndex}-${stableId}`,
|
||||
_groupKey: groupKey,
|
||||
_groupField: groupByField || "",
|
||||
_aggregations: aggregations,
|
||||
|
||||
Reference in New Issue
Block a user