feat(repeat-screen-modal): 테이블 행 편집 모드 제어 기능 구현

- DB 로드 데이터에 _isEditing: false 명시적 설정
- handleEditExternalRow: 수정 모드 전환 함수 추가
- handleCancelEditExternalRow: 수정 취소 및 원본 복원 함수 추가
- renderTableCell: isRowEditable 파라미터 추가로 행 수준 편집 제어
- UPDATE API 요청 형식 { originalData, updatedData }로 수정
- 테이블 작업 컬럼에 수정/수정취소/삭제/복원 버튼 그룹화
This commit is contained in:
SeongHyun Kim 2025-12-02 15:23:25 +09:00
parent b286bc3c63
commit 10d81cb9bc
1 changed files with 127 additions and 38 deletions

View File

@ -9,7 +9,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Loader2, Save, X, Layers, Table as TableIcon, Plus, Trash2, RotateCcw } from "lucide-react";
import { Loader2, Save, X, Layers, Table as TableIcon, Plus, Trash2, RotateCcw, Pencil } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
@ -344,6 +344,8 @@ export function RepeatScreenModalComponent({
_originalData: { ...row },
_isDirty: false,
_isNew: false,
_isEditing: false, // 🆕 v3.8: 로드된 데이터는 읽기 전용
_isDeleted: false,
...row,
}));
@ -733,11 +735,14 @@ export function RepeatScreenModalComponent({
})
);
} else if (_originalData?.id) {
// UPDATE - /edit 엔드포인트 사용 (id를 body에 포함)
const updateData = { ...dataToSave, id: _originalData.id };
console.log(`[RepeatScreenModal] UPDATE 요청: /table-management/tables/${targetTable}/edit`, updateData);
// UPDATE - /edit 엔드포인트 사용 (originalData와 updatedData 형식)
const updatePayload = {
originalData: _originalData,
updatedData: { ...dataToSave, id: _originalData.id },
};
console.log(`[RepeatScreenModal] UPDATE 요청: /table-management/tables/${targetTable}/edit`, updatePayload);
savePromises.push(
apiClient.put(`/table-management/tables/${targetTable}/edit`, updateData).then((res) => {
apiClient.put(`/table-management/tables/${targetTable}/edit`, updatePayload).then((res) => {
console.log("[RepeatScreenModal] UPDATE 응답:", res.data);
savedIds.push(_originalData.id);
return res;
@ -752,7 +757,7 @@ export function RepeatScreenModalComponent({
try {
await Promise.all(savePromises);
// 저장 후: 삭제된 행은 제거, 나머지는 dirty 플래그 초기화
// 저장 후: 삭제된 행은 제거, 나머지는 dirty/editing 플래그 초기화
setExternalTableData((prev) => {
const updated = { ...prev };
if (updated[key]) {
@ -763,7 +768,8 @@ export function RepeatScreenModalComponent({
...row,
_isDirty: false,
_isNew: false,
_originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined, _isDeleted: undefined },
_isEditing: false, // 🆕 v3.8: 수정 모드 해제
_originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined, _isDeleted: undefined, _isEditing: undefined },
}));
}
return updated;
@ -856,6 +862,40 @@ export function RepeatScreenModalComponent({
});
};
// 🆕 v3.8: 수정 모드 전환
const handleEditExternalRow = (cardId: string, rowId: string, contentRowId: string) => {
const key = `${cardId}-${contentRowId}`;
setExternalTableData((prev) => ({
...prev,
[key]: (prev[key] || []).map((row) =>
row._rowId === rowId
? { ...row, _isEditing: true }
: row
),
}));
};
// 🆕 v3.8: 수정 취소
const handleCancelEditExternalRow = (cardId: string, rowId: string, contentRowId: string) => {
const key = `${cardId}-${contentRowId}`;
setExternalTableData((prev) => ({
...prev,
[key]: (prev[key] || []).map((row) =>
row._rowId === rowId
? {
...row._originalData,
_rowId: row._rowId,
_originalData: row._originalData,
_isEditing: false,
_isDirty: false,
_isNew: false,
_isDeleted: false,
}
: row
),
}));
};
// 🆕 v3.1: 외부 테이블 행 데이터 변경
const handleExternalRowDataChange = (cardId: string, contentRowId: string, rowId: string, field: string, value: any) => {
const key = `${cardId}-${contentRowId}`;
@ -1742,8 +1782,8 @@ export function RepeatScreenModalComponent({
{col.label}
</TableHead>
))}
{contentRow.tableCrud?.allowDelete && (
<TableHead className="w-[60px] text-center text-xs"></TableHead>
{(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && (
<TableHead className="w-[80px] text-center text-xs"></TableHead>
)}
</TableRow>
</TableHeader>
@ -1777,33 +1817,66 @@ export function RepeatScreenModalComponent({
row._isDeleted && "line-through text-muted-foreground"
)}
>
{renderTableCell(col, row, (value) =>
handleExternalRowDataChange(card._cardId, contentRow.id, row._rowId, col.field, value)
{renderTableCell(
col,
row,
(value) => handleExternalRowDataChange(card._cardId, contentRow.id, row._rowId, col.field, value),
row._isNew || row._isEditing // 신규 행이거나 수정 모드일 때만 편집 가능
)}
</TableCell>
))}
{contentRow.tableCrud?.allowDelete && (
{(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && (
<TableCell className="text-center">
{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>
)}
<div className="flex items-center justify-center gap-1">
{/* 수정 버튼: 저장된 행(isNew가 아닌)이고 편집 모드가 아닐 때만 표시 */}
{contentRow.tableCrud?.allowUpdate && !row._isNew && !row._isEditing && !row._isDeleted && (
<Button
variant="ghost"
size="sm"
onClick={() => handleEditExternalRow(card._cardId, row._rowId, contentRow.id)}
className="h-7 w-7 p-0 text-blue-600 hover:text-blue-700 hover:bg-blue-50"
title="수정"
>
<Pencil className="h-4 w-4" />
</Button>
)}
{/* 수정 취소 버튼: 편집 모드일 때만 표시 */}
{row._isEditing && !row._isNew && (
<Button
variant="ghost"
size="sm"
onClick={() => handleCancelEditExternalRow(card._cardId, row._rowId, contentRow.id)}
className="h-7 w-7 p-0 text-muted-foreground hover:text-foreground hover:bg-muted"
title="수정 취소"
>
<X className="h-4 w-4" />
</Button>
)}
{/* 삭제/복원 버튼 */}
{contentRow.tableCrud?.allowDelete && (
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"
title="삭제"
>
<Trash2 className="h-4 w-4" />
</Button>
)
)}
</div>
</TableCell>
)}
</TableRow>
@ -1869,8 +1942,11 @@ export function RepeatScreenModalComponent({
key={`${row._rowId}-${col.id}`}
className={cn("text-sm", col.align && `text-${col.align}`)}
>
{renderTableCell(col, row, (value) =>
handleRowDataChange(card._cardId, row._rowId, col.field, value)
{renderTableCell(
col,
row,
(value) => handleRowDataChange(card._cardId, row._rowId, col.field, value),
row._isNew || row._isEditing
)}
</TableCell>
))}
@ -2184,8 +2260,11 @@ function renderContentRow(
key={`${row._rowId}-${col.id}`}
className={cn("text-sm", col.align && `text-${col.align}`)}
>
{renderTableCell(col, row, (value) =>
onRowDataChange(card._cardId, row._rowId, col.field, value)
{renderTableCell(
col,
row,
(value) => onRowDataChange(card._cardId, row._rowId, col.field, value),
row._isNew || row._isEditing
)}
</TableCell>
))}
@ -2459,7 +2538,8 @@ function renderHeaderColumn(
}
// 테이블 셀 렌더링
function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (value: any) => void) {
// 🆕 v3.8: isRowEditable 파라미터 추가 - 행이 편집 가능한 상태인지 (신규 행이거나 수정 모드)
function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (value: any) => void, isRowEditable?: boolean) {
const value = row[col.field];
// Badge 타입
@ -2468,11 +2548,20 @@ function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (va
return <Badge variant={badgeColor as any}>{value || "-"}</Badge>;
}
// 🆕 v3.8: 행 수준 편집 가능 여부 체크
// isRowEditable이 false이면 컬럼 설정과 관계없이 읽기 전용
const canEdit = col.editable && (isRowEditable !== false);
// 읽기 전용
if (!col.editable) {
if (!canEdit) {
if (col.type === "number") {
return <span>{typeof value === "number" ? value.toLocaleString() : value || "-"}</span>;
}
if (col.type === "date") {
// ISO 8601 형식을 표시용으로 변환
const displayDate = value ? (typeof value === 'string' && value.includes('T') ? value.split('T')[0] : value) : "-";
return <span>{displayDate}</span>;
}
return <span>{value || "-"}</span>;
}