diff --git a/backend-node/src/services/nodeFlowExecutionService.ts b/backend-node/src/services/nodeFlowExecutionService.ts index cadfdefc..9bc59d97 100644 --- a/backend-node/src/services/nodeFlowExecutionService.ts +++ b/backend-node/src/services/nodeFlowExecutionService.ts @@ -102,6 +102,80 @@ export interface NodeExecutionSummary { error?: string; } +// ===== ํ—ฌํผ ํ•จ์ˆ˜ ===== + +/** + * ๐Ÿ”ง ์œ ํšจํ•œ ๊ฐ’์ธ์ง€ ์ฒดํฌ (์ค‘๊ด„ํ˜ธ, ๋”ฐ์˜ดํ‘œ, ๋ฐฑ์Šฌ๋ž˜์‹œ ์—†์–ด์•ผ ํ•จ) + * ์ˆซ์ž๋„ ์œ ํšจํ•œ ๊ฐ’์œผ๋กœ ์ฒ˜๋ฆฌ + */ +function isValidDBValue(v: any): boolean { + // ์ˆซ์ž๋ฉด ์œ ํšจ (๋‚˜์ค‘์— ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜๋จ) + if (typeof v === "number" && !isNaN(v)) 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; +} + +/** + * ๐Ÿ”ง ๊ฐ’์„ DB ์ €์žฅ์šฉ์œผ๋กœ ์ •๊ทœํ™” (PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ์ €์žฅ ๋ฐฉ์ง€) + * - JavaScript ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด (์œ ํšจํ•œ ๊ฐ’๋งŒ) + * - PostgreSQL ๋ฐฐ์—ด ํ˜•์‹ ๋ฌธ์ž์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด (์œ ํšจํ•œ ๊ฐ’๋งŒ) + * - ์ค‘์ฒฉ๋œ ์ž˜๋ชป๋œ ํ˜•์‹ โ†’ null + */ +function normalizeValueForDB(value: any): any { + // 1. ๋ฐฐ์—ด์ด๋ฉด ์œ ํšจํ•œ ๊ฐ’๋งŒ ํ•„ํ„ฐ๋ง ํ›„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + if (Array.isArray(value)) { + // ์ˆซ์ž๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๊ณ  ์œ ํšจํ•œ ๊ฐ’๋งŒ ํ•„ํ„ฐ๋ง + const validValues = value + .map(v => typeof v === "number" ? String(v) : v) + .filter(isValidDBValue) + .map(v => typeof v === "number" ? String(v) : v); // ์ตœ์ข… ๋ฌธ์ž์—ด ๋ณ€ํ™˜ + if (validValues.length === 0) { + console.warn(`โš ๏ธ [normalizeValueForDB] ๋ฐฐ์—ด์— ์œ ํšจํ•œ ๊ฐ’ ์—†์Œ:`, value); + return null; + } + const normalized = validValues.join(","); + console.log(`๐Ÿ”ง [normalizeValueForDB] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด:`, { original: value.length, valid: validValues.length, normalized }); + return normalized; + } + + // 2. ๋ฌธ์ž์—ด์ธ๋ฐ ์ž˜๋ชป๋œ ํ˜•์‹์ด๋ฉด ์ •๋ฆฌ + if (typeof value === "string" && value) { + // ์ž˜๋ชป๋œ ํ˜•์‹ ๊ฐ์ง€ + if (value.includes("{") || value.includes("}") || value.includes('\\"') || value.includes("\\\\")) { + console.warn(`โš ๏ธ [normalizeValueForDB] ์ž˜๋ชป๋œ ๋ฌธ์ž์—ด ํ˜•์‹:`, value.substring(0, 80)); + + // ์ •๊ทœํ‘œํ˜„์‹์œผ๋กœ ์œ ํšจํ•œ ์ฝ”๋“œ๋งŒ ์ถ”์ถœ + const codePattern = /\b(CAT_[A-Z0-9_]+|[A-Z]{2,}_[A-Z0-9_]+)\b/g; + const matches = value.match(codePattern); + + if (matches && matches.length > 0) { + const uniqueValues = [...new Set(matches)]; + const normalized = uniqueValues.join(","); + console.log(`๐Ÿ”ง [normalizeValueForDB] ์ฝ”๋“œ ์ถ”์ถœ:`, { count: uniqueValues.length, normalized }); + return normalized; + } + + console.warn(`โš ๏ธ [normalizeValueForDB] ์œ ํšจํ•œ ์ฝ”๋“œ ์—†์Œ, null ๋ฐ˜ํ™˜`); + return null; + } + + // ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด์ด๋ฉด ๊ฐ ๊ฐ’ ๊ฒ€์ฆ + if (value.includes(",")) { + const parts = value.split(",").map(v => v.trim()).filter(isValidDBValue); + if (parts.length === 0) { + return null; + } + return parts.join(","); + } + } + + return value; +} + // ===== ๋ฉ”์ธ ์‹คํ–‰ ์„œ๋น„์Šค ===== export class NodeFlowExecutionService { @@ -1019,10 +1093,12 @@ export class NodeFlowExecutionService { ); } - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); // ๐Ÿ”ฅ ์‚ฝ์ž…๋œ ๊ฐ’์„ ๋ฐ์ดํ„ฐ์— ๋ฐ˜์˜ - insertedData[mapping.targetField] = value; + insertedData[mapping.targetField] = normalizedValue; } // ๐Ÿ†• writer์™€ company_code ์ž๋™ ์ถ”๊ฐ€ (ํ•„๋“œ ๋งคํ•‘์— ์—†๋Š” ๊ฒฝ์šฐ) @@ -1155,9 +1231,11 @@ export class NodeFlowExecutionService { mapping.staticValue !== undefined ? mapping.staticValue : data[mapping.sourceField]; - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); // ๐Ÿ”ฅ ์‚ฝ์ž…๋œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด์— ๋งคํ•‘๋œ ๊ฐ’ ์ ์šฉ - insertedData[mapping.targetField] = value; + insertedData[mapping.targetField] = normalizedValue; }); // ์™ธ๋ถ€ DB๋ณ„ SQL ๋ฌธ๋ฒ• ์ฐจ์ด ์ฒ˜๋ฆฌ @@ -1493,7 +1571,8 @@ export class NodeFlowExecutionService { if (mapping.targetField) { setClauses.push(`${mapping.targetField} = $${paramIndex}`); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + values.push(normalizeValueForDB(value)); paramIndex++; } }); @@ -1556,11 +1635,13 @@ export class NodeFlowExecutionService { // targetField๊ฐ€ ๋น„์–ด์žˆ์ง€ ์•Š์€ ๊ฒฝ์šฐ๋งŒ ์ถ”๊ฐ€ if (mapping.targetField) { setClauses.push(`${mapping.targetField} = $${paramIndex}`); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); paramIndex++; // ๐Ÿ”ฅ ์—…๋ฐ์ดํŠธ๋œ ๊ฐ’์„ ๋ฐ์ดํ„ฐ์— ๋ฐ˜์˜ - updatedData[mapping.targetField] = value; + updatedData[mapping.targetField] = normalizedValue; } else { console.log( `โš ๏ธ targetField๊ฐ€ ๋น„์–ด์žˆ์–ด ์Šคํ‚ต: ${mapping.sourceField}` @@ -1685,10 +1766,12 @@ export class NodeFlowExecutionService { setClauses.push(`${mapping.targetField} = $${paramIndex}`); } - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + const normalizedValue = normalizeValueForDB(value); + values.push(normalizedValue); paramIndex++; // ๐Ÿ”ฅ ์—…๋ฐ์ดํŠธ๋œ ๋ฐ์ดํ„ฐ ๊ฐ์ฒด์— ๋งคํ•‘๋œ ๊ฐ’ ์ ์šฉ - updatedData[mapping.targetField] = value; + updatedData[mapping.targetField] = normalizedValue; }); // WHERE ์กฐ๊ฑด ์ƒ์„ฑ @@ -2317,7 +2400,8 @@ export class NodeFlowExecutionService { ? mapping.staticValue : data[mapping.sourceField]; setClauses.push(`${mapping.targetField} = $${paramIndex}`); - updateValues.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + updateValues.push(normalizeValueForDB(value)); paramIndex++; } }); @@ -2368,7 +2452,8 @@ export class NodeFlowExecutionService { ? mapping.staticValue : data[mapping.sourceField]; columns.push(mapping.targetField); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + values.push(normalizeValueForDB(value)); }); // ๐Ÿ†• writer์™€ company_code ์ž๋™ ์ถ”๊ฐ€ (ํ•„๋“œ ๋งคํ•‘์— ์—†๋Š” ๊ฒฝ์šฐ) @@ -2549,7 +2634,8 @@ export class NodeFlowExecutionService { setClauses.push(`${mapping.targetField} = $${paramIndex}`); } - updateValues.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + updateValues.push(normalizeValueForDB(value)); paramIndex++; } }); @@ -2587,7 +2673,8 @@ export class NodeFlowExecutionService { ? mapping.staticValue : data[mapping.sourceField]; columns.push(mapping.targetField); - values.push(value); + // ๐Ÿ”ง ๋ฐฐ์—ด์„ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ + values.push(normalizeValueForDB(value)); }); let insertSql: string; diff --git a/docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md b/docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md new file mode 100644 index 00000000..3d5ba77c --- /dev/null +++ b/docs/DDD1542/MULTI_SELECT_ARRAY_SERIALIZATION_FIX.md @@ -0,0 +1,253 @@ +# ๋‹ค์ค‘ ์„ ํƒ(Multi-Select) ๋ฐฐ์—ด ์ง๋ ฌํ™” ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ณด๊ณ ์„œ + +## ๋ฌธ์ œ ์š”์•ฝ + +**์ฆ์ƒ**: ๋‹ค์ค‘ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ(TagboxSelect, ์ฒดํฌ๋ฐ•์Šค ๋“ฑ)๋กœ ์„ ํƒํ•œ ๊ฐ’์ด DB์— ์ €์žฅ๋  ๋•Œ ์†์ƒ๋˜๊ฑฐ๋‚˜ `null`๋กœ ์ €์žฅ๋จ + +**์˜ํ–ฅ๋ฐ›๋Š” ๊ธฐ๋Šฅ**: +- ํ’ˆ๋ชฉ์ •๋ณด์˜ `division` (๊ตฌ๋ถ„) ํ•„๋“œ +- ๋ชจ๋“  ๋‹ค์ค‘ ์„ ํƒ ์นดํ…Œ๊ณ ๋ฆฌ ํ•„๋“œ + +**์†์ƒ๋œ ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ**: +``` +{"{\"{\\\"CAT_ML7SR2T9_IM7H\\\",\\\"CAT_ML8ZFQFU_EE5Z\\\"}\"}",...} +``` + +**์ •์ƒ ๋ฐ์ดํ„ฐ ์˜ˆ์‹œ**: +``` +CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z,CAT_ML8ZFVEL_1TOR +``` + +--- + +## ๋ฌธ์ œ ์›์ธ ๋ถ„์„ + +### 1. PostgreSQL์˜ ๋ฐฐ์—ด ์ž๋™ ๋ณ€ํ™˜ + +Node.js์˜ `node-pg` ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” JavaScript ๋ฐฐ์—ด์„ PostgreSQL ๋ฐฐ์—ด ๋ฆฌํ„ฐ๋Ÿด(`{...}`)๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + +```javascript +// JavaScript +["CAT_1", "CAT_2", "CAT_3"] + +// PostgreSQL๋กœ ์ž๋™ ๋ณ€ํ™˜๋จ +{"CAT_1","CAT_2","CAT_3"} +``` + +ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ ์‹œ์Šคํ…œ์€ ์ปค์Šคํ…€ ํ…Œ์ด๋ธ”์—์„œ **์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด**์„ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค: +``` +CAT_1,CAT_2,CAT_3 +``` + +### 2. ์—ฌ๋Ÿฌ ์ €์žฅ ๊ฒฝ๋กœ์˜ ์กด์žฌ + +์ฝ”๋“œ๋ฅผ ๋ถ„์„ํ•œ ๊ฒฐ๊ณผ, ์ €์žฅ ๋กœ์ง์ด ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ๋กœ ๋‚˜๋‰˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค: + +| ๊ฒฝ๋กœ | ํŒŒ์ผ | ์„ค๋ช… | +|------|------|------| +| 1 | `buttonActions.ts` | ๊ธฐ๋ณธ ์ €์žฅ ๋กœ์ง (INSERT/UPDATE) | +| 2 | `EditModal.tsx` | ๋ชจ๋‹ฌ ๋‚ด ์ง์ ‘ ์ €์žฅ (CREATE/UPDATE) | +| 3 | `nodeFlowExecutionService.ts` | ๋ฐฑ์—”๋“œ ๋…ธ๋“œ ํ”Œ๋กœ์šฐ ์ €์žฅ | + +### 3. ์™œ ์ดˆ๊ธฐ ์ˆ˜์ •์ด ์‹คํŒจํ–ˆ๋Š”๊ฐ€? + +#### ์‹œ๋„ 1: `buttonActions.ts`์— ๋ฐฐ์—ด ๋ณ€ํ™˜ ์ถ”๊ฐ€ +```typescript +// buttonActions.ts (๋ผ์ธ 1002-1025) +if (isUpdate) { + for (const key of Object.keys(formData)) { + if (Array.isArray(value)) { + formData[key] = value.join(","); + } + } +} +``` + +**์‹คํŒจ ์ด์œ **: `EditModal`์ด `onSave` ์ฝœ๋ฐฑ์„ ์ œ๊ณตํ•˜๋ฉด, `buttonActions.ts`๋Š” ์ด ์ฝœ๋ฐฑ์„ ๋ฐ”๋กœ ํ˜ธ์ถœํ•˜๊ณ  ๋‚ด๋ถ€ ์ €์žฅ ๋กœ์ง์„ ๊ฑด๋„ˆ๋œ€ + +```typescript +// buttonActions.ts (๋ผ์ธ 545-552) +if (onSave) { + await onSave(); // ๋ฐ”๋กœ ์—ฌ๊ธฐ์„œ EditModal.handleSave()๊ฐ€ ํ˜ธ์ถœ๋จ + return true; // ์•„๋ž˜ ๋ฐฐ์—ด ๋ณ€ํ™˜ ๋กœ์ง์— ๋„๋‹ฌํ•˜์ง€ ์•Š์Œ! +} +``` + +#### ์‹œ๋„ 2: `nodeFlowExecutionService.ts`์— `normalizeValueForDB` ์ถ”๊ฐ€ + +**๋ถ€๋ถ„ ์„ฑ๊ณต**: INSERT์—์„œ๋Š” ๋™์ž‘ํ–ˆ์œผ๋‚˜, EditModal์˜ UPDATE ๊ฒฝ๋กœ๋Š” ์—ฌ์ „ํžˆ ๋ฌธ์ œ + +--- + +## ์ตœ์ข… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + +### ํ•ต์‹ฌ ์ˆ˜์ •: `EditModal.tsx`์— ์ง์ ‘ ๋ฐฐ์—ด ๋ณ€ํ™˜ ์ถ”๊ฐ€ + +EditModal์ด ์ง์ ‘ `dynamicFormApi.updateFormDataPartial`์„ ํ˜ธ์ถœํ•˜๋ฏ€๋กœ, **์ €์žฅ ์ง์ „**์— ๋ฐฐ์—ด์„ ๋ณ€ํ™˜ํ•ด์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค. + +#### ์ˆ˜์ • ์œ„์น˜ 1: UPDATE ๊ฒฝ๋กœ (๋ผ์ธ 957-1002) + +```typescript +// EditModal.tsx - UPDATE ๋ชจ๋“œ +Object.keys(formData).forEach((key) => { + if (formData[key] !== originalData[key]) { + let value = formData[key]; + + if (Array.isArray(value)) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ + const isRepeaterData = value.length > 0 && + typeof value[0] === "object" && + ("_targetTable" in value[0] || "_isNewItem" in value[0]); + + if (!isRepeaterData) { + // ๐Ÿ”ง ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง + const isValidValue = (v: any): boolean => { + if (typeof v === "number") return true; + if (typeof v !== "string") return false; + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) + return false; + return true; + }; + + // ์œ ํšจํ•œ ๊ฐ’๋งŒ ์‰ผํ‘œ๋กœ ์—ฐ๊ฒฐ + const validValues = value.filter(isValidValue); + value = validValues.join(","); + } + } + + changedData[key] = value; + } +}); +``` + +#### ์ˆ˜์ • ์œ„์น˜ 2: CREATE ๊ฒฝ๋กœ (๋ผ์ธ 855-875) + +```typescript +// EditModal.tsx - CREATE ๋ชจ๋“œ +Object.entries(dataToSave).forEach(([key, value]) => { + if (!Array.isArray(value)) { + masterDataToSave[key] = value; + } else { + const isRepeaterData = /* ๋ฆฌํ”ผํ„ฐ ์ฒดํฌ */; + + if (isRepeaterData) { + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ๋Š” ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ) + } else { + // ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด โ†’ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด + const validValues = value.filter(isValidValue); + masterDataToSave[key] = validValues.join(","); + } + } +}); +``` + +#### ์ˆ˜์ • ์œ„์น˜ 3: ๊ทธ๋ฃน UPDATE ๊ฒฝ๋กœ (๋ผ์ธ 630-650) + +๊ทธ๋ฃน ํ’ˆ๋ชฉ ์ˆ˜์ • ์‹œ์—๋„ ๋™์ผํ•œ ๋กœ์ง ์ ์šฉ + +--- + +## ์†์ƒ๋œ ๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ง + +๊ธฐ์กด์— ์†์ƒ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฐฐ์—ด์— ํฌํ•จ๋  ์ˆ˜ ์žˆ์–ด์„œ, ๋ณ€ํ™˜ ์ „ ํ•„ํ„ฐ๋ง์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค: + +```typescript +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; +}; +``` + +**ํ•„ํ„ฐ๋ง ์˜ˆ์‹œ**: +``` +์ž…๋ ฅ ๋ฐฐ์—ด: ['{"CAT_1","CAT_2"}', 'CAT_ML7SR2T9_IM7H', 'CAT_ML8ZFQFU_EE5Z'] + โ†‘ ์†์ƒ๋จ (ํ•„ํ„ฐ๋ง) โ†‘ ์œ ํšจ โ†‘ ์œ ํšจ + +์ถœ๋ ฅ: 'CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z' +``` + +--- + +## ์ˆ˜์ •๋œ ํŒŒ์ผ ๋ชฉ๋ก + +| ํŒŒ์ผ | ์ˆ˜์ • ๋‚ด์šฉ | +|------|-----------| +| `frontend/components/screen/EditModal.tsx` | CREATE/UPDATE/๊ทธ๋ฃนUPDATE ๊ฒฝ๋กœ์— ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ + ์†์ƒ๊ฐ’ ํ•„ํ„ฐ๋ง | +| `frontend/lib/utils/buttonActions.ts` | INSERT ๊ฒฝ๋กœ์— ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ (์ด๋ฏธ ์ˆ˜์ •๋จ) | +| `frontend/lib/registry/components/v2-select/V2SelectRenderer.tsx` | handleChange์—์„œ ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ | +| `backend-node/src/services/nodeFlowExecutionService.ts` | normalizeValueForDB ํ—ฌํผ ์ถ”๊ฐ€ | + +--- + +## ๊ตํ›ˆ ๋ฐ ํ–ฅํ›„ ์ฃผ์˜์‚ฌํ•ญ + +### 1. ์ €์žฅ ๊ฒฝ๋กœ ํŒŒ์•…์˜ ์ค‘์š”์„ฑ + +ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ์ €์žฅ ๋กœ์ง์ด ์—ฌ๋Ÿฌ ๊ฒฝ๋กœ๋กœ ๋ถ„๊ธฐ๋  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, **๋ชจ๋“  ๊ฒฝ๋กœ๋ฅผ ์ถ”์ **ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + +``` +์‚ฌ์šฉ์ž ์ €์žฅ ๋ฒ„ํŠผ ํด๋ฆญ + โ†“ +ButtonPrimaryComponent + โ†“ +buttonActions.handleSave() + โ†“ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ onSave ์ฝœ๋ฐฑ์ด ์žˆ์œผ๋ฉด? โ”‚ +โ”‚ โ†’ EditModal.handleSave() ์ง์ ‘ ํ˜ธ์ถœโ”‚ โ† ์ด ๊ฒฝ๋กœ๋ฅผ ๋†“์นจ! +โ”‚ onSave ์ฝœ๋ฐฑ์ด ์—†์œผ๋ฉด? โ”‚ +โ”‚ โ†’ buttonActions ๋‚ด๋ถ€ ์ €์žฅ ๋กœ์ง โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### 2. ๋กœ๊ทธ ๊ธฐ๋ฐ˜ ๋””๋ฒ„๊น… + +๋กœ๊ทธ๊ฐ€ ์–ด๋””๊นŒ์ง€ ์ฐํžˆ๊ณ  ์–ด๋””์„œ ์•ˆ ์ฐํžˆ๋Š”์ง€๋ฅผ ํ†ตํ•ด ์ฝ”๋“œ ๊ฒฝ๋กœ๋ฅผ ์ถ”์ : + +``` +[์˜ˆ์ƒํ•œ ๋กœ๊ทธ] +buttonActions.ts:512 ๐Ÿ” [handleSave] ์ง„์ž… +buttonActions.ts:1021 ๐Ÿ”ง ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜ โ† ์ด๊ฒŒ ์•ˆ ๋‚˜์˜ด! + +[์‹ค์ œ ๋กœ๊ทธ] +buttonActions.ts:512 ๐Ÿ” [handleSave] ์ง„์ž… +dynamicForm.ts:140 ๐Ÿ”„ ํผ ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ โ† ๋ฐ”๋กœ ์—ฌ๊ธฐ๋กœ ์ ํ”„! +``` + +### 3. ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ vs ๋‹ค์ค‘ ์„ ํƒ ๊ตฌ๋ถ„ + +๋ฐฐ์—ด์ด๋ผ๊ณ  ๋ชจ๋‘ ์‰ผํ‘œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค: + +| ํƒ€์ž… | ์˜ˆ์‹œ | ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ• | +|------|------|-----------| +| ๋‹ค์ค‘ ์„ ํƒ | `["CAT_1", "CAT_2"]` | ์‰ผํ‘œ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ | +| ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ | `[{id: 1, _targetTable: "..."}]` | ๋ณ„๋„ ํ…Œ์ด๋ธ”์— ์ €์žฅ, ๋งˆ์Šคํ„ฐ์—์„œ ์ œ์™ธ | + +--- + +## ํ™•์ธ๋œ ์ •์ƒ ๋™์ž‘ + +``` +EditModal.tsx:1002 ๐Ÿ”ง [EditModal UPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: division + {original: 3, valid: 3, converted: 'CAT_ML7SR2T9_IM7H,CAT_ML8ZFQFU_EE5Z,CAT_ML8ZFVEL_1TOR'} + +dynamicForm.ts:153 โœ… ํผ ๋ฐ์ดํ„ฐ ๋ถ€๋ถ„ ์—…๋ฐ์ดํŠธ ์„ฑ๊ณต +``` + +--- + +## ์ž‘์„ฑ์ผ + +2026-02-05 + +## ์ž‘์„ฑ์ž + +AI Assistant (Claude) diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx index 3dccd0db..e28c83b4 100644 --- a/frontend/components/screen/EditModal.tsx +++ b/frontend/components/screen/EditModal.tsx @@ -618,7 +618,36 @@ export const EditModal: React.FC = ({ className }) => { if (currentValue !== originalValue) { console.log(`๐Ÿ” [ํ’ˆ๋ชฉ ์ˆ˜์ • ๊ฐ์ง€] ${key}: ${originalValue} โ†’ ${currentValue}`); // ๋‚ ์งœ ํ•„๋“œ๋Š” ์ •๊ทœํ™”๋œ ๊ฐ’ ์‚ฌ์šฉ, ๋‚˜๋จธ์ง€๋Š” ์›๋ณธ ๊ฐ’ ์‚ฌ์šฉ - changedData[key] = dateFields.includes(key) ? currentValue : currentData[key]; + let finalValue = dateFields.includes(key) ? currentValue : currentData[key]; + + // ๐Ÿ”ง ๋ฐฐ์—ด์ด๋ฉด ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ (๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ) + if (Array.isArray(finalValue)) { + const isRepeaterData = finalValue.length > 0 && + typeof finalValue[0] === "object" && + finalValue[0] !== null && + ("_targetTable" in finalValue[0] || "_isNewItem" in finalValue[0] || "_existingRecord" in finalValue[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; + if (v.includes("{") || v.includes("}") || v.includes('"') || v.includes("\\")) return false; + return true; + }; + + const validValues = finalValue + .map((v: any) => typeof v === "number" ? String(v) : v) + .filter(isValidValue); + + const stringValue = validValues.join(","); + console.log(`๐Ÿ”ง [EditModal ๊ทธ๋ฃนUPDATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: finalValue.length, valid: validValues.length, converted: stringValue }); + finalValue = stringValue; + } + } + + changedData[key] = finalValue; } }); @@ -819,12 +848,39 @@ export const EditModal: React.FC = ({ className }) => { } // ๐Ÿ†• ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ(๋ฐฐ์—ด)๋ฅผ ๋งˆ์Šคํ„ฐ ์ €์žฅ์—์„œ ์ œ์™ธ (V2Repeater๊ฐ€ ๋ณ„๋„๋กœ ์ €์žฅ) + // ๐Ÿ”ง ๋‹จ, ๋‹ค์ค‘ ์„ ํƒ ๋ฐฐ์—ด์€ ์‰ผํ‘œ ๊ตฌ๋ถ„ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ €์žฅ const masterDataToSave: Record = {}; Object.entries(dataToSave).forEach(([key, value]) => { if (!Array.isArray(value)) { masterDataToSave[key] = value; } else { - console.log(`๐Ÿ”„ [EditModal] ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ): ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); + // ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ์ธ์ง€ ํ™•์ธ (๊ฐ์ฒด ๋ฐฐ์—ด์ด๊ณ  _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) { + console.log(`๐Ÿ”„ [EditModal] ๋ฆฌํ”ผํ„ฐ ๋ฐ์ดํ„ฐ ์ œ์™ธ (๋ณ„๋„ ์ €์žฅ): ${key}, ${value.length}๊ฐœ ํ•ญ๋ชฉ`); + } else { + // ๐Ÿ”ง ์†์ƒ๋œ ๊ฐ’ ํ•„ํ„ฐ๋ง ํ—ฌํผ (์ค‘๊ด„ํ˜ธ, ๋”ฐ์˜ดํ‘œ, ๋ฐฑ์Šฌ๋ž˜์‹œ ํฌํ•จ ์‹œ ๋ฌดํšจ) + 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; + 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); + + const stringValue = validValues.join(","); + console.log(`๐Ÿ”ง [EditModal CREATE] ๋ฐฐ์—ดโ†’๋ฌธ์ž์—ด ๋ณ€ํ™˜: ${key}`, { original: value.length, valid: validValues.length, converted: stringValue }); + masterDataToSave[key] = stringValue; + } } }); @@ -908,7 +964,47 @@ export const EditModal: React.FC = ({ className }) => { const changedData: Record = {}; Object.keys(formData).forEach((key) => { if (formData[key] !== originalData[key]) { - changedData[key] = formData[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; } }); diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx index 389e8366..88dd197f 100644 --- a/frontend/components/screen/ScreenDesigner.tsx +++ b/frontend/components/screen/ScreenDesigner.tsx @@ -6207,6 +6207,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU onDragStart={(e) => startComponentDrag(component, e)} onDragEnd={endDrag} selectedScreen={selectedScreen} + tableName={selectedScreen?.tableName} // ๐Ÿ†• ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋„ ์˜ต์…˜ ๋กœ๋”ฉ์„ ์œ„ํ•ด ์ „๋‹ฌ menuObjid={menuObjid} // ๐Ÿ†• ๋ฉ”๋‰ด OBJID ์ „๋‹ฌ // onZoneComponentDrop ์ œ๊ฑฐ onZoneClick={handleZoneClick} @@ -6375,6 +6376,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU onDragStart={(e) => startComponentDrag(child, e)} onDragEnd={endDrag} selectedScreen={selectedScreen} + tableName={selectedScreen?.tableName} // ๐Ÿ†• ๋””์ž์ธ ๋ชจ๋“œ์—์„œ๋„ ์˜ต์…˜ ๋กœ๋”ฉ์„ ์œ„ํ•ด ์ „๋‹ฌ // onZoneComponentDrop ์ œ๊ฑฐ onZoneClick={handleZoneClick} // ์„ค์ • ๋ณ€๊ฒฝ ํ•ธ๋“ค๋Ÿฌ (์ž์‹ ์ปดํฌ๋„ŒํŠธ์šฉ) @@ -6597,6 +6599,7 @@ export default function ScreenDesigner({ selectedScreen, onBackToList, onScreenU component={relativeButton} isDesignMode={true} formData={{}} + tableName={selectedScreen?.tableName} onDataflowComplete={() => {}} /> diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx index 84dd0d3c..218fe9b0 100644 --- a/frontend/components/v2/V2Select.tsx +++ b/frontend/components/v2/V2Select.tsx @@ -302,6 +302,127 @@ const TagSelect = forwardRef void; + placeholder?: string; + maxSelect?: number; + disabled?: boolean; + className?: string; + style?: React.CSSProperties; +}>(({ options, value = [], onChange, placeholder = "์„ ํƒํ•˜์„ธ์š”", maxSelect, disabled, className, style }, ref) => { + const [open, setOpen] = useState(false); + + // ์„ ํƒ๋œ ์˜ต์…˜๋“ค์˜ ๋ผ๋ฒจ ๊ฐ€์ ธ์˜ค๊ธฐ + const selectedOptions = useMemo(() => + options.filter((o) => value.includes(o.value)), + [options, value] + ); + + // ์ฒดํฌ๋ฐ•์Šค ํ† ๊ธ€ ํ•ธ๋“ค๋Ÿฌ + const handleToggle = useCallback((optionValue: string) => { + const isSelected = value.includes(optionValue); + if (isSelected) { + onChange?.(value.filter((v) => v !== optionValue)); + } else { + if (maxSelect && value.length >= maxSelect) return; + onChange?.([...value, optionValue]); + } + }, [value, maxSelect, onChange]); + + // ํƒœ๊ทธ ์ œ๊ฑฐ ํ•ธ๋“ค๋Ÿฌ + const handleRemove = useCallback((e: React.MouseEvent, optionValue: string) => { + e.stopPropagation(); + onChange?.(value.filter((v) => v !== optionValue)); + }, [value, onChange]); + + // ๐Ÿ”ง ๋†’์ด ์ฒ˜๋ฆฌ: style.height๊ฐ€ ์žˆ์œผ๋ฉด minHeight๋กœ ์‚ฌ์šฉ (๊ธฐ๋ณธ 40px ๋ณด์žฅ) + const triggerStyle: React.CSSProperties = { + minHeight: style?.height || 40, + height: style?.height || "auto", + maxWidth: "100%", // ๐Ÿ”ง ๋ถ€๋ชจ ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋„˜์ง€ ์•Š๋„๋ก + }; + + return ( +
+ + +
+ {selectedOptions.length > 0 ? ( + <> + {selectedOptions.map((option) => ( + + {option.label} + !disabled && handleRemove(e, option.value)} + /> + + ))} + + ) : ( + {placeholder} + )} + +
+
+ +
+ {options.map((option) => { + const isSelected = value.includes(option.value); + return ( +
!disabled && handleToggle(option.value)} + > + + {option.label} +
+ ); + })} + {options.length === 0 && ( +
+ ์˜ต์…˜์ด ์—†์Šต๋‹ˆ๋‹ค +
+ )} +
+
+
+
+ ); +}); +TagboxSelect.displayName = "TagboxSelect"; + /** * ํ† ๊ธ€ ์„ ํƒ ์ปดํฌ๋„ŒํŠธ (Boolean์šฉ) */ @@ -461,6 +582,7 @@ export const V2Select = forwardRef( onChange, tableName, columnName, + isDesignMode, // ๐Ÿ”ง ๋””์ž์ธ ๋ชจ๋“œ (ํด๋ฆญ ๋ฐฉ์ง€) } = props; // config๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ @@ -605,13 +727,13 @@ export const V2Select = forwardRef( const data = response.data; if (data.success && data.data) { // ํŠธ๋ฆฌ ๊ตฌ์กฐ๋ฅผ ํ‰ํƒ„ํ™”ํ•˜์—ฌ ์˜ต์…˜์œผ๋กœ ๋ณ€ํ™˜ - // value๋กœ valueId๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ฑ„๋ฒˆ ๊ทœ์น™ ๋งคํ•‘๊ณผ ์ผ์น˜ํ•˜๋„๋ก ํ•จ + // ๐Ÿ”ง value๋กœ valueCode๋ฅผ ์‚ฌ์šฉ (์ปค์Šคํ…€ ํ…Œ์ด๋ธ” ์ €์žฅ/์กฐํšŒ ํ˜ธํ™˜) const flattenTree = (items: { valueId: number; valueCode: string; valueLabel: string; children?: any[] }[], depth: number = 0): SelectOption[] => { const result: SelectOption[] = []; for (const item of items) { const prefix = depth > 0 ? " ".repeat(depth) + "โ”” " : ""; result.push({ - value: String(item.valueId), // valueId๋ฅผ value๋กœ ์‚ฌ์šฉ (์ฑ„๋ฒˆ ๋งคํ•‘๊ณผ ์ผ์น˜) + value: item.valueCode, // ๐Ÿ”ง valueCode๋ฅผ value๋กœ ์‚ฌ์šฉ label: prefix + item.valueLabel, }); if (item.children && item.children.length > 0) { @@ -639,7 +761,6 @@ export const V2Select = forwardRef( } } else if (!isValidColumnName) { // columnName์ด ์—†๊ฑฐ๋‚˜ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด ๋นˆ ์˜ต์…˜ - console.warn("V2Select: ์œ ํšจํ•œ columnName์ด ์—†์–ด ์˜ต์…˜์„ ๋กœ๋“œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.", { tableName, columnName }); } } @@ -669,6 +790,48 @@ export const V2Select = forwardRef( ? { height: componentHeight } : undefined; + // ๐Ÿ”ง ๋””์ž์ธ ๋ชจ๋“œ์šฉ: ์˜ต์…˜์ด ์—†๊ณ  dropdown/combobox๊ฐ€ ์•„๋‹Œ ๋ชจ๋“œ์ผ ๋•Œ source ์ •๋ณด ํ‘œ์‹œ + const nonDropdownModes = ["radio", "check", "checkbox", "tag", "tagbox", "toggle", "swap"]; + if (options.length === 0 && nonDropdownModes.includes(config.mode || "dropdown")) { + // ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ •๋ณด ๊ธฐ๋ฐ˜ ๋ฉ”์‹œ์ง€ ์ƒ์„ฑ + let sourceInfo = ""; + if (source === "static") { + sourceInfo = "์ •์  ์˜ต์…˜ ์„ค์ • ํ•„์š”"; + } else if (source === "code") { + sourceInfo = codeGroup ? `๊ณตํ†ต์ฝ”๋“œ: ${codeGroup}` : "๊ณตํ†ต์ฝ”๋“œ ์„ค์ • ํ•„์š”"; + } else if (source === "entity") { + sourceInfo = entityTable ? `์—”ํ‹ฐํ‹ฐ: ${entityTable}` : "์—”ํ‹ฐํ‹ฐ ์„ค์ • ํ•„์š”"; + } else if (source === "category") { + const catInfo = categoryTable || tableName || columnName; + sourceInfo = catInfo ? `์นดํ…Œ๊ณ ๋ฆฌ: ${catInfo}` : "์นดํ…Œ๊ณ ๋ฆฌ ์„ค์ • ํ•„์š”"; + } else if (source === "db") { + sourceInfo = table ? `ํ…Œ์ด๋ธ”: ${table}` : "ํ…Œ์ด๋ธ” ์„ค์ • ํ•„์š”"; + } else if (!source || source === "distinct") { + // distinct ๋˜๋Š” ๋ฏธ์„ค์ •์ธ ๊ฒฝ์šฐ - ์ปฌ๋Ÿผ๋ช… ๊ธฐ๋ฐ˜์œผ๋กœ ํ‘œ์‹œ + sourceInfo = columnName ? `์ปฌ๋Ÿผ: ${columnName}` : "๋ฐ์ดํ„ฐ ์†Œ์Šค ์„ค์ • ํ•„์š”"; + } else { + sourceInfo = `์†Œ์Šค: ${source}`; + } + + // ๋ชจ๋“œ ์ด๋ฆ„ ํ•œ๊ธ€ํ™” + const modeNames: Record = { + radio: "๋ผ๋””์˜ค", + check: "์ฒดํฌ๋ฐ•์Šค", + checkbox: "์ฒดํฌ๋ฐ•์Šค", + tag: "ํƒœ๊ทธ", + tagbox: "ํƒœ๊ทธ๋ฐ•์Šค", + toggle: "ํ† ๊ธ€", + swap: "์Šค์™‘", + }; + const modeName = modeNames[config.mode || ""] || config.mode; + + return ( +
+ [{modeName}] {sourceInfo} +
+ ); + } + switch (config.mode) { case "dropdown": case "combobox": // ๐Ÿ”ง ์ฝค๋ณด๋ฐ•์Šค๋Š” ๊ฒ€์ƒ‰ ๊ฐ€๋Šฅํ•œ ๋“œ๋กญ๋‹ค์šด @@ -720,6 +883,19 @@ export const V2Select = forwardRef( /> ); + case "tagbox": + return ( + + ); + case "toggle": return ( ( const componentWidth = size?.width || style?.width; const componentHeight = size?.height || style?.height; - // ๐Ÿ” ๋””๋ฒ„๊น…: ๋†’์ด๊ฐ’ ํ™•์ธ (warn์œผ๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์บก์ฒ˜๋˜๋„๋ก) - console.warn("๐Ÿ” [V2Select] ๋†’์ด ๋””๋ฒ„๊น…:", { - id, - "size?.height": size?.height, - "style?.height": style?.height, - componentHeight, - size, - style, - }); - // ๋ผ๋ฒจ ๋†’์ด ๊ณ„์‚ฐ (๊ธฐ๋ณธ 20px, ์‚ฌ์šฉ์ž ์„ค์ •์— ๋”ฐ๋ผ ์กฐ์ •) const labelFontSize = style?.labelFontSize ? parseInt(String(style.labelFontSize)) : 14; const labelMarginBottom = style?.labelMarginBottom ? parseInt(String(style.labelMarginBottom)) : 4; @@ -777,7 +943,7 @@ export const V2Select = forwardRef(
= ({ ๋“œ๋กญ๋‹ค์šด + ์ฝค๋ณด๋ฐ•์Šค (๊ฒ€์ƒ‰) ๋ผ๋””์˜ค ๋ฒ„ํŠผ ์ฒดํฌ๋ฐ•์Šค ํƒœ๊ทธ ์„ ํƒ + ํƒœ๊ทธ๋ฐ•์Šค (ํƒœ๊ทธ+๋“œ๋กญ๋‹ค์šด) ํ† ๊ธ€ ์Šค์œ„์น˜ ์Šค์™‘ ์„ ํƒ diff --git a/frontend/lib/registry/DynamicComponentRenderer.tsx b/frontend/lib/registry/DynamicComponentRenderer.tsx index 66a6e1a3..e6b13067 100644 --- a/frontend/lib/registry/DynamicComponentRenderer.tsx +++ b/frontend/lib/registry/DynamicComponentRenderer.tsx @@ -211,27 +211,11 @@ export const DynamicComponentRenderer: React.FC = // componentConfig ๋˜๋Š” overrides์—์„œ conditionalConfig๋ฅผ ๊ฐ€์ ธ์™€์„œ formData์™€ ๋น„๊ต const conditionalConfig = (component as any).componentConfig?.conditionalConfig || (component as any).overrides?.conditionalConfig; - // ๋””๋ฒ„๊ทธ: ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ์„ค์ • ํ™•์ธ - if (conditionalConfig?.enabled) { - console.log(`๐Ÿ” [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง] ${component.id}:`, { - conditionalConfig, - formData: props.formData, - hasFormData: !!props.formData - }); - } - + // ์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ์ฒ˜๋ฆฌ if (conditionalConfig?.enabled && props.formData) { const { field, operator, value, action } = conditionalConfig; const fieldValue = props.formData[field]; - console.log(`๐Ÿ” [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ํ‰๊ฐ€] ${component.id}:`, { - field, - fieldValue, - operator, - expectedValue: value, - action - }); - // ์กฐ๊ฑด ํ‰๊ฐ€ let conditionMet = false; switch (operator) { @@ -270,20 +254,10 @@ export const DynamicComponentRenderer: React.FC = } // ์•ก์…˜์— ๋”ฐ๋ผ ๋ Œ๋”๋ง ๊ฒฐ์ • - console.log(`๐Ÿ” [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง ๊ฒฐ๊ณผ] ${component.id}:`, { - conditionMet, - action, - shouldRender: action === "show" ? conditionMet : !conditionMet - }); - if (action === "show" && !conditionMet) { - // "show" ์•ก์…˜: ์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜์ง€ ์•Š์œผ๋ฉด ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ - console.log(`โŒ [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง] ${component.id} ์ˆจ๊น€ ์ฒ˜๋ฆฌ (show ์กฐ๊ฑด ๋ถˆ์ถฉ์กฑ)`); return null; } if (action === "hide" && conditionMet) { - // "hide" ์•ก์…˜: ์กฐ๊ฑด์ด ์ถฉ์กฑ๋˜๋ฉด ๋ Œ๋”๋งํ•˜์ง€ ์•Š์Œ - console.log(`โŒ [์กฐ๊ฑด๋ถ€ ๋ Œ๋”๋ง] ${component.id} ์ˆจ๊น€ ์ฒ˜๋ฆฌ (hide ์กฐ๊ฑด ์ถฉ์กฑ)`); return null; } // "enable"/"disable" ์•ก์…˜์€ conditionalDisabled props๋กœ ์ „๋‹ฌ @@ -297,17 +271,66 @@ export const DynamicComponentRenderer: React.FC = const webType = (component as any).componentConfig?.webType; const tableName = (component as any).tableName; const columnName = (component as any).columnName; - + // ์นดํ…Œ๊ณ ๋ฆฌ ์…€๋ ‰ํŠธ: webType์ด "category"์ด๊ณ  tableName๊ณผ columnName์ด ์žˆ๋Š” ๊ฒฝ์šฐ๋งŒ - // โš ๏ธ ๋‹จ, componentType์ด "select-basic" ๋˜๋Š” "v2-select"์ธ ๊ฒฝ์šฐ๋Š” ComponentRegistry๋กœ ์ฒ˜๋ฆฌ - // (๋‹ค์ค‘์„ ํƒ, ์ฒดํฌ๋ฐ•์Šค, ๋ผ๋””์˜ค ๋“ฑ ๊ณ ๊ธ‰ ๋ชจ๋“œ ์ง€์›) + // โš ๏ธ ๋‹จ, ๋‹ค์Œ ๊ฒฝ์šฐ๋Š” V2SelectRenderer๋กœ ์ง์ ‘ ์ฒ˜๋ฆฌ (๊ณ ๊ธ‰ ๋ชจ๋“œ ์ง€์›): + // 1. componentType์ด "select-basic" ๋˜๋Š” "v2-select"์ธ ๊ฒฝ์šฐ + // 2. config.mode๊ฐ€ dropdown์ด ์•„๋‹Œ ๊ฒฝ์šฐ (radio, check, tagbox ๋“ฑ) + const componentMode = (component as any).componentConfig?.mode || (component as any).config?.mode; + const nonDropdownModes = ["radio", "check", "checkbox", "tag", "tagbox", "toggle", "swap", "combobox"]; + const isNonDropdownMode = componentMode && nonDropdownModes.includes(componentMode); + const shouldUseV2Select = componentType === "select-basic" || componentType === "v2-select" || isNonDropdownMode; + if ( (inputType === "category" || webType === "category") && tableName && columnName && - (componentType === "select-basic" || componentType === "v2-select") + shouldUseV2Select ) { - // select-basic, v2-select๋Š” ComponentRegistry์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ์•„๋ž˜๋กœ ํ†ต๊ณผ + // V2SelectRenderer๋กœ ์ง์ ‘ ๋ Œ๋”๋ง (์นดํ…Œ๊ณ ๋ฆฌ + ๊ณ ๊ธ‰ ๋ชจ๋“œ) + try { + const { V2SelectRenderer } = require("@/lib/registry/components/v2-select/V2SelectRenderer"); + const fieldName = columnName || component.id; + const currentValue = props.formData?.[fieldName] || ""; + + const handleChange = (value: any) => { + if (props.onFormDataChange) { + props.onFormDataChange(fieldName, value); + } + }; + + // V2SelectRenderer์šฉ ์ปดํฌ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์„ฑ + const selectComponent = { + ...component, + componentConfig: { + ...component.componentConfig, + mode: componentMode || "dropdown", + source: "category", + categoryTable: tableName, + categoryColumn: columnName, + }, + tableName, + columnName, + inputType: "category", + webType: "category", + }; + + const rendererProps = { + component: selectComponent, + formData: props.formData, + onFormDataChange: props.onFormDataChange, + isDesignMode: props.isDesignMode, + isInteractive: props.isInteractive ?? !props.isDesignMode, + tableName, + style: (component as any).style, + size: (component as any).size, + }; + + const rendererInstance = new V2SelectRenderer(rendererProps); + return rendererInstance.render(); + } catch (error) { + console.error("โŒ V2SelectRenderer ๋กœ๋“œ ์‹คํŒจ:", error); + } } else if ((inputType === "category" || webType === "category") && tableName && columnName) { try { const { CategorySelectComponent } = require("@/lib/registry/components/category-select/CategorySelectComponent"); @@ -438,19 +461,6 @@ export const DynamicComponentRenderer: React.FC = // ์ปดํฌ๋„ŒํŠธ์˜ columnName์— ํ•ด๋‹นํ•˜๋Š” formData ๊ฐ’ ์ถ”์ถœ const fieldName = (component as any).columnName || (component as any).componentConfig?.columnName || component.id; - - // ๐Ÿ” ํŒŒ์ผ ์—…๋กœ๋“œ ์ปดํฌ๋„ŒํŠธ ๋””๋ฒ„๊น… - if (componentType === "v2-media" || componentType === "file-upload") { - console.log("[DynamicComponentRenderer] ํŒŒ์ผ ์—…๋กœ๋“œ:", { - componentType, - componentId: component.id, - columnName: (component as any).columnName, - configColumnName: (component as any).componentConfig?.columnName, - fieldName, - formDataValue: props.formData?.[fieldName], - formDataKeys: props.formData ? Object.keys(props.formData) : [] - }); - } // ๋‹ค์ค‘ ๋ ˆ์ฝ”๋“œ๋ฅผ ๋‹ค๋ฃจ๋Š” ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฐฐ์—ด ๋ฐ์ดํ„ฐ๋กœ ์ดˆ๊ธฐํ™” let currentValue; diff --git a/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx b/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx index baa4cfa3..ba5d752d 100644 --- a/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx +++ b/frontend/lib/registry/components/category-select/CategorySelectComponent.tsx @@ -91,11 +91,6 @@ export const CategorySelectComponent: React.FC< useEffect(() => { if (!tableName || !columnName) { - console.warn("CategorySelectComponent: tableName ๋˜๋Š” columnName์ด ์—†์Šต๋‹ˆ๋‹ค", { - tableName, - columnName, - component, - }); return; } @@ -128,7 +123,6 @@ export const CategorySelectComponent: React.FC< }; const handleValueChange = (newValue: string) => { - console.log("๐Ÿ”„ ์นดํ…Œ๊ณ ๋ฆฌ ๊ฐ’ ๋ณ€๊ฒฝ:", { oldValue: value, newValue }); onChange?.(newValue); }; @@ -216,7 +210,7 @@ export const CategorySelectComponent: React.FC<