From 10d81cb9bc52e61922a5900188774f99a43f8fc3 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 2 Dec 2025 15:23:25 +0900 Subject: [PATCH] =?UTF-8?q?feat(repeat-screen-modal):=20=ED=85=8C=EC=9D=B4?= =?UTF-8?q?=EB=B8=94=20=ED=96=89=20=ED=8E=B8=EC=A7=91=20=EB=AA=A8=EB=93=9C?= =?UTF-8?q?=20=EC=A0=9C=EC=96=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DB 로드 데이터에 _isEditing: false 명시적 설정 - handleEditExternalRow: 수정 모드 전환 함수 추가 - handleCancelEditExternalRow: 수정 취소 및 원본 복원 함수 추가 - renderTableCell: isRowEditable 파라미터 추가로 행 수준 편집 제어 - UPDATE API 요청 형식 { originalData, updatedData }로 수정 - 테이블 작업 컬럼에 수정/수정취소/삭제/복원 버튼 그룹화 --- .../RepeatScreenModalComponent.tsx | 165 ++++++++++++++---- 1 file changed, 127 insertions(+), 38 deletions(-) diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx index 3bbdf039..48c58392 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx @@ -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} ))} - {contentRow.tableCrud?.allowDelete && ( - 삭제 + {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && ( + 작업 )} @@ -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 // 신규 행이거나 수정 모드일 때만 편집 가능 )} ))} - {contentRow.tableCrud?.allowDelete && ( + {(contentRow.tableCrud?.allowUpdate || contentRow.tableCrud?.allowDelete) && ( - {row._isDeleted ? ( - - ) : ( - - )} +
+ {/* 수정 버튼: 저장된 행(isNew가 아닌)이고 편집 모드가 아닐 때만 표시 */} + {contentRow.tableCrud?.allowUpdate && !row._isNew && !row._isEditing && !row._isDeleted && ( + + )} + {/* 수정 취소 버튼: 편집 모드일 때만 표시 */} + {row._isEditing && !row._isNew && ( + + )} + {/* 삭제/복원 버튼 */} + {contentRow.tableCrud?.allowDelete && ( + row._isDeleted ? ( + + ) : ( + + ) + )} +
)} @@ -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 )} ))} @@ -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 )} ))} @@ -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 {value || "-"}; } + // 🆕 v3.8: 행 수준 편집 가능 여부 체크 + // isRowEditable이 false이면 컬럼 설정과 관계없이 읽기 전용 + const canEdit = col.editable && (isRowEditable !== false); + // 읽기 전용 - if (!col.editable) { + if (!canEdit) { if (col.type === "number") { return {typeof value === "number" ? value.toLocaleString() : value || "-"}; } + if (col.type === "date") { + // ISO 8601 형식을 표시용으로 변환 + const displayDate = value ? (typeof value === 'string' && value.includes('T') ? value.split('T')[0] : value) : "-"; + return {displayDate}; + } return {value || "-"}; }