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 { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; 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 { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -344,6 +344,8 @@ export function RepeatScreenModalComponent({
_originalData: { ...row }, _originalData: { ...row },
_isDirty: false, _isDirty: false,
_isNew: false, _isNew: false,
_isEditing: false, // 🆕 v3.8: 로드된 데이터는 읽기 전용
_isDeleted: false,
...row, ...row,
})); }));
@ -733,11 +735,14 @@ export function RepeatScreenModalComponent({
}) })
); );
} else if (_originalData?.id) { } else if (_originalData?.id) {
// UPDATE - /edit 엔드포인트 사용 (id를 body에 포함) // UPDATE - /edit 엔드포인트 사용 (originalData와 updatedData 형식)
const updateData = { ...dataToSave, id: _originalData.id }; const updatePayload = {
console.log(`[RepeatScreenModal] UPDATE 요청: /table-management/tables/${targetTable}/edit`, updateData); originalData: _originalData,
updatedData: { ...dataToSave, id: _originalData.id },
};
console.log(`[RepeatScreenModal] UPDATE 요청: /table-management/tables/${targetTable}/edit`, updatePayload);
savePromises.push( 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); console.log("[RepeatScreenModal] UPDATE 응답:", res.data);
savedIds.push(_originalData.id); savedIds.push(_originalData.id);
return res; return res;
@ -752,7 +757,7 @@ export function RepeatScreenModalComponent({
try { try {
await Promise.all(savePromises); await Promise.all(savePromises);
// 저장 후: 삭제된 행은 제거, 나머지는 dirty 플래그 초기화 // 저장 후: 삭제된 행은 제거, 나머지는 dirty/editing 플래그 초기화
setExternalTableData((prev) => { setExternalTableData((prev) => {
const updated = { ...prev }; const updated = { ...prev };
if (updated[key]) { if (updated[key]) {
@ -763,7 +768,8 @@ export function RepeatScreenModalComponent({
...row, ...row,
_isDirty: false, _isDirty: false,
_isNew: 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; 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: 외부 테이블 행 데이터 변경 // 🆕 v3.1: 외부 테이블 행 데이터 변경
const handleExternalRowDataChange = (cardId: string, contentRowId: string, rowId: string, field: string, value: any) => { const handleExternalRowDataChange = (cardId: string, contentRowId: string, rowId: string, field: string, value: any) => {
const key = `${cardId}-${contentRowId}`; const key = `${cardId}-${contentRowId}`;
@ -1742,8 +1782,8 @@ export function RepeatScreenModalComponent({
{col.label} {col.label}
</TableHead> </TableHead>
))} ))}
{contentRow.tableCrud?.allowDelete && ( {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && (
<TableHead className="w-[60px] text-center text-xs"></TableHead> <TableHead className="w-[80px] text-center text-xs"></TableHead>
)} )}
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@ -1777,33 +1817,66 @@ export function RepeatScreenModalComponent({
row._isDeleted && "line-through text-muted-foreground" row._isDeleted && "line-through text-muted-foreground"
)} )}
> >
{renderTableCell(col, row, (value) => {renderTableCell(
handleExternalRowDataChange(card._cardId, contentRow.id, row._rowId, col.field, value) col,
row,
(value) => handleExternalRowDataChange(card._cardId, contentRow.id, row._rowId, col.field, value),
row._isNew || row._isEditing // 신규 행이거나 수정 모드일 때만 편집 가능
)} )}
</TableCell> </TableCell>
))} ))}
{contentRow.tableCrud?.allowDelete && ( {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && (
<TableCell className="text-center"> <TableCell className="text-center">
{row._isDeleted ? ( <div className="flex items-center justify-center gap-1">
<Button {/* 수정 버튼: 저장된 행(isNew가 아닌)이고 편집 모드가 아닐 때만 표시 */}
variant="ghost" {contentRow.tableCrud?.allowUpdate && !row._isNew && !row._isEditing && !row._isDeleted && (
size="sm" <Button
onClick={() => handleRestoreExternalRow(card._cardId, row._rowId, contentRow.id)} variant="ghost"
className="h-7 w-7 p-0 text-primary hover:text-primary hover:bg-primary/10" size="sm"
title="삭제 취소" 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"
<RotateCcw className="h-4 w-4" /> title="수정"
</Button> >
) : ( <Pencil className="h-4 w-4" />
<Button </Button>
variant="ghost" )}
size="sm" {/* 수정 취소 버튼: 편집 모드일 때만 표시 */}
onClick={() => handleDeleteExternalRowRequest(card._cardId, row._rowId, contentRow.id, contentRow)} {row._isEditing && !row._isNew && (
className="h-7 w-7 p-0 text-destructive hover:text-destructive hover:bg-destructive/10" <Button
> variant="ghost"
<Trash2 className="h-4 w-4" /> size="sm"
</Button> 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> </TableCell>
)} )}
</TableRow> </TableRow>
@ -1869,8 +1942,11 @@ export function RepeatScreenModalComponent({
key={`${row._rowId}-${col.id}`} key={`${row._rowId}-${col.id}`}
className={cn("text-sm", col.align && `text-${col.align}`)} className={cn("text-sm", col.align && `text-${col.align}`)}
> >
{renderTableCell(col, row, (value) => {renderTableCell(
handleRowDataChange(card._cardId, row._rowId, col.field, value) col,
row,
(value) => handleRowDataChange(card._cardId, row._rowId, col.field, value),
row._isNew || row._isEditing
)} )}
</TableCell> </TableCell>
))} ))}
@ -2184,8 +2260,11 @@ function renderContentRow(
key={`${row._rowId}-${col.id}`} key={`${row._rowId}-${col.id}`}
className={cn("text-sm", col.align && `text-${col.align}`)} className={cn("text-sm", col.align && `text-${col.align}`)}
> >
{renderTableCell(col, row, (value) => {renderTableCell(
onRowDataChange(card._cardId, row._rowId, col.field, value) col,
row,
(value) => onRowDataChange(card._cardId, row._rowId, col.field, value),
row._isNew || row._isEditing
)} )}
</TableCell> </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]; const value = row[col.field];
// Badge 타입 // Badge 타입
@ -2468,11 +2548,20 @@ function renderTableCell(col: TableColumnConfig, row: CardRowData, onChange: (va
return <Badge variant={badgeColor as any}>{value || "-"}</Badge>; 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") { if (col.type === "number") {
return <span>{typeof value === "number" ? value.toLocaleString() : value || "-"}</span>; 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>; return <span>{value || "-"}</span>;
} }