diff --git a/backend-node/src/services/dataService.ts b/backend-node/src/services/dataService.ts
index e275d825..51ab567a 100644
--- a/backend-node/src/services/dataService.ts
+++ b/backend-node/src/services/dataService.ts
@@ -18,45 +18,6 @@ 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;
@@ -661,14 +622,14 @@ class DataService {
return {
success: true,
- data: await maskPasswordColumns(tableName, normalizedGroupRows), // ๐ง ๋ฐฐ์ด๋ก ๋ฐํ! + password ๋ง์คํน
+ data: normalizedGroupRows, // ๐ง ๋ฐฐ์ด๋ก ๋ฐํ!
};
}
}
return {
success: true,
- data: await maskPasswordColumns(tableName, normalizedRows[0]), // ๊ทธ๋ฃนํ ์์ผ๋ฉด ๋จ์ผ ๋ ์ฝ๋ + password ๋ง์คํน
+ data: normalizedRows[0], // ๊ทธ๋ฃนํ ์์ผ๋ฉด ๋จ์ผ ๋ ์ฝ๋
};
}
}
@@ -687,7 +648,7 @@ class DataService {
return {
success: true,
- data: await maskPasswordColumns(tableName, result[0]), // password ๋ง์คํน
+ data: result[0],
};
} catch (error) {
console.error(`๋ ์ฝ๋ ์์ธ ์กฐํ ์ค๋ฅ (${tableName}/${id}):`, error);
diff --git a/backend-node/src/services/dynamicFormService.ts b/backend-node/src/services/dynamicFormService.ts
index ac2377fe..e1242afd 100644
--- a/backend-node/src/services/dynamicFormService.ts
+++ b/backend-node/src/services/dynamicFormService.ts
@@ -2,7 +2,6 @@ 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;
@@ -860,33 +859,6 @@ 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/common/ScreenModal.tsx b/frontend/components/common/ScreenModal.tsx
index 138f560c..b6660709 100644
--- a/frontend/components/common/ScreenModal.tsx
+++ b/frontend/components/common/ScreenModal.tsx
@@ -554,6 +554,16 @@ export const ScreenModal: React.FC = ({ className }) => {
// ํ๋ฉด ๊ด๋ฆฌ์์ ์ค์ ํ ํด์๋ ์ฌ์ฉ (์ฐ์ ์์)
const screenResolution = (layoutData as any).screenResolution || (screenInfo as any).screenResolution;
+ console.log("๐ [ScreenModal] ํด์๋ ๋๋ฒ๊ทธ:", {
+ screenId,
+ v2ScreenResolution: v2LayoutData?.screenResolution,
+ layoutScreenResolution: (layoutData as any).screenResolution,
+ screenInfoResolution: (screenInfo as any).screenResolution,
+ finalScreenResolution: screenResolution,
+ hasWidth: screenResolution?.width,
+ hasHeight: screenResolution?.height,
+ });
+
let dimensions;
if (screenResolution && screenResolution.width && screenResolution.height) {
// ํ๋ฉด ๊ด๋ฆฌ์์ ์ค์ ํ ํด์๋ ์ฌ์ฉ
@@ -563,9 +573,11 @@ export const ScreenModal: React.FC = ({ className }) => {
offsetX: 0,
offsetY: 0,
};
+ console.log("โ
[ScreenModal] ํ๋ฉด๊ด๋ฆฌ ํด์๋ ์ ์ฉ:", dimensions);
} else {
// ํด์๋ ์ ๋ณด๊ฐ ์์ผ๋ฉด ์๋ ๊ณ์ฐ
dimensions = calculateScreenDimensions(components);
+ console.log("โ ๏ธ [ScreenModal] ํด์๋ ์์ - ์๋ ๊ณ์ฐ:", dimensions);
}
setScreenDimensions(dimensions);
@@ -869,16 +881,24 @@ export const ScreenModal: React.FC = ({ className }) => {
// ๋ชจ๋ฌ ํฌ๊ธฐ ์ค์ - ํ๋ฉด๊ด๋ฆฌ ์ค์ ํฌ๊ธฐ + ํค๋/ํธํฐ
const getModalStyle = () => {
if (!screenDimensions) {
+ console.log("โ ๏ธ [ScreenModal] getModalStyle: screenDimensions๊ฐ null - ๊ธฐ๋ณธ ์คํ์ผ ์ฌ์ฉ");
return {
className: "w-fit min-w-[400px] max-w-4xl overflow-hidden",
style: { padding: 0, gap: 0, maxHeight: "calc(100dvh - 8px)" },
};
}
+ const finalWidth = Math.min(screenDimensions.width, window.innerWidth * 0.98);
+ console.log("โ
[ScreenModal] getModalStyle: ํด์๋ ์ ์ฉ๋จ", {
+ screenDimensions,
+ finalWidth: `${finalWidth}px`,
+ viewportWidth: window.innerWidth,
+ });
+
return {
className: "overflow-hidden",
style: {
- width: `${Math.min(screenDimensions.width, window.innerWidth * 0.98)}px`,
+ width: `${finalWidth}px`,
// CSS๊ฐ ์์์ ์ฒ๋ฆฌ: ๋ทฐํฌํธ ์์ ๋ค์ด๊ฐ๋ฉด auto-height, ๋์น๋ฉด max-height๋ก ์ ํ
maxHeight: "calc(100dvh - 8px)",
maxWidth: "98vw",
diff --git a/frontend/components/screen/EditModal.tsx b/frontend/components/screen/EditModal.tsx
index d8ce8e7a..0fd0cfec 100644
--- a/frontend/components/screen/EditModal.tsx
+++ b/frontend/components/screen/EditModal.tsx
@@ -565,12 +565,32 @@ export const EditModal: React.FC = ({ className }) => {
return newActiveIds;
}, [formData, groupData, conditionalLayers, screenData?.components]);
- // ๐ ํ์ฑํ๋ ์กฐ๊ฑด๋ถ ๋ ์ด์ด์ ์ปดํฌ๋ํธ ๊ฐ์ ธ์ค๊ธฐ
+ // ํ์ฑํ๋ ์กฐ๊ฑด๋ถ ๋ ์ด์ด์ ์ปดํฌ๋ํธ ๊ฐ์ ธ์ค๊ธฐ (Zone ์คํ์
์ ์ฉ)
const activeConditionalComponents = useMemo(() => {
return conditionalLayers
.filter((layer) => activeConditionalLayerIds.includes(layer.id))
- .flatMap((layer) => (layer as LayerDefinition & { components: ComponentData[] }).components || []);
- }, [conditionalLayers, activeConditionalLayerIds]);
+ .flatMap((layer) => {
+ const layerWithComps = layer as LayerDefinition & { components: ComponentData[] };
+ const comps = layerWithComps.components || [];
+
+ // Zone ์คํ์
์ ์ฉ: ์กฐ๊ฑด๋ถ ๋ ์ด์ด ์ปดํฌ๋ํธ๋ Zone ๋ด๋ถ ์๋ ์ขํ๋ก ์ ์ฅ๋๋ฏ๋ก
+ // Zone์ ์ ๋ ์ขํ๋ฅผ ๋ํด์ค์ผ EditModal์์ ์ฌ๋ฐ๋ฅธ ์์น์ ๋ ๋๋ง๋จ
+ const associatedZone = zones.find((z) => z.zone_id === (layer as any).zoneId);
+ if (!associatedZone) return comps;
+
+ const zoneOffsetX = associatedZone.x || 0;
+ const zoneOffsetY = associatedZone.y || 0;
+
+ return comps.map((comp) => ({
+ ...comp,
+ position: {
+ ...comp.position,
+ x: parseFloat(comp.position?.x?.toString() || "0") + zoneOffsetX,
+ y: parseFloat(comp.position?.y?.toString() || "0") + zoneOffsetY,
+ },
+ }));
+ });
+ }, [conditionalLayers, activeConditionalLayerIds, zones]);
const handleClose = () => {
setModalState({
@@ -881,14 +901,31 @@ export const EditModal: React.FC = ({ className }) => {
}
}
+ // V2Repeater ์ ์ฅ ์ด๋ฒคํธ ๋ฐ์ (๋ํ
์ผ ํ
์ด๋ธ ๋ฐ์ดํฐ ์ ์ฅ)
+ const hasRepeaterInstances = window.__v2RepeaterInstances && window.__v2RepeaterInstances.size > 0;
+ if (hasRepeaterInstances) {
+ const masterRecordId = groupData[0]?.id || formData.id;
+ window.dispatchEvent(
+ new CustomEvent("repeaterSave", {
+ detail: {
+ parentId: masterRecordId,
+ masterRecordId,
+ mainFormData: formData,
+ tableName: screenData.screenInfo.tableName,
+ },
+ }),
+ );
+ console.log("๐ [EditModal] ๊ทธ๋ฃน ์ ์ฅ ํ repeaterSave ์ด๋ฒคํธ ๋ฐ์:", { masterRecordId });
+ }
+
// ๊ฒฐ๊ณผ ๋ฉ์์ง
const messages: string[] = [];
if (insertedCount > 0) messages.push(`${insertedCount}๊ฐ ์ถ๊ฐ`);
if (updatedCount > 0) messages.push(`${updatedCount}๊ฐ ์์ `);
if (deletedCount > 0) messages.push(`${deletedCount}๊ฐ ์ญ์ `);
- if (messages.length > 0) {
- toast.success(`ํ๋ชฉ์ด ์ ์ฅ๋์์ต๋๋ค (${messages.join(", ")})`);
+ if (messages.length > 0 || hasRepeaterInstances) {
+ toast.success(messages.length > 0 ? `ํ๋ชฉ์ด ์ ์ฅ๋์์ต๋๋ค (${messages.join(", ")})` : "์ ์ฅ๋์์ต๋๋ค.");
// ๋ถ๋ชจ ์ปดํฌ๋ํธ์ onSave ์ฝ๋ฐฑ ์คํ (ํ
์ด๋ธ ์๋ก๊ณ ์นจ)
if (modalState.onSave) {
diff --git a/frontend/components/screen/InteractiveScreenViewer.tsx b/frontend/components/screen/InteractiveScreenViewer.tsx
index 05d8bdc9..252f5c2b 100644
--- a/frontend/components/screen/InteractiveScreenViewer.tsx
+++ b/frontend/components/screen/InteractiveScreenViewer.tsx
@@ -2231,11 +2231,20 @@ export const InteractiveScreenViewer: React.FC = (
}
: component;
- // ๐ ๋ชจ๋ ๋ ์ด์ด์ ์ปดํฌ๋ํธ๋ฅผ ํตํฉ (์กฐ๊ฑด๋ถ ๋ ์ด์ด ๋ด ์ปดํฌ๋ํธ๊ฐ ๊ธฐ๋ณธ ๋ ์ด์ด formData ์ฐธ์กฐ ๊ฐ๋ฅํ๋๋ก)
+ // ๋ชจ๋ ๋ ์ด์ด์ ์ปดํฌ๋ํธ ํตํฉ (์กฐ๊ฑด ํ๊ฐ์ฉ - ํธ๋ฆฌ๊ฑฐ ์ปดํฌ๋ํธ ๊ฒ์์ ํ์)
const allLayerComponents = useMemo(() => {
return layers.flatMap((layer) => layer.components);
}, [layers]);
+ // ๐ง ํ์ฑ ๋ ์ด์ด ์ปดํฌ๋ํธ๋ง ํตํฉ (์ ์ฅ/๋ฐ์ดํฐ ์์ง์ฉ)
+ // ๊ธฐ๋ณธ ๋ ์ด์ด(base) + ํ์ฌ ํ์ฑํ๋ ์กฐ๊ฑด๋ถ ๋ ์ด์ด๋ง ํฌํจ
+ // ๋นํ์ฑ ๋ ์ด์ด์ ์ค๋ณต columnName ์ปดํฌ๋ํธ๊ฐ ์ ์ฅ ๋ฐ์ดํฐ๋ฅผ ์ค์ผ์ํค๋ ๋ฌธ์ ํด๊ฒฐ
+ const visibleLayerComponents = useMemo(() => {
+ return layers
+ .filter((layer) => layer.type === "base" || activeLayerIds.includes(layer.id))
+ .flatMap((layer) => layer.components);
+ }, [layers, activeLayerIds]);
+
// ๐ ๋ ์ด์ด๋ณ ์ปดํฌ๋ํธ ๋ ๋๋ง ํจ์
const renderLayerComponents = useCallback((layer: LayerDefinition) => {
// ํ์ฑํ๋์ง ์์ ๋ ์ด์ด๋ ๋ ๋๋งํ์ง ์์
@@ -2272,7 +2281,7 @@ export const InteractiveScreenViewer: React.FC = (
>
= (
>
= (
>
= (
>
= (
})}
);
- }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, allLayerComponents, layers]);
+ }, [activeLayerIds, handleLayerAction, externalFormData, onFormDataChange, screenInfo, calculateYOffset, visibleLayerComponents, layers]);
return (
@@ -2485,7 +2494,13 @@ export const InteractiveScreenViewer: React.FC = (
setPopupScreen(null);
setPopupFormData({}); // ํ์
๋ซ์ ๋ formData๋ ์ด๊ธฐํ
}}>
-
+
{popupScreen?.title || "์์ธ ์ ๋ณด"}
diff --git a/frontend/components/screen/ScreenDesigner.tsx b/frontend/components/screen/ScreenDesigner.tsx
index 75937daa..8dfe9ae4 100644
--- a/frontend/components/screen/ScreenDesigner.tsx
+++ b/frontend/components/screen/ScreenDesigner.tsx
@@ -5556,8 +5556,12 @@ export default function ScreenDesigner({
return false;
}
- // 6. ์ญ์ (๋จ์ผ/๋ค์ค ์ ํ ์ง์)
- if (e.key === "Delete" && (selectedComponent || groupState.selectedComponents.length > 0)) {
+ // 6. ์ญ์ (๋จ์ผ/๋ค์ค ์ ํ ์ง์) - Delete ๋๋ Backspace(Mac)
+ const isInputFocused = document.activeElement instanceof HTMLInputElement ||
+ document.activeElement instanceof HTMLTextAreaElement ||
+ document.activeElement instanceof HTMLSelectElement ||
+ (document.activeElement as HTMLElement)?.isContentEditable;
+ if ((e.key === "Delete" || (e.key === "Backspace" && !isInputFocused)) && (selectedComponent || groupState.selectedComponents.length > 0)) {
// console.log("๐๏ธ ์ปดํฌ๋ํธ ์ญ์ (๋จ์ถํค)");
e.preventDefault();
e.stopPropagation();
@@ -7419,7 +7423,7 @@ export default function ScreenDesigner({
ํธ์ง: Ctrl+C(๋ณต์ฌ), Ctrl+V(๋ถ์ฌ๋ฃ๊ธฐ), Ctrl+S(์ ์ฅ),
- Ctrl+Z(์คํ์ทจ์), Delete(์ญ์ )
+ Ctrl+Z(์คํ์ทจ์), Delete/Backspace(์ญ์ )
โ ๏ธ
diff --git a/frontend/components/v2/V2Repeater.tsx b/frontend/components/v2/V2Repeater.tsx
index 0f16cd31..734032f3 100644
--- a/frontend/components/v2/V2Repeater.tsx
+++ b/frontend/components/v2/V2Repeater.tsx
@@ -43,6 +43,7 @@ export const V2Repeater: React.FC = ({
onDataChange,
onRowClick,
className,
+ formData: parentFormData,
}) => {
// ์ค์ ๋ณํฉ
const config: V2RepeaterConfig = useMemo(
@@ -153,21 +154,15 @@ export const V2Repeater: React.FC = ({
// ๋ฉ์ธ ํผ ๋ฐ์ดํฐ ๋ณํฉ (์ปค์คํ
ํ
์ด๋ธ ์ฌ์ฉ ์์๋ ๋ฉ์ธ ํผ ๋ฐ์ดํฐ ๋ณํฉ ์ํจ)
let mergedData: Record;
if (config.useCustomTable && config.mainTableName) {
- // ์ปค์คํ
ํ
์ด๋ธ: ๋ฆฌํผํฐ ๋ฐ์ดํฐ๋ง ์ ์ฅ
mergedData = { ...cleanRow };
- // ๐ FK ์๋ ์ฐ๊ฒฐ - foreignKeySourceColumn์ด ์ค์ ๋ ๊ฒฝ์ฐ ํด๋น ์ปฌ๋ผ ๊ฐ ์ฌ์ฉ
if (config.foreignKeyColumn) {
- // foreignKeySourceColumn์ด ์์ผ๋ฉด mainFormData์์ ํด๋น ์ปฌ๋ผ ๊ฐ ์ฌ์ฉ
- // ์์ผ๋ฉด ๋ง์คํฐ ๋ ์ฝ๋ ID ์ฌ์ฉ (๊ธฐ์กด ๋์)
const sourceColumn = config.foreignKeySourceColumn;
let fkValue: any;
if (sourceColumn && mainFormData && mainFormData[sourceColumn] !== undefined) {
- // mainFormData์์ ์ฐธ์กฐ ์ปฌ๋ผ ๊ฐ ๊ฐ์ ธ์ค๊ธฐ
fkValue = mainFormData[sourceColumn];
} else {
- // ๊ธฐ๋ณธ: ๋ง์คํฐ ๋ ์ฝ๋ ID ์ฌ์ฉ
fkValue = masterRecordId;
}
@@ -176,7 +171,6 @@ export const V2Repeater: React.FC = ({
}
}
} else {
- // ๊ธฐ์กด ๋ฐฉ์: ๋ฉ์ธ ํผ ๋ฐ์ดํฐ ๋ณํฉ
const { id: _mainId, ...mainFormDataWithoutId } = mainFormData || {};
mergedData = {
...mainFormDataWithoutId,
@@ -192,7 +186,19 @@ export const V2Repeater: React.FC = ({
}
}
- await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
+ // ๊ธฐ์กด ํ(id ์กด์ฌ)์ UPDATE, ์ ํ์ INSERT
+ const rowId = row.id;
+ if (rowId && typeof rowId === "string" && rowId.includes("-")) {
+ // UUID ํํ์ id๊ฐ ์์ผ๋ฉด ๊ธฐ์กด ๋ฐ์ดํฐ โ UPDATE
+ const { id: _, created_date: _cd, updated_date: _ud, ...updateFields } = filteredData;
+ await apiClient.put(`/table-management/tables/${tableName}/edit`, {
+ originalData: { id: rowId },
+ updatedData: updateFields,
+ });
+ } else {
+ // ์ ํ โ INSERT
+ await apiClient.post(`/table-management/tables/${tableName}/add`, filteredData);
+ }
}
} catch (error) {
console.error("โ V2Repeater ์ ์ฅ ์คํจ:", error);
@@ -228,6 +234,108 @@ export const V2Repeater: React.FC = ({
parentId,
]);
+ // ์์ ๋ชจ๋: useCustomTable + FK ๊ธฐ๋ฐ์ผ๋ก ๊ธฐ์กด ๋ํ
์ผ ๋ฐ์ดํฐ ์๋ ๋ก๋
+ const dataLoadedRef = useRef(false);
+ useEffect(() => {
+ if (dataLoadedRef.current) return;
+ if (!config.useCustomTable || !config.mainTableName || !config.foreignKeyColumn) return;
+ if (!parentFormData) return;
+
+ const fkSourceColumn = config.foreignKeySourceColumn || config.foreignKeyColumn;
+ const fkValue = parentFormData[fkSourceColumn];
+ if (!fkValue) return;
+
+ // ์ด๋ฏธ ๋ฐ์ดํฐ๊ฐ ์์ผ๋ฉด ๋ก๋ํ์ง ์์
+ if (data.length > 0) return;
+
+ const loadExistingData = async () => {
+ try {
+ console.log("๐ฅ [V2Repeater] ์์ ๋ชจ๋ ๋ฐ์ดํฐ ๋ก๋:", {
+ tableName: config.mainTableName,
+ fkColumn: config.foreignKeyColumn,
+ fkValue,
+ });
+
+ const response = await apiClient.post(
+ `/table-management/tables/${config.mainTableName}/data`,
+ {
+ page: 1,
+ size: 1000,
+ search: { [config.foreignKeyColumn]: fkValue },
+ autoFilter: true,
+ }
+ );
+
+ const rows = response.data?.data?.data || response.data?.data?.rows || response.data?.rows || [];
+ if (Array.isArray(rows) && rows.length > 0) {
+ console.log(`โ
[V2Repeater] ๊ธฐ์กด ๋ฐ์ดํฐ ${rows.length}๊ฑด ๋ก๋ ์๋ฃ`);
+
+ // isSourceDisplay ์ปฌ๋ผ์ด ์์ผ๋ฉด ์์ค ํ
์ด๋ธ์์ ํ์ ๋ฐ์ดํฐ ๋ณด๊ฐ
+ const sourceDisplayColumns = config.columns.filter((col) => col.isSourceDisplay);
+ const sourceTable = config.dataSource?.sourceTable;
+ const fkColumn = config.dataSource?.foreignKey;
+ const refKey = config.dataSource?.referenceKey || "id";
+
+ if (sourceDisplayColumns.length > 0 && sourceTable && fkColumn) {
+ try {
+ const fkValues = rows.map((row) => row[fkColumn]).filter(Boolean);
+ const uniqueValues = [...new Set(fkValues)];
+
+ if (uniqueValues.length > 0) {
+ // FK ๊ฐ ๊ธฐ๋ฐ์ผ๋ก ์์ค ํ
์ด๋ธ์์ ํด๋น ๋ ์ฝ๋๋ง ์กฐํ
+ const sourcePromises = uniqueValues.map((val) =>
+ apiClient.post(`/table-management/tables/${sourceTable}/data`, {
+ page: 1, size: 1,
+ search: { [refKey]: val },
+ autoFilter: true,
+ }).then((r) => r.data?.data?.data || r.data?.data?.rows || [])
+ .catch(() => [])
+ );
+ const sourceResults = await Promise.all(sourcePromises);
+ const sourceMap = new Map();
+ sourceResults.flat().forEach((sr: any) => {
+ if (sr[refKey]) sourceMap.set(String(sr[refKey]), sr);
+ });
+
+ // ๊ฐ ํ์ ์์ค ํ
์ด๋ธ์ ํ์ ๋ฐ์ดํฐ ๋ณํฉ
+ // RepeaterTable์ isSourceDisplay ์ปฌ๋ผ์ `_display_${col.key}` ํ๋๋ก ๋ ๋๋งํจ
+ rows.forEach((row: any) => {
+ const sourceRecord = sourceMap.get(String(row[fkColumn]));
+ if (sourceRecord) {
+ sourceDisplayColumns.forEach((col) => {
+ const displayValue = sourceRecord[col.key] ?? null;
+ row[col.key] = displayValue;
+ row[`_display_${col.key}`] = displayValue;
+ });
+ }
+ });
+ console.log("โ
[V2Repeater] ์์ค ํ
์ด๋ธ ํ์ ๋ฐ์ดํฐ ๋ณด๊ฐ ์๋ฃ");
+ }
+ } catch (sourceError) {
+ console.warn("โ ๏ธ [V2Repeater] ์์ค ํ
์ด๋ธ ์กฐํ ์คํจ (ํ์๋ง ์ํฅ):", sourceError);
+ }
+ }
+
+ setData(rows);
+ dataLoadedRef.current = true;
+ if (onDataChange) onDataChange(rows);
+ }
+ } catch (error) {
+ console.error("โ [V2Repeater] ๊ธฐ์กด ๋ฐ์ดํฐ ๋ก๋ ์คํจ:", error);
+ }
+ };
+
+ loadExistingData();
+ }, [
+ config.useCustomTable,
+ config.mainTableName,
+ config.foreignKeyColumn,
+ config.foreignKeySourceColumn,
+ parentFormData,
+ data.length,
+ onDataChange,
+ ]);
+
// ํ์ฌ ํ
์ด๋ธ ์ปฌ๋ผ ์ ๋ณด ๋ก๋
useEffect(() => {
const loadCurrentTableColumnInfo = async () => {
@@ -451,58 +559,71 @@ export const V2Repeater: React.FC = ({
loadCategoryLabels();
}, [data, sourceCategoryColumns]);
+ // ๊ณ์ฐ ๊ท์น ์ ์ฉ (์์ค ํ
์ด๋ธ์ _display_* ํ๋๋ ์ฐธ์กฐ ๊ฐ๋ฅ)
+ const applyCalculationRules = useCallback(
+ (row: any): any => {
+ const rules = config.calculationRules;
+ if (!rules || rules.length === 0) return row;
+
+ const updatedRow = { ...row };
+ for (const rule of rules) {
+ if (!rule.targetColumn || !rule.formula) continue;
+ try {
+ let formula = rule.formula;
+ const fieldMatches = formula.match(/[a-zA-Z_][a-zA-Z0-9_]*/g) || [];
+ for (const field of fieldMatches) {
+ if (field === rule.targetColumn) continue;
+ // ์ง์ ํ๋ โ _display_* ํ๋ ์์ผ๋ก ๊ฐ ํ์
+ const raw = updatedRow[field] ?? updatedRow[`_display_${field}`];
+ const value = parseFloat(raw) || 0;
+ formula = formula.replace(new RegExp(`\\b${field}\\b`, "g"), value.toString());
+ }
+ updatedRow[rule.targetColumn] = new Function(`return ${formula}`)();
+ } catch {
+ updatedRow[rule.targetColumn] = 0;
+ }
+ }
+ return updatedRow;
+ },
+ [config.calculationRules],
+ );
+
+ // _targetTable ๋ฉํ๋ฐ์ดํฐ ํฌํจํ์ฌ onDataChange ํธ์ถ
+ const notifyDataChange = useCallback(
+ (newData: any[]) => {
+ if (!onDataChange) return;
+ const targetTable =
+ config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
+ if (targetTable) {
+ onDataChange(newData.map((row) => ({ ...row, _targetTable: targetTable })));
+ } else {
+ onDataChange(newData);
+ }
+ },
+ [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName],
+ );
+
// ๋ฐ์ดํฐ ๋ณ๊ฒฝ ํธ๋ค๋ฌ
const handleDataChange = useCallback(
(newData: any[]) => {
- setData(newData);
-
- // ๐ _targetTable ๋ฉํ๋ฐ์ดํฐ ํฌํจํ์ฌ ์ ๋ฌ (๋ฐฑ์๋์์ ํ
์ด๋ธ ๋ถ๋ฆฌ์ฉ)
- if (onDataChange) {
- const targetTable =
- config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
-
- if (targetTable) {
- // ๊ฐ ํ์ _targetTable ์ถ๊ฐ
- const dataWithTarget = newData.map((row) => ({
- ...row,
- _targetTable: targetTable,
- }));
- onDataChange(dataWithTarget);
- } else {
- onDataChange(newData);
- }
- }
-
- // ๐ ๋ฐ์ดํฐ ๋ณ๊ฒฝ ์ ์๋์ผ๋ก ์ปฌ๋ผ ๋๋น ์กฐ์
+ const calculated = newData.map(applyCalculationRules);
+ setData(calculated);
+ notifyDataChange(calculated);
setAutoWidthTrigger((prev) => prev + 1);
},
- [onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName],
+ [applyCalculationRules, notifyDataChange],
);
// ํ ๋ณ๊ฒฝ ํธ๋ค๋ฌ
const handleRowChange = useCallback(
(index: number, newRow: any) => {
+ const calculated = applyCalculationRules(newRow);
const newData = [...data];
- newData[index] = newRow;
+ newData[index] = calculated;
setData(newData);
-
- // ๐ _targetTable ๋ฉํ๋ฐ์ดํฐ ํฌํจ
- if (onDataChange) {
- const targetTable =
- config.useCustomTable && config.mainTableName ? config.mainTableName : config.dataSource?.tableName;
-
- if (targetTable) {
- const dataWithTarget = newData.map((row) => ({
- ...row,
- _targetTable: targetTable,
- }));
- onDataChange(dataWithTarget);
- } else {
- onDataChange(newData);
- }
- }
+ notifyDataChange(newData);
},
- [data, onDataChange, config.useCustomTable, config.mainTableName, config.dataSource?.tableName],
+ [data, applyCalculationRules, notifyDataChange],
);
// ํ ์ญ์ ํธ๋ค๋ฌ
diff --git a/frontend/components/v2/V2Select.tsx b/frontend/components/v2/V2Select.tsx
index 2029f473..4fd27cb0 100644
--- a/frontend/components/v2/V2Select.tsx
+++ b/frontend/components/v2/V2Select.tsx
@@ -189,13 +189,11 @@ const DropdownSelect = forwardRef
{
- // value๋ CommandItem์ value (๋ผ๋ฒจ)
- // search๋ ๊ฒ์์ด
+ filter={(itemValue, search) => {
if (!search) return 1;
- const normalizedValue = value.toLowerCase();
- const normalizedSearch = search.toLowerCase();
- if (normalizedValue.includes(normalizedSearch)) return 1;
+ const option = options.find((o) => o.value === itemValue);
+ const label = (option?.label || option?.value || "").toLowerCase();
+ if (label.includes(search.toLowerCase())) return 1;
return 0;
}}
>
@@ -208,7 +206,7 @@ const DropdownSelect = forwardRef handleSelect(option.value)}
>
= ({
const [currentTableColumns, setCurrentTableColumns] = useState([]); // ํ์ฌ ํ
์ด๋ธ ์ปฌ๋ผ
const [entityColumns, setEntityColumns] = useState([]); // ์ํฐํฐ ํ์
์ปฌ๋ผ
const [sourceTableColumns, setSourceTableColumns] = useState([]); // ์์ค(์ํฐํฐ) ํ
์ด๋ธ ์ปฌ๋ผ
- const [calculationRules, setCalculationRules] = useState([]);
+ const [calculationRules, setCalculationRules] = useState(
+ config.calculationRules || []
+ );
const [loadingColumns, setLoadingColumns] = useState(false);
const [loadingSourceColumns, setLoadingSourceColumns] = useState(false);
@@ -553,26 +555,56 @@ export const V2RepeaterConfigPanel: React.FC = ({
updateConfig({ columns: newColumns });
};
+ // ๊ณ์ฐ ๊ท์น์ config์ ๋ฐ์ํ๋ ํฌํผ
+ const syncCalculationRules = (rules: CalculationRule[]) => {
+ setCalculationRules(rules);
+ updateConfig({ calculationRules: rules });
+ };
+
// ๊ณ์ฐ ๊ท์น ์ถ๊ฐ
const addCalculationRule = () => {
- setCalculationRules(prev => [
- ...prev,
+ const newRules = [
+ ...calculationRules,
{ id: `calc_${Date.now()}`, targetColumn: "", formula: "" }
- ]);
+ ];
+ syncCalculationRules(newRules);
};
// ๊ณ์ฐ ๊ท์น ์ญ์
const removeCalculationRule = (id: string) => {
- setCalculationRules(prev => prev.filter(r => r.id !== id));
+ syncCalculationRules(calculationRules.filter(r => r.id !== id));
};
// ๊ณ์ฐ ๊ท์น ์
๋ฐ์ดํธ
const updateCalculationRule = (id: string, field: keyof CalculationRule, value: string) => {
- setCalculationRules(prev =>
- prev.map(r => r.id === id ? { ...r, [field]: value } : r)
+ syncCalculationRules(
+ calculationRules.map(r => r.id === id ? { ...r, [field]: value } : r)
);
};
+ // ์์ ์
๋ ฅ ํ๋์ ์ปฌ๋ผ๋ช
์ฝ์
+ const insertColumnToFormula = (ruleId: string, columnKey: string) => {
+ const rule = calculationRules.find(r => r.id === ruleId);
+ if (!rule) return;
+ const newFormula = rule.formula ? `${rule.formula} ${columnKey}` : columnKey;
+ updateCalculationRule(ruleId, "formula", newFormula);
+ };
+
+ // ์์์ ์์ด ์ปฌ๋ผ๋ช
์ ํ๊ธ ์ ๋ชฉ์ผ๋ก ๋ณํ
+ const formulaToKorean = (formula: string): string => {
+ if (!formula) return "";
+ let result = formula;
+ const allCols = config.columns || [];
+ // ๊ธด ์ปฌ๋ผ๋ช
๋ถํฐ ์นํ (๋ถ๋ถ ๋งค์นญ ๋ฐฉ์ง)
+ const sorted = [...allCols].sort((a, b) => b.key.length - a.key.length);
+ for (const col of sorted) {
+ if (col.title && col.key) {
+ result = result.replace(new RegExp(`\\b${col.key}\\b`, "g"), col.title);
+ }
+ }
+ return result;
+ };
+
// ์ํฐํฐ ์ปฌ๋ผ ์ ํ ์ ์์ค ํ
์ด๋ธ ์๋ ์ค์
const handleEntityColumnSelect = (columnName: string) => {
const selectedEntity = entityColumns.find(c => c.columnName === columnName);
@@ -1374,7 +1406,7 @@ export const V2RepeaterConfigPanel: React.FC = ({
{(isModalMode || isInlineMode) && config.columns.length > 0 && (
<>
-
+
-
- ์: ๊ธ์ก = ์๋ * ๋จ๊ฐ
-
-
+
{calculationRules.map((rule) => (
-
-
-
-
=
-
-
updateCalculationRule(rule.id, "formula", e.target.value)}
- placeholder="quantity * unit_price"
- className="h-7 flex-1 text-xs"
- />
-
-
+
+
+
+ =
+ updateCalculationRule(rule.id, "formula", e.target.value)}
+ placeholder="์ปฌ๋ผ ํด๋ฆญ ๋๋ ์ง์ ์
๋ ฅ"
+ className="h-6 flex-1 font-mono text-[10px]"
+ />
+
+
+
+ {/* ํ๊ธ ์์ ๋ฏธ๋ฆฌ๋ณด๊ธฐ */}
+ {rule.formula && (
+
+ {config.columns.find(c => c.key === rule.targetColumn)?.title || rule.targetColumn || "๊ฒฐ๊ณผ"} = {formulaToKorean(rule.formula)}
+
+ )}
+
+ {/* ์ปฌ๋ผ ์นฉ: ๋ํ
์ผ ์ปฌ๋ผ + ์์ค(ํ๋ชฉ) ์ปฌ๋ผ + ์ฐ์ฐ์ */}
+
+ {config.columns
+ .filter(col => col.key !== rule.targetColumn && !col.isSourceDisplay)
+ .map((col) => (
+
+ ))}
+ {config.columns
+ .filter(col => col.isSourceDisplay)
+ .map((col) => (
+
+ ))}
+ {["+", "-", "*", "/", "(", ")"].map((op) => (
+
+ ))}
+
))}
{calculationRules.length === 0 && (
-
+
๊ณ์ฐ ๊ท์น์ด ์์ต๋๋ค
)}
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 6af5a88f..c2bb436d 100644
--- a/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx
+++ b/frontend/lib/registry/components/selected-items-detail-input/SelectedItemsDetailInputComponent.tsx
@@ -67,6 +67,10 @@ export const SelectedItemsDetailInputComponent: React.FC
์ปดํฌ๋ํธ ์ค์ > component.id
const dataSourceId = useMemo(
() => urlDataSourceId || componentConfig.dataSourceId || component.id || "default",
@@ -228,7 +232,21 @@ export const SelectedItemsDetailInputComponent: React.FC[]> = {};
- if (firstRecord.customer_id && firstRecord.item_id) {
+ // ๋์ ํํฐ ๊ตฌ์ฑ: parentDataMapping์ targetField + sourceKeyField
+ const editFilters: Record = {};
+ const parentMappings = componentConfig.parentDataMapping || [];
+ parentMappings.forEach((mapping: any) => {
+ if (mapping.targetField && firstRecord[mapping.targetField]) {
+ editFilters[mapping.targetField] = firstRecord[mapping.targetField];
+ }
+ });
+ if (firstRecord[sourceKeyField]) {
+ editFilters[sourceKeyField] = firstRecord[sourceKeyField];
+ }
+
+ const hasRequiredKeys = Object.keys(editFilters).length >= 2;
+
+ if (hasRequiredKeys) {
try {
const { dataApi } = await import("@/lib/api/data");
// ๋ชจ๋ sourceTable์ ๋ฐ์ดํฐ๋ฅผ API๋ก ์ ์ฒด ๋ก๋ (์ค๋ณต ํ
์ด๋ธ ์ ๊ฑฐ)
@@ -238,10 +256,7 @@ export const SelectedItemsDetailInputComponent: React.FC {
const groupFields = additionalFields.filter((f) => f.groupId === group.id);
groupFields.forEach((field) => {
- if (field.name === "item_id" && field.autoFillFrom && item.originalData) {
- itemId = item.originalData[field.autoFillFrom] || null;
+ if (field.name === sourceKeyField && field.autoFillFrom && item.originalData) {
+ sourceKeyValue = item.originalData[field.autoFillFrom] || null;
}
});
});
}
// 3์์: fallback (์ตํ์ ์๋จ)
- if (!itemId && item.originalData) {
- itemId = item.originalData.id || null;
+ if (!sourceKeyValue && item.originalData) {
+ sourceKeyValue = item.originalData.id || null;
}
- if (!itemId) {
- console.error("โ [2๋จ๊ณ ์ ์ฅ] item_id๋ฅผ ์ฐพ์ ์ ์์:", item);
+ if (!sourceKeyValue) {
+ console.error(`โ [2๋จ๊ณ ์ ์ฅ] ${sourceKeyField}๋ฅผ ์ฐพ์ ์ ์์:`, item);
continue;
}
- // upsert ๊ณตํต parentKeys: customer_id + item_id (์ ํํ ๋งค์นญ)
- const itemParentKeys = { ...parentKeys, item_id: itemId };
+ // upsert ๊ณตํต parentKeys: parentMapping ํค + sourceKeyField (์ ํํ ๋งค์นญ)
+ const itemParentKeys = { ...parentKeys, [sourceKeyField]: sourceKeyValue };
// === Step 1: ๋ฉ์ธ ํ
์ด๋ธ(customer_item_mapping) ์ ์ฅ ===
// ์ฌ๋ฌ ๊ฐ์ ๋งคํ ๋ ์ฝ๋ ์ง์ (๊ฑฐ๋์ฒ ํ๋ฒ/ํ๋ช
์ด ๋ค์ค์ผ ์ ์์)
@@ -688,11 +703,11 @@ export const SelectedItemsDetailInputComponent: React.FC {
- if (field.name !== "item_id" && field.autoFillFrom && item.originalData) {
+ if (field.name !== sourceKeyField && field.autoFillFrom && item.originalData) {
const value = item.originalData[field.autoFillFrom];
if (value !== undefined && value !== null && !record[field.name]) {
record[field.name] = value;
@@ -1700,7 +1715,7 @@ export const SelectedItemsDetailInputComponent: React.FC f.name !== "item_id" && f.width !== "0px");
+ const sampleFields = (componentConfig.additionalFields || []).filter(f => f.name !== sourceKeyField && f.width !== "0px");
const sampleGroups = componentConfig.fieldGroups || [{ id: "default", title: "์
๋ ฅ ์ ๋ณด", order: 0 }];
const gridCols = sampleGroups.length === 1 ? "grid-cols-1" : "grid-cols-2";
diff --git a/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx b/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx
index 908bc4f1..e531b655 100644
--- a/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx
+++ b/frontend/lib/registry/components/v2-repeater/V2RepeaterRenderer.tsx
@@ -20,6 +20,7 @@ interface V2RepeaterRendererProps {
onRowClick?: (row: any) => void;
onButtonClick?: (action: string, row?: any, buttonConfig?: any) => void;
parentId?: string | number;
+ formData?: Record;
}
const V2RepeaterRenderer: React.FC = ({
@@ -31,6 +32,7 @@ const V2RepeaterRenderer: React.FC = ({
onRowClick,
onButtonClick,
parentId,
+ formData,
}) => {
// component.componentConfig ๋๋ component.config์์ V2RepeaterConfig ์ถ์ถ
const config: V2RepeaterConfig = React.useMemo(() => {
@@ -101,6 +103,7 @@ const V2RepeaterRenderer: React.FC = ({
onRowClick={onRowClick}
onButtonClick={onButtonClick}
className={component?.className}
+ formData={formData}
/>
);
};
diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx
index bb9306c8..f56b0fb3 100644
--- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx
+++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutComponent.tsx
@@ -1183,31 +1183,15 @@ export const SplitPanelLayoutComponent: React.FC
}
// leftItem์ด null์ด๋ฉด join ๋ชจ๋ ์ด์ธ์๋ ๋ฐ์ดํฐ ๋ก๋ ๋ถ๊ฐ
+ // detail ๋ชจ๋: ์ ํ ์ ํ๋ฉด ์๋ฌด๊ฒ๋ ์ ๋ธ, ์ ํํ๋ฉด ํํฐ๋ง
+ // join ๋ชจ๋: ์ ํ ์ ํ๋ฉด ์ ์ฒด, ์ ํํ๋ฉด ํํฐ๋ง
if (!leftItem) return;
setIsLoadingRight(true);
try {
- if (relationshipType === "detail") {
- // ์์ธ ๋ชจ๋: ๋์ผ ํ
์ด๋ธ์ ์์ธ ์ ๋ณด (์ํฐํฐ ์กฐ์ธ ํ์ฑํ)
- const primaryKey = leftItem.id || leftItem.ID || Object.values(leftItem)[0];
-
- // ๐ ์ํฐํฐ ์กฐ์ธ API ์ฌ์ฉ
- const { entityJoinApi } = await import("@/lib/api/entityJoin");
- const rightDetailJoinColumns = extractAdditionalJoinColumns(
- componentConfig.rightPanel?.columns,
- rightTableName,
- );
- const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
- search: { id: primaryKey },
- enableEntityJoin: true,
- size: 1,
- companyCodeOverride: companyCode,
- additionalJoinColumns: rightDetailJoinColumns, // ๐ Entity ์กฐ์ธ ์ปฌ๋ผ ์ ๋ฌ
- });
-
- const detail = result.items && result.items.length > 0 ? result.items[0] : null;
- setRightData(detail);
- } else if (relationshipType === "join") {
+ // detail / join ๋ชจ๋ ๋์ผํ ํํฐ๋ง ๋ก์ง ์ฌ์ฉ
+ // (์ฐจ์ด์ : ์ด๊ธฐ ๋ก๋ ์ฌ๋ถ๋ง ๋ค๋ฆ - detail์ ์ด๊ธฐ ๋ก๋ ์ ํจ)
+ {
// ์กฐ์ธ ๋ชจ๋: ๋ค๋ฅธ ํ
์ด๋ธ์ ๊ด๋ จ ๋ฐ์ดํฐ (์ฌ๋ฌ ๊ฐ)
const keys = componentConfig.rightPanel?.relation?.keys;
const leftTable = componentConfig.leftPanel?.tableName;
@@ -1443,16 +1427,24 @@ export const SplitPanelLayoutComponent: React.FC
// ํญ์ dataFilter (API ์ ๋ฌ์ฉ)
const tabDataFilterForApi = (tabConfig as any).dataFilter;
+ // ํญ์ relation type ํ์ธ (detail์ด๋ฉด ์ด๊ธฐ ์ ์ฒด ๋ก๋ ์ ํจ)
+ const tabRelationType = tabConfig.relation?.type || "join";
+
if (!leftItem) {
- // ์ข์ธก ๋ฏธ์ ํ: ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋ (dataFilter๋ API์ ์ ๋ฌ)
- const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
- enableEntityJoin: true,
- size: 1000,
- companyCodeOverride: companyCode,
- additionalJoinColumns: tabJoinColumns,
- dataFilter: tabDataFilterForApi,
- });
- resultData = result.data || [];
+ if (tabRelationType === "detail") {
+ // detail ๋ชจ๋: ์ ํ ์ ํ๋ฉด ์๋ฌด๊ฒ๋ ์ ๋ธ
+ resultData = [];
+ } else {
+ // join ๋ชจ๋: ์ข์ธก ๋ฏธ์ ํ ์ ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋ (dataFilter๋ API์ ์ ๋ฌ)
+ const result = await entityJoinApi.getTableDataWithJoins(tabTableName, {
+ enableEntityJoin: true,
+ size: 1000,
+ companyCodeOverride: companyCode,
+ additionalJoinColumns: tabJoinColumns,
+ dataFilter: tabDataFilterForApi,
+ });
+ resultData = result.data || [];
+ }
} else if (leftColumn && rightColumn) {
const searchConditions: Record = {};
@@ -1534,22 +1526,30 @@ export const SplitPanelLayoutComponent: React.FC
[componentConfig.rightPanel?.additionalTabs, isDesignMode, toast],
);
- // ํญ ๋ณ๊ฒฝ ํธ๋ค๋ฌ (์ข์ธก ๋ฏธ์ ํ ์์๋ ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋)
+ // ํญ ๋ณ๊ฒฝ ํธ๋ค๋ฌ
const handleTabChange = useCallback(
(newTabIndex: number) => {
setActiveTabIndex(newTabIndex);
+ // ๋ฉ์ธ ํจ๋์ด "detail"(์ ํ ์ ํ์)์ด๋ฉด ์ข์ธก ๋ฏธ์ ํ ์ ๋ฐ์ดํฐ ๋ก๋ํ์ง ์์
+ const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail";
+ const requireSelection = mainRelationType === "detail";
+
if (newTabIndex === 0) {
if (!rightData || (Array.isArray(rightData) && rightData.length === 0)) {
- loadRightData(selectedLeftItem);
+ if (!requireSelection || selectedLeftItem) {
+ loadRightData(selectedLeftItem);
+ }
}
} else {
if (!tabsData[newTabIndex]) {
- loadTabData(newTabIndex, selectedLeftItem);
+ if (!requireSelection || selectedLeftItem) {
+ loadTabData(newTabIndex, selectedLeftItem);
+ }
}
}
},
- [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData],
+ [selectedLeftItem, rightData, tabsData, loadRightData, loadTabData, componentConfig.rightPanel?.relation?.type],
);
// ์ข์ธก ํญ๋ชฉ ์ ํ ํธ๋ค๋ฌ (๋์ผ ํญ๋ชฉ ์ฌํด๋ฆญ ์ ์ ํ ํด์ โ ์ ์ฒด ๋ฐ์ดํฐ ํ์)
@@ -1562,24 +1562,31 @@ export const SplitPanelLayoutComponent: React.FC
selectedLeftItem[leftPk] === item[leftPk];
if (isSameItem) {
- // ์ ํ ํด์ โ ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋
+ // ์ ํ ํด์
setSelectedLeftItem(null);
- setCustomLeftSelectedData({}); // ์ปค์คํ
๋ชจ๋ ์ฐ์ธก ํผ ๋ฐ์ดํฐ ์ด๊ธฐํ
+ setCustomLeftSelectedData({});
setExpandedRightItems(new Set());
setTabsData({});
- if (activeTabIndex === 0) {
- loadRightData(null);
+
+ const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail";
+ if (mainRelationType === "detail") {
+ // "์ ํ ์ ํ์" ๋ชจ๋: ์ ํ ํด์ ์ ๋ฐ์ดํฐ ๋น์
+ setRightData(null);
} else {
- loadTabData(activeTabIndex, null);
- }
- // ์ถ๊ฐ ํญ๋ค๋ ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋
- const tabs = componentConfig.rightPanel?.additionalTabs;
- if (tabs && tabs.length > 0) {
- tabs.forEach((_: any, idx: number) => {
- if (idx + 1 !== activeTabIndex) {
- loadTabData(idx + 1, null);
- }
- });
+ // "์ฐ๊ด ๋ชฉ๋ก" ๋ชจ๋: ์ ํ ํด์ ์ ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋
+ if (activeTabIndex === 0) {
+ loadRightData(null);
+ } else {
+ loadTabData(activeTabIndex, null);
+ }
+ const tabs = componentConfig.rightPanel?.additionalTabs;
+ if (tabs && tabs.length > 0) {
+ tabs.forEach((_: any, idx: number) => {
+ if (idx + 1 !== activeTabIndex) {
+ loadTabData(idx + 1, null);
+ }
+ });
+ }
}
return;
}
@@ -2781,14 +2788,20 @@ export const SplitPanelLayoutComponent: React.FC
if (!isDesignMode && componentConfig.autoLoad !== false) {
loadLeftData();
// ์ข์ธก ๋ฏธ์ ํ ์ํ์์ ์ฐ์ธก ์ ์ฒด ๋ฐ์ดํฐ ๊ธฐ๋ณธ ๋ก๋
+ // join ๋ชจ๋: ์ด๊ธฐ ์ ์ฒด ๋ก๋ / detail ๋ชจ๋: ์ด๊ธฐ ๋ก๋ ์ ํจ
const relationshipType = componentConfig.rightPanel?.relation?.type || "detail";
if (relationshipType === "join") {
loadRightData(null);
- // ์ถ๊ฐ ํญ๋ ์ ์ฒด ๋ฐ์ดํฐ ๋ก๋
+ }
+ // ์ถ๊ฐ ํญ: ๋ฉ์ธ ํจ๋์ด "detail"(์ ํ ์ ํ์)์ด๋ฉด ์ถ๊ฐ ํญ๋ ์ด๊ธฐ ๋ก๋ํ์ง ์์
+ if (relationshipType !== "detail") {
const tabs = componentConfig.rightPanel?.additionalTabs;
if (tabs && tabs.length > 0) {
- tabs.forEach((_: any, idx: number) => {
- loadTabData(idx + 1, null);
+ tabs.forEach((tab: any, idx: number) => {
+ const tabRelType = tab.relation?.type || "join";
+ if (tabRelType === "join") {
+ loadTabData(idx + 1, null);
+ }
});
}
}
@@ -3738,6 +3751,17 @@ export const SplitPanelLayoutComponent: React.FC
const currentTabData = tabsData[activeTabIndex] || [];
const isTabLoading = tabsLoading[activeTabIndex];
+ // ๋ฉ์ธ ํจ๋์ด "detail"(์ ํ ์ ํ์)์ด๋ฉด ์ข์ธก ๋ฏธ์ ํ ์ ์๋ด ๋ฉ์์ง
+ const mainRelationType = componentConfig.rightPanel?.relation?.type || "detail";
+ if (mainRelationType === "detail" && !selectedLeftItem && !isDesignMode) {
+ return (
+
+
์ข์ธก์์ ํญ๋ชฉ์ ์ ํํ์ธ์
+
์ ํํ ํญ๋ชฉ์ ๊ด๋ จ ๋ฐ์ดํฐ๊ฐ ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค
+
+ );
+ }
+
if (isTabLoading) {
return (
@@ -4645,7 +4669,7 @@ export const SplitPanelLayoutComponent: React.FC
) : (
<>
์ข์ธก์์ ํญ๋ชฉ์ ์ ํํ์ธ์
- ์ ํํ ํญ๋ชฉ์ ์์ธ ์ ๋ณด๊ฐ ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค
+ ์ ํํ ํญ๋ชฉ์ ๊ด๋ จ ๋ฐ์ดํฐ๊ฐ ์ฌ๊ธฐ์ ํ์๋ฉ๋๋ค
>
)}
diff --git a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx
index 52fd30a1..d77cb88d 100644
--- a/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx
+++ b/frontend/lib/registry/components/v2-split-panel-layout/SplitPanelLayoutConfigPanel.tsx
@@ -1542,13 +1542,10 @@ export const SplitPanelLayoutConfigPanel: React.FC {
- if (relationshipType === "detail") {
- return leftTableName; // ์์ธ ๋ชจ๋์์๋ ์ข์ธก๊ณผ ๋์ผ
- }
return config.rightPanel?.tableName || "";
- }, [relationshipType, leftTableName, config.rightPanel?.tableName]);
+ }, [config.rightPanel?.tableName]);
// ์ฐ์ธก ํ
์ด๋ธ ์ปฌ๋ผ (๋ก๋๋ ์ปฌ๋ผ ์ฌ์ฉ)
const rightTableColumns = useMemo(() => {
@@ -1567,8 +1564,8 @@ export const SplitPanelLayoutConfigPanel: React.FC {
- // ์์ธ ๋ชจ๋๋ก ๋ณ๊ฒฝ ์ ์ฐ์ธก ํ
์ด๋ธ์ ํ์ฌ ํ๋ฉด ํ
์ด๋ธ๋ก ์ค์
- if (value === "detail" && screenTableName) {
- updateRightPanel({
- relation: { ...config.rightPanel?.relation, type: value },
- tableName: screenTableName,
- });
- } else {
- updateRightPanel({
- relation: { ...config.rightPanel?.relation, type: value },
- });
- }
+ updateRightPanel({
+ relation: { ...config.rightPanel?.relation, type: value },
+ });
}}
>
- {relationshipType === "detail" ? "1๊ฑด ์์ธ๋ณด๊ธฐ" : "์ฐ๊ด ๋ชฉ๋ก"}
+ {relationshipType === "detail" ? "์ ํ ์ ํ์" : "์ฐ๊ด ๋ชฉ๋ก"}
- 1๊ฑด ์์ธ๋ณด๊ธฐ
- ์ข์ธก ํด๋ฆญ ์ ํด๋น ํญ๋ชฉ์ ์์ธ ์ ๋ณด ํ์ (๊ฐ์ ํ
์ด๋ธ)
+ ์ ํ ์ ํ์
+ ์ข์ธก ์ ํ ์์๋ง ์ฐ์ธก ๋ฐ์ดํฐ ํ์ / ๋ฏธ์ ํ ์ ๋น ํ๋ฉด
์ฐ๊ด ๋ชฉ๋ก
- ์ข์ธก ํด๋ฆญ ์ ์ฐ๊ด๋ ๋ฐ์ดํฐ ๋ชฉ๋ก ํ์ / ๋ฏธ์ ํ ์ ์ ์ฒด ํ์
+ ๋ฏธ์ ํ ์ ์ ์ฒด ํ์ / ์ข์ธก ์ ํ ์ ํํฐ๋ง
@@ -2305,7 +2294,7 @@ export const SplitPanelLayoutConfigPanel: React.FC
{/* ์ฐ์ธก ํจ๋ ์ค์ */}
-
์ฐ์ธก ํจ๋ ์ค์ ({relationshipType === "detail" ? "1๊ฑด ์์ธ๋ณด๊ธฐ" : "์ฐ๊ด ๋ชฉ๋ก"})
+
์ฐ์ธก ํจ๋ ์ค์ ({relationshipType === "detail" ? "์ ํ ์ ํ์" : "์ฐ๊ด ๋ชฉ๋ก"})
@@ -2338,63 +2327,49 @@ export const SplitPanelLayoutConfigPanel: React.FC
*/}
{/* ๊ด๊ณ ํ์
์ ๋ฐ๋ผ ํ
์ด๋ธ ์ ํ UI ๋ณ๊ฒฝ */}
- {relationshipType === "detail" ? (
- // ์์ธ ๋ชจ๋: ์ข์ธก๊ณผ ๋์ผํ ํ
์ด๋ธ (์๋ ์ค์ )
-
-
-
-
- {config.leftPanel?.tableName || screenTableName || "ํ
์ด๋ธ์ด ์ง์ ๋์ง ์์"}
-
-
์์ธ ๋ชจ๋์์๋ ์ข์ธก๊ณผ ๋์ผํ ํ
์ด๋ธ์ ์ฌ์ฉํฉ๋๋ค
-
-
- ) : (
- // ์กฐ๊ฑด ํํฐ ๋ชจ๋: ์ ์ฒด ํ
์ด๋ธ์์ ์ ํ ๊ฐ๋ฅ
-
-
-
-
-
-
-
-
-
- ํ
์ด๋ธ์ ์ฐพ์ ์ ์์ต๋๋ค.
-
- {availableRightTables.map((table) => (
- {
- updateRightPanel({ tableName: table.tableName });
- setRightTableOpen(false);
- }}
- >
-
- {table.displayName || table.tableName}
- {table.displayName && ({table.tableName})}
-
- ))}
-
-
-
-
-
- )}
+
+
+
+
+
+
+
+
+
+ ํ
์ด๋ธ์ ์ฐพ์ ์ ์์ต๋๋ค.
+
+ {availableRightTables.map((table) => (
+ {
+ updateRightPanel({ tableName: table.tableName });
+ setRightTableOpen(false);
+ }}
+ >
+
+ {table.displayName || table.tableName}
+ {table.displayName && ({table.tableName})}
+
+ ))}
+
+
+
+
+
diff --git a/frontend/types/v2-repeater.ts b/frontend/types/v2-repeater.ts
index d09ac9e9..fab7a523 100644
--- a/frontend/types/v2-repeater.ts
+++ b/frontend/types/v2-repeater.ts
@@ -180,7 +180,9 @@ export interface V2RepeaterProps {
data?: any[]; // ์ด๊ธฐ ๋ฐ์ดํฐ (์์ผ๋ฉด API๋ก ๋ก๋)
onDataChange?: (data: any[]) => void;
onRowClick?: (row: any) => void;
+ onButtonClick?: (action: string, row?: any, buttonConfig?: any) => void;
className?: string;
+ formData?: Record; // ์์ ๋ชจ๋์์ FK ๊ธฐ๋ฐ ๋ฐ์ดํฐ ๋ก๋์ฉ
}
// ๊ธฐ๋ณธ ์ค์ ๊ฐ