From 8e257f36b279a07078770c28e032ffb7678042f8 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 2 Dec 2025 14:30:29 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20ScreenModal=20selectedData=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=B5=EC=9B=90=20(RepeatScreenModal=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/common/ScreenModal.tsx | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/frontend/components/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx index 53fd0852..de886cfd 100644 --- a/frontend/components/common/ScreenModal.tsx +++ b/frontend/components/common/ScreenModal.tsx @@ -60,6 +60,9 @@ export const ScreenModal: React.FC = ({ className }) => { // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ƒํƒœ (์ˆ˜์ • ๋ชจ๋“œ์—์„œ UPDATE ํŒ๋‹จ์šฉ) const [originalData, setOriginalData] = useState | null>(null); + // ๐Ÿ†• ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ƒํƒœ (RepeatScreenModal ๋“ฑ์—์„œ ์‚ฌ์šฉ) + const [selectedData, setSelectedData] = useState[]>([]); + // ์—ฐ์† ๋“ฑ๋ก ๋ชจ๋“œ ์ƒํƒœ (state๋กœ ๋ณ€๊ฒฝ - ์ฒดํฌ๋ฐ•์Šค UI ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•ด) const [continuousMode, setContinuousMode] = useState(false); @@ -129,12 +132,27 @@ export const ScreenModal: React.FC = ({ className }) => { // ์ „์—ญ ๋ชจ๋‹ฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { const handleOpenModal = (event: CustomEvent) => { - const { screenId, title, description, size, urlParams, editData } = event.detail; + const { screenId, title, description, size, urlParams, editData, selectedData: eventSelectedData, selectedIds } = event.detail; + + console.log("๐Ÿ“ฆ [ScreenModal] ๋ชจ๋‹ฌ ์—ด๊ธฐ ์ด๋ฒคํŠธ ์ˆ˜์‹ :", { + screenId, + title, + selectedData: eventSelectedData, + selectedIds, + }); // ๐Ÿ†• ๋ชจ๋‹ฌ ์—ด๋ฆฐ ์‹œ๊ฐ„ ๊ธฐ๋ก modalOpenedAtRef.current = Date.now(); console.log("๐Ÿ• [ScreenModal] ๋ชจ๋‹ฌ ์—ด๋ฆผ ์‹œ๊ฐ„ ๊ธฐ๋ก:", modalOpenedAtRef.current); + // ๐Ÿ†• ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ (RepeatScreenModal ๋“ฑ์—์„œ ์‚ฌ์šฉ) + if (eventSelectedData && Array.isArray(eventSelectedData)) { + setSelectedData(eventSelectedData); + console.log("๐Ÿ“ฆ [ScreenModal] ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ €์žฅ:", eventSelectedData.length, "๊ฑด"); + } else { + setSelectedData([]); + } + // ๐Ÿ†• URL ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ํ˜„์žฌ URL์— ์ถ”๊ฐ€ if (urlParams && typeof window !== "undefined") { const currentUrl = new URL(window.location.href); @@ -184,6 +202,7 @@ export const ScreenModal: React.FC = ({ className }) => { setScreenData(null); setFormData({}); setOriginalData(null); // ๐Ÿ†• ์›๋ณธ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” + setSelectedData([]); // ๐Ÿ†• ์„ ํƒ๋œ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” setContinuousMode(false); localStorage.setItem("screenModal_continuousMode", "false"); // localStorage์— ์ €์žฅ console.log("๐Ÿ”„ ์—ฐ์† ๋ชจ๋“œ ์ดˆ๊ธฐํ™”: false"); @@ -649,6 +668,7 @@ export const ScreenModal: React.FC = ({ className }) => { id: modalState.screenId!, tableName: screenData.screenInfo?.tableName, }} + groupedData={selectedData} userId={userId} userName={userName} companyCode={user?.companyCode} From b286bc3c637619896e8aa3a5115da58585a2339b Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 2 Dec 2025 14:50:00 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat(repeat-screen-modal):=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?DB=20=EC=97=B0=EB=8F=99=20(=EC=86=8C=ED=94=84=ED=8A=B8=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ์‚ญ์ œ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ _isDeleted ํ”Œ๋ž˜๊ทธ ์„ค์ • (์†Œํ”„ํŠธ ์‚ญ์ œ) - ์‚ญ์ œ๋œ ํ–‰ ์‹œ๊ฐ์  ํ‘œ์‹œ (์ทจ์†Œ์„ , ํˆฌ๋ช…๋„) - ์‚ญ์ œ ์ทจ์†Œ(๋ณต์›) ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ DELETE API ํ˜ธ์ถœํ•˜์—ฌ DB ๋ฐ˜์˜ - ์‚ญ์ œ๋œ ํ–‰ ์ง‘๊ณ„ ๊ณ„์‚ฐ์—์„œ ์ œ์™ธ - axios DELETE ์š”์ฒญ ์‹œ body ์ „๋‹ฌ ๋ฐฉ์‹ ์ˆ˜์ • --- .../RepeatScreenModalComponent.tsx | 130 ++++++++++++++---- 1 file changed, 106 insertions(+), 24 deletions(-) diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx index 2484f1d7..3bbdf039 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx @@ -481,7 +481,8 @@ export function RepeatScreenModalComponent({ // ๊ฐ ์นด๋“œ์˜ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ const updatedCards = groupedCardsData.map((card) => { const key = `${card._cardId}-${tableRowWithExternalSource.id}`; - const externalRows = extData[key] || []; + // ๐Ÿ†• v3.7: ์‚ญ์ œ๋œ ํ–‰์€ ์ง‘๊ณ„์—์„œ ์ œ์™ธ + const externalRows = (extData[key] || []).filter((row) => !row._isDeleted); // ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ const newAggregations: Record = {}; @@ -671,8 +672,35 @@ export function RepeatScreenModalComponent({ inputType: c.inputType }))); - for (const row of dirtyRows) { - const { _rowId, _originalData, _isDirty, _isNew, ...allData } = row; + // ์‚ญ์ œํ•  ํ–‰ (๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ค‘ _isDeleted๊ฐ€ true์ธ ๊ฒƒ) + const deletedRows = dirtyRows.filter((row) => row._isDeleted && row._originalData?.id); + // ์ €์žฅํ•  ํ–‰ (์‚ญ์ œ๋˜์ง€ ์•Š์€ ๊ฒƒ) + const rowsToSave = dirtyRows.filter((row) => !row._isDeleted); + + console.log("[RepeatScreenModal] ์‚ญ์ œ ๋Œ€์ƒ:", deletedRows.length, "๊ฑด"); + console.log("[RepeatScreenModal] ์ €์žฅ ๋Œ€์ƒ:", rowsToSave.length, "๊ฑด"); + + // ๐Ÿ†• v3.7: ์‚ญ์ œ ์ฒ˜๋ฆฌ (๋ฐฐ์—ด ํ˜•ํƒœ๋กœ body์— ์ „๋‹ฌ) + for (const row of deletedRows) { + const deleteId = row._originalData.id; + console.log(`[RepeatScreenModal] DELETE ์š”์ฒญ: /table-management/tables/${targetTable}/delete`, [{ id: deleteId }]); + savePromises.push( + apiClient.request({ + method: "DELETE", + url: `/table-management/tables/${targetTable}/delete`, + data: [{ id: deleteId }], + }).then((res) => { + console.log("[RepeatScreenModal] DELETE ์‘๋‹ต:", res.data); + return { type: "delete", id: deleteId }; + }).catch((err) => { + console.error("[RepeatScreenModal] DELETE ์‹คํŒจ:", err.response?.data || err.message); + throw err; + }) + ); + } + + for (const row of rowsToSave) { + const { _rowId, _originalData, _isDirty, _isNew, _isDeleted, ...allData } = row; // ํ—ˆ์šฉ๋œ ํ•„๋“œ๋งŒ ํ•„ํ„ฐ๋ง const dataToSave: Record = {}; @@ -724,21 +752,30 @@ export function RepeatScreenModalComponent({ try { await Promise.all(savePromises); - // ์ €์žฅ ํ›„ ํ•ด๋‹น ํ‚ค์˜ dirty ํ”Œ๋ž˜๊ทธ๋งŒ ์ดˆ๊ธฐํ™” + // ์ €์žฅ ํ›„: ์‚ญ์ œ๋œ ํ–‰์€ ์ œ๊ฑฐ, ๋‚˜๋จธ์ง€๋Š” dirty ํ”Œ๋ž˜๊ทธ ์ดˆ๊ธฐํ™” setExternalTableData((prev) => { const updated = { ...prev }; if (updated[key]) { - updated[key] = updated[key].map((row) => ({ - ...row, - _isDirty: false, - _isNew: false, - _originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined }, - })); + // ์‚ญ์ œ๋œ ํ–‰์€ ์™„์ „ํžˆ ์ œ๊ฑฐ + updated[key] = updated[key] + .filter((row) => !row._isDeleted) + .map((row) => ({ + ...row, + _isDirty: false, + _isNew: false, + _originalData: { ...row, _rowId: undefined, _originalData: undefined, _isDirty: undefined, _isNew: undefined, _isDeleted: undefined }, + })); } return updated; }); - return { success: true, message: `${dirtyRows.length}๊ฑด ์ €์žฅ ์™„๋ฃŒ`, savedCount: dirtyRows.length, savedIds }; + const savedCount = rowsToSave.length; + const deletedCount = deletedRows.length; + const message = deletedCount > 0 + ? `${savedCount}๊ฑด ์ €์žฅ, ${deletedCount}๊ฑด ์‚ญ์ œ ์™„๋ฃŒ` + : `${savedCount}๊ฑด ์ €์žฅ ์™„๋ฃŒ`; + + return { success: true, message, savedCount, deletedCount, savedIds }; } catch (error: any) { console.error("[RepeatScreenModal] ํ…Œ์ด๋ธ” ์˜์—ญ ์ €์žฅ ์‹คํŒจ:", error); return { success: false, message: error.message || "์ €์žฅ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." }; @@ -774,16 +811,20 @@ export function RepeatScreenModalComponent({ } }; - // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์‚ญ์ œ ์‹คํ–‰ + // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์‚ญ์ œ ์‹คํ–‰ (์†Œํ”„ํŠธ ์‚ญ์ œ - _isDeleted ํ”Œ๋ž˜๊ทธ ์„ค์ •) const handleDeleteExternalRow = (cardId: string, rowId: string, contentRowId: string) => { const key = `${cardId}-${contentRowId}`; setExternalTableData((prev) => { const newData = { ...prev, - [key]: (prev[key] || []).filter((row) => row._rowId !== rowId), + [key]: (prev[key] || []).map((row) => + row._rowId === rowId + ? { ...row, _isDeleted: true, _isDirty: true } + : row + ), }; - // ๐Ÿ†• v3.5: ํ–‰ ์‚ญ์ œ ์‹œ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ + // ๐Ÿ†• v3.5: ํ–‰ ์‚ญ์ œ ์‹œ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ (์‚ญ์ œ๋œ ํ–‰ ์ œ์™ธ) setTimeout(() => { recalculateAggregationsWithExternalData(newData); }, 0); @@ -794,6 +835,27 @@ export function RepeatScreenModalComponent({ setPendingDeleteInfo(null); }; + // ๐Ÿ†• v3.7: ์‚ญ์ œ ์ทจ์†Œ (์†Œํ”„ํŠธ ์‚ญ์ œ ๋ณต์›) + const handleRestoreExternalRow = (cardId: string, rowId: string, contentRowId: string) => { + const key = `${cardId}-${contentRowId}`; + setExternalTableData((prev) => { + const newData = { + ...prev, + [key]: (prev[key] || []).map((row) => + row._rowId === rowId + ? { ...row, _isDeleted: false, _isDirty: true } + : row + ), + }; + + setTimeout(() => { + recalculateAggregationsWithExternalData(newData); + }, 0); + + return newData; + }); + }; + // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ const handleExternalRowDataChange = (cardId: string, contentRowId: string, rowId: string, field: string, value: any) => { const key = `${cardId}-${contentRowId}`; @@ -1700,12 +1762,20 @@ export function RepeatScreenModalComponent({ (externalTableData[`${card._cardId}-${contentRow.id}`] || []).map((row) => ( {(contentRow.tableColumns || []).map((col) => ( {renderTableCell(col, row, (value) => handleExternalRowDataChange(card._cardId, contentRow.id, row._rowId, col.field, value) @@ -1714,14 +1784,26 @@ export function RepeatScreenModalComponent({ ))} {contentRow.tableCrud?.allowDelete && ( - + {row._isDeleted ? ( + + ) : ( + + )} )} From 10d81cb9bc52e61922a5900188774f99a43f8fc3 Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 2 Dec 2025 15:23:25 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat(repeat-screen-modal):=20=ED=85=8C?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=20=ED=96=89=20=ED=8E=B8=EC=A7=91=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EC=96=B4=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=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 || "-"}; } From ae7b21147be77e2a15aa7e25353b0ee44c8d435d Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Tue, 2 Dec 2025 17:44:24 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat(repeat-screen-modal):=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=B1=84=EB=B2=88?= =?UTF-8?q?=20=EA=B7=9C=EC=B9=99=20=EA=B0=92=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - RepeatScreenModal ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์—ฐ๊ด€ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜๋Š” ๊ธฐ๋Šฅ ์ถ”๊ฐ€ - ButtonPrimary ์ €์žฅ ์‹œ ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ’(shipment_plan_no) ํ•จ๊ป˜ ์ €์žฅ - _repeatScreenModal_* ๋ฐ์ดํ„ฐ ๊ฐ์ง€ ์‹œ ๋ฉ”์ธ ํ…Œ์ด๋ธ” ์ค‘๋ณต ์ €์žฅ ๋ฐฉ์ง€ - ๊ธฐ์กด ํ–‰ ์ˆ˜์ • ๋ชจ๋“œ(_isEditing) ์ง€์› - AggregationSaveConfig ํƒ€์ž… ๋ฐ ConfigPanel UI ์ถ”๊ฐ€ --- .../RepeatScreenModalComponent.tsx | 201 +++++++++++++++++- .../RepeatScreenModalConfigPanel.tsx | 157 +++++++++++++- .../components/repeat-screen-modal/types.ts | 23 ++ frontend/lib/utils/buttonActions.ts | 122 ++++++++++- 4 files changed, 494 insertions(+), 9 deletions(-) diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx index 48c58392..85e43ce9 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx @@ -99,6 +99,123 @@ export function RepeatScreenModalComponent({ contentRowId: string; } | null>(null); + // ๐Ÿ†• v3.9: beforeFormSave ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - ButtonPrimary ์ €์žฅ ์‹œ externalTableData๋ฅผ formData์— ๋ณ‘ํ•ฉ + useEffect(() => { + const handleBeforeFormSave = (event: Event) => { + if (!(event instanceof CustomEvent) || !event.detail?.formData) return; + + console.log("[RepeatScreenModal] beforeFormSave ์ด๋ฒคํŠธ ์ˆ˜์‹ "); + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ์—์„œ dirty ํ–‰๋งŒ ์ถ”์ถœํ•˜์—ฌ ์ €์žฅ ๋ฐ์ดํ„ฐ ์ค€๋น„ + const saveDataByTable: Record = {}; + + for (const [key, rows] of Object.entries(externalTableData)) { + // contentRow ์ฐพ๊ธฐ + const contentRow = contentRows.find((r) => key.includes(r.id)); + if (!contentRow?.tableDataSource?.enabled) continue; + + const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable; + + // dirty ํ–‰๋งŒ ํ•„ํ„ฐ๋ง (์‚ญ์ œ๋œ ํ–‰ ์ œ์™ธ) + const dirtyRows = rows.filter((row) => row._isDirty && !row._isDeleted); + + if (dirtyRows.length === 0) continue; + + // ์ €์žฅํ•  ํ•„๋“œ๋งŒ ์ถ”์ถœ + const editableFields = (contentRow.tableColumns || []) + .filter((col) => col.editable) + .map((col) => col.field); + + const joinKeys = (contentRow.tableDataSource.joinConditions || []) + .map((cond) => cond.sourceKey); + + const allowedFields = [...new Set([...editableFields, ...joinKeys])]; + + if (!saveDataByTable[targetTable]) { + saveDataByTable[targetTable] = []; + } + + for (const row of dirtyRows) { + const saveData: Record = {}; + + // ํ—ˆ์šฉ๋œ ํ•„๋“œ๋งŒ ํฌํ•จ + for (const field of allowedFields) { + if (row[field] !== undefined) { + saveData[field] = row[field]; + } + } + + // _isNew ํ”Œ๋ž˜๊ทธ ์œ ์ง€ + saveData._isNew = row._isNew; + saveData._targetTable = targetTable; + + // ๊ธฐ์กด ๋ ˆ์ฝ”๋“œ์˜ ๊ฒฝ์šฐ id ํฌํ•จ + if (!row._isNew && row._originalData?.id) { + saveData.id = row._originalData.id; + } + + saveDataByTable[targetTable].push(saveData); + } + } + + // formData์— ํ…Œ์ด๋ธ”๋ณ„ ์ €์žฅ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ + for (const [tableName, rows] of Object.entries(saveDataByTable)) { + const fieldKey = `_repeatScreenModal_${tableName}`; + event.detail.formData[fieldKey] = rows; + console.log(`[RepeatScreenModal] beforeFormSave - ${tableName} ์ €์žฅ ๋ฐ์ดํ„ฐ:`, rows); + } + + // ๐Ÿ†• v3.9: ์ง‘๊ณ„ ์ €์žฅ ์„ค์ • ์ •๋ณด๋„ formData์— ์ถ”๊ฐ€ + if (grouping?.aggregations && groupedCardsData.length > 0) { + const aggregationSaveConfigs: Array<{ + resultField: string; + aggregatedValue: number; + targetTable: string; + targetColumn: string; + joinKey: { sourceField: string; targetField: string }; + sourceValue: any; // ์กฐ์ธ ํ‚ค ๊ฐ’ + }> = []; + + for (const card of groupedCardsData) { + for (const agg of grouping.aggregations) { + if (agg.saveConfig?.enabled) { + const { saveConfig, resultField } = agg; + const { targetTable, targetColumn, joinKey } = saveConfig; + + if (!targetTable || !targetColumn || !joinKey?.sourceField || !joinKey?.targetField) { + continue; + } + + const aggregatedValue = card._aggregations?.[resultField] ?? 0; + const sourceValue = card._representativeData?.[joinKey.sourceField]; + + if (sourceValue !== undefined) { + aggregationSaveConfigs.push({ + resultField, + aggregatedValue, + targetTable, + targetColumn, + joinKey, + sourceValue, + }); + } + } + } + } + + if (aggregationSaveConfigs.length > 0) { + event.detail.formData._repeatScreenModal_aggregations = aggregationSaveConfigs; + console.log("[RepeatScreenModal] beforeFormSave - ์ง‘๊ณ„ ์ €์žฅ ์„ค์ •:", aggregationSaveConfigs); + } + } + }; + + window.addEventListener("beforeFormSave", handleBeforeFormSave as EventListener); + return () => { + window.removeEventListener("beforeFormSave", handleBeforeFormSave as EventListener); + }; + }, [externalTableData, contentRows, grouping, groupedCardsData]); + // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ useEffect(() => { const loadInitialData = async () => { @@ -795,16 +912,91 @@ export function RepeatScreenModalComponent({ const result = await saveTableAreaData(cardId, contentRowId, contentRow); if (result.success) { console.log("[RepeatScreenModal] ํ…Œ์ด๋ธ” ์˜์—ญ ์ €์žฅ ์„ฑ๊ณต:", result); - // ์„ฑ๊ณต ์•Œ๋ฆผ (ํ•„์š” ์‹œ toast ์ถ”๊ฐ€) + + // ๐Ÿ†• v3.9: ์ง‘๊ณ„ ์ €์žฅ ์„ค์ •์ด ์žˆ๋Š” ๊ฒฝ์šฐ ์—ฐ๊ด€ ํ…Œ์ด๋ธ” ๋™๊ธฐํ™” + const card = groupedCardsData.find((c) => c._cardId === cardId); + if (card && grouping?.aggregations) { + await saveAggregationsToRelatedTables(card, contentRowId); + } } else { console.error("[RepeatScreenModal] ํ…Œ์ด๋ธ” ์˜์—ญ ์ €์žฅ ์‹คํŒจ:", result.message); - // ์‹คํŒจ ์•Œ๋ฆผ (ํ•„์š” ์‹œ toast ์ถ”๊ฐ€) } } finally { setIsSaving(false); } }; + // ๐Ÿ†• v3.9: ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ์—ฐ๊ด€ ํ…Œ์ด๋ธ”์— ์ €์žฅ + const saveAggregationsToRelatedTables = async (card: GroupedCardData, contentRowId: string) => { + if (!grouping?.aggregations) return; + + const savePromises: Promise[] = []; + + for (const agg of grouping.aggregations) { + const saveConfig = agg.saveConfig; + + // ์ €์žฅ ์„ค์ •์ด ์—†๊ฑฐ๋‚˜ ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ์Šคํ‚ต + if (!saveConfig?.enabled) continue; + + // ์ž๋™ ์ €์žฅ์ด ์•„๋‹Œ ๊ฒฝ์šฐ, ๋ ˆ์ด์•„์›ƒ์— ์—ฐ๊ฒฐ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ ํ•„์š” + // (ํ˜„์žฌ๋Š” ์ž๋™ ์ €์žฅ๊ณผ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ - ์ถ”ํ›„ ๋ ˆ์ด์•„์›ƒ ์—ฐ๊ฒฐ ์ฒดํฌ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ) + + // ์ง‘๊ณ„ ๊ฒฐ๊ณผ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + const aggregatedValue = card._aggregations[agg.resultField]; + + if (aggregatedValue === undefined) { + console.warn(`[RepeatScreenModal] ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์—†์Œ: ${agg.resultField}`); + continue; + } + + // ์กฐ์ธ ํ‚ค๋กœ ๋Œ€์ƒ ๋ ˆ์ฝ”๋“œ ์‹๋ณ„ + const sourceKeyValue = card._representativeData[saveConfig.joinKey.sourceField]; + + if (!sourceKeyValue) { + console.warn(`[RepeatScreenModal] ์กฐ์ธ ํ‚ค ๊ฐ’ ์—†์Œ: ${saveConfig.joinKey.sourceField}`); + continue; + } + + console.log(`[RepeatScreenModal] ์ง‘๊ณ„ ์ €์žฅ ์‹œ์ž‘:`, { + aggregation: agg.resultField, + value: aggregatedValue, + targetTable: saveConfig.targetTable, + targetColumn: saveConfig.targetColumn, + joinKey: `${saveConfig.joinKey.sourceField}=${sourceKeyValue} -> ${saveConfig.joinKey.targetField}`, + }); + + // UPDATE API ํ˜ธ์ถœ + const updatePayload = { + originalData: { [saveConfig.joinKey.targetField]: sourceKeyValue }, + updatedData: { + [saveConfig.targetColumn]: aggregatedValue, + [saveConfig.joinKey.targetField]: sourceKeyValue, + }, + }; + + savePromises.push( + apiClient.put(`/table-management/tables/${saveConfig.targetTable}/edit`, updatePayload) + .then((res) => { + console.log(`[RepeatScreenModal] ์ง‘๊ณ„ ์ €์žฅ ์„ฑ๊ณต: ${agg.resultField} -> ${saveConfig.targetTable}.${saveConfig.targetColumn}`); + return res; + }) + .catch((err) => { + console.error(`[RepeatScreenModal] ์ง‘๊ณ„ ์ €์žฅ ์‹คํŒจ: ${agg.resultField}`, err.response?.data || err.message); + throw err; + }) + ); + } + + if (savePromises.length > 0) { + try { + await Promise.all(savePromises); + console.log(`[RepeatScreenModal] ๋ชจ๋“  ์ง‘๊ณ„ ์ €์žฅ ์™„๋ฃŒ: ${savePromises.length}๊ฑด`); + } catch (error) { + console.error("[RepeatScreenModal] ์ผ๋ถ€ ์ง‘๊ณ„ ์ €์žฅ ์‹คํŒจ:", error); + } + } + }; + // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์‚ญ์ œ ์š”์ฒญ const handleDeleteExternalRowRequest = (cardId: string, rowId: string, contentRowId: string, contentRow: CardContentRowConfig) => { if (contentRow.tableCrud?.deleteConfirm?.enabled !== false) { @@ -1002,8 +1194,11 @@ export function RepeatScreenModalComponent({ }); } + // ์•ˆ์ •์ ์ธ _cardId ์ƒ์„ฑ (Date.now() ๋Œ€์‹  groupKey ์‚ฌ์šฉ) + // groupKey๊ฐ€ ์—†์œผ๋ฉด ๋Œ€ํ‘œ ๋ฐ์ดํ„ฐ์˜ id ์‚ฌ์šฉ + const stableId = groupKey || representativeData.id || cardIndex; result.push({ - _cardId: `grouped-card-${cardIndex}-${Date.now()}`, + _cardId: `grouped-card-${cardIndex}-${stableId}`, _groupKey: groupKey, _groupField: groupByField || "", _aggregations: aggregations, diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx index 54949627..44ee5ce6 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalConfigPanel.tsx @@ -766,7 +766,7 @@ function AggregationConfigItem({ const currentSourceType = agg.sourceType || "column"; return ( -
+
+ + {/* ๐Ÿ†• v3.9: ์ €์žฅ ์„ค์ • */} + +
+ ); +} + +// ๐Ÿ†• v3.9: ์ง‘๊ณ„ ์ €์žฅ ์„ค์ • ์„น์…˜ +function AggregationSaveConfigSection({ + agg, + sourceTable, + allTables, + onUpdate, +}: { + agg: AggregationConfig; + sourceTable: string; + allTables: { tableName: string; displayName?: string }[]; + onUpdate: (updates: Partial) => void; +}) { + const saveConfig = agg.saveConfig || { enabled: false, autoSave: false, targetTable: "", targetColumn: "", joinKey: { sourceField: "", targetField: "" } }; + + const updateSaveConfig = (updates: Partial) => { + onUpdate({ + saveConfig: { + ...saveConfig, + ...updates, + }, + }); + }; + + return ( +
+
+ + updateSaveConfig({ enabled: checked })} + className="scale-[0.6]" + /> +
+ + {saveConfig.enabled && ( +
+ {/* ์ž๋™ ์ €์žฅ ์˜ต์…˜ */} +
+
+ +

+ ๋ ˆ์ด์•„์›ƒ์— ์—†์–ด๋„ ์ €์žฅ +

+
+ updateSaveConfig({ autoSave: checked })} + className="scale-[0.6]" + /> +
+ + {/* ๋Œ€์ƒ ํ…Œ์ด๋ธ” */} +
+ + +
+ + {/* ๋Œ€์ƒ ์ปฌ๋Ÿผ */} +
+ + updateSaveConfig({ targetColumn: value })} + placeholder="์ปฌ๋Ÿผ ์„ ํƒ" + /> +
+ + {/* ์กฐ์ธ ํ‚ค ์„ค์ • */} +
+ +
+
+ ์นด๋“œ ํ‚ค (ํ˜„์žฌ ์นด๋“œ ๋ฐ์ดํ„ฐ) + + updateSaveConfig({ + joinKey: { ...saveConfig.joinKey, sourceField: value }, + }) + } + placeholder="์นด๋“œ ํ‚ค ์„ ํƒ" + /> +
+
+ โ†“ +
+
+ ๋Œ€์ƒ ํ‚ค (์—…๋ฐ์ดํŠธํ•  ๋ ˆ์ฝ”๋“œ ์‹๋ณ„) + + updateSaveConfig({ + joinKey: { ...saveConfig.joinKey, targetField: value }, + }) + } + placeholder="๋Œ€์ƒ ํ‚ค ์„ ํƒ" + /> +
+
+
+ + {/* ์„ค์ • ์š”์•ฝ */} + {saveConfig.targetTable && saveConfig.targetColumn && ( +
+
+ ์ €์žฅ ๊ฒฝ๋กœ: + {saveConfig.autoSave && ( + + ์ž๋™ + + )} +
+
+ {saveConfig.targetTable}.{saveConfig.targetColumn} +
+ {saveConfig.joinKey?.sourceField && saveConfig.joinKey?.targetField && ( +
+ ์กฐ์ธ: {saveConfig.joinKey.sourceField} โ†’ {saveConfig.joinKey.targetField} +
+ )} +
+ )} +
+ )}
); } diff --git a/frontend/lib/registry/components/repeat-screen-modal/types.ts b/frontend/lib/registry/components/repeat-screen-modal/types.ts index 7226503e..2191818f 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/types.ts +++ b/frontend/lib/registry/components/repeat-screen-modal/types.ts @@ -265,6 +265,7 @@ export interface ChainedJoinConfig { /** * ์ง‘๊ณ„ ์„ค์ • * ๐Ÿ†• v3.2: ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ๋ฐ ๊ฐ€์ƒ ์ง‘๊ณ„(formula) ์ง€์› + * ๐Ÿ†• v3.9: ์—ฐ๊ด€ ํ…Œ์ด๋ธ” ์ €์žฅ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ */ export interface AggregationConfig { // === ์ง‘๊ณ„ ์†Œ์Šค ํƒ€์ž… === @@ -287,6 +288,28 @@ export interface AggregationConfig { // === ๊ณตํ†ต === resultField: string; // ๊ฒฐ๊ณผ ํ•„๋“œ๋ช… (์˜ˆ: "total_balance_qty") label: string; // ํ‘œ์‹œ ๋ผ๋ฒจ (์˜ˆ: "์ด์ˆ˜์ฃผ์ž”๋Ÿ‰") + + // === ๐Ÿ†• v3.9: ์ €์žฅ ์„ค์ • === + saveConfig?: AggregationSaveConfig; // ์—ฐ๊ด€ ํ…Œ์ด๋ธ” ์ €์žฅ ์„ค์ • +} + +/** + * ๐Ÿ†• v3.9: ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์ €์žฅ ์„ค์ • + * ์ง‘๊ณ„๋œ ๊ฐ’์„ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”์— ๋™๊ธฐํ™” + */ +export interface AggregationSaveConfig { + enabled: boolean; // ์ €์žฅ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + autoSave: boolean; // ์ž๋™ ์ €์žฅ (๋ ˆ์ด์•„์›ƒ์— ์—†์–ด๋„ ์ €์žฅ) + + // ์ €์žฅ ๋Œ€์ƒ + targetTable: string; // ์ €์žฅํ•  ํ…Œ์ด๋ธ” (์˜ˆ: "sales_order_mng") + targetColumn: string; // ์ €์žฅํ•  ์ปฌ๋Ÿผ (์˜ˆ: "plan_qty_total") + + // ์กฐ์ธ ํ‚ค (์–ด๋–ค ๋ ˆ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธํ• ์ง€) + joinKey: { + sourceField: string; // ํ˜„์žฌ ์นด๋“œ์˜ ์กฐ์ธ ํ‚ค (์˜ˆ: "id" ๋˜๋Š” "sales_order_id") + targetField: string; // ๋Œ€์ƒ ํ…Œ์ด๋ธ”์˜ ํ‚ค (์˜ˆ: "id") + }; } /** diff --git a/frontend/lib/utils/buttonActions.ts b/frontend/lib/utils/buttonActions.ts index 5be55b65..235946ce 100644 --- a/frontend/lib/utils/buttonActions.ts +++ b/frontend/lib/utils/buttonActions.ts @@ -5,6 +5,7 @@ import { toast } from "sonner"; import { screenApi } from "@/lib/api/screen"; import { DynamicFormApi } from "@/lib/api/dynamicForm"; import { ImprovedButtonActionExecutor } from "@/lib/utils/improvedButtonActionExecutor"; +import { apiClient } from "@/lib/api/client"; import type { ExtendedControlContext } from "@/types/control-management"; /** @@ -663,11 +664,122 @@ export class ButtonActionExecutor { } } - saveResult = await DynamicFormApi.saveFormData({ - screenId, - tableName, - data: dataWithUserInfo, - }); + // ๐Ÿ†• v3.9: RepeatScreenModal์˜ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ ์ฒ˜๋ฆฌ + const repeatScreenModalKeys = Object.keys(context.formData).filter((key) => + key.startsWith("_repeatScreenModal_") && key !== "_repeatScreenModal_aggregations" + ); + + // RepeatScreenModal ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ…Œ์ด๋ธ”์— ๋Œ€ํ•œ ๋ฉ”์ธ ์ €์žฅ์€ ๊ฑด๋„ˆ๋œ€ + const repeatScreenModalTables = repeatScreenModalKeys.map((key) => key.replace("_repeatScreenModal_", "")); + const shouldSkipMainSave = repeatScreenModalTables.includes(tableName); + + if (shouldSkipMainSave) { + console.log(`โญ๏ธ [handleSave] ${tableName} ๋ฉ”์ธ ์ €์žฅ ๊ฑด๋„ˆ๋œ€ (RepeatScreenModal์—์„œ ์ฒ˜๋ฆฌ)`); + saveResult = { success: true, message: "RepeatScreenModal์—์„œ ์ฒ˜๋ฆฌ" }; + } else { + saveResult = await DynamicFormApi.saveFormData({ + screenId, + tableName, + data: dataWithUserInfo, + }); + } + + if (repeatScreenModalKeys.length > 0) { + console.log("๐Ÿ“ฆ [handleSave] RepeatScreenModal ๋ฐ์ดํ„ฐ ์ €์žฅ ์‹œ์ž‘:", repeatScreenModalKeys); + + // ๐Ÿ†• formData์—์„œ ์ฑ„๋ฒˆ ๊ทœ์น™์œผ๋กœ ์ƒ์„ฑ๋œ ๊ฐ’๋“ค ์ถ”์ถœ (์˜ˆ: shipment_plan_no) + const numberingFields: Record = {}; + for (const [fieldKey, value] of Object.entries(context.formData)) { + // _numberingRuleId๋กœ ๋๋‚˜๋Š” ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น ํ•„๋“œ๋Š” ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ’ + if (context.formData[`${fieldKey}_numberingRuleId`]) { + numberingFields[fieldKey] = value; + } + } + console.log("๐Ÿ“ฆ [handleSave] ์ฑ„๋ฒˆ ๊ทœ์น™ ํ•„๋“œ:", numberingFields); + + for (const key of repeatScreenModalKeys) { + const targetTable = key.replace("_repeatScreenModal_", ""); + const rows = context.formData[key] as any[]; + + if (!Array.isArray(rows) || rows.length === 0) continue; + + console.log(`๐Ÿ“ฆ [handleSave] ${targetTable} ํ…Œ์ด๋ธ” ์ €์žฅ:`, rows); + + for (const row of rows) { + const { _isNew, _targetTable, id, ...dataToSave } = row; + + // ์‚ฌ์šฉ์ž ์ •๋ณด ์ถ”๊ฐ€ + ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ’ ๋ณ‘ํ•ฉ + const dataWithMeta = { + ...dataToSave, + ...numberingFields, // ์ฑ„๋ฒˆ ๊ทœ์น™ ๊ฐ’ (shipment_plan_no ๋“ฑ) + created_by: context.userId, + updated_by: context.userId, + company_code: context.companyCode, + }; + + try { + if (_isNew) { + // INSERT + console.log(`๐Ÿ“ [handleSave] ${targetTable} INSERT:`, dataWithMeta); + const insertResult = await apiClient.post( + `/table-management/tables/${targetTable}/add`, + dataWithMeta + ); + console.log(`โœ… [handleSave] ${targetTable} INSERT ์™„๋ฃŒ:`, insertResult.data); + } else if (id) { + // UPDATE + const originalData = { id }; + const updatedData = { ...dataWithMeta, id }; + console.log(`๐Ÿ“ [handleSave] ${targetTable} UPDATE:`, { originalData, updatedData }); + const updateResult = await apiClient.put( + `/table-management/tables/${targetTable}/edit`, + { originalData, updatedData } + ); + console.log(`โœ… [handleSave] ${targetTable} UPDATE ์™„๋ฃŒ:`, updateResult.data); + } + } catch (error: any) { + console.error(`โŒ [handleSave] ${targetTable} ์ €์žฅ ์‹คํŒจ:`, error.response?.data || error.message); + // ๊ฐœ๋ณ„ ์‹คํŒจ๋Š” ์ „์ฒด ์ €์žฅ์„ ์ค‘๋‹จํ•˜์ง€ ์•Š์Œ + } + } + } + } + + // ๐Ÿ†• v3.9: RepeatScreenModal ์ง‘๊ณ„ ์ €์žฅ ์ฒ˜๋ฆฌ + const aggregationConfigs = context.formData._repeatScreenModal_aggregations as Array<{ + resultField: string; + aggregatedValue: number; + targetTable: string; + targetColumn: string; + joinKey: { sourceField: string; targetField: string }; + sourceValue: any; + }>; + + if (aggregationConfigs && aggregationConfigs.length > 0) { + console.log("๐Ÿ“Š [handleSave] ์ง‘๊ณ„ ์ €์žฅ ์‹œ์ž‘:", aggregationConfigs); + + for (const config of aggregationConfigs) { + const { targetTable, targetColumn, joinKey, aggregatedValue, sourceValue } = config; + + try { + const originalData = { [joinKey.targetField]: sourceValue }; + const updatedData = { + [targetColumn]: aggregatedValue, + [joinKey.targetField]: sourceValue, + }; + + console.log(`๐Ÿ“Š [handleSave] ${targetTable}.${targetColumn} = ${aggregatedValue} (์กฐ์ธ: ${joinKey.sourceField} = ${sourceValue})`); + + const updateResult = await apiClient.put( + `/table-management/tables/${targetTable}/edit`, + { originalData, updatedData } + ); + console.log(`โœ… [handleSave] ${targetTable} ์ง‘๊ณ„ ์ €์žฅ ์™„๋ฃŒ:`, updateResult.data); + } catch (error: any) { + console.error(`โŒ [handleSave] ${targetTable} ์ง‘๊ณ„ ์ €์žฅ ์‹คํŒจ:`, error.response?.data || error.message); + } + } + } } if (!saveResult.success) {