feat(repeat-screen-modal): 테이블 행 편집 모드 제어 기능 구현
- DB 로드 데이터에 _isEditing: false 명시적 설정
- handleEditExternalRow: 수정 모드 전환 함수 추가
- handleCancelEditExternalRow: 수정 취소 및 원본 복원 함수 추가
- renderTableCell: isRowEditable 파라미터 추가로 행 수준 편집 제어
- UPDATE API 요청 형식 { originalData, updatedData }로 수정
- 테이블 작업 컬럼에 수정/수정취소/삭제/복원 버튼 그룹화
This commit is contained in:
parent
b286bc3c63
commit
10d81cb9bc
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue