feat(repeat-screen-modal): 테이블 삭제 기능 DB 연동 (소프트 삭제)

- 삭제 버튼 클릭 시 _isDeleted 플래그 설정 (소프트 삭제)
- 삭제된 행 시각적 표시 (취소선, 투명도)
- 삭제 취소(복원) 기능 추가
- 저장 버튼 클릭 시 DELETE API 호출하여 DB 반영
- 삭제된 행 집계 계산에서 제외
- axios DELETE 요청 시 body 전달 방식 수정
This commit is contained in:
SeongHyun Kim 2025-12-02 14:50:00 +09:00
parent 8e257f36b2
commit b286bc3c63
1 changed files with 106 additions and 24 deletions

View File

@ -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>