diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts index 2150a4af..241bc9e3 100644 --- a/backend-node/src/services/dataService.ts +++ b/backend-node/src/services/dataService.ts @@ -18,6 +18,45 @@ import { pool } from "../database/db"; // ๐Ÿ†• Entity ์กฐ์ธ์„ ์œ„ํ•œ pool impo import { buildDataFilterWhereClause } from "../utils/dataFilterUtil"; // ๐Ÿ†• ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ ์œ ํ‹ธ import { v4 as uuidv4 } from "uuid"; // ๐Ÿ†• UUID ์ƒ์„ฑ +/** + * ๋น„๋ฐ€๋ฒˆํ˜ธ(password) ํƒ€์ž… ์ปฌ๋Ÿผ์˜ ๊ฐ’์„ ๋นˆ ๋ฌธ์ž์—ด๋กœ ๋งˆ์Šคํ‚น + * - table_type_columns์—์„œ input_type = 'password'์ธ ์ปฌ๋Ÿผ์„ ์กฐํšŒ + * - ๋ฐ์ดํ„ฐ ์‘๋‹ต์—์„œ ํ•ด๋‹น ์ปฌ๋Ÿผ ๊ฐ’์„ ๋น„์›Œ์„œ ํ•ด์‹œ๊ฐ’ ๋…ธ์ถœ ๋ฐฉ์ง€ + */ +async function maskPasswordColumns(tableName: string, data: any): Promise { + try { + const passwordCols = await query<{ column_name: string }>( + `SELECT DISTINCT column_name FROM table_type_columns + WHERE table_name = $1 AND input_type = 'password'`, + [tableName] + ); + if (passwordCols.length === 0) return data; + + const passwordColumnNames = new Set(passwordCols.map(c => c.column_name)); + + // ๋‹จ์ผ ๊ฐ์ฒด ์ฒ˜๋ฆฌ + const maskRow = (row: any) => { + if (!row || typeof row !== "object") return row; + const masked = { ...row }; + for (const col of passwordColumnNames) { + if (col in masked) { + masked[col] = ""; // ํ•ด์‹œ๊ฐ’ ๋Œ€์‹  ๋นˆ ๋ฌธ์ž์—ด + } + } + return masked; + }; + + if (Array.isArray(data)) { + return data.map(maskRow); + } + return maskRow(data); + } catch (error) { + // ๋งˆ์Šคํ‚น ์‹คํŒจํ•ด๋„ ์›๋ณธ ๋ฐ์ดํ„ฐ ๋ฐ˜ํ™˜ (์„œ๋น„์Šค ์ค‘๋‹จ ๋ฐฉ์ง€) + console.warn("โš ๏ธ password ์ปฌ๋Ÿผ ๋งˆ์Šคํ‚น ์‹คํŒจ:", error); + return data; + } +} + interface GetTableDataParams { tableName: string; limit?: number; @@ -622,14 +661,14 @@ class DataService { return { success: true, - data: normalizedGroupRows, // ๐Ÿ”ง ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜! + data: await maskPasswordColumns(tableName, normalizedGroupRows), // ๐Ÿ”ง ๋ฐฐ์—ด๋กœ ๋ฐ˜ํ™˜! + password ๋งˆ์Šคํ‚น }; } } return { success: true, - data: normalizedRows[0], // ๊ทธ๋ฃนํ•‘ ์—†์œผ๋ฉด ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ + data: await maskPasswordColumns(tableName, normalizedRows[0]), // ๊ทธ๋ฃนํ•‘ ์—†์œผ๋ฉด ๋‹จ์ผ ๋ ˆ์ฝ”๋“œ + password ๋งˆ์Šคํ‚น }; } } @@ -648,7 +687,7 @@ class DataService { return { success: true, - data: result[0], + data: await maskPasswordColumns(tableName, result[0]), // password ๋งˆ์Šคํ‚น }; } catch (error) { console.error(`๋ ˆ์ฝ”๋“œ ์ƒ์„ธ ์กฐํšŒ ์˜ค๋ฅ˜ (${tableName}/${id}):`, error); diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts index 9e0915ee..28ea6cc9 100644 --- a/backend-node/src/services/dynamicFormService.ts +++ b/backend-node/src/services/dynamicFormService.ts @@ -2,6 +2,7 @@ import { query, queryOne, transaction, getPool } from "../database/db"; import { EventTriggerService } from "./eventTriggerService"; import { DataflowControlService } from "./dataflowControlService"; import tableCategoryValueService from "./tableCategoryValueService"; +import { PasswordUtils } from "../utils/passwordUtils"; export interface FormDataResult { id: number; @@ -859,6 +860,33 @@ export class DynamicFormService { } } + // ๋น„๋ฐ€๋ฒˆํ˜ธ(password) ํƒ€์ž… ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ + // - ๋นˆ ๊ฐ’์ด๋ฉด ๋ณ€๊ฒฝ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ (๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ์œ ์ง€) + // - ๊ฐ’์ด ์žˆ์œผ๋ฉด ์•”ํ˜ธํ™” ํ›„ ์ €์žฅ + try { + const passwordCols = await query<{ column_name: string }>( + `SELECT DISTINCT column_name FROM table_type_columns + WHERE table_name = $1 AND input_type = 'password'`, + [tableName] + ); + for (const { column_name } of passwordCols) { + if (column_name in changedFields) { + const pwValue = changedFields[column_name]; + if (!pwValue || pwValue === "") { + // ๋นˆ ๊ฐ’ โ†’ ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ ์œ ์ง€ (๋ณ€๊ฒฝ ๋ชฉ๋ก์—์„œ ์ œ๊ฑฐ) + delete changedFields[column_name]; + console.log(`๐Ÿ” ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ ${column_name}: ๋นˆ ๊ฐ’์ด๋ฏ€๋กœ ์—…๋ฐ์ดํŠธ ์Šคํ‚ต (๊ธฐ์กด ์œ ์ง€)`); + } else { + // ๊ฐ’ ์žˆ์Œ โ†’ ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ €์žฅ + changedFields[column_name] = PasswordUtils.encrypt(pwValue); + console.log(`๐Ÿ” ๋น„๋ฐ€๋ฒˆํ˜ธ ํ•„๋“œ ${column_name}: ์ƒˆ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ์™„๋ฃŒ`); + } + } + } + } catch (pwError) { + console.warn("โš ๏ธ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ปฌ๋Ÿผ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:", pwError); + } + // ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๊ฐ€ ์—†์œผ๋ฉด ์—…๋ฐ์ดํŠธ ๊ฑด๋„ˆ๋›ฐ๊ธฐ if (Object.keys(changedFields).length === 0) { console.log("๐Ÿ“‹ ๋ณ€๊ฒฝ๋œ ํ•„๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์—…๋ฐ์ดํŠธ๋ฅผ ๊ฑด๋„ˆ๋œ๋‹ˆ๋‹ค."); diff --git a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx index 1eefaa22..6af5a88f 100644 --- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx +++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx @@ -596,10 +596,22 @@ export const SelectedItemsDetailInputComponent: React.FC !!item.originalData?.id); + const isEditMode = urlEditMode || dataHasDbId; + + console.log("[SelectedItemsDetailInput] ์ˆ˜์ • ๋ชจ๋“œ ๊ฐ์ง€:", { + urlEditMode, + dataHasDbId, + isEditMode, + itemCount: items.length, + firstItemId: items[0]?.originalData?.id, + }); // fieldGroup๋ณ„ sourceTable ๋ถ„๋ฅ˜ const groupsByTable = new Map(); @@ -695,6 +707,16 @@ export const SelectedItemsDetailInputComponent: React.FC !!r.id); const shouldDeleteOrphans = isEditMode || mappingHasDbIds; + + console.log(`[SelectedItemsDetailInput] ${mainTable} ์ €์žฅ:`, { + isEditMode, + mappingHasDbIds, + shouldDeleteOrphans, + recordCount: mappingRecords.length, + recordIds: mappingRecords.map(r => r.id || "NEW"), + parentKeys: itemParentKeys, + }); + // ์ €์žฅ๋œ ๋งคํ•‘ ID๋ฅผ ์ถ”์  (๋””ํ…Œ์ผ ํ…Œ์ด๋ธ”์— mapping_id ์ฃผ์ž…์šฉ) let savedMappingIds: string[] = []; try { @@ -782,6 +804,16 @@ export const SelectedItemsDetailInputComponent: React.FC !!r.id); const shouldDeleteDetailOrphans = isEditMode || priceHasDbIds; + + console.log(`[SelectedItemsDetailInput] ${detailTable} ์ €์žฅ:`, { + isEditMode, + priceHasDbIds, + shouldDeleteDetailOrphans, + recordCount: priceRecords.length, + recordIds: priceRecords.map(r => r.id || "NEW"), + parentKeys: itemParentKeys, + }); + try { const detailResult = await dataApi.upsertGroupedRecords( detailTable,