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 { 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,14 +1817,44 @@ 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 ? (
|
||||
<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"
|
||||
|
|
@ -1800,10 +1870,13 @@ export function RepeatScreenModalComponent({
|
|||
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>;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue