diff --git a/backend-node/src/controllers/fileController.ts b/backend-node/src/controllers/fileController.ts index b1e31e3b..d4e8d0cf 100644 --- a/backend-node/src/controllers/fileController.ts +++ b/backend-node/src/controllers/fileController.ts @@ -341,6 +341,64 @@ export const uploadFiles = async ( }); } + // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: ํ•ด๋‹น ํ–‰์˜ attachments ์ปฌ๋Ÿผ ์ž๋™ ์—…๋ฐ์ดํŠธ + const isRecordMode = req.body.isRecordMode === "true" || req.body.isRecordMode === true; + + // ๐Ÿ” ๋””๋ฒ„๊น…: ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ์กฐ๊ฑด ํ™•์ธ + console.log("๐Ÿ” [ํŒŒ์ผ ์—…๋กœ๋“œ] ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ์กฐ๊ฑด ํ™•์ธ:", { + isRecordMode, + linkedTable, + recordId, + columnName, + finalTargetObjid, + "req.body.isRecordMode": req.body.isRecordMode, + "req.body.linkedTable": req.body.linkedTable, + "req.body.recordId": req.body.recordId, + "req.body.columnName": req.body.columnName, + }); + + if (isRecordMode && linkedTable && recordId && columnName) { + try { + // ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ์˜ ๋ชจ๋“  ์ฒจ๋ถ€ํŒŒ์ผ ์กฐํšŒ + const allFiles = await query( + `SELECT objid, real_file_name, file_size, file_ext, file_path, regdate + FROM attach_file_info + WHERE target_objid = $1 AND status = 'ACTIVE' + ORDER BY regdate DESC`, + [finalTargetObjid] + ); + + // attachments JSONB ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ + const attachmentsJson = allFiles.map((f: any) => ({ + objid: f.objid.toString(), + realFileName: f.real_file_name, + fileSize: Number(f.file_size), + fileExt: f.file_ext, + filePath: f.file_path, + regdate: f.regdate?.toISOString(), + })); + + // ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ attachments ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: company_code ํ•„ํ„ฐ ์ถ”๊ฐ€ + await query( + `UPDATE ${linkedTable} + SET ${columnName} = $1::jsonb, updated_date = NOW() + WHERE id = $2 AND company_code = $3`, + [JSON.stringify(attachmentsJson), recordId, companyCode] + ); + + console.log("๐Ÿ“Ž [๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ] attachments ์ปฌ๋Ÿผ ์ž๋™ ์—…๋ฐ์ดํŠธ:", { + tableName: linkedTable, + recordId: recordId, + columnName: columnName, + fileCount: attachmentsJson.length, + }); + } catch (updateError) { + // attachments ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ ์‹คํŒจํ•ด๋„ ํŒŒ์ผ ์—…๋กœ๋“œ๋Š” ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ + console.warn("โš ๏ธ attachments ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ ์‹คํŒจ (๋ฌด์‹œ):", updateError); + } + } + res.json({ success: true, message: `${files.length}๊ฐœ ํŒŒ์ผ ์—…๋กœ๋“œ ์™„๋ฃŒ`, @@ -405,6 +463,56 @@ export const deleteFile = async ( ["DELETED", parseInt(objid)] ); + // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: ํ•ด๋‹น ํ–‰์˜ attachments ์ปฌ๋Ÿผ ์ž๋™ ์—…๋ฐ์ดํŠธ + const targetObjid = fileRecord.target_objid; + if (targetObjid && !targetObjid.startsWith('screen_files:') && !targetObjid.startsWith('temp_')) { + // targetObjid ํŒŒ์‹ฑ: tableName:recordId:columnName ํ˜•์‹ + const parts = targetObjid.split(':'); + if (parts.length >= 3) { + const [tableName, recordId, columnName] = parts; + + try { + // ํ•ด๋‹น ๋ ˆ์ฝ”๋“œ์˜ ๋‚จ์€ ์ฒจ๋ถ€ํŒŒ์ผ ์กฐํšŒ + const remainingFiles = await query( + `SELECT objid, real_file_name, file_size, file_ext, file_path, regdate + FROM attach_file_info + WHERE target_objid = $1 AND status = 'ACTIVE' + ORDER BY regdate DESC`, + [targetObjid] + ); + + // attachments JSONB ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ + const attachmentsJson = remainingFiles.map((f: any) => ({ + objid: f.objid.toString(), + realFileName: f.real_file_name, + fileSize: Number(f.file_size), + fileExt: f.file_ext, + filePath: f.file_path, + regdate: f.regdate?.toISOString(), + })); + + // ํ•ด๋‹น ํ…Œ์ด๋ธ”์˜ attachments ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ + // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: company_code ํ•„ํ„ฐ ์ถ”๊ฐ€ + await query( + `UPDATE ${tableName} + SET ${columnName} = $1::jsonb, updated_date = NOW() + WHERE id = $2 AND company_code = $3`, + [JSON.stringify(attachmentsJson), recordId, fileRecord.company_code] + ); + + console.log("๐Ÿ“Ž [ํŒŒ์ผ ์‚ญ์ œ] attachments ์ปฌ๋Ÿผ ์ž๋™ ์—…๋ฐ์ดํŠธ:", { + tableName, + recordId, + columnName, + remainingFiles: attachmentsJson.length, + }); + } catch (updateError) { + // attachments ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ ์‹คํŒจํ•ด๋„ ํŒŒ์ผ ์‚ญ์ œ๋Š” ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ + console.warn("โš ๏ธ attachments ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ ์‹คํŒจ (๋ฌด์‹œ):", updateError); + } + } + } + res.json({ success: true, message: "ํŒŒ์ผ์ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index 2dfe0770..66c70a77 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -2141,3 +2141,4 @@ export async function multiTableSave( client.release(); } } + diff --git a/backend-node/src/services/adminService.ts b/backend-node/src/services/adminService.ts index c6ab17c6..5ca6b392 100644 --- a/backend-node/src/services/adminService.ts +++ b/backend-node/src/services/adminService.ts @@ -19,15 +19,21 @@ export class AdminService { // menuType์— ๋”ฐ๋ฅธ WHERE ์กฐ๊ฑด ์ƒ์„ฑ const menuTypeCondition = - menuType !== undefined ? `MENU.MENU_TYPE = ${parseInt(menuType)}` : "1 = 1"; - + menuType !== undefined + ? `MENU.MENU_TYPE = ${parseInt(menuType)}` + : "1 = 1"; + // ๋ฉ”๋‰ด ๊ด€๋ฆฌ ํ™”๋ฉด์ธ์ง€ ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”์ธ์ง€ ๊ตฌ๋ถ„ // includeInactive๊ฐ€ true๊ฑฐ๋‚˜ menuType์ด undefined๋ฉด ๋ฉ”๋‰ด ๊ด€๋ฆฌ ํ™”๋ฉด const includeInactive = paramMap.includeInactive === true; const isManagementScreen = includeInactive || menuType === undefined; // ๋ฉ”๋‰ด ๊ด€๋ฆฌ ํ™”๋ฉด: ๋ชจ๋“  ์ƒํƒœ์˜ ๋ฉ”๋‰ด ํ‘œ์‹œ, ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”: active๋งŒ ํ‘œ์‹œ - const statusCondition = isManagementScreen ? "1 = 1" : "MENU.STATUS = 'active'"; - const subStatusCondition = isManagementScreen ? "1 = 1" : "MENU_SUB.STATUS = 'active'"; + const statusCondition = isManagementScreen + ? "1 = 1" + : "MENU.STATUS = 'active'"; + const subStatusCondition = isManagementScreen + ? "1 = 1" + : "MENU_SUB.STATUS = 'active'"; // 1. ๊ถŒํ•œ ๊ทธ๋ฃน ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง (์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ”์ธ ๊ฒฝ์šฐ๋งŒ) let authFilter = ""; @@ -35,7 +41,11 @@ export class AdminService { let queryParams: any[] = [userLang]; let paramIndex = 2; - if (menuType !== undefined && userType !== "SUPER_ADMIN" && !isManagementScreen) { + if ( + menuType !== undefined && + userType !== "SUPER_ADMIN" && + !isManagementScreen + ) { // ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ” + SUPER_ADMIN์ด ์•„๋‹Œ ๊ฒฝ์šฐ๋งŒ ๊ถŒํ•œ ์ฒดํฌ const userRoleGroups = await query( ` @@ -56,45 +66,45 @@ export class AdminService { ); if (userType === "COMPANY_ADMIN") { - // ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž: ์ž๊ธฐ ํšŒ์‚ฌ ๋ฉ”๋‰ด๋Š” ๋ชจ๋‘, ๊ณตํ†ต ๋ฉ”๋‰ด๋Š” ๊ถŒํ•œ ์žˆ๋Š” ๊ฒƒ๋งŒ + // ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž: ๊ถŒํ•œ ๊ทธ๋ฃน ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง ์ ์šฉ if (userRoleGroups.length > 0) { const roleObjids = userRoleGroups.map((rg: any) => rg.role_objid); - // ๋ฃจํŠธ ๋ฉ”๋‰ด: ํšŒ์‚ฌ ์ฝ”๋“œ๋งŒ ์ฒดํฌ (๊ถŒํ•œ ์ฒดํฌ X) - authFilter = `AND MENU.COMPANY_CODE IN ($${paramIndex}, '*')`; + // ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž๋„ ๊ถŒํ•œ ๊ทธ๋ฃน ์„ค์ •์— ๋”ฐ๋ผ ๋ฉ”๋‰ด ํ•„ํ„ฐ๋ง + authFilter = ` + AND MENU.COMPANY_CODE IN ($${paramIndex}, '*') + AND EXISTS ( + SELECT 1 + FROM rel_menu_auth rma + WHERE rma.menu_objid = MENU.OBJID + AND rma.auth_objid = ANY($${paramIndex + 1}) + AND rma.read_yn = 'Y' + ) + `; queryParams.push(userCompanyCode); - const companyParamIndex = paramIndex; paramIndex++; - // ํ•˜์œ„ ๋ฉ”๋‰ด: ํšŒ์‚ฌ ๋ฉ”๋‰ด๋Š” ๋ชจ๋‘, ๊ณตํ†ต ๋ฉ”๋‰ด๋Š” ๊ถŒํ•œ ์ฒดํฌ + // ํ•˜์œ„ ๋ฉ”๋‰ด๋„ ๊ถŒํ•œ ์ฒดํฌ unionFilter = ` - AND ( - MENU_SUB.COMPANY_CODE = $${companyParamIndex} - OR ( - MENU_SUB.COMPANY_CODE = '*' - AND EXISTS ( - SELECT 1 - FROM rel_menu_auth rma - WHERE rma.menu_objid = MENU_SUB.OBJID - AND rma.auth_objid = ANY($${paramIndex}) - AND rma.read_yn = 'Y' - ) - ) + AND MENU_SUB.COMPANY_CODE IN ($${paramIndex - 1}, '*') + AND EXISTS ( + SELECT 1 + FROM rel_menu_auth rma + WHERE rma.menu_objid = MENU_SUB.OBJID + AND rma.auth_objid = ANY($${paramIndex}) + AND rma.read_yn = 'Y' ) `; queryParams.push(roleObjids); paramIndex++; logger.info( - `โœ… ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž: ํšŒ์‚ฌ ${userCompanyCode} ๋ฉ”๋‰ด ์ „์ฒด + ๊ถŒํ•œ ์žˆ๋Š” ๊ณตํ†ต ๋ฉ”๋‰ด` + `โœ… ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž: ๊ถŒํ•œ ์žˆ๋Š” ๋ฉ”๋‰ด๋งŒ (${roleObjids.length}๊ฐœ ๊ทธ๋ฃน)` ); } else { - // ๊ถŒํ•œ ๊ทธ๋ฃน์ด ์—†๋Š” ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž: ์ž๊ธฐ ํšŒ์‚ฌ ๋ฉ”๋‰ด๋งŒ - authFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`; - unionFilter = `AND MENU_SUB.COMPANY_CODE = $${paramIndex}`; - queryParams.push(userCompanyCode); - paramIndex++; - logger.info( - `โœ… ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž (๊ถŒํ•œ ๊ทธ๋ฃน ์—†์Œ): ํšŒ์‚ฌ ${userCompanyCode} ๋ฉ”๋‰ด๋งŒ` + // ๊ถŒํ•œ ๊ทธ๋ฃน์ด ์—†๋Š” ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž: ๋ฉ”๋‰ด ์—†์Œ + logger.warn( + `โš ๏ธ ํšŒ์‚ฌ ๊ด€๋ฆฌ์ž ${userId}๋Š” ๊ถŒํ•œ ๊ทธ๋ฃน์ด ์—†์–ด ๋ฉ”๋‰ด๊ฐ€ ํ‘œ์‹œ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.` ); + return []; } } else { // ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž: ๊ถŒํ•œ ๊ทธ๋ฃน ํ•„์ˆ˜ @@ -131,7 +141,11 @@ export class AdminService { return []; } } - } else if (menuType !== undefined && userType === "SUPER_ADMIN" && !isManagementScreen) { + } else if ( + menuType !== undefined && + userType === "SUPER_ADMIN" && + !isManagementScreen + ) { // ์ขŒ์ธก ์‚ฌ์ด๋“œ๋ฐ” + SUPER_ADMIN: ๊ถŒํ•œ ๊ทธ๋ฃน ์ฒดํฌ ์—†์ด ๋ชจ๋“  ๊ณตํ†ต ๋ฉ”๋‰ด ํ‘œ์‹œ logger.info(`โœ… ์ตœ๊ณ  ๊ด€๋ฆฌ์ž๋Š” ๊ถŒํ•œ ๊ทธ๋ฃน ์ฒดํฌ ์—†์ด ๋ชจ๋“  ๊ณตํ†ต ๋ฉ”๋‰ด ํ‘œ์‹œ`); // unionFilter๋Š” ๋น„์›Œ๋‘  (ํ•˜์œ„ ๋ฉ”๋‰ด๋„ ๊ณตํ†ต ๋ฉ”๋‰ด๋งŒ) @@ -167,7 +181,7 @@ export class AdminService { companyFilter = `AND MENU.COMPANY_CODE = $${paramIndex}`; queryParams.push(userCompanyCode); paramIndex++; - + // ํ•˜์œ„ ๋ฉ”๋‰ด์—๋„ ํšŒ์‚ฌ ํ•„ํ„ฐ๋ง ์ ์šฉ (๊ณตํ†ต ๋ฉ”๋‰ด ์ œ์™ธ) if (unionFilter === "") { unionFilter = `AND MENU_SUB.COMPANY_CODE = $${paramIndex - 1}`; diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 77593fa1..65efcd1b 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -903,6 +903,9 @@ export class DynamicFormService { return `${key} = $${index + 1}::numeric`; } else if (dataType === "boolean") { return `${key} = $${index + 1}::boolean`; + } else if (dataType === 'jsonb' || dataType === 'json') { + // ๐Ÿ†• JSONB/JSON ํƒ€์ž…์€ ๋ช…์‹œ์  ์บ์ŠคํŒ… + return `${key} = $${index + 1}::jsonb`; } else { // ๋ฌธ์ž์—ด ํƒ€์ž…์€ ์บ์ŠคํŒ… ๋ถˆํ•„์š” return `${key} = $${index + 1}`; @@ -910,7 +913,17 @@ export class DynamicFormService { }) .join(", "); - const values: any[] = Object.values(changedFields); + // ๐Ÿ†• JSONB ํƒ€์ž… ๊ฐ’์€ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const values: any[] = Object.keys(changedFields).map((key) => { + const value = changedFields[key]; + const dataType = columnTypes[key]; + + // JSONB/JSON ํƒ€์ž…์ด๊ณ  ๋ฐฐ์—ด/๊ฐ์ฒด์ธ ๊ฒฝ์šฐ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + if ((dataType === 'jsonb' || dataType === 'json') && (Array.isArray(value) || (typeof value === 'object' && value !== null))) { + return JSON.stringify(value); + } + return value; + }); values.push(id); // WHERE ์กฐ๊ฑด์šฉ ID ์ถ”๊ฐ€ // ๐Ÿ”‘ Primary Key ํƒ€์ž…์— ๋งž๊ฒŒ ์บ์ŠคํŒ… diff --git a/backend-node/src/services/numberingRuleService.ts b/backend-node/src/services/numberingRuleService.ts index 5272547a..7ba5c47e 100644 --- a/backend-node/src/services/numberingRuleService.ts +++ b/backend-node/src/services/numberingRuleService.ts @@ -607,7 +607,9 @@ class NumberingRuleService { } const result = await pool.query(query, params); - if (result.rowCount === 0) return null; + if (result.rowCount === 0) { + return null; + } const rule = result.rows[0]; diff --git a/backend-node/src/services/screenManagementService.ts b/backend-node/src/services/screenManagementService.ts index 6628cf4c..9fc0f079 100644 --- a/backend-node/src/services/screenManagementService.ts +++ b/backend-node/src/services/screenManagementService.ts @@ -2360,30 +2360,33 @@ export class ScreenManagementService { const lockId = Buffer.from(companyCode).reduce((acc, byte) => acc + byte, 0); await client.query('SELECT pg_advisory_xact_lock($1)', [lockId]); - // ํ˜„์žฌ ์ตœ๋Œ€ ๋ฒˆํ˜ธ ์กฐํšŒ - const existingScreens = await client.query<{ screen_code: string }>( - `SELECT screen_code FROM screen_definitions - WHERE company_code = $1 AND screen_code LIKE $2 - ORDER BY screen_code DESC - LIMIT 10`, - [companyCode, `${companyCode}%`] + // ํ˜„์žฌ ์ตœ๋Œ€ ๋ฒˆํ˜ธ ์กฐํšŒ (์ˆซ์ž ์ถ”์ถœ ํ›„ ์ •๋ ฌ) + // ํŒจํ„ด: COMPANY_CODE_XXX ๋˜๋Š” COMPANY_CODEXXX + const existingScreens = await client.query<{ screen_code: string; num: number }>( + `SELECT screen_code, + COALESCE( + NULLIF( + regexp_replace(screen_code, $2, '\\1'), + screen_code + )::integer, + 0 + ) as num + FROM screen_definitions + WHERE company_code = $1 + AND screen_code ~ $2 + AND deleted_date IS NULL + ORDER BY num DESC + LIMIT 1`, + [companyCode, `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[_]?(\\d+)$`] ); let maxNumber = 0; - const pattern = new RegExp( - `^${companyCode.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:_)?(\\d+)$` - ); - - for (const screen of existingScreens.rows) { - const match = screen.screen_code.match(pattern); - if (match) { - const number = parseInt(match[1], 10); - if (number > maxNumber) { - maxNumber = number; - } - } + if (existingScreens.rows.length > 0 && existingScreens.rows[0].num) { + maxNumber = existingScreens.rows[0].num; } + console.log(`๐Ÿ”ข ํ˜„์žฌ ์ตœ๋Œ€ ํ™”๋ฉด ์ฝ”๋“œ ๋ฒˆํ˜ธ: ${companyCode} โ†’ ${maxNumber}`); + // count๊ฐœ์˜ ์ฝ”๋“œ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์ƒ์„ฑ const codes: string[] = []; for (let i = 0; i < count; i++) { diff --git a/frontend/components/admin/RoleDetailManagement.tsx b/frontend/components/admin/RoleDetailManagement.tsx index 8f3d8fbb..27a6c07d 100644 --- a/frontend/components/admin/RoleDetailManagement.tsx +++ b/frontend/components/admin/RoleDetailManagement.tsx @@ -9,6 +9,7 @@ import { useRouter } from "next/navigation"; import { AlertCircle } from "lucide-react"; import { DualListBox } from "@/components/common/DualListBox"; import { MenuPermissionsTable } from "./MenuPermissionsTable"; +import { useMenu } from "@/contexts/MenuContext"; interface RoleDetailManagementProps { roleId: string; @@ -25,6 +26,7 @@ interface RoleDetailManagementProps { export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) { const { user: currentUser } = useAuth(); const router = useRouter(); + const { refreshMenus } = useMenu(); const isSuperAdmin = currentUser?.companyCode === "*" && currentUser?.userType === "SUPER_ADMIN"; @@ -178,6 +180,9 @@ export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) { if (response.success) { alert("๋ฉค๋ฒ„๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); loadMembers(); // ์ƒˆ๋กœ๊ณ ์นจ + + // ์‚ฌ์ด๋“œ๋ฐ” ๋ฉ”๋‰ด ์ƒˆ๋กœ๊ณ ์นจ (ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์˜ํ–ฅ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ) + await refreshMenus(); } else { alert(response.message || "๋ฉค๋ฒ„ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } @@ -187,7 +192,7 @@ export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) { } finally { setIsSavingMembers(false); } - }, [roleGroup, selectedUsers, loadMembers]); + }, [roleGroup, selectedUsers, loadMembers, refreshMenus]); // ๋ฉ”๋‰ด ๊ถŒํ•œ ์ €์žฅ ํ•ธ๋“ค๋Ÿฌ const handleSavePermissions = useCallback(async () => { @@ -200,6 +205,9 @@ export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) { if (response.success) { alert("๋ฉ”๋‰ด ๊ถŒํ•œ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); loadMenuPermissions(); // ์ƒˆ๋กœ๊ณ ์นจ + + // ์‚ฌ์ด๋“œ๋ฐ” ๋ฉ”๋‰ด ์ƒˆ๋กœ๊ณ ์นจ (๊ถŒํ•œ ๋ณ€๊ฒฝ ์ฆ‰์‹œ ๋ฐ˜์˜) + await refreshMenus(); } else { alert(response.message || "๋ฉ”๋‰ด ๊ถŒํ•œ ์ €์žฅ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } @@ -209,7 +217,7 @@ export function RoleDetailManagement({ roleId }: RoleDetailManagementProps) { } finally { setIsSavingPermissions(false); } - }, [roleGroup, menuPermissions, loadMenuPermissions]); + }, [roleGroup, menuPermissions, loadMenuPermissions, refreshMenus]); if (isLoading) { return ( diff --git a/frontend/components/screen/CopyScreenModal.tsx b/frontend/components/screen/CopyScreenModal.tsx index c37603c5..f5e71c4c 100644 --- a/frontend/components/screen/CopyScreenModal.tsx +++ b/frontend/components/screen/CopyScreenModal.tsx @@ -166,18 +166,28 @@ export default function CopyScreenModal({ // linkedScreens ๋กœ๋”ฉ์ด ์™„๋ฃŒ๋˜๋ฉด ํ™”๋ฉด ์ฝ”๋“œ ์ƒ์„ฑ useEffect(() => { + // ๋ชจ๋‹ฌ ํ™”๋ฉด๋“ค์˜ ์ฝ”๋“œ๊ฐ€ ๋ชจ๋‘ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + const allModalCodesSet = linkedScreens.length === 0 || + linkedScreens.every(screen => screen.newScreenCode); + console.log("๐Ÿ” ์ฝ”๋“œ ์ƒ์„ฑ ์กฐ๊ฑด ์ฒดํฌ:", { targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreensCount: linkedScreens.length, + allModalCodesSet, }); - if (targetCompanyCode && !loadingLinkedScreens && !screenCode) { + // ์กฐ๊ฑด: ํšŒ์‚ฌ ์ฝ”๋“œ๊ฐ€ ์žˆ๊ณ , ๋กœ๋”ฉ์ด ์™„๋ฃŒ๋˜๊ณ , (๋ฉ”์ธ ์ฝ”๋“œ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ชจ๋‹ฌ ์ฝ”๋“œ๊ฐ€ ์—†์„ ๋•Œ) + const needsCodeGeneration = targetCompanyCode && + !loadingLinkedScreens && + (!screenCode || (linkedScreens.length > 0 && !allModalCodesSet)); + + if (needsCodeGeneration) { console.log("โœ… ํ™”๋ฉด ์ฝ”๋“œ ์ƒ์„ฑ ์‹œ์ž‘ (linkedScreens ๊ฐœ์ˆ˜:", linkedScreens.length, ")"); generateScreenCodes(); } - }, [targetCompanyCode, loadingLinkedScreens, screenCode]); + }, [targetCompanyCode, loadingLinkedScreens, screenCode, linkedScreens]); // ํšŒ์‚ฌ ๋ชฉ๋ก ์กฐํšŒ const loadCompanies = async () => { diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 2a3050fc..0a87db3f 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -678,12 +678,13 @@ export const EditModal: React.FC = ({ className }) => { } // ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์„ค์ •ํ•œ ํฌ๊ธฐ = ์ปจํ…์ธ  ์˜์—ญ ํฌ๊ธฐ - // ์‹ค์ œ ๋ชจ๋‹ฌ ํฌ๊ธฐ = ์ปจํ…์ธ  + ํ—ค๋” + gap + padding + // ์‹ค์ œ ๋ชจ๋‹ฌ ํฌ๊ธฐ = ์ปจํ…์ธ  + ํ—ค๋” + gap + padding + ๋ผ๋ฒจ ๊ณต๊ฐ„ const headerHeight = 52; // DialogHeader (ํƒ€์ดํ‹€ + border-b + py-3) const dialogGap = 16; // DialogContent gap-4 const extraPadding = 24; // ์ถ”๊ฐ€ ์—ฌ๋ฐฑ (์•ˆ์ „ ๋งˆ์ง„) + const labelSpace = 30; // ์ž…๋ ฅ ํ•„๋“œ ์œ„ ๋ผ๋ฒจ ๊ณต๊ฐ„ (-top-6 = 24px + ์—ฌ์œ ) - const totalHeight = screenDimensions.height + headerHeight + dialogGap + extraPadding; + const totalHeight = screenDimensions.height + headerHeight + dialogGap + extraPadding + labelSpace; return { className: "overflow-hidden p-0", @@ -729,7 +730,7 @@ export const EditModal: React.FC = ({ className }) => { className="relative bg-white" style={{ width: screenDimensions?.width || 800, - height: screenDimensions?.height || 600, + height: (screenDimensions?.height || 600) + 30, // ๋ผ๋ฒจ ๊ณต๊ฐ„ ์ถ”๊ฐ€ transformOrigin: "center center", maxWidth: "100%", maxHeight: "100%", @@ -739,13 +740,14 @@ export const EditModal: React.FC = ({ className }) => { // ์ปดํฌ๋„ŒํŠธ ์œ„์น˜๋ฅผ offset๋งŒํผ ์กฐ์ • const offsetX = screenDimensions?.offsetX || 0; const offsetY = screenDimensions?.offsetY || 0; + const labelSpace = 30; // ๋ผ๋ฒจ ๊ณต๊ฐ„ (์ž…๋ ฅ ํ•„๋“œ ์œ„ -top-6 ๋ผ๋ฒจ์šฉ) const adjustedComponent = { ...component, position: { ...component.position, x: parseFloat(component.position?.x?.toString() || "0") - offsetX, - y: parseFloat(component.position?.y?.toString() || "0") - offsetY, + y: parseFloat(component.position?.y?.toString() || "0") - offsetY + labelSpace, // ๋ผ๋ฒจ ๊ณต๊ฐ„ ์ถ”๊ฐ€ }, }; @@ -759,12 +761,27 @@ export const EditModal: React.FC = ({ className }) => { }); } + // ๐Ÿ”‘ ์ฒจ๋ถ€ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ–‰(๋ ˆ์ฝ”๋“œ) ๋‹จ์œ„๋กœ ํŒŒ์ผ์„ ์ €์žฅํ•  ์ˆ˜ ์žˆ๋„๋ก tableName ์ถ”๊ฐ€ + const enrichedFormData = { + ...(groupData.length > 0 ? groupData[0] : formData), + tableName: screenData.screenInfo?.tableName, // ํ…Œ์ด๋ธ”๋ช… ์ถ”๊ฐ€ + screenId: modalState.screenId, // ํ™”๋ฉด ID ์ถ”๊ฐ€ + }; + + // ๐Ÿ” ๋””๋ฒ„๊น…: enrichedFormData ํ™•์ธ + console.log("๐Ÿ”‘ [EditModal] enrichedFormData ์ƒ์„ฑ:", { + "screenData.screenInfo": screenData.screenInfo, + "screenData.screenInfo?.tableName": screenData.screenInfo?.tableName, + "enrichedFormData.tableName": enrichedFormData.tableName, + "enrichedFormData.id": enrichedFormData.id, + }); + return ( 0 ? groupData[0] : formData} + formData={enrichedFormData} onFormDataChange={(fieldName, value) => { // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ์ฒ˜๋ฆฌ if (groupData.length > 0) { diff --git a/frontend/lib/api/client.ts b/frontend/lib/api/client.ts index f4a3ccf7..967e43ca 100644 --- a/frontend/lib/api/client.ts +++ b/frontend/lib/api/client.ts @@ -317,6 +317,11 @@ apiClient.interceptors.response.use( return Promise.reject(error); } + // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ฏธ๋ฆฌ๋ณด๊ธฐ API ์‹คํŒจ๋Š” ์กฐ์šฉํ•˜๊ฒŒ ์ฒ˜๋ฆฌ (ํ™”๋ฉด ๋กœ๋“œ ์‹œ ์ž์ฃผ ๋ฐœ์ƒ) + if (url?.includes("/numbering-rules/") && url?.includes("/preview")) { + return Promise.reject(error); + } + // ๋‹ค๋ฅธ ์—๋Ÿฌ๋“ค์€ ๊ธฐ์กด์ฒ˜๋Ÿผ ์ƒ์„ธ ๋กœ๊ทธ ์ถœ๋ ฅ console.error("API ์‘๋‹ต ์˜ค๋ฅ˜:", { status: status, @@ -324,7 +329,6 @@ apiClient.interceptors.response.use( url: url, data: error.response?.data, message: error.message, - headers: error.config?.headers, }); // 401 ์—๋Ÿฌ ์ฒ˜๋ฆฌ diff --git a/frontend/lib/api/numberingRule.ts b/frontend/lib/api/numberingRule.ts index b531edce..551a8d25 100644 --- a/frontend/lib/api/numberingRule.ts +++ b/frontend/lib/api/numberingRule.ts @@ -109,11 +109,24 @@ export async function deleteNumberingRule(ruleId: string): Promise> { + // ruleId ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + if (!ruleId || ruleId === "undefined" || ruleId === "null") { + return { success: false, error: "์ฑ„๋ฒˆ ๊ทœ์น™ ID๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค" }; + } + try { const response = await apiClient.post(`/numbering-rules/${ruleId}/preview`); + if (!response.data) { + return { success: false, error: "์„œ๋ฒ„ ์‘๋‹ต์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค" }; + } return response.data; } catch (error: any) { - return { success: false, error: error.message || "์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ" }; + const errorMessage = + error.response?.data?.error || + error.response?.data?.message || + error.message || + "์ฝ”๋“œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์‹คํŒจ"; + return { success: false, error: errorMessage }; } } diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index 8dda7864..805fe755 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -10,6 +10,7 @@ import { apiClient } from "@/lib/api/client"; import { FileViewerModal } from "./FileViewerModal"; import { FileManagerModal } from "./FileManagerModal"; import { FileUploadConfig, FileInfo, FileUploadStatus, FileUploadResponse } from "./types"; +import { useAuth } from "@/hooks/useAuth"; import { Upload, File, @@ -92,6 +93,9 @@ const FileUploadComponent: React.FC = ({ onDragEnd, onUpdate, }) => { + // ๐Ÿ”‘ ์ธ์ฆ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ + const { user } = useAuth(); + const [uploadedFiles, setUploadedFiles] = useState([]); const [uploadStatus, setUploadStatus] = useState("idle"); const [dragOver, setDragOver] = useState(false); @@ -102,28 +106,94 @@ const FileUploadComponent: React.FC = ({ const [representativeImageUrl, setRepresentativeImageUrl] = useState(null); const fileInputRef = useRef(null); + // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ํŒ๋‹จ: formData์— id๊ฐ€ ์žˆ์œผ๋ฉด ํŠน์ • ํ–‰์— ์—ฐ๊ฒฐ๋œ ํŒŒ์ผ ๊ด€๋ฆฌ + const isRecordMode = !!(formData?.id && !String(formData.id).startsWith('temp_')); + const recordTableName = formData?.tableName || component.tableName; + const recordId = formData?.id; + // ๐Ÿ”‘ ์ปฌ๋Ÿผ๋ช… ๊ฒฐ์ •: ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ์—์„œ๋Š” ๋ฌด์กฐ๊ฑด 'attachments' ์‚ฌ์šฉ + // component.columnName์ด๋‚˜ component.id๋Š” 'ํŒŒ์ผ_์—…๋กœ๋“œ' ๊ฐ™์€ ํ•œ๊ธ€ ๋ผ๋ฒจ์ผ ์ˆ˜ ์žˆ์–ด์„œ DB ์ปฌ๋Ÿผ๋ช…์œผ๋กœ ๋ถ€์ ํ•ฉ + // ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ component.columnName ๋˜๋Š” component.id ์‚ฌ์šฉ + const columnName = isRecordMode ? 'attachments' : (component.columnName || component.id || 'attachments'); + + // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ์šฉ targetObjid ์ƒ์„ฑ + const getRecordTargetObjid = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + return `${recordTableName}:${recordId}:${columnName}`; + } + return null; + }, [isRecordMode, recordTableName, recordId, columnName]); + + // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์ƒ์„ฑ (localStorage, ์ „์—ญ ์ƒํƒœ์šฉ) + const getUniqueKey = useCallback(() => { + if (isRecordMode && recordTableName && recordId) { + // ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: ํ…Œ์ด๋ธ”๋ช…:๋ ˆ์ฝ”๋“œID:์ปดํฌ๋„ŒํŠธID ํ˜•ํƒœ๋กœ ๊ณ ์œ  ํ‚ค ์ƒ์„ฑ + return `fileUpload_${recordTableName}_${recordId}_${component.id}`; + } + // ๊ธฐ๋ณธ ๋ชจ๋“œ: ์ปดํฌ๋„ŒํŠธ ID๋งŒ ์‚ฌ์šฉ + return `fileUpload_${component.id}`; + }, [isRecordMode, recordTableName, recordId, component.id]); + + // ๐Ÿ” ๋””๋ฒ„๊น…: ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ์ƒํƒœ ๋กœ๊น… + useEffect(() => { + console.log("๐Ÿ“Ž [FileUploadComponent] ๋ชจ๋“œ ํ™•์ธ:", { + isRecordMode, + recordTableName, + recordId, + columnName, + targetObjid: getRecordTargetObjid(), + uniqueKey: getUniqueKey(), + formDataKeys: formData ? Object.keys(formData) : [], + // ๐Ÿ” ์ถ”๊ฐ€ ๋””๋ฒ„๊น…: ์–ด๋””์„œ tableName์ด ์˜ค๋Š”์ง€ ํ™•์ธ + "formData.tableName": formData?.tableName, + "component.tableName": component.tableName, + "component.columnName": component.columnName, + "component.id": component.id, + }); + }, [isRecordMode, recordTableName, recordId, columnName, getRecordTargetObjid, getUniqueKey, formData, component.tableName, component.columnName, component.id]); + + // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ID ๋ณ€๊ฒฝ ์‹œ ํŒŒ์ผ ๋ชฉ๋ก ์ดˆ๊ธฐํ™” ๋ฐ ์ƒˆ๋กœ ๋กœ๋“œ + const prevRecordIdRef = useRef(null); + useEffect(() => { + if (prevRecordIdRef.current !== recordId) { + console.log("๐Ÿ“Ž [FileUploadComponent] ๋ ˆ์ฝ”๋“œ ID ๋ณ€๊ฒฝ ๊ฐ์ง€:", { + prev: prevRecordIdRef.current, + current: recordId, + isRecordMode, + }); + prevRecordIdRef.current = recordId; + + // ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ์—์„œ ๋ ˆ์ฝ”๋“œ ID๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ํŒŒ์ผ ๋ชฉ๋ก ์ดˆ๊ธฐํ™” + if (isRecordMode) { + setUploadedFiles([]); + } + } + }, [recordId, isRecordMode]); + // ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ์ฆ‰์‹œ localStorage์—์„œ ํŒŒ์ผ ๋ณต์› useEffect(() => { if (!component?.id) return; try { - const backupKey = `fileUpload_${component.id}`; + // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ + const backupKey = getUniqueKey(); const backupFiles = localStorage.getItem(backupKey); if (backupFiles) { const parsedFiles = JSON.parse(backupFiles); if (parsedFiles.length > 0) { console.log("๐Ÿš€ ์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ํŒŒ์ผ ์ฆ‰์‹œ ๋ณต์›:", { + uniqueKey: backupKey, componentId: component.id, + recordId: recordId, restoredFiles: parsedFiles.length, files: parsedFiles.map((f: any) => ({ objid: f.objid, name: f.realFileName })), }); setUploadedFiles(parsedFiles); - // ์ „์—ญ ์ƒํƒœ์—๋„ ๋ณต์› + // ์ „์—ญ ์ƒํƒœ์—๋„ ๋ณต์› (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) if (typeof window !== "undefined") { (window as any).globalFileState = { ...(window as any).globalFileState, - [component.id]: parsedFiles, + [backupKey]: parsedFiles, }; } } @@ -131,7 +201,7 @@ const FileUploadComponent: React.FC = ({ } catch (e) { console.warn("์ปดํฌ๋„ŒํŠธ ๋งˆ์šดํŠธ ์‹œ ํŒŒ์ผ ๋ณต์› ์‹คํŒจ:", e); } - }, [component.id]); // component.id๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ์‹คํ–‰ + }, [component.id, getUniqueKey, recordId]); // ๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ๋ณ€๊ฒฝ ์‹œ ์žฌ์‹คํ–‰ // ๐ŸŽฏ ํ™”๋ฉด์„ค๊ณ„ ๋ชจ๋“œ์—์„œ ์‹ค์ œ ํ™”๋ฉด์œผ๋กœ์˜ ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ useEffect(() => { @@ -152,12 +222,14 @@ const FileUploadComponent: React.FC = ({ const newFiles = event.detail.files || []; setUploadedFiles(newFiles); - // localStorage ๋ฐฑ์—… ์—…๋ฐ์ดํŠธ + // localStorage ๋ฐฑ์—… ์—…๋ฐ์ดํŠธ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(newFiles)); console.log("๐Ÿ’พ ํ™”๋ฉด์„ค๊ณ„ ๋ชจ๋“œ ๋™๊ธฐํ™” ํ›„ localStorage ๋ฐฑ์—… ์—…๋ฐ์ดํŠธ:", { + uniqueKey: backupKey, componentId: component.id, + recordId: recordId, fileCount: newFiles.length, }); } catch (e) { @@ -201,6 +273,16 @@ const FileUploadComponent: React.FC = ({ if (!component?.id) return false; try { + // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: ํ•ด๋‹น ํ–‰์˜ ํŒŒ์ผ๋งŒ ์กฐํšŒ + if (isRecordMode && recordTableName && recordId) { + console.log("๐Ÿ“‚ [FileUploadComponent] ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ํŒŒ์ผ ์กฐํšŒ:", { + tableName: recordTableName, + recordId: recordId, + columnName: columnName, + targetObjid: getRecordTargetObjid(), + }); + } + // 1. formData์—์„œ screenId ๊ฐ€์ ธ์˜ค๊ธฐ let screenId = formData?.screenId; @@ -232,11 +314,13 @@ const FileUploadComponent: React.FC = ({ const params = { screenId, componentId: component.id, - tableName: formData?.tableName || component.tableName, - recordId: formData?.id, - columnName: component.columnName || component.id, // ๐Ÿ”‘ columnName์ด ์—†์œผ๋ฉด component.id ์‚ฌ์šฉ + tableName: recordTableName || formData?.tableName || component.tableName, + recordId: recordId || formData?.id, + columnName: columnName, // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ์—์„œ ์‚ฌ์šฉํ•˜๋Š” columnName }; + console.log("๐Ÿ“‚ [FileUploadComponent] ํŒŒ์ผ ์กฐํšŒ ํŒŒ๋ผ๋ฏธํ„ฐ:", params); + const response = await getComponentFiles(params); if (response.success) { @@ -255,11 +339,11 @@ const FileUploadComponent: React.FC = ({ })); - // ๐Ÿ”„ localStorage์˜ ๊ธฐ์กด ํŒŒ์ผ๊ณผ ์„œ๋ฒ„ ํŒŒ์ผ ๋ณ‘ํ•ฉ + // ๐Ÿ”„ localStorage์˜ ๊ธฐ์กด ํŒŒ์ผ๊ณผ ์„œ๋ฒ„ ํŒŒ์ผ ๋ณ‘ํ•ฉ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) let finalFiles = formattedFiles; + const uniqueKey = getUniqueKey(); try { - const backupKey = `fileUpload_${component.id}`; - const backupFiles = localStorage.getItem(backupKey); + const backupFiles = localStorage.getItem(uniqueKey); if (backupFiles) { const parsedBackupFiles = JSON.parse(backupFiles); @@ -268,7 +352,12 @@ const FileUploadComponent: React.FC = ({ const additionalFiles = parsedBackupFiles.filter((f: any) => !serverObjIds.has(f.objid)); finalFiles = [...formattedFiles, ...additionalFiles]; - + console.log("๐Ÿ“‚ [FileUploadComponent] ํŒŒ์ผ ๋ณ‘ํ•ฉ ์™„๋ฃŒ:", { + uniqueKey, + serverFiles: formattedFiles.length, + localFiles: parsedBackupFiles.length, + finalFiles: finalFiles.length, + }); } } catch (e) { console.warn("ํŒŒ์ผ ๋ณ‘ํ•ฉ ์ค‘ ์˜ค๋ฅ˜:", e); @@ -276,11 +365,11 @@ const FileUploadComponent: React.FC = ({ setUploadedFiles(finalFiles); - // ์ „์—ญ ์ƒํƒœ์—๋„ ์ €์žฅ + // ์ „์—ญ ์ƒํƒœ์—๋„ ์ €์žฅ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) if (typeof window !== "undefined") { (window as any).globalFileState = { ...(window as any).globalFileState, - [component.id]: finalFiles, + [uniqueKey]: finalFiles, }; // ๐ŸŒ ์ „์—ญ ํŒŒ์ผ ์ €์žฅ์†Œ์— ๋“ฑ๋ก (ํŽ˜์ด์ง€ ๊ฐ„ ๊ณต์œ ์šฉ) @@ -288,12 +377,12 @@ const FileUploadComponent: React.FC = ({ uploadPage: window.location.pathname, componentId: component.id, screenId: formData?.screenId, + recordId: recordId, }); - // localStorage ๋ฐฑ์—…๋„ ๋ณ‘ํ•ฉ๋œ ํŒŒ์ผ๋กœ ์—…๋ฐ์ดํŠธ + // localStorage ๋ฐฑ์—…๋„ ๋ณ‘ํ•ฉ๋œ ํŒŒ์ผ๋กœ ์—…๋ฐ์ดํŠธ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) try { - const backupKey = `fileUpload_${component.id}`; - localStorage.setItem(backupKey, JSON.stringify(finalFiles)); + localStorage.setItem(uniqueKey, JSON.stringify(finalFiles)); } catch (e) { console.warn("localStorage ๋ฐฑ์—… ์—…๋ฐ์ดํŠธ ์‹คํŒจ:", e); } @@ -304,7 +393,7 @@ const FileUploadComponent: React.FC = ({ console.error("ํŒŒ์ผ ์กฐํšŒ ์˜ค๋ฅ˜:", error); } return false; // ๊ธฐ์กด ๋กœ์ง ์‚ฌ์šฉ - }, [component.id, component.tableName, component.columnName, formData?.screenId, formData?.tableName, formData?.id]); + }, [component.id, component.tableName, component.columnName, formData?.screenId, formData?.tableName, formData?.id, getUniqueKey, recordId, isRecordMode, recordTableName, columnName]); // ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ ๋™๊ธฐํ™” (DB ์šฐ์„ , localStorage๋Š” ๋ณด์กฐ) useEffect(() => { @@ -316,6 +405,8 @@ const FileUploadComponent: React.FC = ({ componentFiles: componentFiles.length, formData: formData, screenId: formData?.screenId, + tableName: formData?.tableName, // ๐Ÿ” ํ…Œ์ด๋ธ”๋ช… ํ™•์ธ + recordId: formData?.id, // ๐Ÿ” ๋ ˆ์ฝ”๋“œ ID ํ™•์ธ currentUploadedFiles: uploadedFiles.length, }); @@ -371,9 +462,9 @@ const FileUploadComponent: React.FC = ({ setUploadedFiles(files); setForceUpdate((prev) => prev + 1); - // localStorage ๋ฐฑ์—…๋„ ์—…๋ฐ์ดํŠธ + // localStorage ๋ฐฑ์—…๋„ ์—…๋ฐ์ดํŠธ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(files)); } catch (e) { console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); @@ -462,10 +553,10 @@ const FileUploadComponent: React.FC = ({ toast.loading("ํŒŒ์ผ์„ ์—…๋กœ๋“œํ•˜๋Š” ์ค‘...", { id: "file-upload" }); try { - // targetObjid ์ƒ์„ฑ - ํ…œํ”Œ๋ฆฟ vs ๋ฐ์ดํ„ฐ ํŒŒ์ผ ๊ตฌ๋ถ„ - const tableName = formData?.tableName || component.tableName || "default_table"; - const recordId = formData?.id; - const columnName = component.columnName || component.id; + // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ์šฐ์„  ์‚ฌ์šฉ + const effectiveTableName = recordTableName || formData?.tableName || component.tableName || "default_table"; + const effectiveRecordId = recordId || formData?.id; + const effectiveColumnName = columnName; // screenId ์ถ”์ถœ (์šฐ์„ ์ˆœ์œ„: formData > URL) let screenId = formData?.screenId; @@ -478,39 +569,56 @@ const FileUploadComponent: React.FC = ({ } let targetObjid; - // ์šฐ์„ ์ˆœ์œ„: 1) ์‹ค์ œ ๋ฐ์ดํ„ฐ (recordId๊ฐ€ ์ˆซ์ž/๋ฌธ์ž์—ด์ด๊ณ  temp_๊ฐ€ ์•„๋‹˜) > 2) ํ…œํ”Œ๋ฆฟ (screenId) > 3) ๊ธฐ๋ณธ๊ฐ’ - const isRealRecord = recordId && typeof recordId !== 'undefined' && !String(recordId).startsWith('temp_'); + // ๐Ÿ”‘ ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ํŒ๋‹จ ๊ฐœ์„  + const effectiveIsRecordMode = isRecordMode || (effectiveRecordId && !String(effectiveRecordId).startsWith('temp_')); - if (isRealRecord && tableName) { - // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํŒŒ์ผ (์ง„์งœ ๋ ˆ์ฝ”๋“œ ID๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ) - targetObjid = `${tableName}:${recordId}:${columnName}`; - console.log("๐Ÿ“ ์‹ค์ œ ๋ฐ์ดํ„ฐ ํŒŒ์ผ ์—…๋กœ๋“œ:", targetObjid); + if (effectiveIsRecordMode && effectiveTableName && effectiveRecordId) { + // ๐ŸŽฏ ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: ํŠน์ • ํ–‰์— ํŒŒ์ผ ์—ฐ๊ฒฐ + targetObjid = `${effectiveTableName}:${effectiveRecordId}:${effectiveColumnName}`; + console.log("๐Ÿ“ [๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ] ํŒŒ์ผ ์—…๋กœ๋“œ:", { + targetObjid, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + }); } else if (screenId) { // ๐Ÿ”‘ ํ…œํ”Œ๋ฆฟ ํŒŒ์ผ (๋ฐฑ์—”๋“œ ์กฐํšŒ ํ˜•์‹๊ณผ ๋™์ผํ•˜๊ฒŒ) - targetObjid = `screen_files:${screenId}:${component.id}:${columnName}`; + targetObjid = `screen_files:${screenId}:${component.id}:${effectiveColumnName}`; + console.log("๐Ÿ“ [ํ…œํ”Œ๋ฆฟ ๋ชจ๋“œ] ํŒŒ์ผ ์—…๋กœ๋“œ:", targetObjid); } else { // ๊ธฐ๋ณธ๊ฐ’ (ํ™”๋ฉด๊ด€๋ฆฌ์—์„œ ์‚ฌ์šฉ) targetObjid = `temp_${component.id}`; - console.log("๐Ÿ“ ๊ธฐ๋ณธ ํŒŒ์ผ ์—…๋กœ๋“œ:", targetObjid); + console.log("๐Ÿ“ [๊ธฐ๋ณธ ๋ชจ๋“œ] ํŒŒ์ผ ์—…๋กœ๋“œ:", targetObjid); } // ๐Ÿ”’ ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ํšŒ์‚ฌ ์ฝ”๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ๊ฒฉ๋ฆฌ) - const userCompanyCode = (window as any).__user__?.companyCode; + const userCompanyCode = user?.companyCode || (window as any).__user__?.companyCode; + + console.log("๐Ÿ“ค [FileUploadComponent] ํŒŒ์ผ ์—…๋กœ๋“œ ์ค€๋น„:", { + userCompanyCode, + isRecordMode: effectiveIsRecordMode, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + targetObjid, + }); const uploadData = { // ๐ŸŽฏ formData์—์„œ ๋ฐฑ์—”๋“œ API ์„ค์ • ๊ฐ€์ ธ์˜ค๊ธฐ autoLink: formData?.autoLink || true, - linkedTable: formData?.linkedTable || tableName, - recordId: formData?.recordId || recordId || `temp_${component.id}`, - columnName: formData?.columnName || columnName, + linkedTable: formData?.linkedTable || effectiveTableName, + recordId: effectiveRecordId || `temp_${component.id}`, + columnName: effectiveColumnName, isVirtualFileColumn: formData?.isVirtualFileColumn || true, docType: component.fileConfig?.docType || "DOCUMENT", docTypeName: component.fileConfig?.docTypeName || "์ผ๋ฐ˜ ๋ฌธ์„œ", companyCode: userCompanyCode, // ๐Ÿ”’ ๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ: ํšŒ์‚ฌ ์ฝ”๋“œ ๋ช…์‹œ์  ์ „๋‹ฌ // ํ˜ธํ™˜์„ฑ์„ ์œ„ํ•œ ๊ธฐ์กด ํ•„๋“œ๋“ค - tableName: tableName, - fieldName: columnName, + tableName: effectiveTableName, + fieldName: effectiveColumnName, targetObjid: targetObjid, // InteractiveDataTable ํ˜ธํ™˜์„ ์œ„ํ•œ targetObjid ์ถ”๊ฐ€ + // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ ํ”Œ๋ž˜๊ทธ + isRecordMode: effectiveIsRecordMode, }; @@ -553,9 +661,9 @@ const FileUploadComponent: React.FC = ({ setUploadedFiles(updatedFiles); setUploadStatus("success"); - // localStorage ๋ฐฑ์—… + // localStorage ๋ฐฑ์—… (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); } catch (e) { console.warn("localStorage ๋ฐฑ์—… ์‹คํŒจ:", e); @@ -563,9 +671,10 @@ const FileUploadComponent: React.FC = ({ // ์ „์—ญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ (๋ชจ๋“  ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ ๋™๊ธฐํ™”) if (typeof window !== "undefined") { - // ์ „์—ญ ํŒŒ์ผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + // ์ „์—ญ ํŒŒ์ผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) const globalFileState = (window as any).globalFileState || {}; - globalFileState[component.id] = updatedFiles; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; (window as any).globalFileState = globalFileState; // ๐ŸŒ ์ „์—ญ ํŒŒ์ผ ์ €์žฅ์†Œ์— ์ƒˆ ํŒŒ์ผ ๋“ฑ๋ก (ํŽ˜์ด์ง€ ๊ฐ„ ๊ณต์œ ์šฉ) @@ -573,12 +682,15 @@ const FileUploadComponent: React.FC = ({ uploadPage: window.location.pathname, componentId: component.id, screenId: formData?.screenId, + recordId: recordId, // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ID ์ถ”๊ฐ€ }); // ๋ชจ๋“  ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ์— ๋™๊ธฐํ™” ์ด๋ฒคํŠธ ๋ฐœ์ƒ const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: component.id, + uniqueKey: uniqueKey, // ๐Ÿ†• ๊ณ ์œ  ํ‚ค ์ถ”๊ฐ€ + recordId: recordId, // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ID ์ถ”๊ฐ€ files: updatedFiles, fileCount: updatedFiles.length, timestamp: Date.now(), @@ -612,22 +724,54 @@ const FileUploadComponent: React.FC = ({ console.warn("โš ๏ธ onUpdate ์ฝœ๋ฐฑ์ด ์—†์Šต๋‹ˆ๋‹ค!"); } + // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: attachments ์ปฌ๋Ÿผ ๋™๊ธฐํ™” (formData ์—…๋ฐ์ดํŠธ) + if (effectiveIsRecordMode && onFormDataChange) { + // ํŒŒ์ผ ์ •๋ณด๋ฅผ ๊ฐ„์†Œํ™”ํ•˜์—ฌ attachments ์ปฌ๋Ÿผ์— ์ €์žฅํ•  ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ + const attachmentsData = updatedFiles.map(file => ({ + objid: file.objid, + realFileName: file.realFileName, + fileSize: file.fileSize, + fileExt: file.fileExt, + filePath: file.filePath, + regdate: file.regdate || new Date().toISOString(), + })); + + console.log("๐Ÿ“Ž [๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ] attachments ์ปฌ๋Ÿผ ๋™๊ธฐํ™”:", { + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + fileCount: attachmentsData.length, + }); + + // onFormDataChange๋ฅผ ํ†ตํ•ด ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์— attachments ์—…๋ฐ์ดํŠธ ์•Œ๋ฆผ + onFormDataChange({ + [effectiveColumnName]: attachmentsData, + // ๐Ÿ†• ๋ฐฑ์—”๋“œ์—์„œ attachments ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ๋ฅผ ์œ„ํ•œ ๋ฉ”ํƒ€ ์ •๋ณด + __attachmentsUpdate: { + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, + files: attachmentsData, + } + }); + } + // ๊ทธ๋ฆฌ๋“œ ํŒŒ์ผ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ if (typeof window !== "undefined") { const refreshEvent = new CustomEvent("refreshFileStatus", { detail: { - tableName: tableName, - recordId: recordId, - columnName: columnName, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, targetObjid: targetObjid, fileCount: updatedFiles.length, }, }); window.dispatchEvent(refreshEvent); console.log("๐Ÿ”„ ๊ทธ๋ฆฌ๋“œ ํŒŒ์ผ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ:", { - tableName, - recordId, - columnName, + tableName: effectiveTableName, + recordId: effectiveRecordId, + columnName: effectiveColumnName, targetObjid, fileCount: updatedFiles.length, }); @@ -705,9 +849,9 @@ const FileUploadComponent: React.FC = ({ const updatedFiles = uploadedFiles.filter((f) => f.objid !== fileId); setUploadedFiles(updatedFiles); - // localStorage ๋ฐฑ์—… ์—…๋ฐ์ดํŠธ + // localStorage ๋ฐฑ์—… ์—…๋ฐ์ดํŠธ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) try { - const backupKey = `fileUpload_${component.id}`; + const backupKey = getUniqueKey(); localStorage.setItem(backupKey, JSON.stringify(updatedFiles)); } catch (e) { console.warn("localStorage ๋ฐฑ์—… ์—…๋ฐ์ดํŠธ ์‹คํŒจ:", e); @@ -715,15 +859,18 @@ const FileUploadComponent: React.FC = ({ // ์ „์—ญ ์ƒํƒœ ์—…๋ฐ์ดํŠธ (๋ชจ๋“  ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ ๋™๊ธฐํ™”) if (typeof window !== "undefined") { - // ์ „์—ญ ํŒŒ์ผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ + // ์ „์—ญ ํŒŒ์ผ ์ƒํƒœ ์—…๋ฐ์ดํŠธ (๋ ˆ์ฝ”๋“œ๋ณ„ ๊ณ ์œ  ํ‚ค ์‚ฌ์šฉ) const globalFileState = (window as any).globalFileState || {}; - globalFileState[component.id] = updatedFiles; + const uniqueKey = getUniqueKey(); + globalFileState[uniqueKey] = updatedFiles; (window as any).globalFileState = globalFileState; // ๋ชจ๋“  ํŒŒ์ผ ์ปดํฌ๋„ŒํŠธ์— ๋™๊ธฐํ™” ์ด๋ฒคํŠธ ๋ฐœ์ƒ const syncEvent = new CustomEvent("globalFileStateChanged", { detail: { componentId: component.id, + uniqueKey: uniqueKey, // ๐Ÿ†• ๊ณ ์œ  ํ‚ค ์ถ”๊ฐ€ + recordId: recordId, // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ID ์ถ”๊ฐ€ files: updatedFiles, fileCount: updatedFiles.length, timestamp: Date.now(), @@ -749,13 +896,42 @@ const FileUploadComponent: React.FC = ({ }); } + // ๐Ÿ†• ๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ: attachments ์ปฌ๋Ÿผ ๋™๊ธฐํ™” (ํŒŒ์ผ ์‚ญ์ œ ํ›„) + if (isRecordMode && onFormDataChange && recordTableName && recordId) { + const attachmentsData = updatedFiles.map(f => ({ + objid: f.objid, + realFileName: f.realFileName, + fileSize: f.fileSize, + fileExt: f.fileExt, + filePath: f.filePath, + regdate: f.regdate || new Date().toISOString(), + })); + + console.log("๐Ÿ“Ž [๋ ˆ์ฝ”๋“œ ๋ชจ๋“œ] ํŒŒ์ผ ์‚ญ์ œ ํ›„ attachments ๋™๊ธฐํ™”:", { + tableName: recordTableName, + recordId: recordId, + columnName: columnName, + remainingFiles: attachmentsData.length, + }); + + onFormDataChange({ + [columnName]: attachmentsData, + __attachmentsUpdate: { + tableName: recordTableName, + recordId: recordId, + columnName: columnName, + files: attachmentsData, + } + }); + } + toast.success(`${fileName} ์‚ญ์ œ ์™„๋ฃŒ`); } catch (error) { console.error("ํŒŒ์ผ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); toast.error("ํŒŒ์ผ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } }, - [uploadedFiles, onUpdate, component.id], + [uploadedFiles, onUpdate, component.id, isRecordMode, onFormDataChange, recordTableName, recordId, columnName, getUniqueKey], ); // ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ Blob URL ๋กœ๋“œ diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx index 6e0432d1..92eb4bb7 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableComponent.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { Plus } from "lucide-react"; import { ItemSelectionModal } from "./ItemSelectionModal"; import { RepeaterTable } from "./RepeaterTable"; -import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig, JoinCondition, DynamicDataSourceOption } from "./types"; import { useCalculation } from "./useCalculation"; import { cn } from "@/lib/utils"; import { apiClient } from "@/lib/api/client"; @@ -293,6 +293,9 @@ export function ModalRepeaterTableComponent({ // ๐Ÿ†• ์ˆ˜์ฃผ์ผ ์ผ๊ด„ ์ ์šฉ ํ”Œ๋ž˜๊ทธ (๋”ฑ ํ•œ ๋ฒˆ๋งŒ ์‹คํ–‰) const [isOrderDateApplied, setIsOrderDateApplied] = useState(false); + + // ๐Ÿ†• ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ํ™œ์„ฑํ™” ์ƒํƒœ (์ปฌ๋Ÿผ๋ณ„๋กœ ํ˜„์žฌ ์„ ํƒ๋œ ์˜ต์…˜ ID) + const [activeDataSources, setActiveDataSources] = useState>({}); // columns๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด sourceColumns๋กœ๋ถ€ํ„ฐ ์ž๋™ ์ƒ์„ฑ const columns = React.useMemo((): RepeaterColumnConfig[] => { @@ -409,6 +412,193 @@ export function ModalRepeaterTableComponent({ }, [localValue, columnName, component?.id, onFormDataChange, targetTable]); const { calculateRow, calculateAll } = useCalculation(calculationRules); + + /** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ณ€๊ฒฝ ์‹œ ํ˜ธ์ถœ + * ํ•ด๋‹น ์ปฌ๋Ÿผ์˜ ๋ชจ๋“  ํ–‰ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กœ์šด ์†Œ์Šค์—์„œ ๋‹ค์‹œ ์กฐํšŒ + */ + const handleDataSourceChange = async (columnField: string, optionId: string) => { + console.log(`๐Ÿ”„ ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ณ€๊ฒฝ: ${columnField} โ†’ ${optionId}`); + + // ํ™œ์„ฑํ™” ์ƒํƒœ ์—…๋ฐ์ดํŠธ + setActiveDataSources((prev) => ({ + ...prev, + [columnField]: optionId, + })); + + // ํ•ด๋‹น ์ปฌ๋Ÿผ ์ฐพ๊ธฐ + const column = columns.find((col) => col.field === columnField); + if (!column?.dynamicDataSource?.enabled) { + console.warn(`โš ๏ธ ์ปฌ๋Ÿผ "${columnField}"์— ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ`); + return; + } + + // ์„ ํƒ๋œ ์˜ต์…˜ ์ฐพ๊ธฐ + const option = column.dynamicDataSource.options.find((opt) => opt.id === optionId); + if (!option) { + console.warn(`โš ๏ธ ์˜ต์…˜ "${optionId}"์„ ์ฐพ์„ ์ˆ˜ ์—†์Œ`); + return; + } + + // ๋ชจ๋“  ํ–‰์— ๋Œ€ํ•ด ์ƒˆ ๊ฐ’ ์กฐํšŒ + const updatedData = await Promise.all( + localValue.map(async (row, index) => { + try { + const newValue = await fetchDynamicValue(option, row); + console.log(` โœ… ํ–‰ ${index}: ${columnField} = ${newValue}`); + return { + ...row, + [columnField]: newValue, + }; + } catch (error) { + console.error(` โŒ ํ–‰ ${index} ์กฐํšŒ ์‹คํŒจ:`, error); + return row; + } + }) + ); + + // ๊ณ„์‚ฐ ํ•„๋“œ ์—…๋ฐ์ดํŠธ ํ›„ ๋ฐ์ดํ„ฐ ๋ฐ˜์˜ + const calculatedData = calculateAll(updatedData); + handleChange(calculatedData); + }; + + /** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์˜ต์…˜์— ๋”ฐ๋ผ ๊ฐ’ ์กฐํšŒ + */ + async function fetchDynamicValue( + option: DynamicDataSourceOption, + rowData: any + ): Promise { + if (option.sourceType === "table" && option.tableConfig) { + // ํ…Œ์ด๋ธ” ์ง์ ‘ ์กฐํšŒ (๋‹จ์ˆœ ์กฐ์ธ) + const { tableName, valueColumn, joinConditions } = option.tableConfig; + + const whereConditions: Record = {}; + for (const cond of joinConditions) { + const value = rowData[cond.sourceField]; + if (value === undefined || value === null) { + console.warn(`โš ๏ธ ์กฐ์ธ ์กฐ๊ฑด์˜ ์†Œ์Šค ํ•„๋“œ "${cond.sourceField}" ๊ฐ’์ด ์—†์Œ`); + return undefined; + } + whereConditions[cond.targetField] = value; + } + + console.log(`๐Ÿ” ํ…Œ์ด๋ธ” ์กฐํšŒ: ${tableName}`, whereConditions); + + const response = await apiClient.post( + `/table-management/tables/${tableName}/data`, + { search: whereConditions, size: 1, page: 1 } + ); + + if (response.data.success && response.data.data?.data?.length > 0) { + return response.data.data.data[0][valueColumn]; + } + return undefined; + + } else if (option.sourceType === "multiTable" && option.multiTableConfig) { + // ํ…Œ์ด๋ธ” ๋ณตํ•ฉ ์กฐ์ธ (2๊ฐœ ์ด์ƒ ํ…Œ์ด๋ธ” ์ˆœ์ฐจ ์กฐ์ธ) + const { joinChain, valueColumn } = option.multiTableConfig; + + if (!joinChain || joinChain.length === 0) { + console.warn("โš ๏ธ ์กฐ์ธ ์ฒด์ธ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค."); + return undefined; + } + + console.log(`๐Ÿ”— ๋ณตํ•ฉ ์กฐ์ธ ์‹œ์ž‘: ${joinChain.length}๋‹จ๊ณ„`); + + // ํ˜„์žฌ ๊ฐ’์„ ์ถ”์  (์ฒซ ๋‹จ๊ณ„๋Š” ํ˜„์žฌ ํ–‰์—์„œ ์‹œ์ž‘) + let currentValue: any = null; + let currentRow: any = null; + + for (let i = 0; i < joinChain.length; i++) { + const step = joinChain[i]; + const { tableName, joinCondition, outputField } = step; + + // ์กฐ์ธ ์กฐ๊ฑด ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + let fromValue: any; + if (i === 0) { + // ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„: ํ˜„์žฌ ํ–‰์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + fromValue = rowData[joinCondition.fromField]; + console.log(` ๐Ÿ“ ๋‹จ๊ณ„ ${i + 1}: ํ˜„์žฌํ–‰.${joinCondition.fromField} = ${fromValue}`); + } else { + // ์ดํ›„ ๋‹จ๊ณ„: ์ด์ „ ์กฐํšŒ ๊ฒฐ๊ณผ์—์„œ ๊ฐ’ ๊ฐ€์ ธ์˜ค๊ธฐ + fromValue = currentRow?.[joinCondition.fromField] || currentValue; + console.log(` ๐Ÿ“ ๋‹จ๊ณ„ ${i + 1}: ์ด์ „๊ฒฐ๊ณผ.${joinCondition.fromField} = ${fromValue}`); + } + + if (fromValue === undefined || fromValue === null) { + console.warn(`โš ๏ธ ๋‹จ๊ณ„ ${i + 1}: ์กฐ์ธ ์กฐ๊ฑด ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค. (${joinCondition.fromField})`); + return undefined; + } + + // ํ…Œ์ด๋ธ” ์กฐํšŒ + const whereConditions: Record = { + [joinCondition.toField]: fromValue + }; + + console.log(` ๐Ÿ” ๋‹จ๊ณ„ ${i + 1}: ${tableName} ์กฐํšŒ`, whereConditions); + + try { + const response = await apiClient.post( + `/table-management/tables/${tableName}/data`, + { search: whereConditions, size: 1, page: 1 } + ); + + if (response.data.success && response.data.data?.data?.length > 0) { + currentRow = response.data.data.data[0]; + currentValue = outputField ? currentRow[outputField] : currentRow; + console.log(` โœ… ๋‹จ๊ณ„ ${i + 1} ์„ฑ๊ณต:`, { outputField, value: currentValue }); + } else { + console.warn(` โš ๏ธ ๋‹จ๊ณ„ ${i + 1}: ์กฐํšŒ ๊ฒฐ๊ณผ ์—†์Œ`); + return undefined; + } + } catch (error) { + console.error(` โŒ ๋‹จ๊ณ„ ${i + 1} ์กฐํšŒ ์‹คํŒจ:`, error); + return undefined; + } + } + + // ์ตœ์ข… ๊ฐ’ ๋ฐ˜ํ™˜ (๋งˆ์ง€๋ง‰ ํ…Œ์ด๋ธ”์—์„œ valueColumn ๊ฐ€์ ธ์˜ค๊ธฐ) + const finalValue = currentRow?.[valueColumn]; + console.log(`๐ŸŽฏ ๋ณตํ•ฉ ์กฐ์ธ ์™„๋ฃŒ: ${valueColumn} = ${finalValue}`); + return finalValue; + + } else if (option.sourceType === "api" && option.apiConfig) { + // ์ „์šฉ API ํ˜ธ์ถœ (๋ณต์žกํ•œ ๋‹ค์ค‘ ์กฐ์ธ) + const { endpoint, method = "GET", parameterMappings, responseValueField } = option.apiConfig; + + // ํŒŒ๋ผ๋ฏธํ„ฐ ๋นŒ๋“œ + const params: Record = {}; + for (const mapping of parameterMappings) { + const value = rowData[mapping.sourceField]; + if (value !== undefined && value !== null) { + params[mapping.paramName] = value; + } + } + + console.log(`๐Ÿ” API ํ˜ธ์ถœ: ${method} ${endpoint}`, params); + + let response; + if (method === "POST") { + response = await apiClient.post(endpoint, params); + } else { + response = await apiClient.get(endpoint, { params }); + } + + if (response.data.success && response.data.data) { + // responseValueField๋กœ ๊ฐ’ ์ถ”์ถœ (์ค‘์ฒฉ ๊ฒฝ๋กœ ์ง€์›: "data.price") + const keys = responseValueField.split("."); + let value = response.data.data; + for (const key of keys) { + value = value?.[key]; + } + return value; + } + return undefined; + } + + return undefined; + } // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ์— ๊ณ„์‚ฐ ํ•„๋“œ ์ ์šฉ useEffect(() => { @@ -579,6 +769,8 @@ export function ModalRepeaterTableComponent({ onDataChange={handleChange} onRowChange={handleRowChange} onRowDelete={handleRowDelete} + activeDataSources={activeDataSources} + onDataSourceChange={handleDataSourceChange} /> {/* ํ•ญ๋ชฉ ์„ ํƒ ๋ชจ๋‹ฌ */} diff --git a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx index 507ab54d..7a11bdb1 100644 --- a/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/ModalRepeaterTableConfigPanel.tsx @@ -9,7 +9,8 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Plus, X, Check, ChevronsUpDown } from "lucide-react"; -import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule } from "./types"; +import { ModalRepeaterTableProps, RepeaterColumnConfig, ColumnMapping, CalculationRule, DynamicDataSourceConfig, DynamicDataSourceOption, MultiTableJoinStep } from "./types"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { cn } from "@/lib/utils"; @@ -169,6 +170,10 @@ export function ModalRepeaterTableConfigPanel({ const [openTableCombo, setOpenTableCombo] = useState(false); const [openTargetTableCombo, setOpenTargetTableCombo] = useState(false); const [openUniqueFieldCombo, setOpenUniqueFieldCombo] = useState(false); + + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • ๋ชจ๋‹ฌ + const [dynamicSourceModalOpen, setDynamicSourceModalOpen] = useState(false); + const [editingDynamicSourceColumnIndex, setEditingDynamicSourceColumnIndex] = useState(null); // config ๋ณ€๊ฒฝ ์‹œ localConfig ๋™๊ธฐํ™” (cleanupInitialConfig ์ ์šฉ) useEffect(() => { @@ -397,6 +402,101 @@ export function ModalRepeaterTableConfigPanel({ updateConfig({ calculationRules: rules }); }; + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • ํ•จ์ˆ˜๋“ค + const openDynamicSourceModal = (columnIndex: number) => { + setEditingDynamicSourceColumnIndex(columnIndex); + setDynamicSourceModalOpen(true); + }; + + const toggleDynamicDataSource = (columnIndex: number, enabled: boolean) => { + const columns = [...(localConfig.columns || [])]; + if (enabled) { + columns[columnIndex] = { + ...columns[columnIndex], + dynamicDataSource: { + enabled: true, + options: [], + }, + }; + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { dynamicDataSource, ...rest } = columns[columnIndex]; + columns[columnIndex] = rest; + } + updateConfig({ columns }); + }; + + const addDynamicSourceOption = (columnIndex: number) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + const newOption: DynamicDataSourceOption = { + id: `option_${Date.now()}`, + label: "์ƒˆ ์˜ต์…˜", + sourceType: "table", + tableConfig: { + tableName: "", + valueColumn: "", + joinConditions: [], + }, + }; + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + enabled: true, + options: [...(col.dynamicDataSource?.options || []), newOption], + }, + }; + updateConfig({ columns }); + }; + + const updateDynamicSourceOption = (columnIndex: number, optionIndex: number, updates: Partial) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + const options = [...(col.dynamicDataSource?.options || [])]; + options[optionIndex] = { ...options[optionIndex], ...updates }; + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + options, + }, + }; + updateConfig({ columns }); + }; + + const removeDynamicSourceOption = (columnIndex: number, optionIndex: number) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + const options = [...(col.dynamicDataSource?.options || [])]; + options.splice(optionIndex, 1); + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + options, + }, + }; + updateConfig({ columns }); + }; + + const setDefaultDynamicSourceOption = (columnIndex: number, optionId: string) => { + const columns = [...(localConfig.columns || [])]; + const col = columns[columnIndex]; + + columns[columnIndex] = { + ...col, + dynamicDataSource: { + ...col.dynamicDataSource!, + defaultOptionId: optionId, + }, + }; + updateConfig({ columns }); + }; + return (
{/* ์†Œ์Šค/์ €์žฅ ํ…Œ์ด๋ธ” ์„ค์ • */} @@ -1327,6 +1427,60 @@ export function ModalRepeaterTableConfigPanel({ )}
+ + {/* 6. ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • */} +
+
+ + toggleDynamicDataSource(index, checked)} + /> +
+

+ ์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ์œผ๋กœ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ „ํ™˜ (์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€, ํ’ˆ๋ชฉ๋ณ„ ๋‹จ๊ฐ€) +

+ + {col.dynamicDataSource?.enabled && ( +
+
+ + {col.dynamicDataSource.options.length}๊ฐœ ์˜ต์…˜ ์„ค์ •๋จ + + +
+ + {/* ์˜ต์…˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {col.dynamicDataSource.options.length > 0 && ( +
+ {col.dynamicDataSource.options.map((opt) => ( + + {opt.label} + {col.dynamicDataSource?.defaultOptionId === opt.id && " (๊ธฐ๋ณธ)"} + + ))} +
+ )} +
+ )} +
))} @@ -1493,6 +1647,650 @@ export function ModalRepeaterTableConfigPanel({ + + {/* ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • ๋ชจ๋‹ฌ */} + + + + + ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + {editingDynamicSourceColumnIndex !== null && localConfig.columns?.[editingDynamicSourceColumnIndex] && ( + + ({localConfig.columns[editingDynamicSourceColumnIndex].label}) + + )} + + + ์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ ์‹œ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค ์˜ต์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค + + + + {editingDynamicSourceColumnIndex !== null && localConfig.columns?.[editingDynamicSourceColumnIndex] && ( +
+ {/* ์˜ต์…˜ ๋ชฉ๋ก */} +
+ {(localConfig.columns[editingDynamicSourceColumnIndex].dynamicDataSource?.options || []).map((option, optIndex) => ( +
+
+
+ ์˜ต์…˜ {optIndex + 1} + {localConfig.columns![editingDynamicSourceColumnIndex].dynamicDataSource?.defaultOptionId === option.id && ( + ๊ธฐ๋ณธ + )} +
+
+ {localConfig.columns![editingDynamicSourceColumnIndex].dynamicDataSource?.defaultOptionId !== option.id && ( + + )} + +
+
+ + {/* ์˜ต์…˜ ๋ผ๋ฒจ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { label: e.target.value })} + placeholder="์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€" + className="h-8 text-xs" + /> +
+ + {/* ์†Œ์Šค ํƒ€์ž… */} +
+ + +
+ + {/* ํ…Œ์ด๋ธ” ์ง์ ‘ ์กฐํšŒ ์„ค์ • */} + {option.sourceType === "table" && ( +
+

ํ…Œ์ด๋ธ” ์กฐํšŒ ์„ค์ •

+ + {/* ์ฐธ์กฐ ํ…Œ์ด๋ธ” */} +
+ + +
+ + {/* ๊ฐ’ ์ปฌ๋Ÿผ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + tableConfig: { ...option.tableConfig!, valueColumn: value }, + })} + /> +
+ + {/* ์กฐ์ธ ์กฐ๊ฑด */} +
+
+ + +
+ + {(option.tableConfig?.joinConditions || []).map((cond, condIndex) => ( +
+ + = + { + const newConditions = [...(option.tableConfig?.joinConditions || [])]; + newConditions[condIndex] = { ...newConditions[condIndex], targetField: value }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + tableConfig: { ...option.tableConfig!, joinConditions: newConditions }, + }); + }} + /> + +
+ ))} +
+
+ )} + + {/* ํ…Œ์ด๋ธ” ๋ณตํ•ฉ ์กฐ์ธ ์„ค์ • (2๊ฐœ ์ด์ƒ ํ…Œ์ด๋ธ”) */} + {option.sourceType === "multiTable" && ( +
+
+

๋ณตํ•ฉ ์กฐ์ธ ์„ค์ •

+

+ ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์กฐ์ธํ•ฉ๋‹ˆ๋‹ค +

+
+ + {/* ์กฐ์ธ ์ฒด์ธ */} +
+
+ + +
+ + {/* ์‹œ์ž‘์  ์•ˆ๋‚ด */} +
+

์‹œ์ž‘: ํ˜„์žฌ ํ–‰ ๋ฐ์ดํ„ฐ

+

+ ์ฒซ ๋ฒˆ์งธ ์กฐ์ธ์€ ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค +

+
+ + {/* ์กฐ์ธ ๋‹จ๊ณ„๋“ค */} + {(option.multiTableConfig?.joinChain || []).map((step, stepIndex) => ( +
+
+
+
+ {stepIndex + 1} +
+ ์กฐ์ธ ๋‹จ๊ณ„ {stepIndex + 1} +
+ +
+ + {/* ์กฐ์ธํ•  ํ…Œ์ด๋ธ” */} +
+ + +
+ + {/* ์กฐ์ธ ์กฐ๊ฑด */} +
+
+ + {stepIndex === 0 ? ( + + ) : ( + { + const newChain = [...(option.multiTableConfig?.joinChain || [])]; + newChain[stepIndex] = { + ...newChain[stepIndex], + joinCondition: { ...newChain[stepIndex].joinCondition, fromField: e.target.value } + }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, + }); + }} + placeholder={option.multiTableConfig?.joinChain[stepIndex - 1]?.outputField || "์ด์ „ ์ถœ๋ ฅ ํ•„๋“œ"} + className="h-8 text-xs" + /> + )} +
+ +
+ = +
+ +
+ + { + const newChain = [...(option.multiTableConfig?.joinChain || [])]; + newChain[stepIndex] = { + ...newChain[stepIndex], + joinCondition: { ...newChain[stepIndex].joinCondition, toField: value } + }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, + }); + }} + /> +
+
+ + {/* ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ „๋‹ฌํ•  ํ•„๋“œ */} +
+ + { + const newChain = [...(option.multiTableConfig?.joinChain || [])]; + newChain[stepIndex] = { ...newChain[stepIndex], outputField: value }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, joinChain: newChain }, + }); + }} + /> +

+ {stepIndex < (option.multiTableConfig?.joinChain.length || 0) - 1 + ? "๋‹ค์Œ ์กฐ์ธ ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉํ•  ํ•„๋“œ" + : "๋งˆ์ง€๋ง‰ ๋‹จ๊ณ„๋ฉด ๋น„์›Œ๋‘์„ธ์š”"} +

+
+ + {/* ์กฐ์ธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {step.tableName && step.joinCondition.fromField && step.joinCondition.toField && ( +
+ + {stepIndex === 0 ? "ํ˜„์žฌํ–‰" : option.multiTableConfig?.joinChain[stepIndex - 1]?.tableName} + + .{step.joinCondition.fromField} + = + {step.tableName} + .{step.joinCondition.toField} + {step.outputField && ( + + โ†’ {step.outputField} + + )} +
+ )} +
+ ))} + + {/* ์กฐ์ธ ์ฒด์ธ์ด ์—†์„ ๋•Œ ์•ˆ๋‚ด */} + {(!option.multiTableConfig?.joinChain || option.multiTableConfig.joinChain.length === 0) && ( +
+

+ ์กฐ์ธ ์ฒด์ธ์ด ์—†์Šต๋‹ˆ๋‹ค +

+

+ "์กฐ์ธ ์ถ”๊ฐ€" ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜์—ฌ ํ…Œ์ด๋ธ” ์กฐ์ธ์„ ์„ค์ •ํ•˜์„ธ์š” +

+
+ )} +
+ + {/* ์ตœ์ข… ๊ฐ’ ์ปฌ๋Ÿผ */} + {option.multiTableConfig?.joinChain && option.multiTableConfig.joinChain.length > 0 && ( +
+ + { + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + multiTableConfig: { ...option.multiTableConfig!, valueColumn: value }, + }); + }} + /> +

+ ๋งˆ์ง€๋ง‰ ํ…Œ์ด๋ธ”์—์„œ ๊ฐ€์ ธ์˜ฌ ๊ฐ’ +

+
+ )} + + {/* ์ „์ฒด ์กฐ์ธ ๊ฒฝ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */} + {option.multiTableConfig?.joinChain && option.multiTableConfig.joinChain.length > 0 && ( +
+

์กฐ์ธ ๊ฒฝ๋กœ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

+
+ {option.multiTableConfig.joinChain.map((step, idx) => ( +
+ {idx === 0 && ( + <> + ํ˜„์žฌํ–‰ + .{step.joinCondition.fromField} + โ†’ + + )} + {step.tableName} + .{step.joinCondition.toField} + {step.outputField && idx < option.multiTableConfig!.joinChain.length - 1 && ( + <> + โ†’ + {step.outputField} + + )} +
+ ))} + {option.multiTableConfig.valueColumn && ( +
+ ์ตœ์ข… ๊ฐ’: + {option.multiTableConfig.joinChain[option.multiTableConfig.joinChain.length - 1]?.tableName}.{option.multiTableConfig.valueColumn} +
+ )} +
+
+ )} +
+ )} + + {/* API ํ˜ธ์ถœ ์„ค์ • */} + {option.sourceType === "api" && ( +
+

API ํ˜ธ์ถœ ์„ค์ •

+

+ ๋ณต์žกํ•œ ๋‹ค์ค‘ ์กฐ์ธ์€ ๋ฐฑ์—”๋“œ API๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค +

+ + {/* API ์—”๋“œํฌ์ธํŠธ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + apiConfig: { ...option.apiConfig!, endpoint: e.target.value }, + })} + placeholder="/api/price/customer" + className="h-8 text-xs font-mono" + /> +
+ + {/* HTTP ๋ฉ”์„œ๋“œ */} +
+ + +
+ + {/* ํŒŒ๋ผ๋ฏธํ„ฐ ๋งคํ•‘ */} +
+
+ + +
+ + {(option.apiConfig?.parameterMappings || []).map((mapping, mapIndex) => ( +
+ { + const newMappings = [...(option.apiConfig?.parameterMappings || [])]; + newMappings[mapIndex] = { ...newMappings[mapIndex], paramName: e.target.value }; + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + apiConfig: { ...option.apiConfig!, parameterMappings: newMappings }, + }); + }} + placeholder="ํŒŒ๋ผ๋ฏธํ„ฐ๋ช…" + className="h-7 text-[10px] flex-1" + /> + = + + +
+ ))} +
+ + {/* ์‘๋‹ต ๊ฐ’ ํ•„๋“œ */} +
+ + updateDynamicSourceOption(editingDynamicSourceColumnIndex, optIndex, { + apiConfig: { ...option.apiConfig!, responseValueField: e.target.value }, + })} + placeholder="price (๋˜๋Š” data.price)" + className="h-8 text-xs font-mono" + /> +

+ API ์‘๋‹ต์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ (์ค‘์ฒฉ ๊ฒฝ๋กœ ์ง€์›: data.price) +

+
+
+ )} +
+ ))} + + {/* ์˜ต์…˜ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + +
+ + {/* ์•ˆ๋‚ด */} +
+

์‚ฌ์šฉ ์˜ˆ์‹œ

+
    +
  • - ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€: customer_item_price ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ
  • +
  • - ํ’ˆ๋ชฉ๋ณ„ ๋‹จ๊ฐ€: item_info ํ…Œ์ด๋ธ”์—์„œ ๊ธฐ์ค€ ๋‹จ๊ฐ€ ์กฐํšŒ
  • +
  • - ๊ณ„์•ฝ ๋‹จ๊ฐ€: ์ „์šฉ API๋กœ ๋ณต์žกํ•œ ์กฐ์ธ ์ฒ˜๋ฆฌ
  • +
+
+
+ )} + + + + +
+
); } diff --git a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx index 703256b2..410fd9a6 100644 --- a/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx +++ b/frontend/lib/registry/components/modal-repeater-table/RepeaterTable.tsx @@ -4,7 +4,8 @@ import React, { useState, useEffect } from "react"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; -import { Trash2 } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Trash2, ChevronDown, Check } from "lucide-react"; import { RepeaterColumnConfig } from "./types"; import { cn } from "@/lib/utils"; @@ -14,6 +15,9 @@ interface RepeaterTableProps { onDataChange: (newData: any[]) => void; onRowChange: (index: number, newRow: any) => void; onRowDelete: (index: number) => void; + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ๊ด€๋ จ + activeDataSources?: Record; // ์ปฌ๋Ÿผ๋ณ„ ํ˜„์žฌ ํ™œ์„ฑํ™”๋œ ๋ฐ์ดํ„ฐ ์†Œ์Šค ID + onDataSourceChange?: (columnField: string, optionId: string) => void; // ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ } export function RepeaterTable({ @@ -22,11 +26,16 @@ export function RepeaterTable({ onDataChange, onRowChange, onRowDelete, + activeDataSources = {}, + onDataSourceChange, }: RepeaterTableProps) { const [editingCell, setEditingCell] = useState<{ rowIndex: number; field: string; } | null>(null); + + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค Popover ์—ด๋ฆผ ์ƒํƒœ + const [openPopover, setOpenPopover] = useState(null); // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ๊ฐ์ง€ (ํ•„์š”์‹œ ํ™œ์„ฑํ™”) // useEffect(() => { @@ -144,16 +153,79 @@ export function RepeaterTable({ # - {columns.map((col) => ( + {columns.map((col) => { + const hasDynamicSource = col.dynamicDataSource?.enabled && col.dynamicDataSource.options.length > 0; + const activeOptionId = activeDataSources[col.field] || col.dynamicDataSource?.defaultOptionId; + const activeOption = hasDynamicSource + ? col.dynamicDataSource!.options.find(opt => opt.id === activeOptionId) || col.dynamicDataSource!.options[0] + : null; + + return ( + {hasDynamicSource ? ( + setOpenPopover(open ? col.field : null)} + > + + + + +
+ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ +
+ {col.dynamicDataSource!.options.map((option) => ( + + ))} +
+
+ ) : ( + <> {col.label} {col.required && *} + + )} - ))} + ); + })} ์‚ญ์ œ diff --git a/frontend/lib/registry/components/modal-repeater-table/types.ts b/frontend/lib/registry/components/modal-repeater-table/types.ts index c0cac4a9..6097aaf3 100644 --- a/frontend/lib/registry/components/modal-repeater-table/types.ts +++ b/frontend/lib/registry/components/modal-repeater-table/types.ts @@ -10,7 +10,7 @@ export interface ModalRepeaterTableProps { sourceColumnLabels?: Record; // ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ๋ผ๋ฒจ (์ปฌ๋Ÿผ๋ช… -> ํ‘œ์‹œ ๋ผ๋ฒจ) sourceSearchFields?: string[]; // ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ํ•„๋“œ๋“ค - // ๐Ÿ†• ์ €์žฅ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ค์ • + // ์ €์žฅ ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์„ค์ • targetTable?: string; // ์ €์žฅํ•  ํ…Œ์ด๋ธ” (์˜ˆ: "sales_order_mng") // ๋ชจ๋‹ฌ ์„ค์ • @@ -25,14 +25,14 @@ export interface ModalRepeaterTableProps { calculationRules?: CalculationRule[]; // ์ž๋™ ๊ณ„์‚ฐ ๊ทœ์น™ // ๋ฐ์ดํ„ฐ - value: any[]; // ํ˜„์žฌ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ๋“ค - onChange: (newData: any[]) => void; // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ + value: Record[]; // ํ˜„์žฌ ์ถ”๊ฐ€๋œ ํ•ญ๋ชฉ๋“ค + onChange: (newData: Record[]) => void; // ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ ์ฝœ๋ฐฑ // ์ค‘๋ณต ์ฒดํฌ uniqueField?: string; // ์ค‘๋ณต ์ฒดํฌํ•  ํ•„๋“œ (์˜ˆ: "item_code") // ํ•„ํ„ฐ๋ง - filterCondition?: Record; + filterCondition?: Record; companyCode?: string; // ์Šคํƒ€์ผ @@ -47,11 +47,92 @@ export interface RepeaterColumnConfig { calculated?: boolean; // ๊ณ„์‚ฐ ํ•„๋“œ ์—ฌ๋ถ€ width?: string; // ์ปฌ๋Ÿผ ๋„ˆ๋น„ required?: boolean; // ํ•„์ˆ˜ ์ž…๋ ฅ ์—ฌ๋ถ€ - defaultValue?: any; // ๊ธฐ๋ณธ๊ฐ’ + defaultValue?: string | number | boolean; // ๊ธฐ๋ณธ๊ฐ’ selectOptions?: { value: string; label: string }[]; // select์ผ ๋•Œ ์˜ต์…˜ - // ๐Ÿ†• ์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ • + // ์ปฌ๋Ÿผ ๋งคํ•‘ ์„ค์ • mapping?: ColumnMapping; // ์ด ์ปฌ๋Ÿผ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์–ด๋””์„œ ๊ฐ€์ ธ์˜ฌ์ง€ ์„ค์ • + + // ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค (์ปฌ๋Ÿผ ํ—ค๋” ํด๋ฆญ์œผ๋กœ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ „ํ™˜) + dynamicDataSource?: DynamicDataSourceConfig; +} + +/** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • + * ์ปฌ๋Ÿผ ํ—ค๋”๋ฅผ ํด๋ฆญํ•˜์—ฌ ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ์ „ํ™˜ํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ + * ์˜ˆ: ๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€, ํ’ˆ๋ชฉ๋ณ„ ๋‹จ๊ฐ€, ๊ธฐ์ค€ ๋‹จ๊ฐ€ ๋“ฑ์„ ์„ ํƒ + */ +export interface DynamicDataSourceConfig { + enabled: boolean; + options: DynamicDataSourceOption[]; + defaultOptionId?: string; // ๊ธฐ๋ณธ ์„ ํƒ ์˜ต์…˜ ID +} + +/** + * ๋™์  ๋ฐ์ดํ„ฐ ์†Œ์Šค ์˜ต์…˜ + * ๊ฐ ์˜ต์…˜์€ ๋‹ค๋ฅธ ํ…Œ์ด๋ธ”/API์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐฉ๋ฒ•์„ ์ •์˜ + */ +export interface DynamicDataSourceOption { + id: string; + label: string; // ํ‘œ์‹œ ๋ผ๋ฒจ (์˜ˆ: "๊ฑฐ๋ž˜์ฒ˜๋ณ„ ๋‹จ๊ฐ€") + + // ์กฐํšŒ ๋ฐฉ์‹ + sourceType: "table" | "multiTable" | "api"; + + // ํ…Œ์ด๋ธ” ์ง์ ‘ ์กฐํšŒ (๋‹จ์ˆœ ์กฐ์ธ - 1๊ฐœ ํ…Œ์ด๋ธ”) + tableConfig?: { + tableName: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”๋ช… + valueColumn: string; // ๊ฐ€์ ธ์˜ฌ ๊ฐ’ ์ปฌ๋Ÿผ + joinConditions: { + sourceField: string; // ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ + targetField: string; // ์ฐธ์กฐ ํ…Œ์ด๋ธ”์˜ ํ•„๋“œ + }[]; + }; + + // ํ…Œ์ด๋ธ” ๋ณตํ•ฉ ์กฐ์ธ (2๊ฐœ ์ด์ƒ ํ…Œ์ด๋ธ” ์กฐ์ธ) + multiTableConfig?: { + // ์กฐ์ธ ์ฒด์ธ ์ •์˜ (์ˆœ์„œ๋Œ€๋กœ ์กฐ์ธ) + joinChain: MultiTableJoinStep[]; + // ์ตœ์ข…์ ์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ๊ฐ’ ์ปฌ๋Ÿผ (๋งˆ์ง€๋ง‰ ํ…Œ์ด๋ธ”์—์„œ) + valueColumn: string; + }; + + // ์ „์šฉ API ํ˜ธ์ถœ (๋ณต์žกํ•œ ๋‹ค์ค‘ ์กฐ์ธ) + apiConfig?: { + endpoint: string; // API ์—”๋“œํฌ์ธํŠธ (์˜ˆ: "/api/price/customer") + method?: "GET" | "POST"; // HTTP ๋ฉ”์„œ๋“œ (๊ธฐ๋ณธ: GET) + parameterMappings: { + paramName: string; // API ํŒŒ๋ผ๋ฏธํ„ฐ๋ช… + sourceField: string; // ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ + }[]; + responseValueField: string; // ์‘๋‹ต์—์„œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ํ•„๋“œ + }; +} + +/** + * ๋ณตํ•ฉ ์กฐ์ธ ๋‹จ๊ณ„ ์ •์˜ + * ์˜ˆ: item_info.item_number โ†’ customer_item.item_code โ†’ customer_item.id โ†’ customer_item_price.customer_item_id + */ +export interface MultiTableJoinStep { + // ์กฐ์ธํ•  ํ…Œ์ด๋ธ” + tableName: string; + // ์กฐ์ธ ์กฐ๊ฑด + joinCondition: { + // ์ด์ „ ๋‹จ๊ณ„์˜ ํ•„๋“œ (์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๋Š” ํ˜„์žฌ ํ–‰์˜ ํ•„๋“œ) + fromField: string; + // ์ด ํ…Œ์ด๋ธ”์˜ ํ•„๋“œ + toField: string; + }; + // ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์ „๋‹ฌํ•  ํ•„๋“œ (๋‹ค์Œ ์กฐ์ธ์— ์‚ฌ์šฉ) + outputField?: string; + // ์ถ”๊ฐ€ ํ•„ํ„ฐ ์กฐ๊ฑด (์„ ํƒ์‚ฌํ•ญ) + additionalFilters?: { + field: string; + operator: "=" | "!=" | ">" | "<" | ">=" | "<="; + value: string | number | boolean; + // ๊ฐ’์ด ํ˜„์žฌ ํ–‰์—์„œ ์˜ค๋Š” ๊ฒฝ์šฐ + valueFromField?: string; + }[]; } /** @@ -101,11 +182,10 @@ export interface ItemSelectionModalProps { sourceColumns: string[]; sourceSearchFields?: string[]; multiSelect?: boolean; - filterCondition?: Record; + filterCondition?: Record; modalTitle: string; - alreadySelected: any[]; // ์ด๋ฏธ ์„ ํƒ๋œ ํ•ญ๋ชฉ๋“ค (์ค‘๋ณต ๋ฐฉ์ง€์šฉ) + alreadySelected: Record[]; // ์ด๋ฏธ ์„ ํƒ๋œ ํ•ญ๋ชฉ๋“ค (์ค‘๋ณต ๋ฐฉ์ง€์šฉ) uniqueField?: string; - onSelect: (items: any[]) => void; + onSelect: (items: Record[]) => void; columnLabels?: Record; // ์ปฌ๋Ÿผ๋ช… -> ๋ผ๋ฒจ๋ช… ๋งคํ•‘ } - diff --git a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx index 85e43ce9..980bbfe9 100644 --- a/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx +++ b/frontend/lib/registry/components/repeat-screen-modal/RepeatScreenModalComponent.tsx @@ -55,7 +55,8 @@ export function RepeatScreenModalComponent({ ...props }: RepeatScreenModalComponentProps) { // props์—์„œ๋„ groupedData๋ฅผ ์ถ”์ถœ (DynamicWebTypeRenderer์—์„œ ์ „๋‹ฌ๋  ์ˆ˜ ์žˆ์Œ) - const groupedData = propsGroupedData || (props as any).groupedData; + // DynamicComponentRenderer์—์„œ๋Š” _groupedData๋กœ ์ „๋‹ฌ๋จ + const groupedData = propsGroupedData || (props as any).groupedData || (props as any)._groupedData; const componentConfig = { ...config, ...component?.config, @@ -99,25 +100,99 @@ export function RepeatScreenModalComponent({ contentRowId: string; } | null>(null); + // ๐Ÿ†• v3.13: ์™ธ๋ถ€์—์„œ ์ €์žฅ ํŠธ๋ฆฌ๊ฑฐ ๊ฐ€๋Šฅํ•˜๋„๋ก ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€ + useEffect(() => { + const handleTriggerSave = async (event: Event) => { + if (!(event instanceof CustomEvent)) return; + + console.log("[RepeatScreenModal] triggerRepeatScreenModalSave ์ด๋ฒคํŠธ ์ˆ˜์‹ "); + + try { + setIsSaving(true); + + // ๊ธฐ์กด ๋ฐ์ดํ„ฐ ์ €์žฅ + if (cardMode === "withTable") { + await saveGroupedData(); + } else { + await saveSimpleData(); + } + + // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ + await saveExternalTableData(); + + // ์—ฐ๋™ ์ €์žฅ ์ฒ˜๋ฆฌ (syncSaves) + await processSyncSaves(); + + console.log("[RepeatScreenModal] ์™ธ๋ถ€ ํŠธ๋ฆฌ๊ฑฐ ์ €์žฅ ์™„๋ฃŒ"); + + // ์ €์žฅ ์™„๋ฃŒ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new CustomEvent("repeatScreenModalSaveComplete", { + detail: { success: true } + })); + + // ์„ฑ๊ณต ์ฝœ๋ฐฑ ์‹คํ–‰ + if (event.detail?.onSuccess) { + event.detail.onSuccess(); + } + } catch (error: any) { + console.error("[RepeatScreenModal] ์™ธ๋ถ€ ํŠธ๋ฆฌ๊ฑฐ ์ €์žฅ ์‹คํŒจ:", error); + + // ์ €์žฅ ์‹คํŒจ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + window.dispatchEvent(new CustomEvent("repeatScreenModalSaveComplete", { + detail: { success: false, error: error.message } + })); + + // ์‹คํŒจ ์ฝœ๋ฐฑ ์‹คํ–‰ + if (event.detail?.onError) { + event.detail.onError(error); + } + } finally { + setIsSaving(false); + } + }; + + window.addEventListener("triggerRepeatScreenModalSave", handleTriggerSave as EventListener); + return () => { + window.removeEventListener("triggerRepeatScreenModalSave", handleTriggerSave as EventListener); + }; + }, [cardMode, groupedCardsData, externalTableData, contentRows]); + // ๐Ÿ†• v3.9: beforeFormSave ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ - ButtonPrimary ์ €์žฅ ์‹œ externalTableData๋ฅผ formData์— ๋ณ‘ํ•ฉ useEffect(() => { const handleBeforeFormSave = (event: Event) => { if (!(event instanceof CustomEvent) || !event.detail?.formData) return; console.log("[RepeatScreenModal] beforeFormSave ์ด๋ฒคํŠธ ์ˆ˜์‹ "); + console.log("[RepeatScreenModal] beforeFormSave - externalTableData:", externalTableData); + console.log("[RepeatScreenModal] beforeFormSave - groupedCardsData:", groupedCardsData.length, "๊ฐœ ์นด๋“œ"); // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ์—์„œ dirty ํ–‰๋งŒ ์ถ”์ถœํ•˜์—ฌ ์ €์žฅ ๋ฐ์ดํ„ฐ ์ค€๋น„ const saveDataByTable: Record = {}; for (const [key, rows] of Object.entries(externalTableData)) { + // key ํ˜•์‹: cardId-contentRowId + const keyParts = key.split("-"); + const cardId = keyParts.slice(0, -1).join("-"); // contentRowId๋ฅผ ์ œ์™ธํ•œ ๋‚˜๋จธ์ง€๊ฐ€ cardId + // contentRow ์ฐพ๊ธฐ const contentRow = contentRows.find((r) => key.includes(r.id)); if (!contentRow?.tableDataSource?.enabled) continue; + // ๐Ÿ†• v3.13: ํ•ด๋‹น ์นด๋“œ์˜ ๋Œ€ํ‘œ ๋ฐ์ดํ„ฐ ์ฐพ๊ธฐ (joinConditions์˜ targetKey ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•ด) + const card = groupedCardsData.find((c) => c._cardId === cardId); + const representativeData = card?._representativeData || {}; + const targetTable = contentRow.tableCrud?.targetTable || contentRow.tableDataSource.sourceTable; - // dirty ํ–‰๋งŒ ํ•„ํ„ฐ๋ง (์‚ญ์ œ๋œ ํ–‰ ์ œ์™ธ) - const dirtyRows = rows.filter((row) => row._isDirty && !row._isDeleted); + // dirty ํ–‰ ๋˜๋Š” ์ƒˆ๋กœ์šด ํ–‰ ํ•„ํ„ฐ๋ง (์‚ญ์ œ๋œ ํ–‰ ์ œ์™ธ) + // ๐Ÿ†• v3.13: _isNew ํ–‰๋„ ํฌํ•จ (์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ํ–‰์€ _isDirty๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ) + const dirtyRows = rows.filter((row) => (row._isDirty || row._isNew) && !row._isDeleted); + + console.log(`[RepeatScreenModal] beforeFormSave - ${targetTable} ํ–‰ ํ•„ํ„ฐ๋ง:`, { + totalRows: rows.length, + dirtyRows: dirtyRows.length, + rowDetails: rows.map(r => ({ _isDirty: r._isDirty, _isNew: r._isNew, _isDeleted: r._isDeleted })) + }); if (dirtyRows.length === 0) continue; @@ -126,8 +201,9 @@ export function RepeatScreenModalComponent({ .filter((col) => col.editable) .map((col) => col.field); - const joinKeys = (contentRow.tableDataSource.joinConditions || []) - .map((cond) => cond.sourceKey); + // ๐Ÿ†• v3.13: joinConditions์—์„œ sourceKey (์ €์žฅ ๋Œ€์ƒ ํ…Œ์ด๋ธ”์˜ FK ์ปฌ๋Ÿผ) ์ถ”์ถœ + const joinConditions = contentRow.tableDataSource.joinConditions || []; + const joinKeys = joinConditions.map((cond) => cond.sourceKey); const allowedFields = [...new Set([...editableFields, ...joinKeys])]; @@ -145,6 +221,17 @@ export function RepeatScreenModalComponent({ } } + // ๐Ÿ†• v3.13: joinConditions๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ FK ๊ฐ’ ์ž๋™ ์ฑ„์šฐ๊ธฐ + // ์˜ˆ: sales_order_id (sourceKey) = card์˜ id (targetKey) + for (const joinCond of joinConditions) { + const { sourceKey, targetKey } = joinCond; + // sourceKey๊ฐ€ ์ €์žฅ ๋ฐ์ดํ„ฐ์— ์—†๊ฑฐ๋‚˜ null์ธ ๊ฒฝ์šฐ, ์นด๋“œ์˜ ๋Œ€ํ‘œ ๋ฐ์ดํ„ฐ์—์„œ targetKey ๊ฐ’์„ ๊ฐ€์ ธ์˜ด + if (!saveData[sourceKey] && representativeData[targetKey] !== undefined) { + saveData[sourceKey] = representativeData[targetKey]; + console.log(`[RepeatScreenModal] beforeFormSave - FK ์ž๋™ ์ฑ„์šฐ๊ธฐ: ${sourceKey} = ${representativeData[targetKey]} (from card.${targetKey})`); + } + } + // _isNew ํ”Œ๋ž˜๊ทธ ์œ ์ง€ saveData._isNew = row._isNew; saveData._targetTable = targetTable; @@ -590,18 +677,26 @@ export function RepeatScreenModalComponent({ if (!hasExternalAggregation) return; - // contentRows์—์„œ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์žˆ๋Š” table ํƒ€์ž… ํ–‰ ์ฐพ๊ธฐ - const tableRowWithExternalSource = contentRows.find( + // contentRows์—์„œ ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์žˆ๋Š” ๋ชจ๋“  table ํƒ€์ž… ํ–‰ ์ฐพ๊ธฐ + const tableRowsWithExternalSource = contentRows.filter( (row) => row.type === "table" && row.tableDataSource?.enabled ); - if (!tableRowWithExternalSource) return; + if (tableRowsWithExternalSource.length === 0) return; // ๊ฐ ์นด๋“œ์˜ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ const updatedCards = groupedCardsData.map((card) => { - const key = `${card._cardId}-${tableRowWithExternalSource.id}`; + // ๐Ÿ†• v3.11: ํ…Œ์ด๋ธ” ํ–‰ ID๋ณ„๋กœ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ €์žฅ + const externalRowsByTableId: Record = {}; + const allExternalRows: any[] = []; + + for (const tableRow of tableRowsWithExternalSource) { + const key = `${card._cardId}-${tableRow.id}`; // ๐Ÿ†• v3.7: ์‚ญ์ œ๋œ ํ–‰์€ ์ง‘๊ณ„์—์„œ ์ œ์™ธ - const externalRows = (extData[key] || []).filter((row) => !row._isDeleted); + const rows = (extData[key] || []).filter((row) => !row._isDeleted); + externalRowsByTableId[tableRow.id] = rows; + allExternalRows.push(...rows); + } // ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ const newAggregations: Record = {}; @@ -616,7 +711,7 @@ export function RepeatScreenModalComponent({ if (isExternalTable) { // ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ์ง‘๊ณ„ newAggregations[agg.resultField] = calculateColumnAggregation( - externalRows, + allExternalRows, agg.sourceField || "", agg.type || "sum" ); @@ -626,12 +721,28 @@ export function RepeatScreenModalComponent({ calculateColumnAggregation(card._rows, agg.sourceField || "", agg.type || "sum"); } } else if (sourceType === "formula" && agg.formula) { + // ๐Ÿ†• v3.11: externalTableRefs ๊ธฐ๋ฐ˜์œผ๋กœ ํ•„ํ„ฐ๋ง๋œ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ + let filteredExternalRows: any[]; + + if (agg.externalTableRefs && agg.externalTableRefs.length > 0) { + // ํŠน์ • ํ…Œ์ด๋ธ”๋งŒ ์ฐธ์กฐ + filteredExternalRows = []; + for (const tableId of agg.externalTableRefs) { + if (externalRowsByTableId[tableId]) { + filteredExternalRows.push(...externalRowsByTableId[tableId]); + } + } + } else { + // ๋ชจ๋“  ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ (๊ธฐ์กด ๋™์ž‘) + filteredExternalRows = allExternalRows; + } + // ๊ฐ€์ƒ ์ง‘๊ณ„ (์—ฐ์‚ฐ์‹) - ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ํฌํ•จํ•˜์—ฌ ์žฌ๊ณ„์‚ฐ newAggregations[agg.resultField] = evaluateFormulaWithContext( agg.formula, card._representativeData, card._rows, - externalRows, + filteredExternalRows, newAggregations // ์ด์ „ ์ง‘๊ณ„ ๊ฒฐ๊ณผ ์ฐธ์กฐ ); } @@ -654,8 +765,8 @@ export function RepeatScreenModalComponent({ }); }; - // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์ถ”๊ฐ€ - const handleAddExternalRow = (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => { + // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์ถ”๊ฐ€ (v3.13: ์ž๋™ ์ฑ„๋ฒˆ ๊ธฐ๋Šฅ ์ถ”๊ฐ€) + const handleAddExternalRow = async (cardId: string, contentRowId: string, contentRow: CardContentRowConfig) => { const key = `${cardId}-${contentRowId}`; const card = groupedCardsData.find((c) => c._cardId === cardId) || cardsData.find((c) => c._cardId === cardId); const representativeData = (card as GroupedCardData)?._representativeData || card || {}; @@ -707,6 +818,41 @@ export function RepeatScreenModalComponent({ } } + // ๐Ÿ†• v3.13: ์ž๋™ ์ฑ„๋ฒˆ ์ฒ˜๋ฆฌ + const rowNumbering = contentRow.tableCrud?.rowNumbering; + console.log("[RepeatScreenModal] ์ฑ„๋ฒˆ ์„ค์ • ํ™•์ธ:", { + tableCrud: contentRow.tableCrud, + rowNumbering, + enabled: rowNumbering?.enabled, + targetColumn: rowNumbering?.targetColumn, + numberingRuleId: rowNumbering?.numberingRuleId, + }); + if (rowNumbering?.enabled && rowNumbering.targetColumn && rowNumbering.numberingRuleId) { + try { + console.log("[RepeatScreenModal] ์ž๋™ ์ฑ„๋ฒˆ ์‹œ์ž‘:", { + targetColumn: rowNumbering.targetColumn, + numberingRuleId: rowNumbering.numberingRuleId, + }); + + // ์ฑ„๋ฒˆ API ํ˜ธ์ถœ (allocate: ์‹ค์ œ ์‹œํ€€์Šค ์ฆ๊ฐ€) + const { allocateNumberingCode } = await import("@/lib/api/numberingRule"); + const response = await allocateNumberingCode(rowNumbering.numberingRuleId); + + if (response.success && response.data) { + newRowData[rowNumbering.targetColumn] = response.data.generatedCode; + + console.log("[RepeatScreenModal] ์ž๋™ ์ฑ„๋ฒˆ ์™„๋ฃŒ:", { + column: rowNumbering.targetColumn, + generatedCode: response.data.generatedCode, + }); + } else { + console.warn("[RepeatScreenModal] ์ฑ„๋ฒˆ ์‹คํŒจ:", response); + } + } catch (error) { + console.error("[RepeatScreenModal] ์ฑ„๋ฒˆ API ํ˜ธ์ถœ ์‹คํŒจ:", error); + } + } + console.log("[RepeatScreenModal] ์ƒˆ ํ–‰ ์ถ”๊ฐ€:", { cardId, contentRowId, @@ -1009,26 +1155,70 @@ export function RepeatScreenModalComponent({ } }; - // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์‚ญ์ œ ์‹คํ–‰ (์†Œํ”„ํŠธ ์‚ญ์ œ - _isDeleted ํ”Œ๋ž˜๊ทธ ์„ค์ •) - const handleDeleteExternalRow = (cardId: string, rowId: string, contentRowId: string) => { + // ๐Ÿ†• v3.14: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰ ์‚ญ์ œ ์‹คํ–‰ (์ฆ‰์‹œ DELETE API ํ˜ธ์ถœ) + const handleDeleteExternalRow = async (cardId: string, rowId: string, contentRowId: string) => { const key = `${cardId}-${contentRowId}`; + const rows = externalTableData[key] || []; + const targetRow = rows.find((row) => row._rowId === rowId); + + // ๊ธฐ์กด DB ๋ฐ์ดํ„ฐ์ธ ๊ฒฝ์šฐ (id๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) ์ฆ‰์‹œ ์‚ญ์ œ + if (targetRow?._originalData?.id) { + try { + const contentRow = contentRows.find((r) => r.id === contentRowId); + const targetTable = contentRow?.tableCrud?.targetTable || contentRow?.tableDataSource?.sourceTable; + + if (!targetTable) { + console.error("[RepeatScreenModal] ์‚ญ์ œ ๋Œ€์ƒ ํ…Œ์ด๋ธ”์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + return; + } + + console.log(`[RepeatScreenModal] DELETE API ํ˜ธ์ถœ: ${targetTable}, id=${targetRow._originalData.id}`); + + // ๋ฐฑ์—”๋“œ๋Š” ๋ฐฐ์—ด ํ˜•ํƒœ์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋Œ€ํ•จ + await apiClient.request({ + method: "DELETE", + url: `/table-management/tables/${targetTable}/delete`, + data: [{ id: targetRow._originalData.id }], + }); + + console.log(`[RepeatScreenModal] DELETE ์„ฑ๊ณต: ${targetTable}, id=${targetRow._originalData.id}`); + + // ์„ฑ๊ณต ์‹œ UI์—์„œ ์™„์ „ํžˆ ์ œ๊ฑฐ setExternalTableData((prev) => { const newData = { ...prev, - [key]: (prev[key] || []).map((row) => - row._rowId === rowId - ? { ...row, _isDeleted: true, _isDirty: true } - : row - ), + [key]: prev[key].filter((row) => row._rowId !== rowId), }; - // ๐Ÿ†• v3.5: ํ–‰ ์‚ญ์ œ ์‹œ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ (์‚ญ์ œ๋œ ํ–‰ ์ œ์™ธ) + // ํ–‰ ์‚ญ์ œ ์‹œ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ setTimeout(() => { recalculateAggregationsWithExternalData(newData); }, 0); return newData; }); + } catch (error: any) { + console.error(`[RepeatScreenModal] DELETE ์‹คํŒจ:`, error.response?.data || error.message); + // ์—๋Ÿฌ ์‹œ์—๋„ ๋‹ค์ด์–ผ๋กœ๊ทธ ๋‹ซ๊ธฐ + } + } else { + // ์ƒˆ๋กœ ์ถ”๊ฐ€๋œ ํ–‰ (์•„์ง DB์— ์—†์Œ) - UI์—์„œ๋งŒ ์ œ๊ฑฐ + console.log(`[RepeatScreenModal] ์ƒˆ ํ–‰ ์‚ญ์ œ (DB ์—†์Œ): rowId=${rowId}`); + setExternalTableData((prev) => { + const newData = { + ...prev, + [key]: prev[key].filter((row) => row._rowId !== rowId), + }; + + // ํ–‰ ์‚ญ์ œ ์‹œ ์ง‘๊ณ„ ์žฌ๊ณ„์‚ฐ + setTimeout(() => { + recalculateAggregationsWithExternalData(newData); + }, 0); + + return newData; + }); + } + setDeleteConfirmOpen(false); setPendingDeleteInfo(null); }; @@ -1323,8 +1513,13 @@ export function RepeatScreenModalComponent({ for (const fn of extAggFunctions) { const regex = new RegExp(`${fn}\\(\\{(\\w+)\\}\\)`, "g"); expression = expression.replace(regex, (match, fieldName) => { - if (!externalRows || externalRows.length === 0) return "0"; + if (!externalRows || externalRows.length === 0) { + console.log(`[SUM_EXT] ${fieldName}: ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์—†์Œ`); + return "0"; + } const values = externalRows.map((row) => Number(row[fieldName]) || 0); + const sum = values.reduce((a, b) => a + b, 0); + console.log(`[SUM_EXT] ${fieldName}: ${externalRows.length}๊ฐœ ํ–‰, ๊ฐ’๋“ค:`, values, `ํ•ฉ๊ณ„: ${sum}`); const baseFn = fn.replace("_EXT", ""); switch (baseFn) { case "SUM": @@ -1525,6 +1720,9 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์ €์žฅ await saveExternalTableData(); + // ๐Ÿ†• v3.12: ์—ฐ๋™ ์ €์žฅ ์ฒ˜๋ฆฌ (syncSaves) + await processSyncSaves(); + alert("์ €์žฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); } catch (error: any) { console.error("์ €์žฅ ์‹คํŒจ:", error); @@ -1582,6 +1780,102 @@ export function RepeatScreenModalComponent({ }); }; + // ๐Ÿ†• v3.12: ์—ฐ๋™ ์ €์žฅ ์ฒ˜๋ฆฌ (syncSaves) + const processSyncSaves = async () => { + const syncPromises: Promise[] = []; + + // contentRows์—์„œ syncSaves๊ฐ€ ์„ค์ •๋œ ํ…Œ์ด๋ธ” ํ–‰ ์ฐพ๊ธฐ + for (const contentRow of contentRows) { + if (contentRow.type !== "table") continue; + if (!contentRow.tableCrud?.syncSaves?.length) continue; + + const sourceTable = contentRow.tableDataSource?.sourceTable; + if (!sourceTable) continue; + + // ์ด ํ…Œ์ด๋ธ” ํ–‰์˜ ๋ชจ๋“  ์นด๋“œ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ + for (const card of groupedCardsData) { + const key = `${card._cardId}-${contentRow.id}`; + const rows = (externalTableData[key] || []).filter((row) => !row._isDeleted); + + // ๊ฐ syncSave ์„ค์ • ์ฒ˜๋ฆฌ + for (const syncSave of contentRow.tableCrud.syncSaves) { + if (!syncSave.enabled) continue; + if (!syncSave.sourceColumn || !syncSave.targetTable || !syncSave.targetColumn) continue; + + // ์กฐ์ธ ํ‚ค ๊ฐ’ ์ˆ˜์ง‘ (์ค‘๋ณต ์ œ๊ฑฐ) + const joinKeyValues = new Set(); + for (const row of rows) { + const keyValue = row[syncSave.joinKey.sourceField]; + if (keyValue !== undefined && keyValue !== null) { + joinKeyValues.add(keyValue); + } + } + + // ๊ฐ ์กฐ์ธ ํ‚ค๋ณ„๋กœ ์ง‘๊ณ„ ๊ณ„์‚ฐ ๋ฐ ์—…๋ฐ์ดํŠธ + for (const keyValue of joinKeyValues) { + // ํ•ด๋‹น ์กฐ์ธ ํ‚ค์— ํ•ด๋‹นํ•˜๋Š” ํ–‰๋“ค๋งŒ ํ•„ํ„ฐ๋ง + const filteredRows = rows.filter( + (row) => row[syncSave.joinKey.sourceField] === keyValue + ); + + // ์ง‘๊ณ„ ๊ณ„์‚ฐ + let aggregatedValue: number = 0; + const values = filteredRows.map((row) => Number(row[syncSave.sourceColumn]) || 0); + + switch (syncSave.aggregationType) { + case "sum": + aggregatedValue = values.reduce((a, b) => a + b, 0); + break; + case "count": + aggregatedValue = values.length; + break; + case "avg": + aggregatedValue = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0; + break; + case "min": + aggregatedValue = values.length > 0 ? Math.min(...values) : 0; + break; + case "max": + aggregatedValue = values.length > 0 ? Math.max(...values) : 0; + break; + case "latest": + aggregatedValue = values.length > 0 ? values[values.length - 1] : 0; + break; + } + + console.log(`[SyncSave] ${sourceTable}.${syncSave.sourceColumn} โ†’ ${syncSave.targetTable}.${syncSave.targetColumn}`, { + joinKey: keyValue, + aggregationType: syncSave.aggregationType, + values, + aggregatedValue, + }); + + // ๋Œ€์ƒ ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ + syncPromises.push( + apiClient + .put(`/table-management/tables/${syncSave.targetTable}/data/${keyValue}`, { + [syncSave.targetColumn]: aggregatedValue, + }) + .then(() => { + console.log(`[SyncSave] ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${syncSave.targetTable}.${syncSave.targetColumn} = ${aggregatedValue} (id=${keyValue})`); + }) + .catch((err) => { + console.error(`[SyncSave] ์—…๋ฐ์ดํŠธ ์‹คํŒจ:`, err); + throw err; + }) + ); + } + } + } + } + + if (syncPromises.length > 0) { + console.log(`[SyncSave] ${syncPromises.length}๊ฐœ ์—ฐ๋™ ์ €์žฅ ์ฒ˜๋ฆฌ ์ค‘...`); + await Promise.all(syncPromises); + console.log(`[SyncSave] ์—ฐ๋™ ์ €์žฅ ์™„๋ฃŒ`); + } + }; + // ๐Ÿ†• v3.1: Footer ๋ฒ„ํŠผ ํด๋ฆญ ํ•ธ๋“ค๋Ÿฌ const handleFooterButtonClick = async (btn: FooterButtonConfig) => { switch (btn.action) { @@ -1928,27 +2222,10 @@ export function RepeatScreenModalComponent({ // ๐Ÿ†• v3.1: ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ ์†Œ์Šค ์‚ฌ์šฉ
{/* ํ…Œ์ด๋ธ” ํ—ค๋” ์˜์—ญ: ์ œ๋ชฉ + ๋ฒ„ํŠผ๋“ค */} - {(contentRow.tableTitle || contentRow.tableCrud?.allowSave || contentRow.tableCrud?.allowCreate) && ( + {(contentRow.tableTitle || contentRow.tableCrud?.allowCreate) && (
{contentRow.tableTitle || ""}
- {/* ์ €์žฅ ๋ฒ„ํŠผ - allowSave๊ฐ€ true์ผ ๋•Œ๋งŒ ํ‘œ์‹œ */} - {contentRow.tableCrud?.allowSave && ( - - )} {/* ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} {contentRow.tableCrud?.allowCreate && ( + +
+ + {/* ์ง‘๊ณ„ ๋ชฉ๋ก */} + {localAggregations.length === 0 ? ( +
+

์ง‘๊ณ„ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค

+

+ ์œ„์˜ ๋ฒ„ํŠผ์œผ๋กœ ์ปฌ๋Ÿผ ์ง‘๊ณ„ ๋˜๋Š” ๊ฐ€์ƒ ์ง‘๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š” +

+
+ ) : ( +
+ {localAggregations.map((agg, index) => ( + updateAggregation(index, updates)} + onRemove={() => removeAggregation(index)} + onMove={(direction) => moveAggregation(index, direction)} + /> + ))} +
+ )} +
+ +
+ + + + + + + + ); +} + +// ์ง‘๊ณ„ ์„ค์ • ์•„์ดํ…œ (๋ชจ๋‹ฌ์šฉ - ๋” ๋„“์€ ๊ณต๊ฐ„ ํ™œ์šฉ) +function AggregationConfigItemModal({ + agg, + index, + totalCount, + sourceTable, + allTables, + existingAggregations, + contentRows, + onUpdate, + onRemove, + onMove, +}: { + agg: AggregationConfig; + index: number; + totalCount: number; + sourceTable: string; + allTables: { tableName: string; displayName?: string }[]; + existingAggregations: AggregationConfig[]; + contentRows: CardContentRowConfig[]; + onUpdate: (updates: Partial) => void; + onRemove: () => void; + onMove: (direction: "up" | "down") => void; +}) { + const [localLabel, setLocalLabel] = useState(agg.label || ""); + const [localResultField, setLocalResultField] = useState(agg.resultField || ""); + const [localFormula, setLocalFormula] = useState(agg.formula || ""); + + useEffect(() => { + setLocalLabel(agg.label || ""); + setLocalResultField(agg.resultField || ""); + setLocalFormula(agg.formula || ""); + }, [agg.label, agg.resultField, agg.formula]); + + // ํ˜„์žฌ ์ง‘๊ณ„๋ณด๋‹ค ์•ž์— ์ •์˜๋œ ์ง‘๊ณ„๋“ค๋งŒ ์ฐธ์กฐ ๊ฐ€๋Šฅ (์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€) + const referenceableAggregations = existingAggregations.slice(0, index); + + const currentSourceType = agg.sourceType || "column"; + const isFormula = currentSourceType === "formula"; + + return ( +
+ {/* ํ—ค๋” */} +
+
+ {/* ์ˆœ์„œ ๋ณ€๊ฒฝ ๋ฒ„ํŠผ */} +
+ + +
+ + {isFormula ? "๊ฐ€์ƒ" : "์ง‘๊ณ„"} {index + 1} + + {agg.label || "(๋ผ๋ฒจ ์—†์Œ)"} +
+ +
+ + {/* ์ง‘๊ณ„ ํƒ€์ž… ์„ ํƒ */} +
+
+ + +
+ +
+ + setLocalResultField(e.target.value)} + onBlur={() => onUpdate({ resultField: localResultField })} + placeholder="์˜ˆ: total_order_qty" + className="h-9 text-sm" + /> +
+
+ + {/* ์ปฌ๋Ÿผ ์ง‘๊ณ„ ์„ค์ • */} + {!isFormula && ( +
+
+ + +
+ +
+ + onUpdate({ sourceField: value })} + placeholder="์ปฌ๋Ÿผ ์„ ํƒ" + /> +
+ +
+ + +
+
+ )} + + {/* ๊ฐ€์ƒ ์ง‘๊ณ„ (์—ฐ์‚ฐ์‹) ์„ค์ • */} + {isFormula && ( +
+
+ +
+ {localFormula || "์•„๋ž˜์—์„œ ์š”์†Œ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”"} +
+
+ + {/* ์—ฐ์‚ฐ์ž */} +
+ +
+ {["+", "-", "*", "/", "(", ")"].map((op) => ( + + ))} + +
+
+ + {/* ์ด์ „ ์ง‘๊ณ„ ์ฐธ์กฐ */} + {referenceableAggregations.length > 0 && ( +
+ +
+ {referenceableAggregations.map((refAgg) => ( + + ))} +
+
+ )} + + {/* ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ง‘๊ณ„ */} +
+ + { + const newFormula = localFormula + formulaPart; + setLocalFormula(newFormula); + onUpdate({ formula: newFormula }); + }} + /> +
+ + {/* ๐Ÿ†• v3.11: SUM_EXT ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์„ ํƒ */} + {localFormula.includes("_EXT") && ( + onUpdate({ externalTableRefs: refs })} + /> + )} +
+ )} + + {/* ๋ผ๋ฒจ ๋ฐ ์ˆจ๊น€ ์„ค์ • */} +
+
+ + setLocalLabel(e.target.value)} + onBlur={() => onUpdate({ label: localLabel })} + placeholder="์˜ˆ: ์ด์ˆ˜์ฃผ๋Ÿ‰" + className="h-9 text-sm" + /> +
+
+ +
+ onUpdate({ hidden: checked })} + className="scale-90" + /> + + {agg.hidden ? "์ˆจ๊น€" : "ํ‘œ์‹œ"} + +
+
+
+ {agg.hidden && ( +

+ ์ด ์ง‘๊ณ„๋Š” ์—ฐ์‚ฐ์—๋งŒ ์‚ฌ์šฉ๋˜๋ฉฐ ๋ ˆ์ด์•„์›ƒ์—์„œ ์„ ํƒํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. +

+ )} +
+ ); +} + +// ์ˆ˜์‹์— ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ง‘๊ณ„ ์ถ”๊ฐ€ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ +function FormulaColumnAggregator({ + sourceTable, + allTables, + onAdd, +}: { + sourceTable: string; + allTables: { tableName: string; displayName?: string }[]; + onAdd: (formulaPart: string) => void; +}) { + // ๋ฐ์ดํ„ฐ ์†Œ์Šค ํƒ€์ž…: "current" (ํ˜„์žฌ ์นด๋“œ), "external" (์™ธ๋ถ€ ํ…Œ์ด๋ธ” ํ–‰) + const [dataSourceType, setDataSourceType] = useState<"current" | "external">("current"); + const [selectedTable, setSelectedTable] = useState(sourceTable); + const [selectedColumn, setSelectedColumn] = useState(""); + const [selectedFunction, setSelectedFunction] = useState("SUM"); + + // ๋ฐ์ดํ„ฐ ์†Œ์Šค ํƒ€์ž… ๋ณ€๊ฒฝ ์‹œ ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™” + useEffect(() => { + if (dataSourceType === "current") { + setSelectedTable(sourceTable); + } + }, [dataSourceType, sourceTable]); + + const handleAdd = () => { + if (!selectedColumn) return; + + // ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋Š” ํ•ญ์ƒ _EXT ์ ‘๋ฏธ์‚ฌ ์‚ฌ์šฉ + const funcName = dataSourceType === "external" ? `${selectedFunction}_EXT` : selectedFunction; + const formulaPart = `${funcName}({${selectedColumn}})`; + onAdd(formulaPart); + setSelectedColumn(""); + }; + + return ( +
+ {/* ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ ํƒ */} +
+ + +
+ + {dataSourceType === "external" && ( +

+ ๋ ˆ์ด์•„์›ƒ์˜ ํ…Œ์ด๋ธ” ํ–‰์—์„œ ์กฐํšŒํ•œ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค (๊ฐ™์€ ํ’ˆ๋ชฉ์˜ ๋‹ค๋ฅธ ์ˆ˜์ฃผ ๋“ฑ) +

+ )} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ ); +} + +// ๐Ÿ†• v3.11: SUM_EXT ์ฐธ์กฐ ํ…Œ์ด๋ธ” ์„ ํƒ ์ปดํฌ๋„ŒํŠธ +function ExternalTableRefSelector({ + contentRows, + selectedRefs, + onUpdate, +}: { + contentRows: CardContentRowConfig[]; + selectedRefs: string[]; + onUpdate: (refs: string[]) => void; +}) { + // ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ํ™œ์„ฑํ™”๋œ ํ…Œ์ด๋ธ” ํ–‰๋งŒ ํ•„ํ„ฐ๋ง + const tableRowsWithExternalSource = contentRows.filter( + (row) => row.type === "table" && row.tableDataSource?.enabled + ); + + if (tableRowsWithExternalSource.length === 0) { + return ( +
+

+ ๋ ˆ์ด์•„์›ƒ์— ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์„ค์ •๋œ ํ…Œ์ด๋ธ” ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค. +

+
+ ); + } + + const isAllSelected = selectedRefs.length === 0; + + const handleToggleTable = (tableId: string) => { + if (selectedRefs.includes(tableId)) { + // ์ด๋ฏธ ์„ ํƒ๋œ ๊ฒฝ์šฐ ์ œ๊ฑฐ + const newRefs = selectedRefs.filter((id) => id !== tableId); + onUpdate(newRefs); + } else { + // ์„ ํƒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ถ”๊ฐ€ + onUpdate([...selectedRefs, tableId]); + } + }; + + const handleSelectAll = () => { + onUpdate([]); // ๋นˆ ๋ฐฐ์—ด = ๋ชจ๋“  ํ…Œ์ด๋ธ” ์‚ฌ์šฉ + }; + + return ( +
+
+ + +
+ +

+ SUM_EXT ํ•จ์ˆ˜๊ฐ€ ์ฐธ์กฐํ•  ํ…Œ์ด๋ธ”์„ ์„ ํƒํ•˜์„ธ์š”. ์„ ํƒํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋“  ์™ธ๋ถ€ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. +

+ +
+ {tableRowsWithExternalSource.map((row) => { + const isSelected = selectedRefs.length === 0 || selectedRefs.includes(row.id); + const tableTitle = row.title || row.tableDataSource?.sourceTable || row.id; + const tableName = row.tableDataSource?.sourceTable || ""; + + return ( +
handleToggleTable(row.id)} + > + {}} // onClick์—์„œ ์ฒ˜๋ฆฌ + className="h-4 w-4 rounded border-gray-300 text-amber-600 focus:ring-amber-500" + /> +
+

{tableTitle}

+

+ ํ…Œ์ด๋ธ”: {tableName} | ID: {row.id.slice(-10)} +

+
+
+ ); + })} +
+ + {selectedRefs.length > 0 && ( +

+ ์„ ํƒ๋œ ํ…Œ์ด๋ธ”: {selectedRefs.length}๊ฐœ (ํŠน์ • ํ…Œ์ด๋ธ”๋งŒ ์ฐธ์กฐ) +

+ )} +
+ ); +} + +// ๐Ÿ†• v3.12: ์—ฐ๋™ ์ €์žฅ ์„ค์ • ์„น์…˜ +function SyncSaveConfigSection({ + row, + allTables, + onUpdateRow, +}: { + row: CardContentRowConfig; + allTables: { tableName: string; displayName?: string }[]; + onUpdateRow: (updates: Partial) => void; +}) { + const syncSaves = row.tableCrud?.syncSaves || []; + const sourceTable = row.tableDataSource?.sourceTable || ""; + + // ์—ฐ๋™ ์ €์žฅ ์ถ”๊ฐ€ + const addSyncSave = () => { + const newSyncSave: SyncSaveConfig = { + id: `sync-${Date.now()}`, + enabled: true, + sourceColumn: "", + aggregationType: "sum", + targetTable: "", + targetColumn: "", + joinKey: { + sourceField: "", + targetField: "id", + }, + }; + + onUpdateRow({ + tableCrud: { + ...row.tableCrud, + allowCreate: row.tableCrud?.allowCreate || false, + allowUpdate: row.tableCrud?.allowUpdate || false, + allowDelete: row.tableCrud?.allowDelete || false, + syncSaves: [...syncSaves, newSyncSave], + }, + }); + }; + + // ์—ฐ๋™ ์ €์žฅ ์‚ญ์ œ + const removeSyncSave = (index: number) => { + const newSyncSaves = [...syncSaves]; + newSyncSaves.splice(index, 1); + + onUpdateRow({ + tableCrud: { + ...row.tableCrud, + allowCreate: row.tableCrud?.allowCreate || false, + allowUpdate: row.tableCrud?.allowUpdate || false, + allowDelete: row.tableCrud?.allowDelete || false, + syncSaves: newSyncSaves, + }, + }); + }; + + // ์—ฐ๋™ ์ €์žฅ ์—…๋ฐ์ดํŠธ + const updateSyncSave = (index: number, updates: Partial) => { + const newSyncSaves = [...syncSaves]; + newSyncSaves[index] = { ...newSyncSaves[index], ...updates }; + + onUpdateRow({ + tableCrud: { + ...row.tableCrud, + allowCreate: row.tableCrud?.allowCreate || false, + allowUpdate: row.tableCrud?.allowUpdate || false, + allowDelete: row.tableCrud?.allowDelete || false, + syncSaves: newSyncSaves, + }, + }); + }; + + return ( +
+
+ + +
+ + {syncSaves.length === 0 ? ( +

+ ์—ฐ๋™ ์ €์žฅ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€ ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์„ค์ •ํ•˜์„ธ์š”. +

+ ) : ( +
+ {syncSaves.map((sync, index) => ( + updateSyncSave(index, updates)} + onRemove={() => removeSyncSave(index)} + /> + ))} +
+ )} +
+ ); +} + +// ๐Ÿ†• v3.12: ๊ฐœ๋ณ„ ์—ฐ๋™ ์ €์žฅ ์„ค์ • ์•„์ดํ…œ +function SyncSaveConfigItem({ + sync, + index, + sourceTable, + allTables, + onUpdate, + onRemove, +}: { + sync: SyncSaveConfig; + index: number; + sourceTable: string; + allTables: { tableName: string; displayName?: string }[]; + onUpdate: (updates: Partial) => void; + onRemove: () => void; +}) { + return ( +
+ {/* ํ—ค๋” */} +
+
+ onUpdate({ enabled: checked })} + className="scale-[0.6]" + /> + + ์—ฐ๋™ {index + 1} + +
+ +
+ + {/* ์†Œ์Šค ์„ค์ • */} +
+
+ + onUpdate({ sourceColumn: value })} + placeholder="์ปฌ๋Ÿผ ์„ ํƒ" + /> +
+
+ + +
+
+ + {/* ๋Œ€์ƒ ์„ค์ • */} +
+
+ + +
+
+ + onUpdate({ targetColumn: value })} + placeholder="์ปฌ๋Ÿผ ์„ ํƒ" + /> +
+
+ + {/* ์กฐ์ธ ํ‚ค ์„ค์ • */} +
+
+ + onUpdate({ joinKey: { ...sync.joinKey, sourceField: value } })} + placeholder="์˜ˆ: sales_order_id" + /> +
+
+ + onUpdate({ joinKey: { ...sync.joinKey, targetField: value } })} + placeholder="์˜ˆ: id" + /> +
+
+ + {/* ์„ค์ • ์š”์•ฝ */} + {sync.sourceColumn && sync.targetTable && sync.targetColumn && ( +

+ {sourceTable}.{sync.sourceColumn}์˜ {sync.aggregationType.toUpperCase()} ๊ฐ’์„{" "} + {sync.targetTable}.{sync.targetColumn}์— ์ €์žฅ +

+ )} +
+ ); +} + +// ๐Ÿ†• v3.13: ํ–‰ ์ถ”๊ฐ€ ์‹œ ์ž๋™ ์ฑ„๋ฒˆ ์„ค์ • ์„น์…˜ +function RowNumberingConfigSection({ + row, + onUpdateRow, +}: { + row: CardContentRowConfig; + onUpdateRow: (updates: Partial) => void; +}) { + const [numberingRules, setNumberingRules] = useState<{ id: string; name: string; code: string }[]>([]); + const [isLoading, setIsLoading] = useState(false); + + const rowNumbering = row.tableCrud?.rowNumbering; + const tableColumns = row.tableColumns || []; + + // ์ฑ„๋ฒˆ ๊ทœ์น™ ๋ชฉ๋ก ๋กœ๋“œ (์˜ต์…˜์„ค์ • > ์ฝ”๋“œ์„ค์ •์—์„œ ๋“ฑ๋ก๋œ ์ „์ฒด ๋ชฉ๋ก) + useEffect(() => { + const loadNumberingRules = async () => { + setIsLoading(true); + try { + const { getNumberingRules } = await import("@/lib/api/numberingRule"); + const response = await getNumberingRules(); + if (response.success && response.data) { + setNumberingRules(response.data.map((rule: any, index: number) => ({ + id: String(rule.ruleId || rule.id || `rule-${index}`), + name: rule.ruleName || rule.name || "์ด๋ฆ„ ์—†์Œ", + code: rule.ruleId || rule.code || "", + }))); + } + } catch (error) { + console.error("์ฑ„๋ฒˆ ๊ทœ์น™ ๋กœ๋“œ ์‹คํŒจ:", error); + setNumberingRules([]); + } finally { + setIsLoading(false); + } + }; + loadNumberingRules(); + }, []); + + // ์ฑ„๋ฒˆ ์„ค์ • ์—…๋ฐ์ดํŠธ + const updateRowNumbering = (updates: Partial) => { + const currentNumbering = row.tableCrud?.rowNumbering || { + enabled: false, + targetColumn: "", + numberingRuleId: "", + }; + + onUpdateRow({ + tableCrud: { + ...row.tableCrud, + allowCreate: row.tableCrud?.allowCreate || false, + allowUpdate: row.tableCrud?.allowUpdate || false, + allowDelete: row.tableCrud?.allowDelete || false, + rowNumbering: { + ...currentNumbering, + ...updates, + }, + }, + }); + }; + + return ( +
+
+
+ updateRowNumbering({ enabled: checked })} + className="scale-90" + /> + +
+
+ +

+ "์ถ”๊ฐ€" ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์ง€์ •ํ•œ ์ปฌ๋Ÿผ์— ์ž๋™์œผ๋กœ ๋ฒˆํ˜ธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + (์˜ต์…˜์„ค์ • > ์ฝ”๋“œ์„ค์ •์—์„œ ๋“ฑ๋กํ•œ ์ฑ„๋ฒˆ ๊ทœ์น™ ์‚ฌ์šฉ) +

+ + {rowNumbering?.enabled && ( +
+ {/* ๋Œ€์ƒ ์ปฌ๋Ÿผ ์„ ํƒ */} +
+ + +

+ ์ฑ„๋ฒˆ ๊ฒฐ๊ณผ๊ฐ€ ์ €์žฅ๋  ์ปฌ๋Ÿผ (์ˆ˜์ • ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋Š” ์ปฌ๋Ÿผ ์„ค์ •์—์„œ ์กฐ์ ˆ) +

+
+ + {/* ์ฑ„๋ฒˆ ๊ทœ์น™ ์„ ํƒ */} +
+ + + {numberingRules.length === 0 && !isLoading && ( +

+ ๋“ฑ๋ก๋œ ์ฑ„๋ฒˆ ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค. ์˜ต์…˜์„ค์ • > ์ฝ”๋“œ์„ค์ •์—์„œ ์ถ”๊ฐ€ํ•˜์„ธ์š”. +

+ )} +
+ + {/* ์„ค์ • ์š”์•ฝ */} + {rowNumbering.targetColumn && rowNumbering.numberingRuleId && ( +
+ "์ถ”๊ฐ€" ํด๋ฆญ ์‹œ {rowNumbering.targetColumn} ์ปฌ๋Ÿผ์— ์ž๋™ ์ฑ„๋ฒˆ +
+ )} +
+ )} +
+ ); +} + +// ๐Ÿ†• ๋ ˆ์ด์•„์›ƒ ์„ค์ • ์ „์šฉ ๋ชจ๋‹ฌ +function LayoutSettingsModal({ + open, + onOpenChange, + contentRows, + allTables, + dataSourceTable, + aggregations, + onSave, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + contentRows: CardContentRowConfig[]; + allTables: { tableName: string; displayName?: string }[]; + dataSourceTable: string; + aggregations: AggregationConfig[]; + onSave: (contentRows: CardContentRowConfig[]) => void; +}) { + // ๋กœ์ปฌ ์ƒํƒœ๋กœ ํ–‰ ๋ชฉ๋ก ๊ด€๋ฆฌ + const [localRows, setLocalRows] = useState(contentRows); + + // ๋ชจ๋‹ฌ ์—ด๋ฆด ๋•Œ ์ดˆ๊ธฐํ™” + useEffect(() => { + if (open) { + setLocalRows(contentRows); + } + }, [open, contentRows]); + + // ํ–‰ ์ถ”๊ฐ€ + const addRow = (type: CardContentRowConfig["type"]) => { + const newRow: CardContentRowConfig = { + id: `crow-${Date.now()}`, + type, + ...(type === "header" || type === "fields" + ? { columns: [], layout: "horizontal", gap: "16px" } + : {}), + ...(type === "aggregation" + ? { aggregationFields: [], aggregationLayout: "horizontal" } + : {}), + ...(type === "table" + ? { tableColumns: [], showTableHeader: true } + : {}), + }; + setLocalRows([...localRows, newRow]); + }; + + // ํ–‰ ์‚ญ์ œ + const removeRow = (index: number) => { + const newRows = [...localRows]; + newRows.splice(index, 1); + setLocalRows(newRows); + }; + + // ํ–‰ ์—…๋ฐ์ดํŠธ + const updateRow = (index: number, updates: Partial) => { + const newRows = [...localRows]; + newRows[index] = { ...newRows[index], ...updates }; + setLocalRows(newRows); + }; + + // ํ–‰ ์ˆœ์„œ ๋ณ€๊ฒฝ + const moveRow = (index: number, direction: "up" | "down") => { + const newIndex = direction === "up" ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= localRows.length) return; + + const newRows = [...localRows]; + [newRows[index], newRows[newIndex]] = [newRows[newIndex], newRows[index]]; + setLocalRows(newRows); + }; + + // ์ปฌ๋Ÿผ ์ถ”๊ฐ€ (header/fields์šฉ) + const addColumn = (rowIndex: number) => { + const newRows = [...localRows]; + const newCol: CardColumnConfig = { + id: `col-${Date.now()}`, + field: "", + label: "", + type: "text", + width: "auto", + editable: false, + }; + newRows[rowIndex].columns = [...(newRows[rowIndex].columns || []), newCol]; + setLocalRows(newRows); + }; + + // ์ปฌ๋Ÿผ ์‚ญ์ œ + const removeColumn = (rowIndex: number, colIndex: number) => { + const newRows = [...localRows]; + newRows[rowIndex].columns?.splice(colIndex, 1); + setLocalRows(newRows); + }; + + // ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ + const updateColumn = (rowIndex: number, colIndex: number, updates: Partial) => { + const newRows = [...localRows]; + if (newRows[rowIndex].columns) { + newRows[rowIndex].columns![colIndex] = { + ...newRows[rowIndex].columns![colIndex], + ...updates, + }; + } + setLocalRows(newRows); + }; + + // ์ง‘๊ณ„ ํ•„๋“œ ์ถ”๊ฐ€ + const addAggField = (rowIndex: number) => { + const newRows = [...localRows]; + const newAggField: AggregationDisplayConfig = { + aggregationResultField: "", + label: "", + }; + newRows[rowIndex].aggregationFields = [...(newRows[rowIndex].aggregationFields || []), newAggField]; + setLocalRows(newRows); + }; + + // ์ง‘๊ณ„ ํ•„๋“œ ์‚ญ์ œ + const removeAggField = (rowIndex: number, fieldIndex: number) => { + const newRows = [...localRows]; + newRows[rowIndex].aggregationFields?.splice(fieldIndex, 1); + setLocalRows(newRows); + }; + + // ์ง‘๊ณ„ ํ•„๋“œ ์—…๋ฐ์ดํŠธ + const updateAggField = (rowIndex: number, fieldIndex: number, updates: Partial) => { + const newRows = [...localRows]; + if (newRows[rowIndex].aggregationFields) { + newRows[rowIndex].aggregationFields![fieldIndex] = { + ...newRows[rowIndex].aggregationFields![fieldIndex], + ...updates, + }; + } + setLocalRows(newRows); + }; + + // ์ง‘๊ณ„ ํ•„๋“œ ์ˆœ์„œ ๋ณ€๊ฒฝ + const moveAggField = (rowIndex: number, fieldIndex: number, direction: "up" | "down") => { + const newRows = [...localRows]; + const fields = newRows[rowIndex].aggregationFields; + if (!fields) return; + + const newIndex = direction === "up" ? fieldIndex - 1 : fieldIndex + 1; + if (newIndex < 0 || newIndex >= fields.length) return; + + [fields[fieldIndex], fields[newIndex]] = [fields[newIndex], fields[fieldIndex]]; + setLocalRows(newRows); + }; + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + const addTableColumn = (rowIndex: number) => { + const newRows = [...localRows]; + const newCol: TableColumnConfig = { + id: `tcol-${Date.now()}`, + field: "", + label: "", + type: "text", + editable: false, + }; + newRows[rowIndex].tableColumns = [...(newRows[rowIndex].tableColumns || []), newCol]; + setLocalRows(newRows); + }; + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์‚ญ์ œ + const removeTableColumn = (rowIndex: number, colIndex: number) => { + const newRows = [...localRows]; + newRows[rowIndex].tableColumns?.splice(colIndex, 1); + setLocalRows(newRows); + }; + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์—…๋ฐ์ดํŠธ + const updateTableColumn = (rowIndex: number, colIndex: number, updates: Partial) => { + const newRows = [...localRows]; + if (newRows[rowIndex].tableColumns) { + newRows[rowIndex].tableColumns![colIndex] = { + ...newRows[rowIndex].tableColumns![colIndex], + ...updates, + }; + } + setLocalRows(newRows); + }; + + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ˆœ์„œ ๋ณ€๊ฒฝ + const moveTableColumn = (rowIndex: number, colIndex: number, direction: "up" | "down") => { + const newRows = [...localRows]; + const cols = newRows[rowIndex].tableColumns; + if (!cols) return; + + const newIndex = direction === "up" ? colIndex - 1 : colIndex + 1; + if (newIndex < 0 || newIndex >= cols.length) return; + + [cols[colIndex], cols[newIndex]] = [cols[newIndex], cols[colIndex]]; + setLocalRows(newRows); + }; + + // ์ €์žฅ + const handleSave = () => { + onSave(localRows); + onOpenChange(false); + }; + + // ํ–‰ ํƒ€์ž…๋ณ„ ์ƒ‰์ƒ + const getRowTypeColor = (type: CardContentRowConfig["type"]) => { + switch (type) { + case "header": return "bg-blue-100 border-blue-300"; + case "aggregation": return "bg-orange-100 border-orange-300"; + case "table": return "bg-green-100 border-green-300"; + case "fields": return "bg-purple-100 border-purple-300"; + default: return "bg-gray-100 border-gray-300"; + } + }; + + const getRowTypeLabel = (type: CardContentRowConfig["type"]) => { + switch (type) { + case "header": return "ํ—ค๋”"; + case "aggregation": return "์ง‘๊ณ„"; + case "table": return "ํ…Œ์ด๋ธ”"; + case "fields": return "ํ•„๋“œ"; + default: return type; + } + }; + + return ( + + + + ๋ ˆ์ด์•„์›ƒ ์„ค์ • + + ์นด๋“œ ๋‚ด๋ถ€์˜ ํ–‰(ํ—ค๋”, ์ง‘๊ณ„, ํ…Œ์ด๋ธ”, ํ•„๋“œ)์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ํ–‰์€ ์ˆœ์„œ๋ฅผ ๋ณ€๊ฒฝํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + + + +
+ +
+ {/* ํ–‰ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} +
+ + + + +
+ + {/* ํ–‰ ๋ชฉ๋ก */} + {localRows.length === 0 ? ( +
+

๋ ˆ์ด์•„์›ƒ ํ–‰์ด ์—†์Šต๋‹ˆ๋‹ค

+

+ ์œ„์˜ ๋ฒ„ํŠผ์œผ๋กœ ํ—ค๋”, ์ง‘๊ณ„, ํ…Œ์ด๋ธ”, ํ•„๋“œ ํ–‰์„ ์ถ”๊ฐ€ํ•˜์„ธ์š” +

+
+ ) : ( +
+ {localRows.map((row, index) => ( + updateRow(index, updates)} + onRemoveRow={() => removeRow(index)} + onMoveRow={(direction) => moveRow(index, direction)} + onAddColumn={() => addColumn(index)} + onRemoveColumn={(colIndex) => removeColumn(index, colIndex)} + onUpdateColumn={(colIndex, updates) => updateColumn(index, colIndex, updates)} + onAddAggField={() => addAggField(index)} + onRemoveAggField={(fieldIndex) => removeAggField(index, fieldIndex)} + onUpdateAggField={(fieldIndex, updates) => updateAggField(index, fieldIndex, updates)} + onMoveAggField={(fieldIndex, direction) => moveAggField(index, fieldIndex, direction)} + onAddTableColumn={() => addTableColumn(index)} + onRemoveTableColumn={(colIndex) => removeTableColumn(index, colIndex)} + onUpdateTableColumn={(colIndex, updates) => updateTableColumn(index, colIndex, updates)} + onMoveTableColumn={(colIndex, direction) => moveTableColumn(index, colIndex, direction)} + getRowTypeColor={getRowTypeColor} + getRowTypeLabel={getRowTypeLabel} + /> + ))} +
+ )} +
+
+
+ + + + + +
+
+ ); +} + +// ๋ ˆ์ด์•„์›ƒ ํ–‰ ์„ค์ • (๋ชจ๋‹ฌ์šฉ) +function LayoutRowConfigModal({ + row, + rowIndex, + totalRows, + allTables, + dataSourceTable, + aggregations, + onUpdateRow, + onRemoveRow, + onMoveRow, + onAddColumn, + onRemoveColumn, + onUpdateColumn, + onAddAggField, + onRemoveAggField, + onUpdateAggField, + onMoveAggField, + onAddTableColumn, + onRemoveTableColumn, + onUpdateTableColumn, + onMoveTableColumn, + getRowTypeColor, + getRowTypeLabel, +}: { + row: CardContentRowConfig; + rowIndex: number; + totalRows: number; + allTables: { tableName: string; displayName?: string }[]; + dataSourceTable: string; + aggregations: AggregationConfig[]; + onUpdateRow: (updates: Partial) => void; + onRemoveRow: () => void; + onMoveRow: (direction: "up" | "down") => void; + onAddColumn: () => void; + onRemoveColumn: (colIndex: number) => void; + onUpdateColumn: (colIndex: number, updates: Partial) => void; + onAddAggField: () => void; + onRemoveAggField: (fieldIndex: number) => void; + onUpdateAggField: (fieldIndex: number, updates: Partial) => void; + onMoveAggField: (fieldIndex: number, direction: "up" | "down") => void; + onAddTableColumn: () => void; + onRemoveTableColumn: (colIndex: number) => void; + onUpdateTableColumn: (colIndex: number, updates: Partial) => void; + onMoveTableColumn: (colIndex: number, direction: "up" | "down") => void; + getRowTypeColor: (type: CardContentRowConfig["type"]) => string; + getRowTypeLabel: (type: CardContentRowConfig["type"]) => string; +}) { + const [isExpanded, setIsExpanded] = useState(true); + + return ( +
+ {/* ํ–‰ ํ—ค๋” */} +
+
+ {/* ์ˆœ์„œ ๋ณ€๊ฒฝ ๋ฒ„ํŠผ */} +
+ + +
+ {getRowTypeLabel(row.type)} {rowIndex + 1} + + {row.type === "header" || row.type === "fields" + ? `${(row.columns || []).length}๊ฐœ ์ปฌ๋Ÿผ` + : row.type === "aggregation" + ? `${(row.aggregationFields || []).length}๊ฐœ ํ•„๋“œ` + : row.type === "table" + ? `${(row.tableColumns || []).length}๊ฐœ ์ปฌ๋Ÿผ` + : ""} + +
+
+ + +
+
+ + {/* ํ–‰ ๋‚ด์šฉ */} + {isExpanded && ( +
+ {/* ํ—ค๋”/ํ•„๋“œ ํƒ€์ž… */} + {(row.type === "header" || row.type === "fields") && ( +
+
+
+ + +
+
+ + +
+
+ + onUpdateRow({ gap: e.target.value })} + placeholder="16px" + className="h-8 text-xs" + /> +
+
+ + {/* ์ปฌ๋Ÿผ ๋ชฉ๋ก */} +
+
+ + +
+ {(row.columns || []).map((col, colIndex) => ( +
+
+ ์ปฌ๋Ÿผ {colIndex + 1} + +
+
+
+ + onUpdateColumn(colIndex, { field: value })} + placeholder="ํ•„๋“œ ์„ ํƒ" + /> +
+
+ + onUpdateColumn(colIndex, { label: e.target.value })} + placeholder="๋ผ๋ฒจ" + className="h-6 text-[10px]" + /> +
+
+ + +
+
+ + onUpdateColumn(colIndex, { width: e.target.value })} + placeholder="auto" + className="h-6 text-[10px]" + /> +
+
+
+ ))} +
+
+ )} + + {/* ์ง‘๊ณ„ ํƒ€์ž… */} + {row.type === "aggregation" && ( +
+
+
+ + +
+ {row.aggregationLayout === "grid" && ( +
+ + +
+ )} +
+ + {/* ์ง‘๊ณ„ ํ•„๋“œ ๋ชฉ๋ก */} +
+
+ + +
+ {aggregations.filter(a => !a.hidden).length === 0 && ( +

+ ๊ทธ๋ฃน ํƒญ์—์„œ ๋จผ์ € ์ง‘๊ณ„๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š” +

+ )} + {(row.aggregationFields || []).map((field, fieldIndex) => ( +
+
+
+ + + ์ง‘๊ณ„ {fieldIndex + 1} +
+ +
+
+
+ + +
+
+ + onUpdateAggField(fieldIndex, { label: e.target.value })} + placeholder="๋ผ๋ฒจ" + className="h-6 text-[10px]" + /> +
+
+ + +
+
+ + +
+
+
+ ))} +
+
+ )} + + {/* ํ…Œ์ด๋ธ” ํƒ€์ž… */} + {row.type === "table" && ( +
+
+
+ + onUpdateRow({ tableTitle: e.target.value })} + placeholder="ํ…Œ์ด๋ธ” ์ œ๋ชฉ" + className="h-8 text-xs" + /> +
+
+ +
+ onUpdateRow({ showTableHeader: checked })} + className="scale-90" + /> + {row.showTableHeader !== false ? "ํ‘œ์‹œ" : "์ˆจ๊น€"} +
+
+
+ + onUpdateRow({ tableMaxHeight: e.target.value })} + placeholder="์˜ˆ: 300px" + className="h-8 text-xs" + /> +
+
+ + {/* ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • */} +
+
+ + onUpdateRow({ + tableDataSource: { ...row.tableDataSource, enabled: checked, sourceTable: "", joinConditions: [] } + })} + className="scale-90" + /> +
+ {row.tableDataSource?.enabled && ( +
+
+ + +
+
+ )} +
+ + {/* CRUD ์„ค์ • */} +
+ +
+
+ + onUpdateRow({ + tableCrud: { ...row.tableCrud, allowCreate: checked, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: row.tableCrud?.allowDelete || false }, + }) + } + className="scale-90" + /> + +
+
+ + onUpdateRow({ + tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: checked, allowDelete: row.tableCrud?.allowDelete || false }, + }) + } + className="scale-90" + /> + +
+
+ + onUpdateRow({ + tableCrud: { ...row.tableCrud, allowCreate: row.tableCrud?.allowCreate || false, allowUpdate: row.tableCrud?.allowUpdate || false, allowDelete: checked }, + }) + } + className="scale-90" + /> + +
+
+ {row.tableCrud?.allowDelete && ( +
+ + onUpdateRow({ + tableCrud: { ...row.tableCrud!, deleteConfirm: { enabled: checked } }, + }) + } + className="scale-75" + /> + +
+ )} +
+ + {/* ๐Ÿ†• v3.13: ํ–‰ ์ถ”๊ฐ€ ์‹œ ์ž๋™ ์ฑ„๋ฒˆ ์„ค์ • */} + {row.tableCrud?.allowCreate && ( + + )} + + {/* ๐Ÿ†• v3.12: ์—ฐ๋™ ์ €์žฅ ์„ค์ • */} + + + {/* ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋ชฉ๋ก */} +
+
+ + +
+ {(row.tableColumns || []).map((col, colIndex) => ( +
+
+
+ + + ์ปฌ๋Ÿผ {colIndex + 1} +
+ +
+
+
+ + onUpdateTableColumn(colIndex, { field: value })} + placeholder="ํ•„๋“œ ์„ ํƒ" + /> +
+
+ + onUpdateTableColumn(colIndex, { label: e.target.value })} + placeholder="๋ผ๋ฒจ" + className="h-6 text-[10px]" + /> +
+
+ + +
+
+ +
+ onUpdateTableColumn(colIndex, { editable: checked })} + className="scale-75" + /> + {col.editable ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"} +
+
+
+ +
+ onUpdateTableColumn(colIndex, { hidden: checked })} + className="scale-75" + /> + {col.hidden ? "์˜ˆ" : "์•„๋‹ˆ์˜ค"} +
+
+
+
+ ))} +
+
+ )} +
+ )} +
+ ); +} + // ์ง‘๊ณ„ ์„ค์ • ์•„์ดํ…œ (๋กœ์ปฌ ์ƒํƒœ ๊ด€๋ฆฌ๋กœ ์ž…๋ ฅ ์‹œ ๋ฆฌ๋ Œ๋”๋ง ๋ฐฉ์ง€) // ๐Ÿ†• v3.2: ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ๋ฐ ๊ฐ€์ƒ ์ง‘๊ณ„(formula) ์ง€์› function AggregationConfigItem({ @@ -1194,6 +3198,12 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM const [allTables, setAllTables] = useState<{ tableName: string; displayName?: string }[]>([]); + // ์ง‘๊ณ„ ์„ค์ • ๋ชจ๋‹ฌ ์ƒํƒœ + const [aggregationModalOpen, setAggregationModalOpen] = useState(false); + + // ๋ ˆ์ด์•„์›ƒ ์„ค์ • ๋ชจ๋‹ฌ ์ƒํƒœ + const [layoutModalOpen, setLayoutModalOpen] = useState(false); + // ํƒญ ์ƒํƒœ ์œ ์ง€ (๋ชจ๋“ˆ ๋ ˆ๋ฒจ ๋ณ€์ˆ˜์™€ ๋™๊ธฐํ™”) const [activeTab, setActiveTab] = useState(persistedActiveTab); @@ -1536,6 +3546,21 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM updateConfig({ contentRows: newRows }); }; + // ํ–‰(Row) ์ˆœ์„œ ๋ณ€๊ฒฝ + const moveContentRow = (rowIndex: number, direction: "up" | "down") => { + const rows = localConfig.contentRows || []; + if (rows.length <= 1) return; + + const newIndex = direction === "up" ? rowIndex - 1 : rowIndex + 1; + if (newIndex < 0 || newIndex >= rows.length) return; + + // ํ–‰ ์œ„์น˜ ๊ตํ™˜ + const newRows = [...rows]; + [newRows[rowIndex], newRows[newIndex]] = [newRows[newIndex], newRows[rowIndex]]; + + updateConfig({ contentRows: newRows }); + }; + // === (๋ ˆ๊ฑฐ์‹œ) Simple ๋ชจ๋“œ ํ–‰/์ปฌ๋Ÿผ ๊ด€๋ จ ํ•จ์ˆ˜ === const addRow = () => { const newRow: CardRowConfig = { @@ -1760,48 +3785,42 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM
-
- -
-

- ์ปฌ๋Ÿผ ์ง‘๊ณ„: ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ์˜ ํ•ฉ๊ณ„/๊ฐœ์ˆ˜ ๋“ฑ | ๊ฐ€์ƒ ์ง‘๊ณ„: ์—ฐ์‚ฐ์‹์œผ๋กœ ๊ณ„์‚ฐ -

- + {/* ํ˜„์žฌ ์ง‘๊ณ„ ๋ชฉ๋ก ์š”์•ฝ */} + {(localConfig.grouping?.aggregations || []).length > 0 ? ( +
{(localConfig.grouping?.aggregations || []).map((agg, index) => ( - updateAggregation(index, updates)} - onRemove={() => removeAggregation(index)} - /> - ))} - - {(localConfig.grouping?.aggregations || []).length === 0 && ( +
+ + {agg.hidden && [์ˆจ๊น€]} + {agg.label || agg.resultField} + + + {agg.sourceType === "formula" ? "๊ฐ€์ƒ" : agg.type?.toUpperCase() || "SUM"} + +
+ ))} +
+ ) : (

์ง‘๊ณ„ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค

@@ -1814,86 +3833,78 @@ export function RepeatScreenModalConfigPanel({ config, onChange }: RepeatScreenM {/* === ๋ ˆ์ด์•„์›ƒ ์„ค์ • ํƒญ === */} -
- {/* ํ–‰ ์ถ”๊ฐ€ ๋ฒ„ํŠผ๋“ค */} -
-

ํ–‰ ์ถ”๊ฐ€

-
+
+
+

๋ ˆ์ด์•„์›ƒ ํ–‰

-