diff --git a/backend-node/src/controllers/screenGroupController.ts b/backend-node/src/controllers/screenGroupController.ts index b53454b9..0e97e2e2 100644 --- a/backend-node/src/controllers/screenGroupController.ts +++ b/backend-node/src/controllers/screenGroupController.ts @@ -1488,13 +1488,13 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons SELECT sd.screen_id, sd.screen_name, - sd.table_name as main_table, - jsonb_array_elements_text( + sd.table_name::text as main_table, + jsonb_array_elements( COALESCE( sl.properties->'componentConfig'->'columns', '[]'::jsonb ) - )::jsonb->>'columnName' as column_name + )->>'columnName' as column_name FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id WHERE sd.screen_id = ANY($1) @@ -1507,7 +1507,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons SELECT sd.screen_id, sd.screen_name, - sd.table_name as main_table, + sd.table_name::text as main_table, COALESCE( sl.properties->'componentConfig'->>'bindField', sl.properties->>'bindField', @@ -1530,7 +1530,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons SELECT sd.screen_id, sd.screen_name, - sd.table_name as main_table, + sd.table_name::text as main_table, sl.properties->'componentConfig'->>'valueField' as column_name FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id @@ -1543,7 +1543,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons SELECT sd.screen_id, sd.screen_name, - sd.table_name as main_table, + sd.table_name::text as main_table, sl.properties->'componentConfig'->>'parentFieldId' as column_name FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id @@ -1556,7 +1556,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons SELECT sd.screen_id, sd.screen_name, - sd.table_name as main_table, + sd.table_name::text as main_table, sl.properties->'componentConfig'->>'cascadingParentField' as column_name FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id @@ -1569,7 +1569,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons SELECT sd.screen_id, sd.screen_name, - sd.table_name as main_table, + sd.table_name::text as main_table, sl.properties->'componentConfig'->>'controlField' as column_name FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id @@ -1750,7 +1750,7 @@ export const getScreenSubTables = async (req: AuthenticatedRequest, res: Respons sd.table_name as main_table, sl.properties->>'componentType' as component_type, sl.properties->'componentConfig'->'rightPanel'->'relation' as right_panel_relation, - sl.properties->'componentConfig'->'rightPanel'->'tableName' as right_panel_table, + sl.properties->'componentConfig'->'rightPanel'->>'tableName' as right_panel_table, sl.properties->'componentConfig'->'rightPanel'->'columns' as right_panel_columns FROM screen_definitions sd JOIN screen_layouts sl ON sd.screen_id = sl.screen_id 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 e1242afd..ac2377fe 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/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 38b6da5a..d8ce8e7a 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -113,6 +113,10 @@ export const EditModal: React.FC = ({ className }) => { // ํผ ๋ฐ์ดํ„ฐ ์ƒํƒœ (ํŽธ์ง‘ ๋ฐ์ดํ„ฐ๋กœ ์ดˆ๊ธฐํ™”๋จ) const [formData, setFormData] = useState>({}); const [originalData, setOriginalData] = useState>({}); + // INSERT/UPDATE ํŒ๋‹จ์šฉ ํ”Œ๋ž˜๊ทธ (์ด๋ฒคํŠธ์—์„œ ๋ช…์‹œ์ ์œผ๋กœ ์ „๋‹ฌ๋ฐ›์Œ) + // true = INSERT (๋“ฑ๋ก/๋ณต์‚ฌ), false = UPDATE (์ˆ˜์ •) + // originalData ์ƒํƒœ์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ์ด๋ฒคํŠธ์˜ isCreateMode ๊ฐ’์„ ์ง์ ‘ ์‚ฌ์šฉ + const [isCreateModeFlag, setIsCreateModeFlag] = useState(true); // ๐Ÿ†• ๊ทธ๋ฃน ๋ฐ์ดํ„ฐ ์ƒํƒœ (๊ฐ™์€ order_no์˜ ๋ชจ๋“  ํ’ˆ๋ชฉ) const [groupData, setGroupData] = useState[]>([]); @@ -271,13 +275,19 @@ export const EditModal: React.FC = ({ className }) => { // ํŽธ์ง‘ ๋ฐ์ดํ„ฐ๋กœ ํผ ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” setFormData(editData || {}); - // ๐Ÿ†• isCreateMode๊ฐ€ true์ด๋ฉด originalData๋ฅผ ๋นˆ ๊ฐ์ฒด๋กœ ์„ค์ • (INSERT ๋ชจ๋“œ) - // originalData๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด INSERT, ์žˆ์œผ๋ฉด UPDATE๋กœ ์ฒ˜๋ฆฌ๋จ + // originalData: changedData ๊ณ„์‚ฐ(PATCH)์—๋งŒ ์‚ฌ์šฉ + // INSERT/UPDATE ํŒ๋‹จ์—๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ setOriginalData(isCreateMode ? {} : editData || {}); + // INSERT/UPDATE ํŒ๋‹จ: ์ด๋ฒคํŠธ์˜ isCreateMode ํ”Œ๋ž˜๊ทธ๋ฅผ ์ง์ ‘ ์ €์žฅ + // isCreateMode=true(๋ณต์‚ฌ/๋“ฑ๋ก) โ†’ INSERT, false/undefined(์ˆ˜์ •) โ†’ UPDATE + setIsCreateModeFlag(!!isCreateMode); - if (isCreateMode) { - console.log("[EditModal] ์ƒ์„ฑ ๋ชจ๋“œ๋กœ ์—ด๋ฆผ, ์ดˆ๊ธฐ๊ฐ’:", editData); - } + console.log("[EditModal] ๋ชจ๋‹ฌ ์—ด๋ฆผ:", { + mode: isCreateMode ? "INSERT (์ƒ์„ฑ/๋ณต์‚ฌ)" : "UPDATE (์ˆ˜์ •)", + hasEditData: !!editData, + editDataId: editData?.id, + isCreateMode, + }); }; const handleCloseEditModal = () => { @@ -579,6 +589,7 @@ export const EditModal: React.FC = ({ className }) => { setZones([]); setConditionalLayers([]); setOriginalData({}); + setIsCreateModeFlag(true); // ๊ธฐ๋ณธ๊ฐ’์€ INSERT (์•ˆ์ „ ๋ฐฉํ–ฅ) setGroupData([]); // ๐Ÿ†• setOriginalGroupData([]); // ๐Ÿ†• }; @@ -942,8 +953,31 @@ export const EditModal: React.FC = ({ className }) => { return; } - // originalData๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด INSERT, ์žˆ์œผ๋ฉด UPDATE - const isCreateMode = Object.keys(originalData).length === 0; + // ======================================== + // INSERT/UPDATE ํŒ๋‹จ (์žฌ์„ค๊ณ„) + // ======================================== + // ํŒ๋‹จ ๊ธฐ์ค€: + // 1. isCreateModeFlag === true โ†’ ๋ฌด์กฐ๊ฑด INSERT (๋ณต์‚ฌ/๋“ฑ๋ก ๋ชจ๋“œ ๋ณดํ˜ธ) + // 2. isCreateModeFlag === false โ†’ formData.id ์žˆ์œผ๋ฉด UPDATE, ์—†์œผ๋ฉด INSERT + // originalData๋Š” INSERT/UPDATE ํŒ๋‹จ์— ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ (changedData ๊ณ„์‚ฐ์—๋งŒ ์‚ฌ์šฉ) + // ======================================== + let isCreateMode: boolean; + + if (isCreateModeFlag) { + // ์ด๋ฒคํŠธ์—์„œ ๋ช…์‹œ์ ์œผ๋กœ INSERT ๋ชจ๋“œ๋กœ ์ง€์ •๋จ (๋“ฑ๋ก/๋ณต์‚ฌ) + isCreateMode = true; + } else { + // ์ˆ˜์ • ๋ชจ๋“œ: formData์— id๊ฐ€ ์žˆ์œผ๋ฉด UPDATE, ์—†์œผ๋ฉด INSERT + isCreateMode = !formData.id; + } + + console.log("[EditModal] ์ €์žฅ ๋ชจ๋“œ ํŒ๋‹จ:", { + isCreateMode, + isCreateModeFlag, + formDataId: formData.id, + originalDataLength: Object.keys(originalData).length, + tableName: screenData.screenInfo.tableName, + }); if (isCreateMode) { // INSERT ๋ชจ๋“œ @@ -1134,70 +1168,57 @@ export const EditModal: React.FC = ({ className }) => { throw new Error(response.message || "์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."); } } else { - // UPDATE ๋ชจ๋“œ - ๊ธฐ์กด ๋กœ์ง - const changedData: Record = {}; - Object.keys(formData).forEach((key) => { - if (formData[key] !== originalData[key]) { - let value = formData[key]; - - // ๐Ÿ”ง ๋ฐฐ์—ด์ด๋ฉด ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ) - if (Array.isArray(value)) { - // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _targetTable ๋˜๋Š” _isNewItem์ด ์žˆ์œผ๋ฉด ๋ฆฌํ”ผํ„ฐ) - const isRepeaterData = value.length > 0 && - typeof value[0] === "object" && - value[0] !== null && - ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); - - if (!isRepeaterData) { - // ๐Ÿ”ง ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง ํ—ฌํผ (์ค‘๊ด„ํ˜ธ, ๋”ฐ์˜ดํ‘œ, ๋ฐฑ์Šฌ๋ž˜์‹œ ํฌํ•จ ์‹œ ๋ฌดํšจ) - const isValidValue = (v: any): boolean => { - if (typeof v === "number" && !isNaN(v)) return true; - if (typeof v !== "string") return false; - if (!v || v.trim() === "") return false; - // ์†์ƒ๋œ PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ๊ฐ์ง€ - if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false; - return true; - }; - - // ๐Ÿ”ง ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง) - const validValues = value - .map((v: any) => typeof v === "number" ? String(v) : v) - .filter(isValidValue); - - if (validValues.length !== value.length) { - console.warn(`โš ๏ธ [EditModal UPDATE] ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง: ${key}`, { - before: value.length, - after: validValues.length, - removed: value.filter((v: any) => !isValidValue(v)) - }); - } - - const stringValue = validValues.join(","); - console.log(`๐Ÿ”ง [EditModal UPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value.length, valid: validValues.length, converted: stringValue }); - value = stringValue; - } - } - - changedData[key] = value; - } - }); + // UPDATE ๋ชจ๋“œ - PUT (์ „์ฒด ์—…๋ฐ์ดํŠธ) + // originalData ๋น„๊ต ์—†์ด formData ์ „์ฒด๋ฅผ ๋ณด๋ƒ„ + const recordId = formData.id; - if (Object.keys(changedData).length === 0) { - toast.info("๋ณ€๊ฒฝ๋œ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."); - handleClose(); + if (!recordId) { + console.error("[EditModal] UPDATE ์‹คํŒจ: formData์— id๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.", { + formDataKeys: Object.keys(formData), + }); + toast.error("์ˆ˜์ •ํ•  ๋ ˆ์ฝ”๋“œ์˜ ID๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); return; } - // ๊ธฐ๋ณธํ‚ค ํ™•์ธ (id ๋˜๋Š” ์ฒซ ๋ฒˆ์งธ ํ‚ค) - const recordId = originalData.id || Object.values(originalData)[0]; + // ๋ฐฐ์—ด ๊ฐ’ โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด ๋ณ€ํ™˜ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ) + const dataToSave: Record = {}; + Object.entries(formData).forEach(([key, value]) => { + if (Array.isArray(value)) { + const isRepeaterData = value.length > 0 && + typeof value[0] === "object" && + value[0] !== null && + ("_targetTable" in value[0] || "_isNewItem" in value[0] || "_existingRecord" in value[0]); + + if (isRepeaterData) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋Š” ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ) + return; + } + // ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด + const validValues = value + .map((v: any) => typeof v === "number" ? String(v) : v) + .filter((v: any) => { + if (typeof v === "number") return true; + if (typeof v !== "string") return false; + if (!v || v.trim() === "") return false; + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false; + return true; + }); + dataToSave[key] = validValues.join(","); + } else { + dataToSave[key] = value; + } + }); - // UPDATE ์•ก์…˜ ์‹คํ–‰ - const response = await dynamicFormApi.updateFormDataPartial( + console.log("[EditModal] UPDATE(PUT) ์‹คํ–‰:", { recordId, - originalData, - changedData, - screenData.screenInfo.tableName, - ); + fieldCount: Object.keys(dataToSave).length, + tableName: screenData.screenInfo.tableName, + }); + + const response = await dynamicFormApi.updateFormData(recordId, { + tableName: screenData.screenInfo.tableName, + data: dataToSave, + }); if (response.success) { toast.success("๋ฐ์ดํ„ฐ๊ฐ€ ์ˆ˜์ •๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); diff --git a/frontend/components/screen/StyleEditor.tsx b/frontend/components/screen/StyleEditor.tsx index f265115b..3add842c 100644 --- a/frontend/components/screen/StyleEditor.tsx +++ b/frontend/components/screen/StyleEditor.tsx @@ -33,6 +33,15 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd onStyleChange(newStyle); }; + // ์ˆซ์ž๋งŒ ์ž…๋ ฅํ–ˆ์„ ๋•Œ ์ž๋™์œผ๋กœ px ๋ถ™์—ฌ์ฃผ๋Š” ํ•ธ๋“ค๋Ÿฌ + const autoPxProperties: (keyof ComponentStyle)[] = ["fontSize", "borderWidth", "borderRadius"]; + const handlePxBlur = (property: keyof ComponentStyle) => { + const val = localStyle[property]; + if (val && /^\d+(\.\d+)?$/.test(String(val))) { + handleStyleChange(property, `${val}px`); + } + }; + const toggleSection = (section: string) => { setOpenSections((prev) => ({ ...prev, @@ -66,6 +75,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="1px" value={localStyle.borderWidth || ""} onChange={(e) => handleStyleChange("borderWidth", e.target.value)} + onBlur={() => handlePxBlur("borderWidth")} className="h-6 w-full px-2 py-0 text-xs" /> @@ -121,6 +131,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="5px" value={localStyle.borderRadius || ""} onChange={(e) => handleStyleChange("borderRadius", e.target.value)} + onBlur={() => handlePxBlur("borderRadius")} className="h-6 w-full px-2 py-0 text-xs" /> @@ -209,6 +220,7 @@ export default function StyleEditor({ style, onStyleChange, className }: StyleEd placeholder="14px" value={localStyle.fontSize || ""} onChange={(e) => handleStyleChange("fontSize", e.target.value)} + onBlur={() => handlePxBlur("fontSize")} className="h-6 w-full px-2 py-0 text-xs" /> diff --git a/frontend/components/v2/V2Input.tsx b/frontend/components/v2/V2Input.tsx index dac766d5..09a43ca9 100644 --- a/frontend/components/v2/V2Input.tsx +++ b/frontend/components/v2/V2Input.tsx @@ -82,8 +82,9 @@ const TextInput = forwardRef< disabled?: boolean; className?: string; columnName?: string; + inputStyle?: React.CSSProperties; } ->(({ value, onChange, format = "none", placeholder, readonly, disabled, className, columnName }, ref) => { +>(({ value, onChange, format = "none", placeholder, readonly, disabled, className, columnName, inputStyle }, ref) => { // ๊ฒ€์ฆ ์ƒํƒœ const [hasBlurred, setHasBlurred] = useState(false); const [validationError, setValidationError] = useState(""); @@ -210,6 +211,7 @@ const TextInput = forwardRef< hasError && "border-destructive focus-visible:ring-destructive", className, )} + style={inputStyle} /> {hasError && (

{validationError}

@@ -234,8 +236,9 @@ const NumberInput = forwardRef< readonly?: boolean; disabled?: boolean; className?: string; + inputStyle?: React.CSSProperties; } ->(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className }, ref) => { +>(({ value, onChange, min, max, step = 1, placeholder, readonly, disabled, className, inputStyle }, ref) => { const handleChange = useCallback( (e: React.ChangeEvent) => { const val = e.target.value; @@ -268,6 +271,7 @@ const NumberInput = forwardRef< readOnly={readonly} disabled={disabled} className={cn("h-full w-full", className)} + style={inputStyle} /> ); }); @@ -285,8 +289,9 @@ const PasswordInput = forwardRef< readonly?: boolean; disabled?: boolean; className?: string; + inputStyle?: React.CSSProperties; } ->(({ value, onChange, placeholder, readonly, disabled, className }, ref) => { +>(({ value, onChange, placeholder, readonly, disabled, className, inputStyle }, ref) => { const [showPassword, setShowPassword] = useState(false); return ( @@ -300,6 +305,7 @@ const PasswordInput = forwardRef< readOnly={readonly} disabled={disabled} className={cn("h-full w-full pr-10", className)} + style={inputStyle} />