diff --git a/backend-node/src/controllers/tableManagementController.ts b/backend-node/src/controllers/tableManagementController.ts index c4c29503..8b1f859d 100644 --- a/backend-node/src/controllers/tableManagementController.ts +++ b/backend-node/src/controllers/tableManagementController.ts @@ -62,10 +62,10 @@ export async function getColumnList( try { const { tableName } = req.params; const { page = 1, size = 50 } = req.query; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -74,7 +74,9 @@ export async function getColumnList( [req.user.userId] ); companyCode = userResult[0]?.company_code; - logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์ปฌ๋Ÿผ ๋ชฉ๋ก): ${req.user.userId} โ†’ ${companyCode}`); + logger.info( + `DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์ปฌ๋Ÿผ ๋ชฉ๋ก): ${req.user.userId} โ†’ ${companyCode}` + ); } logger.info( @@ -139,10 +141,10 @@ export async function updateColumnSettings( try { const { tableName, columnName } = req.params; const settings: ColumnSettings = req.body; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -154,7 +156,9 @@ export async function updateColumnSettings( logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ: ${req.user.userId} โ†’ ${companyCode}`); } - logger.info(`=== ์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}, company: ${companyCode} ===`); + logger.info( + `=== ์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}.${columnName}, company: ${companyCode} ===` + ); if (!tableName || !columnName) { const response: ApiResponse = { @@ -194,7 +198,8 @@ export async function updateColumnSettings( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); @@ -209,7 +214,9 @@ export async function updateColumnSettings( companyCode // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ ); - logger.info(`์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}.${columnName}, company: ${companyCode}`); + logger.info( + `์ปฌ๋Ÿผ ์„ค์ • ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ: ${tableName}.${columnName}, company: ${companyCode}` + ); const response: ApiResponse = { success: true, @@ -243,10 +250,10 @@ export async function updateAllColumnSettings( try { const { tableName } = req.params; const columnSettings: ColumnSettings[] = req.body; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -264,7 +271,9 @@ export async function updateAllColumnSettings( logger.info(`[DEBUG] req.user?.userId: ${req.user?.userId}`); logger.info(`[DEBUG] companyCode ์ตœ์ข…๊ฐ’: ${companyCode}`); - logger.info(`=== ์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}, company: ${companyCode} ===`); + logger.info( + `=== ์ „์ฒด ์ปฌ๋Ÿผ ์„ค์ • ์ผ๊ด„ ์—…๋ฐ์ดํŠธ ์‹œ์ž‘: ${tableName}, company: ${companyCode} ===` + ); if (!tableName) { const response: ApiResponse = { @@ -305,7 +314,8 @@ export async function updateAllColumnSettings( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); @@ -543,10 +553,10 @@ export async function updateColumnInputType( try { const { tableName, columnName } = req.params; const { inputType, detailSettings } = req.body; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -588,7 +598,8 @@ export async function updateColumnInputType( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); @@ -1085,10 +1096,10 @@ export async function getColumnWebTypes( ): Promise { try { const { tableName } = req.params; - + // ๐Ÿ”ฅ ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (JWT์—์„œ ๋˜๋Š” DB์—์„œ ์กฐํšŒ) let companyCode = req.user?.companyCode; - + if (!companyCode && req.user?.userId) { // JWT์— ์—†์œผ๋ฉด DB์—์„œ ์กฐํšŒ const { query } = require("../database/db"); @@ -1097,7 +1108,9 @@ export async function getColumnWebTypes( [req.user.userId] ); companyCode = userResult[0]?.company_code; - logger.info(`DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์กฐํšŒ): ${req.user.userId} โ†’ ${companyCode}`); + logger.info( + `DB์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ ์กฐํšŒ (์กฐํšŒ): ${req.user.userId} โ†’ ${companyCode}` + ); } logger.info( @@ -1129,7 +1142,8 @@ export async function getColumnWebTypes( message: "ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.", error: { code: "MISSING_COMPANY_CODE", - details: "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", + details: + "์‚ฌ์šฉ์ž ์ •๋ณด์—์„œ ํšŒ์‚ฌ ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ํ•˜์„ธ์š”.", }, }; res.status(400).json(response); diff --git a/backend-node/src/routes/dataRoutes.ts b/backend-node/src/routes/dataRoutes.ts index fb0f1518..7d1f0a88 100644 --- a/backend-node/src/routes/dataRoutes.ts +++ b/backend-node/src/routes/dataRoutes.ts @@ -51,21 +51,26 @@ router.get( } } + // ํšŒ์‚ฌ ์ฝ”๋“œ ์ถ”์ถœ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ ํ•„ํ„ฐ๋ง) + const userCompany = req.user?.companyCode; + console.log(`๐Ÿ”— ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ:`, { leftTable, rightTable, leftColumn, rightColumn, leftValue, + userCompany, }); - // ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ + // ์กฐ์ธ ๋ฐ์ดํ„ฐ ์กฐํšŒ (ํšŒ์‚ฌ ์ฝ”๋“œ ์ „๋‹ฌ) const result = await dataService.getJoinedData( leftTable as string, rightTable as string, leftColumn as string, rightColumn as string, - leftValue as string + leftValue as string, + userCompany ); if (!result.success) { @@ -352,8 +357,25 @@ router.post( console.log(`โž• ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ: ${tableName}`, data); + // company_code์™€ company_name ์ž๋™ ์ถ”๊ฐ€ (๋ฉ€ํ‹ฐํ…Œ๋„Œ์‹œ) + const enrichedData = { ...data }; + + // ํ…Œ์ด๋ธ”์— company_code ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ + const hasCompanyCode = await dataService.checkColumnExists(tableName, "company_code"); + if (hasCompanyCode && req.user?.companyCode) { + enrichedData.company_code = req.user.companyCode; + console.log(`๐Ÿข company_code ์ž๋™ ์ถ”๊ฐ€: ${req.user.companyCode}`); + } + + // ํ…Œ์ด๋ธ”์— company_name ์ปฌ๋Ÿผ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๊ณ  ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ + const hasCompanyName = await dataService.checkColumnExists(tableName, "company_name"); + if (hasCompanyName && req.user?.companyName) { + enrichedData.company_name = req.user.companyName; + console.log(`๐Ÿข company_name ์ž๋™ ์ถ”๊ฐ€: ${req.user.companyName}`); + } + // ๋ ˆ์ฝ”๋“œ ์ƒ์„ฑ - const result = await dataService.createRecord(tableName, data); + const result = await dataService.createRecord(tableName, enrichedData); if (!result.success) { return res.status(400).json(result); @@ -437,6 +459,58 @@ router.put( * ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ API * DELETE /api/data/{tableName}/{id} */ +/** + * ๋ณตํ•ฉํ‚ค ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ API (POST) + * POST /api/data/:tableName/delete + * Body: { user_id: 'xxx', dept_code: 'yyy' } + */ +router.post( + "/:tableName/delete", + authenticateToken, + async (req: AuthenticatedRequest, res) => { + try { + const { tableName } = req.params; + const compositeKey = req.body; + + // ์ž…๋ ฅ๊ฐ’ ๊ฒ€์ฆ + if (!tableName || typeof tableName !== "string") { + return res.status(400).json({ + success: false, + message: "ํ…Œ์ด๋ธ”๋ช…์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.", + error: "INVALID_TABLE_NAME", + }); + } + + if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) { + return res.status(400).json({ + success: false, + message: "์œ ํšจํ•˜์ง€ ์•Š์€ ํ…Œ์ด๋ธ”๋ช…์ž…๋‹ˆ๋‹ค.", + error: "INVALID_TABLE_NAME", + }); + } + + console.log(`๐Ÿ—‘๏ธ ๋ณตํ•ฉํ‚ค ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ: ${tableName}`, compositeKey); + + // ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ (๋ณตํ•ฉํ‚ค ๊ฐ์ฒด ์ „๋‹ฌ) + const result = await dataService.deleteRecord(tableName, compositeKey); + + if (!result.success) { + return res.status(400).json(result); + } + + console.log(`โœ… ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์„ฑ๊ณต: ${tableName}`); + return res.json(result); + } catch (error: any) { + console.error(`๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์˜ค๋ฅ˜ (${req.params.tableName}):`, error); + return res.status(500).json({ + success: false, + message: "๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + error: error.message, + }); + } + } +); + router.delete( "/:tableName/:id", authenticateToken, diff --git a/backend-node/src/services/authService.ts b/backend-node/src/services/authService.ts index 7c4f4c8d..11e34576 100644 --- a/backend-node/src/services/authService.ts +++ b/backend-node/src/services/authService.ts @@ -165,12 +165,13 @@ export class AuthService { const authNames = authResult.map((row) => row.auth_name).join(","); // 3. ํšŒ์‚ฌ ์ •๋ณด ์กฐํšŒ (Raw Query ์ „ํ™˜) - // Note: ํ˜„์žฌ ํšŒ์‚ฌ ์ •๋ณด๋Š” PersonBean์— ์ง์ ‘ ์‚ฌ์šฉ๋˜์ง€ ์•Š์ง€๋งŒ ํ–ฅํ›„ ํ™•์žฅ์„ ์œ„ํ•ด ์œ ์ง€ const companyResult = await query<{ company_name: string }>( "SELECT company_name FROM company_mng WHERE company_code = $1", [userInfo.company_code || "ILSHIN"] ); + const companyName = companyResult.length > 0 ? companyResult[0].company_name : undefined; + // DB์—์„œ ์กฐํšŒํ•œ ์›๋ณธ ์‚ฌ์šฉ์ž ์ •๋ณด ์ƒ์„ธ ๋กœ๊ทธ //console.log("๐Ÿ” AuthService - DB ์›๋ณธ ์‚ฌ์šฉ์ž ์ •๋ณด:", { // userId: userInfo.user_id, @@ -205,6 +206,7 @@ export class AuthService { partnerObjid: userInfo.partner_objid || undefined, authName: authNames || undefined, companyCode: companyCode, + companyName: companyName, // ํšŒ์‚ฌ๋ช… ์ถ”๊ฐ€ photo: userInfo.photo ? `data:image/jpeg;base64,${Buffer.from(userInfo.photo).toString("base64")}` : undefined, diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index 462ebb4d..0cf7ad6b 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -231,6 +231,9 @@ class DataService { const columns = await this.getTableColumnsSimple(tableName); + // PK ์ปฌ๋Ÿผ ์ •๋ณด ์กฐํšŒ + const pkColumns = await this.getPrimaryKeyColumns(tableName); + // ์ปฌ๋Ÿผ ๋ผ๋ฒจ ์ •๋ณด ์ถ”๊ฐ€ const columnsWithLabels = await Promise.all( columns.map(async (column) => { @@ -244,6 +247,7 @@ class DataService { dataType: column.data_type, isNullable: column.is_nullable === "YES", defaultValue: column.column_default, + isPrimaryKey: pkColumns.includes(column.column_name), // PK ์—ฌ๋ถ€ ์ถ”๊ฐ€ }; }) ); @@ -262,6 +266,26 @@ class DataService { } } + /** + * ํ…Œ์ด๋ธ”์˜ Primary Key ์ปฌ๋Ÿผ ๋ชฉ๋ก ์กฐํšŒ + */ + private async getPrimaryKeyColumns(tableName: string): Promise { + try { + const result = await query<{ attname: string }>( + `SELECT a.attname + FROM pg_index i + JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) + WHERE i.indrelid = $1::regclass AND i.indisprimary`, + [tableName] + ); + + return result.map((row) => row.attname); + } catch (error) { + console.error(`PK ์ปฌ๋Ÿผ ์กฐํšŒ ์˜ค๋ฅ˜ (${tableName}):`, error); + return []; + } + } + /** * ํ…Œ์ด๋ธ” ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ */ @@ -286,7 +310,7 @@ class DataService { /** * ํŠน์ • ์ปฌ๋Ÿผ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ */ - private async checkColumnExists( + async checkColumnExists( tableName: string, columnName: string ): Promise { @@ -409,7 +433,8 @@ class DataService { rightTable: string, leftColumn: string, rightColumn: string, - leftValue?: string | number + leftValue?: string | number, + userCompany?: string ): Promise> { try { // ์™ผ์ชฝ ํ…Œ์ด๋ธ” ์ ‘๊ทผ ๊ฒ€์ฆ @@ -425,18 +450,42 @@ class DataService { } let queryText = ` - SELECT r.* + SELECT DISTINCT r.* FROM "${rightTable}" r INNER JOIN "${leftTable}" l ON l."${leftColumn}" = r."${rightColumn}" `; const values: any[] = []; + const whereConditions: string[] = []; + let paramIndex = 1; + + // ์ขŒ์ธก ๊ฐ’ ํ•„ํ„ฐ๋ง if (leftValue !== undefined && leftValue !== null) { - queryText += ` WHERE l."${leftColumn}" = $1`; + whereConditions.push(`l."${leftColumn}" = $${paramIndex}`); values.push(leftValue); + paramIndex++; } + // ์šฐ์ธก ํ…Œ์ด๋ธ” ํšŒ์‚ฌ๋ณ„ ํ•„ํ„ฐ๋ง (company_code ์ปฌ๋Ÿผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ) + if (userCompany && userCompany !== "*") { + const hasCompanyCode = await this.checkColumnExists(rightTable, "company_code"); + if (hasCompanyCode) { + whereConditions.push(`r.company_code = $${paramIndex}`); + values.push(userCompany); + paramIndex++; + console.log(`๐Ÿข ์šฐ์ธก ํŒจ๋„ ํšŒ์‚ฌ๋ณ„ ํ•„ํ„ฐ๋ง ์ ์šฉ: ${rightTable}.company_code = ${userCompany}`); + } + } + + // WHERE ์ ˆ ์ถ”๊ฐ€ + if (whereConditions.length > 0) { + queryText += ` WHERE ${whereConditions.join(" AND ")}`; + } + + console.log("๐Ÿ” ์กฐ์ธ ์ฟผ๋ฆฌ ์‹คํ–‰:", queryText); + console.log("๐Ÿ“Š ์กฐ์ธ ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ:", values); + const result = await query(queryText, values); return { @@ -512,6 +561,11 @@ class DataService { return validation.error!; } + // _relationInfo ์ถ”์ถœ (์กฐ์ธ ๊ด€๊ณ„ ์—…๋ฐ์ดํŠธ์šฉ) + const relationInfo = data._relationInfo; + const cleanData = { ...data }; + delete cleanData._relationInfo; + // Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ const pkResult = await query<{ attname: string }>( `SELECT a.attname @@ -526,8 +580,8 @@ class DataService { pkColumn = pkResult[0].attname; } - const columns = Object.keys(data); - const values = Object.values(data); + const columns = Object.keys(cleanData); + const values = Object.values(cleanData); const setClause = columns .map((col, index) => `"${col}" = $${index + 1}`) .join(", "); @@ -550,6 +604,35 @@ class DataService { }; } + // ๐Ÿ”— ์กฐ์ธ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ”์˜ FK๋„ ์—…๋ฐ์ดํŠธ + if (relationInfo && relationInfo.rightTable && relationInfo.leftColumn && relationInfo.rightColumn) { + const { rightTable, leftColumn, rightColumn, oldLeftValue } = relationInfo; + const newLeftValue = cleanData[leftColumn]; + + // leftColumn ๊ฐ’์ด ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ์—๋งŒ ์šฐ์ธก ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ + if (newLeftValue !== undefined && newLeftValue !== oldLeftValue) { + console.log("๐Ÿ”— ์กฐ์ธ ๊ด€๊ณ„ FK ์—…๋ฐ์ดํŠธ:", { + rightTable, + rightColumn, + oldValue: oldLeftValue, + newValue: newLeftValue, + }); + + try { + const updateRelatedQuery = ` + UPDATE "${rightTable}" + SET "${rightColumn}" = $1 + WHERE "${rightColumn}" = $2 + `; + const updateResult = await query(updateRelatedQuery, [newLeftValue, oldLeftValue]); + console.log(`โœ… ์—ฐ๊ฒฐ๋œ ${rightTable} ํ…Œ์ด๋ธ”์˜ ${updateResult.length}๊ฐœ ๋ ˆ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ`); + } catch (relError) { + console.error("โŒ ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ ์‹คํŒจ:", relError); + // ์—ฐ๊ฒฐ๋œ ํ…Œ์ด๋ธ” ์—…๋ฐ์ดํŠธ ์‹คํŒจ ์‹œ ๋กค๋ฐฑ์€ ํ•˜์ง€ ์•Š๊ณ  ๊ฒฝ๊ณ ๋งŒ ๋กœ๊ทธ + } + } + } + return { success: true, data: result[0], @@ -569,7 +652,7 @@ class DataService { */ async deleteRecord( tableName: string, - id: string | number + id: string | number | Record ): Promise> { try { // ํ…Œ์ด๋ธ” ์ ‘๊ทผ ๊ฒ€์ฆ @@ -578,28 +661,53 @@ class DataService { return validation.error!; } - // Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ + // Primary Key ์ปฌ๋Ÿผ ์ฐพ๊ธฐ (๋ณตํ•ฉํ‚ค ์ง€์›) const pkResult = await query<{ attname: string }>( `SELECT a.attname FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey) - WHERE i.indrelid = $1::regclass AND i.indisprimary`, + WHERE i.indrelid = $1::regclass AND i.indisprimary + ORDER BY a.attnum`, [tableName] ); - let pkColumn = "id"; - if (pkResult.length > 0) { - pkColumn = pkResult[0].attname; + let whereClauses: string[] = []; + let params: any[] = []; + + if (pkResult.length > 1) { + // ๋ณตํ•ฉํ‚ค์ธ ๊ฒฝ์šฐ: id๊ฐ€ ๊ฐ์ฒด์—ฌ์•ผ ํ•จ + console.log(`๐Ÿ”‘ ๋ณตํ•ฉํ‚ค ํ…Œ์ด๋ธ”: ${tableName}, PK: [${pkResult.map(r => r.attname).join(', ')}]`); + + if (typeof id === 'object' && !Array.isArray(id)) { + // id๊ฐ€ ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ: { user_id: 'xxx', dept_code: 'yyy' } + pkResult.forEach((pk, index) => { + whereClauses.push(`"${pk.attname}" = $${index + 1}`); + params.push(id[pk.attname]); + }); + } else { + // id๊ฐ€ ๋ฌธ์ž์—ด/์ˆซ์ž์ธ ๊ฒฝ์šฐ: ์ฒซ ๋ฒˆ์งธ PK๋งŒ ์‚ฌ์šฉ (ํ•˜์œ„ ํ˜ธํ™˜์„ฑ) + whereClauses.push(`"${pkResult[0].attname}" = $1`); + params.push(id); + } + } else { + // ๋‹จ์ผํ‚ค์ธ ๊ฒฝ์šฐ + const pkColumn = pkResult.length > 0 ? pkResult[0].attname : "id"; + whereClauses.push(`"${pkColumn}" = $1`); + params.push(typeof id === 'object' ? id[pkColumn] : id); } - const queryText = `DELETE FROM "${tableName}" WHERE "${pkColumn}" = $1`; - await query(queryText, [id]); + const queryText = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(' AND ')}`; + console.log(`๐Ÿ—‘๏ธ ์‚ญ์ œ ์ฟผ๋ฆฌ:`, queryText, params); + + const result = await query(queryText, params); + + console.log(`โœ… ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์™„๋ฃŒ: ${tableName}, ์˜ํ–ฅ๋ฐ›์€ ํ–‰: ${result.length}`); return { success: true, }; } catch (error) { - console.error(`๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์˜ค๋ฅ˜ (${tableName}/${id}):`, error); + console.error(`๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์˜ค๋ฅ˜ (${tableName}):`, error); return { success: false, message: "๋ ˆ์ฝ”๋“œ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", diff --git a/backend-node/src/types/auth.ts b/backend-node/src/types/auth.ts index 35a2c0f5..6abd1e39 100644 --- a/backend-node/src/types/auth.ts +++ b/backend-node/src/types/auth.ts @@ -61,6 +61,7 @@ export interface PersonBean { partnerObjid?: string; authName?: string; companyCode?: string; + companyName?: string; // ํšŒ์‚ฌ๋ช… ์ถ”๊ฐ€ photo?: string; locale?: string; // ๊ถŒํ•œ ๋ ˆ๋ฒจ ์ •๋ณด (3๋‹จ๊ณ„ ์ฒด๊ณ„) @@ -94,6 +95,7 @@ export interface JwtPayload { userName: string; deptName?: string; companyCode?: string; + companyName?: string; // ํšŒ์‚ฌ๋ช… ์ถ”๊ฐ€ userType?: string; userTypeName?: string; iat?: number; diff --git a/backend-node/src/utils/jwtUtils.ts b/backend-node/src/utils/jwtUtils.ts index f65781fc..44f75cbc 100644 --- a/backend-node/src/utils/jwtUtils.ts +++ b/backend-node/src/utils/jwtUtils.ts @@ -17,6 +17,7 @@ export class JwtUtils { userName: userInfo.userName, deptName: userInfo.deptName, companyCode: userInfo.companyCode, + companyName: userInfo.companyName, // ํšŒ์‚ฌ๋ช… ์ถ”๊ฐ€ userType: userInfo.userType, userTypeName: userInfo.userTypeName, }; @@ -45,6 +46,7 @@ export class JwtUtils { userName: decoded.userName, deptName: decoded.deptName, companyCode: decoded.companyCode, + companyName: decoded.companyName, // ํšŒ์‚ฌ๋ช… ์ถ”๊ฐ€ userType: decoded.userType, userTypeName: decoded.userTypeName, }; diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 4401bb12..6823e2d5 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -183,6 +183,15 @@ body { background: hsl(var(--background)); } +/* Button ๊ธฐ๋ณธ ์ปค์„œ ์Šคํƒ€์ผ */ +button { + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; +} + /* ===== Dialog/Modal Overlay ===== */ /* Radix UI Dialog Overlay - 60% ๋ถˆํˆฌ๋ช…๋„ ๋ฐฐ๊ฒฝ */ [data-radix-dialog-overlay], diff --git a/frontend/lib/api/data.ts b/frontend/lib/api/data.ts index 3f53db1f..208308ff 100644 --- a/frontend/lib/api/data.ts +++ b/frontend/lib/api/data.ts @@ -83,7 +83,7 @@ export const dataApi = { */ createRecord: async (tableName: string, data: Record): Promise => { const response = await apiClient.post(`/data/${tableName}`, data); - return response.data?.data || response.data; + return response.data; // success, data, message ํฌํ•จ๋œ ์ „์ฒด ์‘๋‹ต ๋ฐ˜ํ™˜ }, /** @@ -94,15 +94,23 @@ export const dataApi = { */ updateRecord: async (tableName: string, id: string | number, data: Record): Promise => { const response = await apiClient.put(`/data/${tableName}/${id}`, data); - return response.data?.data || response.data; + return response.data; // success, data, message ํฌํ•จ๋œ ์ „์ฒด ์‘๋‹ต ๋ฐ˜ํ™˜ }, /** * ๋ ˆ์ฝ”๋“œ ์‚ญ์ œ * @param tableName ํ…Œ์ด๋ธ”๋ช… - * @param id ๋ ˆ์ฝ”๋“œ ID + * @param id ๋ ˆ์ฝ”๋“œ ID ๋˜๋Š” ๋ณตํ•ฉํ‚ค ๊ฐ์ฒด */ - deleteRecord: async (tableName: string, id: string | number): Promise => { - await apiClient.delete(`/data/${tableName}/${id}`); + deleteRecord: async (tableName: string, id: string | number | Record): Promise => { + // ๋ณตํ•ฉํ‚ค ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ POST๋กœ ์ „๋‹ฌ + if (typeof id === 'object' && !Array.isArray(id)) { + const response = await apiClient.post(`/data/${tableName}/delete`, id); + return response.data; + } + + // ๋‹จ์ผ ID์ธ ๊ฒฝ์šฐ ๊ธฐ์กด ๋ฐฉ์‹ + const response = await apiClient.delete(`/data/${tableName}/${id}`); + return response.data; // success, message ํฌํ•จ๋œ ์ „์ฒด ์‘๋‹ต ๋ฐ˜ํ™˜ }, }; diff --git a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx index f5eefe3c..8dda7864 100644 --- a/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx +++ b/frontend/lib/registry/components/file-upload/FileUploadComponent.tsx @@ -771,7 +771,17 @@ const FileUploadComponent: React.FC = ({ return; } - console.log("๐Ÿ–ผ๏ธ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹œ์ž‘:", file.realFileName); + // objid๊ฐ€ ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ๋กœ๋“œ ์ค‘๋‹จ + if (!file.objid || file.objid === "0" || file.objid === "") { + console.warn("โš ๏ธ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ: objid๊ฐ€ ์—†์Œ", file); + setRepresentativeImageUrl(null); + return; + } + + console.log("๐Ÿ–ผ๏ธ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹œ์ž‘:", { + objid: file.objid, + fileName: file.realFileName, + }); // API ํด๋ผ์ด์–ธํŠธ๋ฅผ ํ†ตํ•ด Blob์œผ๋กœ ๋‹ค์šด๋กœ๋“œ (์ธ์ฆ ํ† ํฐ ํฌํ•จ) const response = await apiClient.get(`/files/download/${file.objid}`, { @@ -792,8 +802,12 @@ const FileUploadComponent: React.FC = ({ setRepresentativeImageUrl(url); console.log("โœ… ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์„ฑ๊ณต:", url); - } catch (error) { - console.error("โŒ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ:", error); + } catch (error: any) { + console.error("โŒ ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ ๋กœ๋“œ ์‹คํŒจ:", { + file: file.realFileName, + objid: file.objid, + error: error?.response?.status || error?.message, + }); setRepresentativeImageUrl(null); } }, diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx index 3da7ce27..dff4ee3a 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutComponent.tsx @@ -6,10 +6,12 @@ import { SplitPanelLayoutConfig } from "./types"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp } from "lucide-react"; +import { Plus, Search, GripVertical, Loader2, ChevronDown, ChevronUp, Save, ChevronRight, Pencil, Trash2 } from "lucide-react"; import { dataApi } from "@/lib/api/data"; import { useToast } from "@/hooks/use-toast"; import { tableTypeApi } from "@/lib/api/screen"; +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; export interface SplitPanelLayoutComponentProps extends ComponentRendererProps { // ์ถ”๊ฐ€ props @@ -45,8 +47,25 @@ export const SplitPanelLayoutComponent: React.FC const [isLoadingLeft, setIsLoadingLeft] = useState(false); const [isLoadingRight, setIsLoadingRight] = useState(false); const [rightTableColumns, setRightTableColumns] = useState([]); // ์šฐ์ธก ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ์ •๋ณด + const [expandedItems, setExpandedItems] = useState>(new Set()); // ํŽผ์ณ์ง„ ํ•ญ๋ชฉ๋“ค const { toast } = useToast(); + // ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์ƒํƒœ + const [showAddModal, setShowAddModal] = useState(false); + const [addModalPanel, setAddModalPanel] = useState<"left" | "right" | "left-item" | null>(null); + const [addModalFormData, setAddModalFormData] = useState>({}); + + // ์ˆ˜์ • ๋ชจ๋‹ฌ ์ƒํƒœ + const [showEditModal, setShowEditModal] = useState(false); + const [editModalPanel, setEditModalPanel] = useState<"left" | "right" | null>(null); + const [editModalItem, setEditModalItem] = useState(null); + const [editModalFormData, setEditModalFormData] = useState>({}); + + // ์‚ญ์ œ ํ™•์ธ ๋ชจ๋‹ฌ ์ƒํƒœ + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [deleteModalPanel, setDeleteModalPanel] = useState<"left" | "right" | null>(null); + const [deleteModalItem, setDeleteModalItem] = useState(null); + // ๋ฆฌ์‚ฌ์ด์ € ๋“œ๋ž˜๊ทธ ์ƒํƒœ const [isDragging, setIsDragging] = useState(false); const [leftWidth, setLeftWidth] = useState(splitRatio); @@ -81,6 +100,53 @@ export const SplitPanelLayoutComponent: React.FC border: isSelected ? "2px solid #3b82f6" : "1px solid #e5e7eb", }; + // ๊ณ„์ธต ๊ตฌ์กฐ ๋นŒ๋“œ ํ•จ์ˆ˜ (ํŠธ๋ฆฌ ๊ตฌ์กฐ ์œ ์ง€) + const buildHierarchy = useCallback((items: any[]): any[] => { + if (!items || items.length === 0) return []; + + const itemAddConfig = componentConfig.leftPanel?.itemAddConfig; + if (!itemAddConfig) return items.map(item => ({ ...item, children: [] })); // ๊ณ„์ธต ์„ค์ •์ด ์—†์œผ๋ฉด ํ‰๋ฉด ๋ชฉ๋ก + + const { sourceColumn, parentColumn } = itemAddConfig; + if (!sourceColumn || !parentColumn) return items.map(item => ({ ...item, children: [] })); + + // ID๋ฅผ ํ‚ค๋กœ ํ•˜๋Š” ๋งต ์ƒ์„ฑ + const itemMap = new Map(); + const rootItems: any[] = []; + + // ๋ชจ๋“  ํ•ญ๋ชฉ์„ ๋งต์— ์ถ”๊ฐ€ํ•˜๊ณ  children ๋ฐฐ์—ด ์ดˆ๊ธฐํ™” + items.forEach(item => { + const id = item[sourceColumn]; + itemMap.set(id, { ...item, children: [], level: 0 }); + }); + + // ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„ ์„ค์ • + items.forEach(item => { + const id = item[sourceColumn]; + const parentId = item[parentColumn]; + const currentItem = itemMap.get(id); + + if (!currentItem) return; + + if (!parentId || parentId === null || parentId === '') { + // ์ตœ์ƒ์œ„ ํ•ญ๋ชฉ + rootItems.push(currentItem); + } else { + // ๋ถ€๋ชจ๊ฐ€ ์žˆ๋Š” ํ•ญ๋ชฉ + const parentItem = itemMap.get(parentId); + if (parentItem) { + currentItem.level = parentItem.level + 1; + parentItem.children.push(currentItem); + } else { + // ๋ถ€๋ชจ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์œผ๋ฉด ์ตœ์ƒ์œ„๋กœ ์ฒ˜๋ฆฌ + rootItems.push(currentItem); + } + } + }); + + return rootItems; + }, [componentConfig.leftPanel?.itemAddConfig]); + // ์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋กœ๋“œ const loadLeftData = useCallback(async () => { const leftTableName = componentConfig.leftPanel?.tableName; @@ -93,7 +159,10 @@ export const SplitPanelLayoutComponent: React.FC size: 100, // searchTerm ์ œ๊ฑฐ - ํด๋ผ์ด์–ธํŠธ ์‚ฌ์ด๋“œ์—์„œ ํ•„ํ„ฐ๋ง }); - setLeftData(result.data); + + // ๊ณ„์ธต ๊ตฌ์กฐ ๋นŒ๋“œ + const hierarchicalData = buildHierarchy(result.data); + setLeftData(hierarchicalData); } catch (error) { console.error("์ขŒ์ธก ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹คํŒจ:", error); toast({ @@ -104,7 +173,7 @@ export const SplitPanelLayoutComponent: React.FC } finally { setIsLoadingLeft(false); } - }, [componentConfig.leftPanel?.tableName, isDesignMode, toast]); + }, [componentConfig.leftPanel?.tableName, isDesignMode, toast, buildHierarchy]); // ์šฐ์ธก ๋ฐ์ดํ„ฐ ๋กœ๋“œ const loadRightData = useCallback( @@ -208,6 +277,382 @@ export const SplitPanelLayoutComponent: React.FC loadRightTableColumns(); }, [componentConfig.rightPanel?.tableName, isDesignMode]); + // ํ•ญ๋ชฉ ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ํ† ๊ธ€ + const toggleExpand = useCallback((itemId: any) => { + setExpandedItems(prev => { + const newSet = new Set(prev); + if (newSet.has(itemId)) { + newSet.delete(itemId); + } else { + newSet.add(itemId); + } + return newSet; + }); + }, []); + + // ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ + const handleAddClick = useCallback((panel: "left" | "right") => { + setAddModalPanel(panel); + setAddModalFormData({}); + setShowAddModal(true); + }, []); + + // ์ˆ˜์ • ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ + const handleEditClick = useCallback((panel: "left" | "right", item: any) => { + setEditModalPanel(panel); + setEditModalItem(item); + setEditModalFormData({ ...item }); + setShowEditModal(true); + }, []); + + // ์ˆ˜์ • ๋ชจ๋‹ฌ ์ €์žฅ + const handleEditModalSave = useCallback(async () => { + const tableName = editModalPanel === "left" + ? componentConfig.leftPanel?.tableName + : componentConfig.rightPanel?.tableName; + + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + const primaryKey = editModalItem[sourceColumn] || editModalItem.id || editModalItem.ID; + + if (!tableName || !primaryKey) { + toast({ + title: "์ˆ˜์ • ์˜ค๋ฅ˜", + description: "ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” Primary Key๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + + try { + console.log("๐Ÿ“ ๋ฐ์ดํ„ฐ ์ˆ˜์ •:", { tableName, primaryKey, data: editModalFormData }); + + // ํ”„๋ก ํŠธ์—”๋“œ ์ „์šฉ ํ•„๋“œ ์ œ๊ฑฐ (children, level ๋“ฑ) + const cleanData = { ...editModalFormData }; + delete cleanData.children; + delete cleanData.level; + + // ์ขŒ์ธก ํŒจ๋„ ์ˆ˜์ • ์‹œ, ์กฐ์ธ ๊ด€๊ณ„ ์ •๋ณด ํฌํ•จ + let updatePayload: any = cleanData; + + if (editModalPanel === "left" && componentConfig.rightPanel?.relation?.type === "join") { + // ์กฐ์ธ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ๊ด€๊ณ„ ์ •๋ณด๋ฅผ ํŽ˜์ด๋กœ๋“œ์— ์ถ”๊ฐ€ + updatePayload._relationInfo = { + rightTable: componentConfig.rightPanel.tableName, + leftColumn: componentConfig.rightPanel.relation.leftColumn, + rightColumn: componentConfig.rightPanel.relation.rightColumn, + oldLeftValue: editModalItem[componentConfig.rightPanel.relation.leftColumn], + }; + console.log("๐Ÿ”— ์กฐ์ธ ๊ด€๊ณ„ ์ •๋ณด ์ถ”๊ฐ€:", updatePayload._relationInfo); + } + + const result = await dataApi.updateRecord(tableName, primaryKey, updatePayload); + + if (result.success) { + toast({ + title: "์„ฑ๊ณต", + description: "๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + }); + + // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + setShowEditModal(false); + setEditModalFormData({}); + setEditModalItem(null); + + // ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ + if (editModalPanel === "left") { + loadLeftData(); + // ์šฐ์ธก ํŒจ๋„๋„ ์ƒˆ๋กœ๊ณ ์นจ (FK๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ์ˆ˜ ์žˆ์Œ) + if (selectedLeftItem) { + loadRightData(selectedLeftItem); + } + } else if (editModalPanel === "right" && selectedLeftItem) { + loadRightData(selectedLeftItem); + } + } else { + toast({ + title: "์ˆ˜์ • ์‹คํŒจ", + description: result.message || "๋ฐ์ดํ„ฐ ์ˆ˜์ •์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + } + } catch (error: any) { + console.error("๋ฐ์ดํ„ฐ ์ˆ˜์ • ์˜ค๋ฅ˜:", error); + toast({ + title: "์˜ค๋ฅ˜", + description: error?.response?.data?.message || "๋ฐ์ดํ„ฐ ์ˆ˜์ • ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + } + }, [editModalPanel, componentConfig, editModalItem, editModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]); + + // ์‚ญ์ œ ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ + const handleDeleteClick = useCallback((panel: "left" | "right", item: any) => { + setDeleteModalPanel(panel); + setDeleteModalItem(item); + setShowDeleteModal(true); + }, []); + + // ์‚ญ์ œ ํ™•์ธ + const handleDeleteConfirm = useCallback(async () => { + // ์šฐ์ธก ํŒจ๋„ ์‚ญ์ œ ์‹œ ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ํ™•์ธ + let tableName = deleteModalPanel === "left" + ? componentConfig.leftPanel?.tableName + : componentConfig.rightPanel?.tableName; + + // ์šฐ์ธก ํŒจ๋„ + ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ๋ชจ๋“œ์ธ ๊ฒฝ์šฐ + if (deleteModalPanel === "right" && componentConfig.rightPanel?.addConfig?.targetTable) { + tableName = componentConfig.rightPanel.addConfig.targetTable; + console.log("๐Ÿ”— ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ๋ชจ๋“œ: ์‚ญ์ œ ๋Œ€์ƒ ํ…Œ์ด๋ธ” =", tableName); + } + + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + let primaryKey: any = deleteModalItem[sourceColumn] || deleteModalItem.id || deleteModalItem.ID; + + // ๋ณตํ•ฉํ‚ค ์ฒ˜๋ฆฌ: deleteModalItem ์ „์ฒด๋ฅผ ์ „๋‹ฌ (๋ฐฑ์—”๋“œ์—์„œ ๋ณตํ•ฉํ‚ค ์ž๋™ ์ฒ˜๋ฆฌ) + if (deleteModalItem && typeof deleteModalItem === 'object') { + primaryKey = deleteModalItem; + console.log("๐Ÿ”‘ ๋ณตํ•ฉํ‚ค ๊ฐ€๋Šฅ์„ฑ: ์ „์ฒด ๊ฐ์ฒด ์ „๋‹ฌ", primaryKey); + } + + if (!tableName || !primaryKey) { + toast({ + title: "์‚ญ์ œ ์˜ค๋ฅ˜", + description: "ํ…Œ์ด๋ธ”๋ช… ๋˜๋Š” Primary Key๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + + try { + console.log("๐Ÿ—‘๏ธ ๋ฐ์ดํ„ฐ ์‚ญ์ œ:", { tableName, primaryKey }); + + const result = await dataApi.deleteRecord(tableName, primaryKey); + + if (result.success) { + toast({ + title: "์„ฑ๊ณต", + description: "๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + }); + + // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + setShowDeleteModal(false); + setDeleteModalItem(null); + + // ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ + if (deleteModalPanel === "left") { + loadLeftData(); + // ์‚ญ์ œ๋œ ํ•ญ๋ชฉ์ด ์„ ํƒ๋˜์–ด ์žˆ์—ˆ์œผ๋ฉด ์„ ํƒ ํ•ด์ œ + if (selectedLeftItem && selectedLeftItem[sourceColumn] === primaryKey) { + setSelectedLeftItem(null); + setRightData(null); + } + } else if (deleteModalPanel === "right" && selectedLeftItem) { + loadRightData(selectedLeftItem); + } + } else { + toast({ + title: "์‚ญ์ œ ์‹คํŒจ", + description: result.message || "๋ฐ์ดํ„ฐ ์‚ญ์ œ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + } + } catch (error: any) { + console.error("๋ฐ์ดํ„ฐ ์‚ญ์ œ ์˜ค๋ฅ˜:", error); + + // ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์—๋Ÿฌ ์ฒ˜๋ฆฌ + let errorMessage = "๋ฐ์ดํ„ฐ ์‚ญ์ œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + if (error?.response?.data?.error?.includes("foreign key")) { + errorMessage = "์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๋‹ค๋ฅธ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด ์‚ญ์ œํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + } + + toast({ + title: "์˜ค๋ฅ˜", + description: errorMessage, + variant: "destructive", + }); + } + }, [deleteModalPanel, componentConfig, deleteModalItem, toast, selectedLeftItem, loadLeftData, loadRightData]); + + // ํ•ญ๋ชฉ๋ณ„ ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํ•ธ๋“ค๋Ÿฌ (์ขŒ์ธก ํ•ญ๋ชฉ์˜ + ๋ฒ„ํŠผ - ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€) + const handleItemAddClick = useCallback((item: any) => { + const itemAddConfig = componentConfig.leftPanel?.itemAddConfig; + + if (!itemAddConfig) { + toast({ + title: "์„ค์ • ์˜ค๋ฅ˜", + description: "ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€ ์„ค์ •์ด ์—†์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + + const { sourceColumn, parentColumn } = itemAddConfig; + + if (!sourceColumn || !parentColumn) { + toast({ + title: "์„ค์ • ์˜ค๋ฅ˜", + description: "ํ˜„์žฌ ํ•ญ๋ชฉ ID ์ปฌ๋Ÿผ๊ณผ ์ƒ์œ„ ํ•ญ๋ชฉ ์ €์žฅ ์ปฌ๋Ÿผ์„ ์„ค์ •ํ•ด์ฃผ์„ธ์š”.", + variant: "destructive", + }); + return; + } + + // ์„ ํƒ๋œ ํ•ญ๋ชฉ์˜ sourceColumn ๊ฐ’์„ ๊ฐ€์ ธ์™€์„œ parentColumn์— ๋งคํ•‘ + const sourceValue = item[sourceColumn]; + + if (!sourceValue) { + toast({ + title: "๋ฐ์ดํ„ฐ ์˜ค๋ฅ˜", + description: `์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ${sourceColumn} ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค.`, + variant: "destructive", + }); + return; + } + + // ์ขŒ์ธก ํŒจ๋„ ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์—ด๊ธฐ (parentColumn ๊ฐ’ ๋ฏธ๋ฆฌ ์ฑ„์šฐ๊ธฐ) + setAddModalPanel("left-item"); + setAddModalFormData({ [parentColumn]: sourceValue }); + setShowAddModal(true); + }, [componentConfig, toast]); + + // ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์ €์žฅ + const handleAddModalSave = useCallback(async () => { + // ํ…Œ์ด๋ธ”๋ช…๊ณผ ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ๊ฒฐ์ • + let tableName: string | undefined; + let modalColumns: Array<{ name: string; label: string; required?: boolean }> | undefined; + let finalData = { ...addModalFormData }; + + if (addModalPanel === "left") { + tableName = componentConfig.leftPanel?.tableName; + modalColumns = componentConfig.leftPanel?.addModalColumns; + } else if (addModalPanel === "right") { + // ์šฐ์ธก ํŒจ๋„: ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ์„ค์ •์ด ์žˆ๋Š”์ง€ ํ™•์ธ + const addConfig = componentConfig.rightPanel?.addConfig; + + if (addConfig?.targetTable) { + // ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ๋ชจ๋“œ + tableName = addConfig.targetTable; + modalColumns = componentConfig.rightPanel?.addModalColumns; + + // ์ขŒ์ธก ํŒจ๋„์—์„œ ์„ ํƒ๋œ ๊ฐ’ ์ž๋™ ์ฑ„์šฐ๊ธฐ + if (addConfig.leftPanelColumn && addConfig.targetColumn && selectedLeftItem) { + const leftValue = selectedLeftItem[addConfig.leftPanelColumn]; + finalData[addConfig.targetColumn] = leftValue; + console.log(`๐Ÿ”— ์ขŒ์ธก ํŒจ๋„ ๊ฐ’ ์ž๋™ ์ฑ„์›€: ${addConfig.targetColumn} = ${leftValue}`); + } + + // ์ž๋™ ์ฑ„์›€ ์ปฌ๋Ÿผ ์ถ”๊ฐ€ + if (addConfig.autoFillColumns) { + Object.entries(addConfig.autoFillColumns).forEach(([key, value]) => { + finalData[key] = value; + }); + console.log("๐Ÿ”ง ์ž๋™ ์ฑ„์›€ ์ปฌ๋Ÿผ:", addConfig.autoFillColumns); + } + } else { + // ์ผ๋ฐ˜ ํ…Œ์ด๋ธ” ๋ชจ๋“œ + tableName = componentConfig.rightPanel?.tableName; + modalColumns = componentConfig.rightPanel?.addModalColumns; + } + } else if (addModalPanel === "left-item") { + // ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€ (์ขŒ์ธก ํ…Œ์ด๋ธ”์— ์ถ”๊ฐ€) + tableName = componentConfig.leftPanel?.tableName; + modalColumns = componentConfig.leftPanel?.itemAddConfig?.addModalColumns; + } + + if (!tableName) { + toast({ + title: "ํ…Œ์ด๋ธ” ์˜ค๋ฅ˜", + description: "ํ…Œ์ด๋ธ”๋ช…์ด ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + return; + } + + // ํ•„์ˆ˜ ํ•„๋“œ ๊ฒ€์ฆ + const requiredFields = (modalColumns || []).filter(col => col.required); + for (const field of requiredFields) { + if (!addModalFormData[field.name]) { + toast({ + title: "์ž…๋ ฅ ์˜ค๋ฅ˜", + description: `${field.label}์€(๋Š”) ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.`, + variant: "destructive", + }); + return; + } + } + + try { + console.log("๐Ÿ“ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€:", { tableName, data: finalData }); + + const result = await dataApi.createRecord(tableName, finalData); + + if (result.success) { + toast({ + title: "์„ฑ๊ณต", + description: "๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค.", + }); + + // ๋ชจ๋‹ฌ ๋‹ซ๊ธฐ + setShowAddModal(false); + setAddModalFormData({}); + + // ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ + if (addModalPanel === "left" || addModalPanel === "left-item") { + // ์ขŒ์ธก ํŒจ๋„ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ (์ผ๋ฐ˜ ์ถ”๊ฐ€ ๋˜๋Š” ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€) + loadLeftData(); + } else if (addModalPanel === "right" && selectedLeftItem) { + // ์šฐ์ธก ํŒจ๋„ ๋ฐ์ดํ„ฐ ์ƒˆ๋กœ๊ณ ์นจ + loadRightData(selectedLeftItem); + } + } else { + toast({ + title: "์ €์žฅ ์‹คํŒจ", + description: result.message || "๋ฐ์ดํ„ฐ ์ถ”๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.", + variant: "destructive", + }); + } + } catch (error: any) { + console.error("๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ์˜ค๋ฅ˜:", error); + + // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”์ถœ + let errorMessage = "๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + + if (error?.response?.data) { + const responseData = error.response.data; + + // ๋ฐฑ์—”๋“œ์—์„œ ๋ฐ˜ํ™˜ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ํ™•์ธ + if (responseData.error) { + // ์ค‘๋ณต ํ‚ค ์—๋Ÿฌ ์ฒ˜๋ฆฌ + if (responseData.error.includes("duplicate key")) { + errorMessage = "์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๊ฐ’์ž…๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๊ฐ’์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."; + } + // NOT NULL ์ œ์•ฝ์กฐ๊ฑด ์—๋Ÿฌ + else if (responseData.error.includes("null value")) { + const match = responseData.error.match(/column "(\w+)"/); + const columnName = match ? match[1] : "ํ•„์ˆ˜"; + errorMessage = `${columnName} ํ•„๋“œ๋Š” ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.`; + } + // ์™ธ๋ž˜ํ‚ค ์ œ์•ฝ์กฐ๊ฑด ์—๋Ÿฌ + else if (responseData.error.includes("foreign key")) { + errorMessage = "์ฐธ์กฐํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + } + // ๊ธฐํƒ€ ์—๋Ÿฌ + else { + errorMessage = responseData.message || responseData.error; + } + } else if (responseData.message) { + errorMessage = responseData.message; + } + } + + toast({ + title: "์˜ค๋ฅ˜", + description: errorMessage, + variant: "destructive", + }); + } + }, [addModalPanel, componentConfig, addModalFormData, toast, selectedLeftItem, loadLeftData, loadRightData]); + // ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ useEffect(() => { if (!isDesignMode && componentConfig.autoLoad !== false) { @@ -295,8 +740,12 @@ export const SplitPanelLayoutComponent: React.FC {componentConfig.leftPanel?.title || "์ขŒ์ธก ํŒจ๋„"} - {componentConfig.leftPanel?.showAdd && ( - @@ -367,30 +816,156 @@ export const SplitPanelLayoutComponent: React.FC }) : leftData; - return filteredLeftData.length > 0 ? ( - // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ - filteredLeftData.map((item, index) => { - const itemId = item.id || item.ID || item[Object.keys(item)[0]] || index; - const isSelected = - selectedLeftItem && (selectedLeftItem.id === itemId || selectedLeftItem === item); - // ์ฒซ ๋ฒˆ์งธ 2-3๊ฐœ ํ•„๋“œ๋ฅผ ํ‘œ์‹œ - const keys = Object.keys(item).filter((k) => k !== "id" && k !== "ID"); - const displayTitle = item[keys[0]] || item.name || item.title || `ํ•ญ๋ชฉ ${index + 1}`; - const displaySubtitle = keys[1] ? item[keys[1]] : null; + // ์žฌ๊ท€ ๋ Œ๋”๋ง ํ•จ์ˆ˜ + const renderTreeItem = (item: any, index: number): React.ReactNode => { + const sourceColumn = componentConfig.leftPanel?.itemAddConfig?.sourceColumn || 'id'; + const itemId = item[sourceColumn] || item.id || item.ID || index; + const isSelected = selectedLeftItem && (selectedLeftItem[sourceColumn] === itemId || selectedLeftItem === item); + const hasChildren = item.children && item.children.length > 0; + const isExpanded = expandedItems.has(itemId); + const level = item.level || 0; - return ( + // ์กฐ์ธ์— ์‚ฌ์šฉํ•˜๋Š” leftColumn์„ ํ•„์ˆ˜๋กœ ํ‘œ์‹œ + const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; + let displayFields: { label: string; value: any }[] = []; + + // ๋””๋ฒ„๊ทธ ๋กœ๊ทธ + if (index === 0) { + console.log("๐Ÿ” ์ขŒ์ธก ํŒจ๋„ ํ‘œ์‹œ ๋กœ์ง:"); + console.log(" - leftColumn (์กฐ์ธ ํ‚ค):", leftColumn); + console.log(" - item keys:", Object.keys(item)); + } + + if (leftColumn) { + // ์กฐ์ธ ๋ชจ๋“œ: leftColumn ๊ฐ’์„ ์ฒซ ๋ฒˆ์งธ๋กœ ํ‘œ์‹œ (ํ•„์ˆ˜) + displayFields.push({ + label: leftColumn, + value: item[leftColumn], + }); + + // ์ถ”๊ฐ€๋กœ ๋‹ค๋ฅธ ์˜๋ฏธ์žˆ๋Š” ํ•„๋“œ 1-2๊ฐœ ํ‘œ์‹œ (name, title ๋“ฑ) + const additionalKeys = Object.keys(item).filter( + (k) => k !== "id" && k !== "ID" && k !== leftColumn && + (k.includes("name") || k.includes("title") || k.includes("desc")) + ); + + if (additionalKeys.length > 0) { + displayFields.push({ + label: additionalKeys[0], + value: item[additionalKeys[0]], + }); + } + + if (index === 0) { + console.log(" โœ… ์กฐ์ธ ํ‚ค ๊ธฐ๋ฐ˜ ํ‘œ์‹œ:", displayFields); + } + } else { + // ์ƒ์„ธ ๋ชจ๋“œ ๋˜๋Š” ์„ค์ • ์—†์Œ: ์ž๋™์œผ๋กœ ์ฒซ 2๊ฐœ ํ•„๋“œ ํ‘œ์‹œ + const keys = Object.keys(item).filter((k) => k !== "id" && k !== "ID"); + displayFields = keys.slice(0, 2).map((key) => ({ + label: key, + value: item[key], + })); + + if (index === 0) { + console.log(" โš ๏ธ ์กฐ์ธ ํ‚ค ์—†์Œ, ์ž๋™ ์„ ํƒ:", displayFields); + } + } + + const displayTitle = displayFields[0]?.value || item.name || item.title || `ํ•ญ๋ชฉ ${index + 1}`; + const displaySubtitle = displayFields[1]?.value || null; + + return ( + + {/* ํ˜„์žฌ ํ•ญ๋ชฉ */}
handleLeftItemSelect(item)} - className={`cursor-pointer rounded-md p-3 transition-colors hover:bg-muted ${ + className={`group relative cursor-pointer rounded-md p-3 transition-colors hover:bg-muted ${ isSelected ? "bg-primary/10 text-primary" : "text-foreground" }`} + style={{ paddingLeft: `${12 + level * 24}px` }} > -
{displayTitle}
- {displaySubtitle &&
{displaySubtitle}
} +
{ + handleLeftItemSelect(item); + if (hasChildren) { + toggleExpand(itemId); + } + }} + > + {/* ํŽผ์น˜๊ธฐ/์ ‘๊ธฐ ์•„์ด์ฝ˜ */} + {hasChildren ? ( +
+ {isExpanded ? ( + + ) : ( + + )} +
+ ) : ( +
+ )} + + {/* ํ•ญ๋ชฉ ๋‚ด์šฉ */} +
+
{displayTitle}
+ {displaySubtitle &&
{displaySubtitle}
} +
+ + {/* ํ•ญ๋ชฉ๋ณ„ ๋ฒ„ํŠผ๋“ค */} + {!isDesignMode && ( +
+ {/* ์ˆ˜์ • ๋ฒ„ํŠผ */} + + + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + + + {/* ํ•ญ๋ชฉ๋ณ„ ์ถ”๊ฐ€ ๋ฒ„ํŠผ */} + {componentConfig.leftPanel?.showItemAddButton && ( + + )} +
+ )} +
- ); - }) + + {/* ์ž์‹ ํ•ญ๋ชฉ๋“ค (์ ‘ํ˜€์žˆ์œผ๋ฉด ํ‘œ์‹œ ์•ˆํ•จ) */} + {hasChildren && isExpanded && item.children.map((child: any, childIndex: number) => renderTreeItem(child, childIndex))} + + ); + }; + + return filteredLeftData.length > 0 ? ( + // ์‹ค์ œ ๋ฐ์ดํ„ฐ ํ‘œ์‹œ + filteredLeftData.map((item, index) => renderTreeItem(item, index)) ) : ( // ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ
@@ -432,11 +1007,20 @@ export const SplitPanelLayoutComponent: React.FC {componentConfig.rightPanel?.title || "์šฐ์ธก ํŒจ๋„"} - {componentConfig.rightPanel?.showAdd && ( - + {!isDesignMode && ( +
+ {componentConfig.rightPanel?.showAdd && ( + + )} + {/* ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ •/์‚ญ์ œ๋Š” ๊ฐ ์นด๋“œ์—์„œ ์ฒ˜๋ฆฌ */} +
)}
{componentConfig.rightPanel?.showSearch && ( @@ -488,25 +1072,59 @@ export const SplitPanelLayoutComponent: React.FC {filteredData.map((item, index) => { const itemId = item.id || item.ID || index; const isExpanded = expandedRightItems.has(itemId); - const firstValues = Object.entries(item) - .filter(([key]) => !key.toLowerCase().includes("id")) - .slice(0, 3); - const allValues = Object.entries(item).filter( - ([key, value]) => value !== null && value !== undefined && value !== "", - ); + + // ์šฐ์ธก ํŒจ๋„ ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ค์ • ํ™•์ธ + const rightColumns = componentConfig.rightPanel?.columns; + let firstValues: [string, any][] = []; + let allValues: [string, any][] = []; + + if (index === 0) { + console.log("๐Ÿ” ์šฐ์ธก ํŒจ๋„ ํ‘œ์‹œ ๋กœ์ง:"); + console.log(" - rightColumns:", rightColumns); + console.log(" - item keys:", Object.keys(item)); + } + + if (rightColumns && rightColumns.length > 0) { + // ์„ค์ •๋œ ์ปฌ๋Ÿผ๋งŒ ํ‘œ์‹œ + firstValues = rightColumns + .slice(0, 3) + .map((col) => [col.name, item[col.name]] as [string, any]) + .filter(([_, value]) => value !== null && value !== undefined && value !== ""); + + allValues = rightColumns + .map((col) => [col.name, item[col.name]] as [string, any]) + .filter(([_, value]) => value !== null && value !== undefined && value !== ""); + + if (index === 0) { + console.log(" โœ… ์„ค์ •๋œ ์ปฌ๋Ÿผ ์‚ฌ์šฉ:", rightColumns.map(c => c.name)); + } + } else { + // ์„ค์ • ์—†์œผ๋ฉด ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‘œ์‹œ (๊ธฐ์กด ๋กœ์ง) + firstValues = Object.entries(item) + .filter(([key]) => !key.toLowerCase().includes("id")) + .slice(0, 3); + + allValues = Object.entries(item).filter( + ([key, value]) => value !== null && value !== undefined && value !== "", + ); + + if (index === 0) { + console.log(" โš ๏ธ ์ปฌ๋Ÿผ ์„ค์ • ์—†์Œ, ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‘œ์‹œ"); + } + } return (
- {/* ์š”์•ฝ ์ •๋ณด (ํด๋ฆญ ๊ฐ€๋Šฅ) */} -
toggleRightItemExpansion(itemId)} - className="cursor-pointer p-3 transition-colors hover:bg-muted" - > + {/* ์š”์•ฝ ์ •๋ณด */} +
-
+
toggleRightItemExpansion(itemId)} + > {firstValues.map(([key, value], idx) => (
{getColumnLabel(key)}
@@ -516,12 +1134,44 @@ export const SplitPanelLayoutComponent: React.FC
))}
-
- {isExpanded ? ( - - ) : ( - +
+ {/* ์ˆ˜์ • ๋ฒ„ํŠผ */} + {!isDesignMode && ( + )} + {/* ์‚ญ์ œ ๋ฒ„ํŠผ */} + {!isDesignMode && ( + + )} + {/* ํ™•์žฅ/์ ‘๊ธฐ ๋ฒ„ํŠผ */} +
@@ -565,21 +1215,39 @@ export const SplitPanelLayoutComponent: React.FC })() ) : ( // ์ƒ์„ธ ๋ชจ๋“œ: ๋‹จ์ผ ๊ฐ์ฒด๋ฅผ ์ƒ์„ธ ์ •๋ณด๋กœ ํ‘œ์‹œ -
- {Object.entries(rightData).map(([key, value]) => { - // null, undefined, ๋นˆ ๋ฌธ์ž์—ด ์ œ์™ธ - if (value === null || value === undefined || value === "") return null; + (() => { + const rightColumns = componentConfig.rightPanel?.columns; + let displayEntries: [string, any][] = []; - return ( -
-
- {key} -
-
{String(value)}
-
+ if (rightColumns && rightColumns.length > 0) { + // ์„ค์ •๋œ ์ปฌ๋Ÿผ๋งŒ ํ‘œ์‹œ + displayEntries = rightColumns + .map((col) => [col.name, rightData[col.name]] as [string, any]) + .filter(([_, value]) => value !== null && value !== undefined && value !== ""); + + console.log("๐Ÿ” ์ƒ์„ธ ๋ชจ๋“œ ํ‘œ์‹œ ๋กœ์ง:"); + console.log(" โœ… ์„ค์ •๋œ ์ปฌ๋Ÿผ ์‚ฌ์šฉ:", rightColumns.map(c => c.name)); + } else { + // ์„ค์ • ์—†์œผ๋ฉด ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‘œ์‹œ + displayEntries = Object.entries(rightData).filter( + ([_, value]) => value !== null && value !== undefined && value !== "" ); - })} -
+ console.log(" โš ๏ธ ์ปฌ๋Ÿผ ์„ค์ • ์—†์Œ, ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‘œ์‹œ"); + } + + return ( +
+ {displayEntries.map(([key, value]) => ( +
+
+ {getColumnLabel(key)} +
+
{String(value)}
+
+ ))} +
+ ); + })() ) ) : selectedLeftItem && isDesignMode ? ( // ๋””์ž์ธ ๋ชจ๋“œ: ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ @@ -614,6 +1282,239 @@ export const SplitPanelLayoutComponent: React.FC
+ + {/* ์ถ”๊ฐ€ ๋ชจ๋‹ฌ */} + + + + + {addModalPanel === "left" + ? `${componentConfig.leftPanel?.title} ์ถ”๊ฐ€` + : addModalPanel === "right" + ? `${componentConfig.rightPanel?.title} ์ถ”๊ฐ€` + : `ํ•˜์œ„ ${componentConfig.leftPanel?.title} ์ถ”๊ฐ€`} + + + {addModalPanel === "left-item" + ? "์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ํ•˜์œ„ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ํ•„์ˆ˜ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”." + : "์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. ํ•„์ˆ˜ ํ•ญ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."} + + + +
+ {(() => { + // ์–ด๋–ค ์ปฌ๋Ÿผ๋“ค์„ ํ‘œ์‹œํ• ์ง€ ๊ฒฐ์ • + let modalColumns: Array<{ name: string; label: string; required?: boolean }> | undefined; + + if (addModalPanel === "left") { + modalColumns = componentConfig.leftPanel?.addModalColumns; + } else if (addModalPanel === "right") { + modalColumns = componentConfig.rightPanel?.addModalColumns; + } else if (addModalPanel === "left-item") { + modalColumns = componentConfig.leftPanel?.itemAddConfig?.addModalColumns; + } + + return modalColumns?.map((col, index) => { + // ํ•ญ๋ชฉ๋ณ„ ์ถ”๊ฐ€ ๋ฒ„ํŠผ์œผ๋กœ ์—ด๋ ธ์„ ๋•Œ, parentColumn์€ ๋ฏธ๋ฆฌ ์ฑ„์›Œ์ ธ ์žˆ๊ณ  ์ˆ˜์ • ๋ถˆ๊ฐ€ + const isPreFilled = addModalPanel === "left-item" + && componentConfig.leftPanel?.itemAddConfig?.parentColumn === col.name + && addModalFormData[col.name]; + + return ( +
+ + { + setAddModalFormData(prev => ({ + ...prev, + [col.name]: e.target.value + })); + }} + placeholder={`${col.label} ์ž…๋ ฅ`} + className="h-8 text-xs sm:h-10 sm:text-sm" + required={col.required} + disabled={isPreFilled} + /> +
+ ); + }); + })()} +
+ + + + + +
+
+ + {/* ์ˆ˜์ • ๋ชจ๋‹ฌ */} + + + + + {editModalPanel === "left" + ? `${componentConfig.leftPanel?.title} ์ˆ˜์ •` + : `${componentConfig.rightPanel?.title} ์ˆ˜์ •`} + + + ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ํ•ญ๋ชฉ์„ ๋ณ€๊ฒฝํ•ด์ฃผ์„ธ์š”. + + + +
+ {editModalItem && (() => { + // ์ขŒ์ธก ํŒจ๋„ ์ˆ˜์ •: leftColumn๋งŒ ์ˆ˜์ • ๊ฐ€๋Šฅ + if (editModalPanel === "left") { + const leftColumn = componentConfig.rightPanel?.relation?.leftColumn; + + // leftColumn๋งŒ ํ‘œ์‹œ + if (!leftColumn || editModalFormData[leftColumn] === undefined) { + return

์ˆ˜์ • ๊ฐ€๋Šฅํ•œ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค.

; + } + + return ( +
+ + { + setEditModalFormData(prev => ({ + ...prev, + [leftColumn]: e.target.value + })); + }} + placeholder={`${leftColumn} ์ž…๋ ฅ`} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ ); + } + + // ์šฐ์ธก ํŒจ๋„ ์ˆ˜์ •: ์šฐ์ธก ํŒจ๋„์— ์„ค์ •๋œ ํ‘œ์‹œ ์ปฌ๋Ÿผ๋“ค๋งŒ + if (editModalPanel === "right") { + const rightColumns = componentConfig.rightPanel?.columns; + + if (rightColumns && rightColumns.length > 0) { + // ์„ค์ •๋œ ์ปฌ๋Ÿผ๋งŒ ํ‘œ์‹œ + return rightColumns.map((col) => ( +
+ + { + setEditModalFormData(prev => ({ + ...prev, + [col.name]: e.target.value + })); + }} + placeholder={`${col.label || col.name} ์ž…๋ ฅ`} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ )); + } else { + // ์„ค์ •์ด ์—†์œผ๋ฉด ๋ชจ๋“  ์ปฌ๋Ÿผ ํ‘œ์‹œ (company_code, company_name ์ œ์™ธ) + return Object.entries(editModalFormData) + .filter(([key]) => key !== 'company_code' && key !== 'company_name') + .map(([key, value]) => ( +
+ + { + setEditModalFormData(prev => ({ + ...prev, + [key]: e.target.value + })); + }} + placeholder={`${key} ์ž…๋ ฅ`} + className="h-8 text-xs sm:h-10 sm:text-sm" + /> +
+ )); + } + } + + return null; + })()} +
+ + + + + +
+
+ + {/* ์‚ญ์ œ ํ™•์ธ ๋ชจ๋‹ฌ */} + + + + ์‚ญ์ œ ํ™•์ธ + + ์ •๋ง๋กœ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? +
์ด ์ž‘์—…์€ ๋˜๋Œ๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. +
+
+ + + + + +
+
); }; diff --git a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx index d53d90e9..bce30f8f 100644 --- a/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx +++ b/frontend/lib/registry/components/split-panel-layout/SplitPanelLayoutConfigPanel.tsx @@ -9,7 +9,7 @@ import { Slider } from "@/components/ui/slider"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; -import { Check, ChevronsUpDown, ArrowRight } from "lucide-react"; +import { Check, ChevronsUpDown, ArrowRight, Plus, X } from "lucide-react"; import { cn } from "@/lib/utils"; import { SplitPanelLayoutConfig } from "./types"; import { TableInfo, ColumnInfo } from "@/types/screen"; @@ -74,6 +74,61 @@ export const SplitPanelLayoutConfigPanel: React.FC { + const leftTableName = config.leftPanel?.tableName || screenTableName; + if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showAdd) { + const currentAddModalColumns = config.leftPanel?.addModalColumns || []; + const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns); + + // PK๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ + if (updatedColumns.length !== currentAddModalColumns.length) { + console.log(`๐Ÿ”„ ์ขŒ์ธก ํŒจ๋„: PK ์ปฌ๋Ÿผ ์ž๋™ ์ถ”๊ฐ€ (${leftTableName})`); + updateLeftPanel({ addModalColumns: updatedColumns }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showAdd]); + + // ์ขŒ์ธก ํŒจ๋„ ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€ ๋ชจ๋‹ฌ PK ์ž๋™ ์ถ”๊ฐ€ + useEffect(() => { + const leftTableName = config.leftPanel?.tableName || screenTableName; + if (leftTableName && loadedTableColumns[leftTableName] && config.leftPanel?.showItemAddButton) { + const currentAddModalColumns = config.leftPanel?.itemAddConfig?.addModalColumns || []; + const updatedColumns = ensurePrimaryKeysInAddModal(leftTableName, currentAddModalColumns); + + // PK๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ + if (updatedColumns.length !== currentAddModalColumns.length) { + console.log(`๐Ÿ”„ ์ขŒ์ธก ํŒจ๋„ ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€: PK ์ปฌ๋Ÿผ ์ž๋™ ์ถ”๊ฐ€ (${leftTableName})`); + updateLeftPanel({ + itemAddConfig: { + ...config.leftPanel?.itemAddConfig, + addModalColumns: updatedColumns, + parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "", + sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "", + } + }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config.leftPanel?.tableName, screenTableName, loadedTableColumns, config.leftPanel?.showItemAddButton]); + + // ์šฐ์ธก ํŒจ๋„ ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ์™„๋ฃŒ ์‹œ PK ์ž๋™ ์ถ”๊ฐ€ + useEffect(() => { + const rightTableName = config.rightPanel?.tableName; + if (rightTableName && loadedTableColumns[rightTableName] && config.rightPanel?.showAdd) { + const currentAddModalColumns = config.rightPanel?.addModalColumns || []; + const updatedColumns = ensurePrimaryKeysInAddModal(rightTableName, currentAddModalColumns); + + // PK๊ฐ€ ์ถ”๊ฐ€๋˜์—ˆ์œผ๋ฉด ์—…๋ฐ์ดํŠธ + if (updatedColumns.length !== currentAddModalColumns.length) { + console.log(`๐Ÿ”„ ์šฐ์ธก ํŒจ๋„: PK ์ปฌ๋Ÿผ ์ž๋™ ์ถ”๊ฐ€ (${rightTableName})`); + updateRightPanel({ addModalColumns: updatedColumns }); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config.rightPanel?.tableName, loadedTableColumns, config.rightPanel?.showAdd]); + // ํ…Œ์ด๋ธ” ์ปฌ๋Ÿผ ๋กœ๋“œ ํ•จ์ˆ˜ const loadTableColumns = async (tableName: string) => { if (loadedTableColumns[tableName] || loadingColumns[tableName]) { @@ -98,6 +153,7 @@ export const SplitPanelLayoutConfigPanel: React.FC = [] + ) => { + const tableColumns = loadedTableColumns[tableName]; + if (!tableColumns) { + console.warn(`โš ๏ธ ํ…Œ์ด๋ธ” ${tableName}์˜ ์ปฌ๋Ÿผ ์ •๋ณด๊ฐ€ ๋กœ๋“œ๋˜์ง€ ์•Š์Œ`); + return existingColumns; + } + + // PK ์ปฌ๋Ÿผ ์ฐพ๊ธฐ + const pkColumns = tableColumns.filter((col) => col.isPrimaryKey); + console.log(`๐Ÿ”‘ ํ…Œ์ด๋ธ” ${tableName}์˜ PK ์ปฌ๋Ÿผ:`, pkColumns.map(c => c.columnName)); + + // ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๋Š” ์ปฌ๋Ÿผ (๋ฐฑ์—”๋“œ์—์„œ ์ž๋™ ์ถ”๊ฐ€) + const autoHandledColumns = ['company_code', 'company_name']; + + // ๊ธฐ์กด ์ปฌ๋Ÿผ ์ด๋ฆ„ ๋ชฉ๋ก + const existingColumnNames = existingColumns.map((col) => col.name); + + // PK ์ปฌ๋Ÿผ์„ ๋งจ ์•ž์— ์ถ”๊ฐ€ (์ด๋ฏธ ์žˆ๊ฑฐ๋‚˜ ์ž๋™ ์ฒ˜๋ฆฌ๋˜๋Š” ์ปฌ๋Ÿผ์€ ์ œ์™ธ) + const pkColumnsToAdd = pkColumns + .filter((col) => !existingColumnNames.includes(col.columnName)) + .filter((col) => !autoHandledColumns.includes(col.columnName)) // ์ž๋™ ์ฒ˜๋ฆฌ ์ปฌ๋Ÿผ ์ œ์™ธ + .map((col) => ({ + name: col.columnName, + label: col.columnLabel || col.columnName, + required: true, // PK๋Š” ํ•ญ์ƒ ํ•„์ˆ˜ + })); + + if (pkColumnsToAdd.length > 0) { + console.log(`โœ… PK ์ปฌ๋Ÿผ ${pkColumnsToAdd.length}๊ฐœ ์ž๋™ ์ถ”๊ฐ€:`, pkColumnsToAdd.map(c => c.name)); + } + + return [...pkColumnsToAdd, ...existingColumns]; + }; + const updateLeftPanel = (updates: Partial) => { const newConfig = { ...config, @@ -268,6 +362,451 @@ export const SplitPanelLayoutConfigPanel: React.FC updateLeftPanel({ showAdd: checked })} />
+ +
+ + updateLeftPanel({ showItemAddButton: checked })} + /> +
+ + {/* ํ•ญ๋ชฉ๋ณ„ + ๋ฒ„ํŠผ ์„ค์ • (ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€) */} + {config.leftPanel?.showItemAddButton && ( +
+ +

+ + ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ์„ ํƒ๋œ ํ•ญ๋ชฉ์˜ ํ•˜์œ„ ํ•ญ๋ชฉ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค (์˜ˆ: ๋ถ€์„œ โ†’ ํ•˜์œ„ ๋ถ€์„œ) +

+ + {/* ํ˜„์žฌ ํ•ญ๋ชฉ์˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ ์ปฌ๋Ÿผ (sourceColumn) */} +
+ +

+ ์„ ํƒ๋œ ํ•ญ๋ชฉ์˜ ์–ด๋–ค ์ปฌ๋Ÿผ ๊ฐ’์„ ์‚ฌ์šฉํ• ์ง€ (์˜ˆ: dept_code) +

+ + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {leftTableColumns + .filter((column) => !['company_code', 'company_name'].includes(column.columnName)) + .map((column) => ( + { + updateLeftPanel({ + itemAddConfig: { + ...config.leftPanel?.itemAddConfig, + sourceColumn: value, + parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "", + } + }); + }} + className="text-xs" + > + + {column.columnLabel || column.columnName} + + ({column.columnName}) + + + ))} + + + + +
+ + {/* ์ƒ์œ„ ํ•ญ๋ชฉ ID๋ฅผ ์ €์žฅํ•  ์ปฌ๋Ÿผ (parentColumn) */} +
+ +

+ ํ•˜์œ„ ํ•ญ๋ชฉ์—์„œ ์ƒ์œ„ ํ•ญ๋ชฉ ID๋ฅผ ์ €์žฅํ•  ์ปฌ๋Ÿผ (์˜ˆ: parent_dept_code) +

+ + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {leftTableColumns + .filter((column) => !['company_code', 'company_name'].includes(column.columnName)) + .map((column) => ( + { + updateLeftPanel({ + itemAddConfig: { + ...config.leftPanel?.itemAddConfig, + parentColumn: value, + sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "", + } + }); + }} + className="text-xs" + > + + {column.columnLabel || column.columnName} + + ({column.columnName}) + + + ))} + + + + +
+ + {/* ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ์„ค์ • */} +
+
+ + +
+

+ ํ•˜์œ„ ํ•ญ๋ชฉ ์ถ”๊ฐ€ ์‹œ ์ž…๋ ฅ๋ฐ›์„ ํ•„๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š” +

+ +
+ {(config.leftPanel?.itemAddConfig?.addModalColumns || []).length === 0 ? ( +
+

์„ค์ •๋œ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค

+
+ ) : ( + (config.leftPanel?.itemAddConfig?.addModalColumns || []).map((col, index) => { + const column = leftTableColumns.find(c => c.columnName === col.name); + const isPK = column?.isPrimaryKey || false; + + return ( +
+ {isPK && ( + + PK + + )} +
+ + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {leftTableColumns + .filter((column) => !['company_code', 'company_name'].includes(column.columnName)) + .map((column) => ( + { + const newColumns = [...(config.leftPanel?.itemAddConfig?.addModalColumns || [])]; + newColumns[index] = { + ...newColumns[index], + name: value, + label: column.columnLabel || value, + }; + updateLeftPanel({ + itemAddConfig: { + ...config.leftPanel?.itemAddConfig, + addModalColumns: newColumns, + parentColumn: config.leftPanel?.itemAddConfig?.parentColumn || "", + sourceColumn: config.leftPanel?.itemAddConfig?.sourceColumn || "", + } + }); + }} + className="text-xs" + > + + {column.columnLabel || column.columnName} + + ({column.columnName}) + + + ))} + + + + +
+
+ +
+ +
+ ); + }) + )} +
+
+
+ )} + + {/* ์ขŒ์ธก ํŒจ๋„ ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ์„ค์ • */} + {config.leftPanel?.showAdd && ( +
+
+ + +
+

+ ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ์— ํ‘œ์‹œ๋  ์ž…๋ ฅ ํ•„๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š” +

+ +
+ {(config.leftPanel?.addModalColumns || []).length === 0 ? ( +
+

์„ค์ •๋œ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค

+
+ ) : ( + (config.leftPanel?.addModalColumns || []).map((col, index) => { + // ํ˜„์žฌ ์ปฌ๋Ÿผ์ด PK์ธ์ง€ ํ™•์ธ + const column = leftTableColumns.find(c => c.columnName === col.name); + const isPK = column?.isPrimaryKey || false; + + return ( +
+ {isPK && ( + + PK + + )} +
+ + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {leftTableColumns + .filter((column) => !['company_code', 'company_name'].includes(column.columnName)) + .map((column) => ( + { + const newColumns = [...(config.leftPanel?.addModalColumns || [])]; + newColumns[index] = { + ...newColumns[index], + name: value, + label: column.columnLabel || value, + }; + updateLeftPanel({ addModalColumns: newColumns }); + }} + className="text-xs" + > + + {column.columnLabel || column.columnName} + + ({column.columnName}) + + + ))} + + + + +
+
+ +
+ +
+ ); + }) + )} +
+
+ )}
{/* ์šฐ์ธก ํŒจ๋„ ์„ค์ • */} @@ -467,6 +1006,357 @@ export const SplitPanelLayoutConfigPanel: React.FC updateRightPanel({ showAdd: checked })} />
+ + {/* ์šฐ์ธก ํŒจ๋„ ํ‘œ์‹œ ์ปฌ๋Ÿผ ์„ค์ • */} +
+
+ + +
+

+ ์šฐ์ธก ํŒจ๋„์— ํ‘œ์‹œํ•  ์ปฌ๋Ÿผ์„ ์„ ํƒํ•˜์„ธ์š”. ์„ ํƒํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋“  ์ปฌ๋Ÿผ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค. +

+ + {/* ์„ ํƒ๋œ ์ปฌ๋Ÿผ ๋ชฉ๋ก */} +
+ {(config.rightPanel?.columns || []).length === 0 ? ( +
+

์„ค์ •๋œ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค

+

+ ์ปฌ๋Ÿผ์„ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋“  ์ปฌ๋Ÿผ์ด ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค +

+
+ ) : ( + (config.rightPanel?.columns || []).map((col, index) => ( +
+
+ + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {rightTableColumns.map((column) => ( + { + const newColumns = [...(config.rightPanel?.columns || [])]; + newColumns[index] = { + ...newColumns[index], + name: value, + label: column.columnLabel || value, + }; + updateRightPanel({ columns: newColumns }); + }} + className="text-xs" + > + + {column.columnLabel || column.columnName} + + ({column.columnName}) + + + ))} + + + + +
+ +
+ )) + )} +
+
+ + {/* ์šฐ์ธก ํŒจ๋„ ์ถ”๊ฐ€ ๋ชจ๋‹ฌ ์ปฌ๋Ÿผ ์„ค์ • */} + {config.rightPanel?.showAdd && ( +
+
+ + +
+

+ ์ถ”๊ฐ€ ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ ๋ชจ๋‹ฌ์— ํ‘œ์‹œ๋  ์ž…๋ ฅ ํ•„๋“œ๋ฅผ ์„ ํƒํ•˜์„ธ์š” +

+ +
+ {(config.rightPanel?.addModalColumns || []).length === 0 ? ( +
+

์„ค์ •๋œ ์ปฌ๋Ÿผ์ด ์—†์Šต๋‹ˆ๋‹ค

+
+ ) : ( + (config.rightPanel?.addModalColumns || []).map((col, index) => { + // ํ˜„์žฌ ์ปฌ๋Ÿผ์ด PK์ธ์ง€ ํ™•์ธ + const column = rightTableColumns.find(c => c.columnName === col.name); + const isPK = column?.isPrimaryKey || false; + + return ( +
+ {isPK && ( + + PK + + )} +
+ + + + + + + + ์ปฌ๋Ÿผ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. + + {rightTableColumns + .filter((column) => !['company_code', 'company_name'].includes(column.columnName)) + .map((column) => ( + { + const newColumns = [...(config.rightPanel?.addModalColumns || [])]; + newColumns[index] = { + ...newColumns[index], + name: value, + label: column.columnLabel || value, + }; + updateRightPanel({ addModalColumns: newColumns }); + }} + className="text-xs" + > + + {column.columnLabel || column.columnName} + + ({column.columnName}) + + + ))} + + + + +
+
+ +
+ +
+ ); + }) + )} +
+ + {/* ์ค‘๊ณ„ ํ…Œ์ด๋ธ” ์„ค์ • */} +
+ +

+ ์ค‘๊ณ„ ํ…Œ์ด๋ธ”์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค +

+ +
+ + { + const addConfig = config.rightPanel?.addConfig || {}; + updateRightPanel({ + addConfig: { + ...addConfig, + targetTable: e.target.value, + }, + }); + }} + placeholder="์˜ˆ: user_dept" + className="mt-1 h-8 text-xs" + /> +

+ ๋ฐ์ดํ„ฐ๊ฐ€ ์‹ค์ œ๋กœ ์ €์žฅ๋  ์ค‘๊ณ„ ํ…Œ์ด๋ธ”๋ช… +

+
+ +
+ + { + const addConfig = config.rightPanel?.addConfig || {}; + updateRightPanel({ + addConfig: { + ...addConfig, + leftPanelColumn: e.target.value, + }, + }); + }} + placeholder="์˜ˆ: dept_code" + className="mt-1 h-8 text-xs" + /> +

+ ์ขŒ์ธก ํŒจ๋„์—์„œ ์„ ํƒํ•œ ํ•ญ๋ชฉ์˜ ์–ด๋–ค ์ปฌ๋Ÿผ๊ฐ’์„ ๊ฐ€์ ธ์˜ฌ์ง€ +

+
+ +
+ + { + const addConfig = config.rightPanel?.addConfig || {}; + updateRightPanel({ + addConfig: { + ...addConfig, + targetColumn: e.target.value, + }, + }); + }} + placeholder="์˜ˆ: dept_code" + className="mt-1 h-8 text-xs" + /> +

+ ์ค‘๊ณ„ ํ…Œ์ด๋ธ”์˜ ์–ด๋–ค ์ปฌ๋Ÿผ์— ์ขŒ์ธก๊ฐ’์„ ์ €์žฅํ• ์ง€ +

+
+ +
+ +