feat(repeat-screen-modal): 테이블 삭제 기능 DB 연동 (소프트 삭제)
- 삭제 버튼 클릭 시 _isDeleted 플래그 설정 (소프트 삭제) - 삭제된 행 시각적 표시 (취소선, 투명도) - 삭제 취소(복원) 기능 추가 - 저장 버튼 클릭 시 DELETE API 호출하여 DB 반영 - 삭제된 행 집계 계산에서 제외 - axios DELETE 요청 시 body 전달 방식 수정
This commit is contained in:
parent
8e257f36b2
commit
b286bc3c63
|
|
@ -481,7 +481,8 @@ export function RepeatScreenModalComponent({
|
|||
// 각 카드의 집계 재계산
|
||||
const updatedCards = groupedCardsData.map((card) => {
|
||||
const key = `${card._cardId}-${tableRowWithExternalSource.id}`;
|
||||
const externalRows = extData[key] || [];
|
||||
// 🆕 v3.7: 삭제된 행은 집계에서 제외
|
||||
const externalRows = (extData[key] || []).filter((row) => !row._isDeleted);
|
||||
|
||||
// 집계 재계산
|
||||
const newAggregations: Record<string, number> = {};
|
||||
|
|
@ -671,8 +672,35 @@ export function RepeatScreenModalComponent({
|
|||
inputType: c.inputType
|
||||
})));
|
||||
|
||||
for (const row of dirtyRows) {
|
||||
const { _rowId, _originalData, _isDirty, _isNew, ...allData } = row;
|
||||
// 삭제할 행 (기존 데이터 중 _isDeleted가 true인 것)
|
||||
const deletedRows = dirtyRows.filter((row) => row._isDeleted && row._originalData?.id);
|
||||
// 저장할 행 (삭제되지 않은 것)
|
||||
const rowsToSave = dirtyRows.filter((row) => !row._isDeleted);
|
||||
|
||||
console.log("[RepeatScreenModal] 삭제 대상:", deletedRows.length, "건");
|
||||
console.log("[RepeatScreenModal] 저장 대상:", rowsToSave.length, "건");
|
||||
|
||||
// 🆕 v3.7: 삭제 처리 (배열 형태로 body에 전달)
|
||||
for (const row of deletedRows) {
|
||||
const deleteId = row._originalData.id;
|
||||
console.log(`[RepeatScreenModal] DELETE 요청: /table-management/tables/${targetTable}/delete`, [{ id: deleteId }]);
|
||||
savePromises.push(
|
||||
apiClient.request({
|
||||
method: "DELETE",
|
||||
url: `/table-management/tables/${targetTable}/delete`,
|
||||
data: [{ id: deleteId }],
|
||||
}).then((res) => {
|
||||
console.log("[RepeatScreenModal] DELETE 응답:", res.data);
|
||||
return { type: "delete", id: deleteId };
|
||||
}).catch((err) => {
|
||||
console.error("[RepeatScreenModal] DELETE 실패:", err.response?.data || err.message);
|
||||
throw err;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
for (const row of rowsToSave) {
|
||||
const { _rowId, _originalData, _isDirty, _isNew, _isDeleted, ...allData } = row;
|
||||
|
||||
// 허용된 필드만 필터링
|
||||
const dataToSave: Record<string, any> = {};
|
||||
|
|
@ -724,21 +752,30 @@ export function RepeatScreenModalComponent({
|
|||
try {
|
||||
await Promise.all(savePromises);
|
||||
|
||||
// 저장 후 해당 키의 dirty 플래그만 초기화
|
||||
// 저장 후: 삭제된 행은 제거, 나머지는 dirty 플래그 초기화
|
||||
setExternalTableData((prev) => {
|
||||
const updated = { ...prev };
|
||||
if (updated[key]) {
|
||||
updated[key] = updated[key].map((row) => ({
|
||||
...row,
|
||||
_isDirty: false,
|
||||
_isNew: false,
|
||||
_originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined },
|
||||
}));
|
||||
// 삭제된 행은 완전히 제거
|
||||
updated[key] = updated[key]
|
||||
.filter((row) => !row._isDeleted)
|
||||
.map((row) => ({
|
||||
...row,
|
||||
_isDirty: false,
|
||||
_isNew: false,
|
||||
_originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined, _isDeleted: undefined },
|
||||
}));
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
|
||||
return { success: true, message: `${dirtyRows.length}건 저장 완료`, savedCount: dirtyRows.length, savedIds };
|
||||
const savedCount = rowsToSave.length;
|
||||
const deletedCount = deletedRows.length;
|
||||
const message = deletedCount > 0
|
||||
? `${savedCount}건 저장, ${deletedCount}건 삭제 완료`
|
||||
: `${savedCount}건 저장 완료`;
|
||||
|
||||
return { success: true, message, savedCount, deletedCount, savedIds };
|
||||
} catch (error: any) {
|
||||
console.error("[RepeatScreenModal] 테이블 영역 저장 실패:", error);
|
||||
return { success: false, message: error.message || "저장 중 오류가 발생했습니다." };
|
||||
|
|
@ -774,16 +811,20 @@ export function RepeatScreenModalComponent({
|
|||
}
|
||||
};
|
||||
|
||||
// 🆕 v3.1: 외부 테이블 행 삭제 실행
|
||||
// 🆕 v3.1: 외부 테이블 행 삭제 실행 (소프트 삭제 - _isDeleted 플래그 설정)
|
||||
const handleDeleteExternalRow = (cardId: string, rowId: string, contentRowId: string) => {
|
||||
const key = `${cardId}-${contentRowId}`;
|
||||
setExternalTableData((prev) => {
|
||||
const newData = {
|
||||
...prev,
|
||||
[key]: (prev[key] || []).filter((row) => row._rowId !== rowId),
|
||||
[key]: (prev[key] || []).map((row) =>
|
||||
row._rowId === rowId
|
||||
? { ...row, _isDeleted: true, _isDirty: true }
|
||||
: row
|
||||
),
|
||||
};
|
||||
|
||||
// 🆕 v3.5: 행 삭제 시 집계 재계산
|
||||
// 🆕 v3.5: 행 삭제 시 집계 재계산 (삭제된 행 제외)
|
||||
setTimeout(() => {
|
||||
recalculateAggregationsWithExternalData(newData);
|
||||
}, 0);
|
||||
|
|
@ -794,6 +835,27 @@ export function RepeatScreenModalComponent({
|
|||
setPendingDeleteInfo(null);
|
||||
};
|
||||
|
||||
// 🆕 v3.7: 삭제 취소 (소프트 삭제 복원)
|
||||
const handleRestoreExternalRow = (cardId: string, rowId: string, contentRowId: string) => {
|
||||
const key = `${cardId}-${contentRowId}`;
|
||||
setExternalTableData((prev) => {
|
||||
const newData = {
|
||||
...prev,
|
||||
[key]: (prev[key] || []).map((row) =>
|
||||
row._rowId === rowId
|
||||
? { ...row, _isDeleted: false, _isDirty: true }
|
||||
: row
|
||||
),
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
recalculateAggregationsWithExternalData(newData);
|
||||
}, 0);
|
||||
|
||||
return newData;
|
||||
});
|
||||
};
|
||||
|
||||
// 🆕 v3.1: 외부 테이블 행 데이터 변경
|
||||
const handleExternalRowDataChange = (cardId: string, contentRowId: string, rowId: string, field: string, value: any) => {
|
||||
const key = `${cardId}-${contentRowId}`;
|
||||
|
|
@ -1700,12 +1762,20 @@ export function RepeatScreenModalComponent({
|
|||
(externalTableData[`${card._cardId}-${contentRow.id}`] || []).map((row) => (
|
||||
<TableRow
|
||||
key={row._rowId}
|
||||
className={cn(row._isDirty && "bg-primary/5", row._isNew && "bg-green-50 dark:bg-green-950")}
|
||||
className={cn(
|
||||
row._isDirty && "bg-primary/5",
|
||||
row._isNew && "bg-green-50 dark:bg-green-950",
|
||||
row._isDeleted && "bg-destructive/10 opacity-60"
|
||||
)}
|
||||
>
|
||||
{(contentRow.tableColumns || []).map((col) => (
|
||||
<TableCell
|
||||
key={`${row._rowId}-${col.id}`}
|
||||
className={cn("text-sm", col.align && `text-${col.align}`)}
|
||||
className={cn(
|
||||
"text-sm",
|
||||
col.align && `text-${col.align}`,
|
||||
row._isDeleted && "line-through text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{renderTableCell(col, row, (value) =>
|
||||
handleExternalRowDataChange(card._cardId, contentRow.id, row._rowId, col.field, value)
|
||||
|
|
@ -1714,14 +1784,26 @@ export function RepeatScreenModalComponent({
|
|||
))}
|
||||
{contentRow.tableCrud?.allowDelete && (
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteExternalRowRequest(card._cardId, row._rowId, contentRow.id, contentRow)}
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
{row._isDeleted ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRestoreExternalRow(card._cardId, row._rowId, contentRow.id)}
|
||||
className="h-7 w-7 p-0 text-primary hover:text-primary hover:bg-primary/10"
|
||||
title="삭제 취소"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteExternalRowRequest(card._cardId, row._rowId, contentRow.id, contentRow)}
|
||||
className="h-7 w-7 p-0 text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
|
|
|
|||
Loading…
Reference in New Issue