Compare commits
3 Commits
6735142db4
...
fcdaa68ddc
| Author | SHA1 | Date |
|---|---|---|
|
|
fcdaa68ddc | |
|
|
91f9bb9d12 | |
|
|
2b747a1030 |
|
|
@ -1646,7 +1646,18 @@ export class NodeFlowExecutionService {
|
||||||
// WHERE 조건 생성
|
// WHERE 조건 생성
|
||||||
const whereClauses: string[] = [];
|
const whereClauses: string[] = [];
|
||||||
whereConditions?.forEach((condition: any) => {
|
whereConditions?.forEach((condition: any) => {
|
||||||
const condValue = data[condition.field];
|
// 🔥 수정: sourceField가 있으면 소스 데이터에서 값을 가져옴
|
||||||
|
let condValue: any;
|
||||||
|
if (condition.sourceField) {
|
||||||
|
condValue = data[condition.sourceField];
|
||||||
|
} else if (
|
||||||
|
condition.staticValue !== undefined &&
|
||||||
|
condition.staticValue !== ""
|
||||||
|
) {
|
||||||
|
condValue = condition.staticValue;
|
||||||
|
} else {
|
||||||
|
condValue = data[condition.field];
|
||||||
|
}
|
||||||
|
|
||||||
if (condition.operator === "IS NULL") {
|
if (condition.operator === "IS NULL") {
|
||||||
whereClauses.push(`${condition.field} IS NULL`);
|
whereClauses.push(`${condition.field} IS NULL`);
|
||||||
|
|
@ -1987,7 +1998,18 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
// WHERE 조건 생성
|
// WHERE 조건 생성
|
||||||
whereConditions?.forEach((condition: any) => {
|
whereConditions?.forEach((condition: any) => {
|
||||||
const condValue = data[condition.field];
|
// 🔥 수정: sourceField가 있으면 소스 데이터에서 값을 가져옴
|
||||||
|
let condValue: any;
|
||||||
|
if (condition.sourceField) {
|
||||||
|
condValue = data[condition.sourceField];
|
||||||
|
} else if (
|
||||||
|
condition.staticValue !== undefined &&
|
||||||
|
condition.staticValue !== ""
|
||||||
|
) {
|
||||||
|
condValue = condition.staticValue;
|
||||||
|
} else {
|
||||||
|
condValue = data[condition.field];
|
||||||
|
}
|
||||||
|
|
||||||
if (condition.operator === "IS NULL") {
|
if (condition.operator === "IS NULL") {
|
||||||
whereClauses.push(`${condition.field} IS NULL`);
|
whereClauses.push(`${condition.field} IS NULL`);
|
||||||
|
|
@ -2889,7 +2911,26 @@ export class NodeFlowExecutionService {
|
||||||
|
|
||||||
const values: any[] = [];
|
const values: any[] = [];
|
||||||
const clauses = conditions.map((condition, index) => {
|
const clauses = conditions.map((condition, index) => {
|
||||||
const value = data ? data[condition.field] : condition.value;
|
// 🔥 수정: sourceField가 있으면 소스 데이터에서 값을 가져오고,
|
||||||
|
// 없으면 staticValue 또는 기존 field 사용
|
||||||
|
let value: any;
|
||||||
|
if (data) {
|
||||||
|
if (condition.sourceField) {
|
||||||
|
// sourceField가 있으면 소스 데이터에서 해당 필드의 값을 가져옴
|
||||||
|
value = data[condition.sourceField];
|
||||||
|
} else if (
|
||||||
|
condition.staticValue !== undefined &&
|
||||||
|
condition.staticValue !== ""
|
||||||
|
) {
|
||||||
|
// staticValue가 있으면 사용
|
||||||
|
value = condition.staticValue;
|
||||||
|
} else {
|
||||||
|
// 둘 다 없으면 기존 방식 (field로 값 조회)
|
||||||
|
value = data[condition.field];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = condition.value;
|
||||||
|
}
|
||||||
values.push(value);
|
values.push(value);
|
||||||
|
|
||||||
// 연산자를 SQL 문법으로 변환
|
// 연산자를 SQL 문법으로 변환
|
||||||
|
|
|
||||||
|
|
@ -284,15 +284,91 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}));
|
}));
|
||||||
}, [leftData, leftGrouping]);
|
}, [leftData, leftGrouping]);
|
||||||
|
|
||||||
// 셀 값 포맷팅 함수 (카테고리 타입 처리)
|
// 날짜 포맷팅 헬퍼 함수
|
||||||
|
const formatDateValue = useCallback((value: any, dateFormat: string): string => {
|
||||||
|
if (!value) return "-";
|
||||||
|
const date = new Date(value);
|
||||||
|
if (isNaN(date.getTime())) return String(value);
|
||||||
|
|
||||||
|
if (dateFormat === "relative") {
|
||||||
|
// 상대 시간 (예: 3일 전, 2시간 전)
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffSec = Math.floor(diffMs / 1000);
|
||||||
|
const diffMin = Math.floor(diffSec / 60);
|
||||||
|
const diffHour = Math.floor(diffMin / 60);
|
||||||
|
const diffDay = Math.floor(diffHour / 24);
|
||||||
|
const diffMonth = Math.floor(diffDay / 30);
|
||||||
|
const diffYear = Math.floor(diffMonth / 12);
|
||||||
|
|
||||||
|
if (diffYear > 0) return `${diffYear}년 전`;
|
||||||
|
if (diffMonth > 0) return `${diffMonth}개월 전`;
|
||||||
|
if (diffDay > 0) return `${diffDay}일 전`;
|
||||||
|
if (diffHour > 0) return `${diffHour}시간 전`;
|
||||||
|
if (diffMin > 0) return `${diffMin}분 전`;
|
||||||
|
return "방금 전";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 포맷 문자열 치환
|
||||||
|
return dateFormat
|
||||||
|
.replace("YYYY", String(date.getFullYear()))
|
||||||
|
.replace("MM", String(date.getMonth() + 1).padStart(2, "0"))
|
||||||
|
.replace("DD", String(date.getDate()).padStart(2, "0"))
|
||||||
|
.replace("HH", String(date.getHours()).padStart(2, "0"))
|
||||||
|
.replace("mm", String(date.getMinutes()).padStart(2, "0"))
|
||||||
|
.replace("ss", String(date.getSeconds()).padStart(2, "0"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 숫자 포맷팅 헬퍼 함수
|
||||||
|
const formatNumberValue = useCallback((value: any, format: any): string => {
|
||||||
|
if (value === null || value === undefined || value === "") return "-";
|
||||||
|
const num = typeof value === "number" ? value : parseFloat(String(value));
|
||||||
|
if (isNaN(num)) return String(value);
|
||||||
|
|
||||||
|
const options: Intl.NumberFormatOptions = {
|
||||||
|
minimumFractionDigits: format?.decimalPlaces ?? 0,
|
||||||
|
maximumFractionDigits: format?.decimalPlaces ?? 10,
|
||||||
|
useGrouping: format?.thousandSeparator ?? false,
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = num.toLocaleString("ko-KR", options);
|
||||||
|
if (format?.prefix) result = format.prefix + result;
|
||||||
|
if (format?.suffix) result = result + format.suffix;
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 셀 값 포맷팅 함수 (카테고리 타입 처리 + 날짜/숫자 포맷)
|
||||||
const formatCellValue = useCallback(
|
const formatCellValue = useCallback(
|
||||||
(
|
(
|
||||||
columnName: string,
|
columnName: string,
|
||||||
value: any,
|
value: any,
|
||||||
categoryMappings: Record<string, Record<string, { label: string; color?: string }>>,
|
categoryMappings: Record<string, Record<string, { label: string; color?: string }>>,
|
||||||
|
format?: {
|
||||||
|
type?: "number" | "currency" | "date" | "text";
|
||||||
|
thousandSeparator?: boolean;
|
||||||
|
decimalPlaces?: number;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
dateFormat?: string;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
if (value === null || value === undefined) return "-";
|
if (value === null || value === undefined) return "-";
|
||||||
|
|
||||||
|
// 🆕 날짜 포맷 적용
|
||||||
|
if (format?.type === "date" || format?.dateFormat) {
|
||||||
|
return formatDateValue(value, format?.dateFormat || "YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 숫자 포맷 적용
|
||||||
|
if (
|
||||||
|
format?.type === "number" ||
|
||||||
|
format?.type === "currency" ||
|
||||||
|
format?.thousandSeparator ||
|
||||||
|
format?.decimalPlaces !== undefined
|
||||||
|
) {
|
||||||
|
return formatNumberValue(value, format);
|
||||||
|
}
|
||||||
|
|
||||||
// 🆕 카테고리 매핑 찾기 (여러 키 형태 시도)
|
// 🆕 카테고리 매핑 찾기 (여러 키 형태 시도)
|
||||||
// 1. 전체 컬럼명 (예: "item_info.material")
|
// 1. 전체 컬럼명 (예: "item_info.material")
|
||||||
// 2. 컬럼명만 (예: "material")
|
// 2. 컬럼명만 (예: "material")
|
||||||
|
|
@ -323,10 +399,28 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🆕 자동 날짜 감지 (ISO 8601 형식 또는 Date 객체)
|
||||||
|
if (typeof value === "string" && value.match(/^\d{4}-\d{2}-\d{2}(T|\s)/)) {
|
||||||
|
return formatDateValue(value, "YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 자동 숫자 감지 (숫자 또는 숫자 문자열) - 소수점 있으면 정수로 변환
|
||||||
|
if (typeof value === "number") {
|
||||||
|
// 숫자인 경우 정수로 표시 (소수점 제거)
|
||||||
|
return Number.isInteger(value) ? String(value) : String(Math.round(value * 100) / 100);
|
||||||
|
}
|
||||||
|
if (typeof value === "string" && /^-?\d+\.?\d*$/.test(value.trim())) {
|
||||||
|
// 숫자 문자열인 경우 (예: "5.00" → "5")
|
||||||
|
const num = parseFloat(value);
|
||||||
|
if (!isNaN(num)) {
|
||||||
|
return Number.isInteger(num) ? String(num) : String(Math.round(num * 100) / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 일반 값
|
// 일반 값
|
||||||
return String(value);
|
return String(value);
|
||||||
},
|
},
|
||||||
[],
|
[formatDateValue, formatNumberValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 좌측 데이터 로드
|
// 좌측 데이터 로드
|
||||||
|
|
@ -405,24 +499,76 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
setRightData(detail);
|
setRightData(detail);
|
||||||
} else if (relationshipType === "join") {
|
} else if (relationshipType === "join") {
|
||||||
// 조인 모드: 다른 테이블의 관련 데이터 (여러 개)
|
// 조인 모드: 다른 테이블의 관련 데이터 (여러 개)
|
||||||
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
const keys = componentConfig.rightPanel?.relation?.keys;
|
||||||
const rightColumn = componentConfig.rightPanel?.relation?.foreignKey;
|
|
||||||
const leftTable = componentConfig.leftPanel?.tableName;
|
const leftTable = componentConfig.leftPanel?.tableName;
|
||||||
|
|
||||||
if (leftColumn && rightColumn && leftTable) {
|
// 🆕 복합키 지원
|
||||||
const leftValue = leftItem[leftColumn];
|
if (keys && keys.length > 0 && leftTable) {
|
||||||
const joinedData = await dataApi.getJoinedData(
|
// 복합키: 여러 조건으로 필터링
|
||||||
leftTable,
|
const { entityJoinApi } = await import("@/lib/api/entityJoin");
|
||||||
rightTableName,
|
|
||||||
leftColumn,
|
// 복합키 조건 생성
|
||||||
rightColumn,
|
const searchConditions: Record<string, any> = {};
|
||||||
leftValue,
|
keys.forEach((key) => {
|
||||||
componentConfig.rightPanel?.dataFilter, // 🆕 데이터 필터 전달
|
if (key.leftColumn && key.rightColumn && leftItem[key.leftColumn] !== undefined) {
|
||||||
true, // 🆕 Entity 조인 활성화
|
searchConditions[key.rightColumn] = leftItem[key.leftColumn];
|
||||||
componentConfig.rightPanel?.columns, // 🆕 표시 컬럼 전달 (item_info.item_name 등)
|
}
|
||||||
componentConfig.rightPanel?.deduplication, // 🆕 중복 제거 설정 전달
|
});
|
||||||
);
|
|
||||||
setRightData(joinedData || []); // 모든 관련 레코드 (배열)
|
console.log("🔗 [분할패널] 복합키 조건:", searchConditions);
|
||||||
|
|
||||||
|
// 엔티티 조인 API로 데이터 조회
|
||||||
|
const result = await entityJoinApi.getTableDataWithJoins(rightTableName, {
|
||||||
|
search: searchConditions,
|
||||||
|
enableEntityJoin: true,
|
||||||
|
size: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🔗 [분할패널] 복합키 조회 결과:", result);
|
||||||
|
|
||||||
|
// 추가 dataFilter 적용
|
||||||
|
let filteredData = result.data || [];
|
||||||
|
const dataFilter = componentConfig.rightPanel?.dataFilter;
|
||||||
|
if (dataFilter?.enabled && dataFilter.conditions?.length > 0) {
|
||||||
|
filteredData = filteredData.filter((item: any) => {
|
||||||
|
return dataFilter.conditions.every((cond: any) => {
|
||||||
|
const value = item[cond.column];
|
||||||
|
const condValue = cond.value;
|
||||||
|
switch (cond.operator) {
|
||||||
|
case "equals":
|
||||||
|
return value === condValue;
|
||||||
|
case "notEquals":
|
||||||
|
return value !== condValue;
|
||||||
|
case "contains":
|
||||||
|
return String(value).includes(String(condValue));
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setRightData(filteredData);
|
||||||
|
} else {
|
||||||
|
// 단일키 (하위 호환성)
|
||||||
|
const leftColumn = componentConfig.rightPanel?.relation?.leftColumn;
|
||||||
|
const rightColumn = componentConfig.rightPanel?.relation?.foreignKey;
|
||||||
|
|
||||||
|
if (leftColumn && rightColumn && leftTable) {
|
||||||
|
const leftValue = leftItem[leftColumn];
|
||||||
|
const joinedData = await dataApi.getJoinedData(
|
||||||
|
leftTable,
|
||||||
|
rightTableName,
|
||||||
|
leftColumn,
|
||||||
|
rightColumn,
|
||||||
|
leftValue,
|
||||||
|
componentConfig.rightPanel?.dataFilter, // 🆕 데이터 필터 전달
|
||||||
|
true, // 🆕 Entity 조인 활성화
|
||||||
|
componentConfig.rightPanel?.columns, // 🆕 표시 컬럼 전달 (item_info.item_name 등)
|
||||||
|
componentConfig.rightPanel?.deduplication, // 🆕 중복 제거 설정 전달
|
||||||
|
);
|
||||||
|
setRightData(joinedData || []); // 모든 관련 레코드 (배열)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -840,7 +986,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
primaryKeyValue = item[firstKey];
|
primaryKeyValue = item[firstKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`✅ 수정 모달 열기:`, {
|
console.log("✅ 수정 모달 열기:", {
|
||||||
tableName: rightTableName,
|
tableName: rightTableName,
|
||||||
primaryKeyName,
|
primaryKeyName,
|
||||||
primaryKeyValue,
|
primaryKeyValue,
|
||||||
|
|
@ -1527,6 +1673,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
leftColumnLabels[colName] || (typeof col === "object" ? col.label : null) || colName,
|
leftColumnLabels[colName] || (typeof col === "object" ? col.label : null) || colName,
|
||||||
width: typeof col === "object" ? col.width : 150,
|
width: typeof col === "object" ? col.width : 150,
|
||||||
align: (typeof col === "object" ? col.align : "left") as "left" | "center" | "right",
|
align: (typeof col === "object" ? col.align : "left") as "left" | "center" | "right",
|
||||||
|
format: typeof col === "object" ? col.format : undefined, // 🆕 포맷 설정 포함
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
: Object.keys(filteredData[0] || {})
|
: Object.keys(filteredData[0] || {})
|
||||||
|
|
@ -1537,6 +1684,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
label: leftColumnLabels[key] || key,
|
label: leftColumnLabels[key] || key,
|
||||||
width: 150,
|
width: 150,
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
|
format: undefined, // 🆕 기본값
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// 🔧 그룹화된 데이터 렌더링
|
// 🔧 그룹화된 데이터 렌더링
|
||||||
|
|
@ -1587,7 +1735,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(col.name, item[col.name], leftCategoryMappings)}
|
{formatCellValue(
|
||||||
|
col.name,
|
||||||
|
item[col.name],
|
||||||
|
leftCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1643,7 +1796,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(col.name, item[col.name], leftCategoryMappings)}
|
{formatCellValue(col.name, item[col.name], leftCategoryMappings, col.format)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -1747,7 +1900,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
} else {
|
} else {
|
||||||
// 설정된 컬럼이 없으면 자동으로 첫 2개 필드 표시
|
// 설정된 컬럼이 없으면 자동으로 첫 2개 필드 표시
|
||||||
const keys = Object.keys(item).filter(
|
const keys = Object.keys(item).filter(
|
||||||
(k) => k !== "id" && k !== "ID" && k !== "children" && k !== "level" && shouldShowField(k)
|
(k) => k !== "id" && k !== "ID" && k !== "children" && k !== "level" && shouldShowField(k),
|
||||||
);
|
);
|
||||||
displayFields = keys.slice(0, 2).map((key) => ({
|
displayFields = keys.slice(0, 2).map((key) => ({
|
||||||
label: leftColumnLabels[key] || key,
|
label: leftColumnLabels[key] || key,
|
||||||
|
|
@ -1960,6 +2113,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
? displayColumns.map((col) => ({
|
? displayColumns.map((col) => ({
|
||||||
...col,
|
...col,
|
||||||
label: rightColumnLabels[col.name] || col.label || col.name,
|
label: rightColumnLabels[col.name] || col.label || col.name,
|
||||||
|
format: col.format, // 🆕 포맷 설정 유지
|
||||||
}))
|
}))
|
||||||
: Object.keys(filteredData[0] || {})
|
: Object.keys(filteredData[0] || {})
|
||||||
.filter((key) => shouldShowField(key))
|
.filter((key) => shouldShowField(key))
|
||||||
|
|
@ -1969,6 +2123,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
label: rightColumnLabels[key] || key,
|
label: rightColumnLabels[key] || key,
|
||||||
width: 150,
|
width: 150,
|
||||||
align: "left" as const,
|
align: "left" as const,
|
||||||
|
format: undefined, // 🆕 기본값
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -2014,7 +2169,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
className="px-3 py-2 text-sm whitespace-nowrap text-gray-900"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(col.name, item[col.name], rightCategoryMappings)}
|
{formatCellValue(col.name, item[col.name], rightCategoryMappings, col.format)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{!isDesignMode && (
|
{!isDesignMode && (
|
||||||
|
|
@ -2022,7 +2177,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<div className="flex justify-end gap-1">
|
<div className="flex justify-end gap-1">
|
||||||
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
{(componentConfig.rightPanel?.editButton?.enabled ?? true) && (
|
||||||
<Button
|
<Button
|
||||||
variant={componentConfig.rightPanel?.editButton?.buttonVariant || "outline"}
|
variant={
|
||||||
|
componentConfig.rightPanel?.editButton?.buttonVariant || "outline"
|
||||||
|
}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -2030,20 +2187,22 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}}
|
}}
|
||||||
className="h-7"
|
className="h-7"
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3 mr-1" />
|
<Pencil className="mr-1 h-3 w-3" />
|
||||||
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<button
|
{(componentConfig.rightPanel?.deleteButton?.enabled ?? true) && (
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
handleDeleteClick("right", item);
|
e.stopPropagation();
|
||||||
}}
|
handleDeleteClick("right", item);
|
||||||
className="rounded p-1 transition-colors hover:bg-red-100"
|
}}
|
||||||
title="삭제"
|
className="rounded p-1 transition-colors hover:bg-red-100"
|
||||||
>
|
title={componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
|
||||||
<Trash2 className="h-4 w-4 text-red-600" />
|
>
|
||||||
</button>
|
<Trash2 className="h-4 w-4 text-red-600" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
|
|
@ -2083,26 +2242,28 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
// 🆕 엔티티 조인 컬럼 처리 (예: item_info.item_number → item_number 또는 item_id_name)
|
// 🆕 엔티티 조인 컬럼 처리 (예: item_info.item_number → item_number 또는 item_id_name)
|
||||||
let value = item[col.name];
|
let value = item[col.name];
|
||||||
if (value === undefined && col.name.includes('.')) {
|
if (value === undefined && col.name.includes(".")) {
|
||||||
const columnName = col.name.split('.').pop();
|
const columnName = col.name.split(".").pop();
|
||||||
// 1차: 컬럼명 그대로 (예: item_number)
|
// 1차: 컬럼명 그대로 (예: item_number)
|
||||||
value = item[columnName || ''];
|
value = item[columnName || ""];
|
||||||
// 2차: item_info.item_number → item_id_name 또는 item_id_item_number 형식 확인
|
// 2차: item_info.item_number → item_id_name 또는 item_id_item_number 형식 확인
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
const parts = col.name.split('.');
|
const parts = col.name.split(".");
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
const refTable = parts[0]; // item_info
|
const refTable = parts[0]; // item_info
|
||||||
const refColumn = parts[1]; // item_number 또는 item_name
|
const refColumn = parts[1]; // item_number 또는 item_name
|
||||||
// FK 컬럼명 추론: item_info → item_id
|
// FK 컬럼명 추론: item_info → item_id
|
||||||
const fkColumn = refTable.replace('_info', '').replace('_mng', '') + '_id';
|
const fkColumn = refTable.replace("_info", "").replace("_mng", "") + "_id";
|
||||||
|
|
||||||
// 백엔드에서 반환하는 별칭 패턴:
|
// 백엔드에서 반환하는 별칭 패턴:
|
||||||
// 1) item_id_name (기본 referenceColumn)
|
// 1) item_id_name (기본 referenceColumn)
|
||||||
// 2) item_id_item_name (추가 컬럼)
|
// 2) item_id_item_name (추가 컬럼)
|
||||||
if (refColumn === refTable.replace('_info', '').replace('_mng', '') + '_number' ||
|
if (
|
||||||
refColumn === refTable.replace('_info', '').replace('_mng', '') + '_code') {
|
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_number" ||
|
||||||
|
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_code"
|
||||||
|
) {
|
||||||
// 기본 참조 컬럼 (item_number, customer_code 등)
|
// 기본 참조 컬럼 (item_number, customer_code 등)
|
||||||
const aliasKey = fkColumn + '_name';
|
const aliasKey = fkColumn + "_name";
|
||||||
value = item[aliasKey];
|
value = item[aliasKey];
|
||||||
} else {
|
} else {
|
||||||
// 추가 컬럼 (item_name, customer_name 등)
|
// 추가 컬럼 (item_name, customer_name 등)
|
||||||
|
|
@ -2120,26 +2281,28 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
.map((col) => {
|
.map((col) => {
|
||||||
// 🆕 엔티티 조인 컬럼 처리
|
// 🆕 엔티티 조인 컬럼 처리
|
||||||
let value = item[col.name];
|
let value = item[col.name];
|
||||||
if (value === undefined && col.name.includes('.')) {
|
if (value === undefined && col.name.includes(".")) {
|
||||||
const columnName = col.name.split('.').pop();
|
const columnName = col.name.split(".").pop();
|
||||||
// 1차: 컬럼명 그대로
|
// 1차: 컬럼명 그대로
|
||||||
value = item[columnName || ''];
|
value = item[columnName || ""];
|
||||||
// 2차: {fk_column}_name 또는 {fk_column}_{ref_column} 형식 확인
|
// 2차: {fk_column}_name 또는 {fk_column}_{ref_column} 형식 확인
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
const parts = col.name.split('.');
|
const parts = col.name.split(".");
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
const refTable = parts[0]; // item_info
|
const refTable = parts[0]; // item_info
|
||||||
const refColumn = parts[1]; // item_number 또는 item_name
|
const refColumn = parts[1]; // item_number 또는 item_name
|
||||||
// FK 컬럼명 추론: item_info → item_id
|
// FK 컬럼명 추론: item_info → item_id
|
||||||
const fkColumn = refTable.replace('_info', '').replace('_mng', '') + '_id';
|
const fkColumn = refTable.replace("_info", "").replace("_mng", "") + "_id";
|
||||||
|
|
||||||
// 백엔드에서 반환하는 별칭 패턴:
|
// 백엔드에서 반환하는 별칭 패턴:
|
||||||
// 1) item_id_name (기본 referenceColumn)
|
// 1) item_id_name (기본 referenceColumn)
|
||||||
// 2) item_id_item_name (추가 컬럼)
|
// 2) item_id_item_name (추가 컬럼)
|
||||||
if (refColumn === refTable.replace('_info', '').replace('_mng', '') + '_number' ||
|
if (
|
||||||
refColumn === refTable.replace('_info', '').replace('_mng', '') + '_code') {
|
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_number" ||
|
||||||
|
refColumn === refTable.replace("_info", "").replace("_mng", "") + "_code"
|
||||||
|
) {
|
||||||
// 기본 참조 컬럼
|
// 기본 참조 컬럼
|
||||||
const aliasKey = fkColumn + '_name';
|
const aliasKey = fkColumn + "_name";
|
||||||
value = item[aliasKey];
|
value = item[aliasKey];
|
||||||
} else {
|
} else {
|
||||||
// 추가 컬럼
|
// 추가 컬럼
|
||||||
|
|
@ -2158,11 +2321,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
firstValues = Object.entries(item)
|
firstValues = Object.entries(item)
|
||||||
.filter(([key]) => !key.toLowerCase().includes("id"))
|
.filter(([key]) => !key.toLowerCase().includes("id"))
|
||||||
.slice(0, summaryCount)
|
.slice(0, summaryCount)
|
||||||
.map(([key, value]) => [key, value, ''] as [string, any, string]);
|
.map(([key, value]) => [key, value, ""] as [string, any, string]);
|
||||||
|
|
||||||
allValues = Object.entries(item)
|
allValues = Object.entries(item)
|
||||||
.filter(([key, value]) => value !== null && value !== undefined && value !== "")
|
.filter(([key, value]) => value !== null && value !== undefined && value !== "")
|
||||||
.map(([key, value]) => [key, value, ''] as [string, any, string]);
|
.map(([key, value]) => [key, value, ""] as [string, any, string]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -2180,27 +2343,12 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||||
{firstValues.map(([key, value, label], idx) => {
|
{firstValues.map(([key, value, label], idx) => {
|
||||||
// 포맷 설정 및 볼드 설정 찾기
|
// 포맷 설정 및 볼드 설정 찾기
|
||||||
const colConfig = rightColumns?.find(c => c.name === key);
|
const colConfig = rightColumns?.find((c) => c.name === key);
|
||||||
const format = colConfig?.format;
|
const format = colConfig?.format;
|
||||||
const boldValue = colConfig?.bold ?? false;
|
const boldValue = colConfig?.bold ?? false;
|
||||||
|
|
||||||
// 🆕 카테고리 매핑 적용
|
// 🆕 포맷 적용 (날짜/숫자/카테고리)
|
||||||
const formattedValue = formatCellValue(key, value, rightCategoryMappings);
|
const displayValue = formatCellValue(key, value, rightCategoryMappings, format);
|
||||||
|
|
||||||
// 숫자 포맷 적용 (카테고리가 아닌 경우만)
|
|
||||||
let displayValue: React.ReactNode = formattedValue;
|
|
||||||
if (typeof formattedValue === 'string' && value !== null && value !== undefined && value !== "" && format) {
|
|
||||||
const numValue = typeof value === 'number' ? value : parseFloat(String(value));
|
|
||||||
if (!isNaN(numValue)) {
|
|
||||||
displayValue = numValue.toLocaleString('ko-KR', {
|
|
||||||
minimumFractionDigits: format.decimalPlaces ?? 0,
|
|
||||||
maximumFractionDigits: format.decimalPlaces ?? 10,
|
|
||||||
useGrouping: format.thousandSeparator ?? false,
|
|
||||||
});
|
|
||||||
if (format.prefix) displayValue = format.prefix + displayValue;
|
|
||||||
if (format.suffix) displayValue = displayValue + format.suffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showLabel = componentConfig.rightPanel?.summaryShowLabel ?? true;
|
const showLabel = componentConfig.rightPanel?.summaryShowLabel ?? true;
|
||||||
|
|
||||||
|
|
@ -2212,7 +2360,7 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={`text-foreground text-sm ${boldValue ? 'font-semibold' : ''}`}
|
className={`text-foreground text-sm ${boldValue ? "font-semibold" : ""}`}
|
||||||
>
|
>
|
||||||
{displayValue}
|
{displayValue}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -2233,19 +2381,19 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
}}
|
}}
|
||||||
className="h-7"
|
className="h-7"
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3 mr-1" />
|
<Pencil className="mr-1 h-3 w-3" />
|
||||||
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
{componentConfig.rightPanel?.editButton?.buttonLabel || "수정"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{/* 삭제 버튼 */}
|
{/* 삭제 버튼 */}
|
||||||
{!isDesignMode && (
|
{!isDesignMode && (componentConfig.rightPanel?.deleteButton?.enabled ?? true) && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDeleteClick("right", item);
|
handleDeleteClick("right", item);
|
||||||
}}
|
}}
|
||||||
className="rounded p-1 transition-colors hover:bg-red-100"
|
className="rounded p-1 transition-colors hover:bg-red-100"
|
||||||
title="삭제"
|
title={componentConfig.rightPanel?.deleteButton?.buttonLabel || "삭제"}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4 text-red-600" />
|
<Trash2 className="h-4 w-4 text-red-600" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -2274,26 +2422,11 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
<tbody className="divide-border divide-y">
|
<tbody className="divide-border divide-y">
|
||||||
{allValues.map(([key, value, label]) => {
|
{allValues.map(([key, value, label]) => {
|
||||||
// 포맷 설정 찾기
|
// 포맷 설정 찾기
|
||||||
const colConfig = rightColumns?.find(c => c.name === key);
|
const colConfig = rightColumns?.find((c) => c.name === key);
|
||||||
const format = colConfig?.format;
|
const format = colConfig?.format;
|
||||||
|
|
||||||
// 🆕 카테고리 매핑 적용
|
// 🆕 포맷 적용 (날짜/숫자/카테고리)
|
||||||
const formattedValue = formatCellValue(key, value, rightCategoryMappings);
|
const displayValue = formatCellValue(key, value, rightCategoryMappings, format);
|
||||||
|
|
||||||
// 숫자 포맷 적용 (카테고리가 아닌 경우만)
|
|
||||||
let displayValue: React.ReactNode = formattedValue;
|
|
||||||
if (typeof formattedValue === 'string' && value !== null && value !== undefined && value !== "" && format) {
|
|
||||||
const numValue = typeof value === 'number' ? value : parseFloat(String(value));
|
|
||||||
if (!isNaN(numValue)) {
|
|
||||||
displayValue = numValue.toLocaleString('ko-KR', {
|
|
||||||
minimumFractionDigits: format.decimalPlaces ?? 0,
|
|
||||||
maximumFractionDigits: format.decimalPlaces ?? 10,
|
|
||||||
useGrouping: format.thousandSeparator ?? false,
|
|
||||||
});
|
|
||||||
if (format.prefix) displayValue = format.prefix + displayValue;
|
|
||||||
if (format.suffix) displayValue = displayValue + format.suffix;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={key} className="hover:bg-muted">
|
<tr key={key} className="hover:bg-muted">
|
||||||
|
|
@ -2310,8 +2443,8 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted-foreground py-8 text-center text-sm">
|
<div className="text-muted-foreground py-8 text-center text-sm">
|
||||||
|
|
@ -2336,7 +2469,10 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
console.log("🔍 [디버깅] 상세 모드 표시 로직:");
|
console.log("🔍 [디버깅] 상세 모드 표시 로직:");
|
||||||
console.log(" 📋 rightData 전체:", rightData);
|
console.log(" 📋 rightData 전체:", rightData);
|
||||||
console.log(" 📋 rightData keys:", Object.keys(rightData));
|
console.log(" 📋 rightData keys:", Object.keys(rightData));
|
||||||
console.log(" ⚙️ 설정된 컬럼:", rightColumns.map((c) => `${c.name} (${c.label})`));
|
console.log(
|
||||||
|
" ⚙️ 설정된 컬럼:",
|
||||||
|
rightColumns.map((c) => `${c.name} (${c.label})`),
|
||||||
|
);
|
||||||
|
|
||||||
// 설정된 컬럼만 표시
|
// 설정된 컬럼만 표시
|
||||||
displayEntries = rightColumns
|
displayEntries = rightColumns
|
||||||
|
|
@ -2345,9 +2481,9 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
let value = rightData[col.name];
|
let value = rightData[col.name];
|
||||||
console.log(` 🔎 컬럼 "${col.name}": 직접 접근 = ${value}`);
|
console.log(` 🔎 컬럼 "${col.name}": 직접 접근 = ${value}`);
|
||||||
|
|
||||||
if (value === undefined && col.name.includes('.')) {
|
if (value === undefined && col.name.includes(".")) {
|
||||||
const columnName = col.name.split('.').pop();
|
const columnName = col.name.split(".").pop();
|
||||||
value = rightData[columnName || ''];
|
value = rightData[columnName || ""];
|
||||||
console.log(` → 변환 후 "${columnName}" 접근 = ${value}`);
|
console.log(` → 변환 후 "${columnName}" 접근 = ${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -104,10 +104,15 @@ export interface SplitPanelLayoutConfig {
|
||||||
|
|
||||||
// 좌측 선택 항목과의 관계 설정
|
// 좌측 선택 항목과의 관계 설정
|
||||||
relation?: {
|
relation?: {
|
||||||
type: "join" | "detail"; // 관계 타입
|
type?: "join" | "detail"; // 관계 타입 (optional - 하위 호환성)
|
||||||
leftColumn?: string; // 좌측 테이블의 연결 컬럼
|
leftColumn?: string; // 좌측 테이블의 연결 컬럼 (단일키 - 하위 호환성)
|
||||||
rightColumn?: string; // 우측 테이블의 연결 컬럼 (join용)
|
rightColumn?: string; // 우측 테이블의 연결 컬럼 (단일키 - 하위 호환성)
|
||||||
foreignKey?: string; // 우측 테이블의 외래키 컬럼명
|
foreignKey?: string; // 우측 테이블의 외래키 컬럼명
|
||||||
|
// 🆕 복합키 지원 (여러 컬럼으로 조인)
|
||||||
|
keys?: Array<{
|
||||||
|
leftColumn: string; // 좌측 테이블의 조인 컬럼
|
||||||
|
rightColumn: string; // 우측 테이블의 조인 컬럼
|
||||||
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 우측 패널 추가 시 중계 테이블 설정 (N:M 관계)
|
// 우측 패널 추가 시 중계 테이블 설정 (N:M 관계)
|
||||||
|
|
@ -150,6 +155,14 @@ export interface SplitPanelLayoutConfig {
|
||||||
buttonVariant?: "default" | "outline" | "ghost"; // 버튼 스타일 (기본: "outline")
|
buttonVariant?: "default" | "outline" | "ghost"; // 버튼 스타일 (기본: "outline")
|
||||||
groupByColumns?: string[]; // 🆕 그룹핑 기준 컬럼들 (예: ["customer_id", "item_id"])
|
groupByColumns?: string[]; // 🆕 그룹핑 기준 컬럼들 (예: ["customer_id", "item_id"])
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 🆕 삭제 버튼 설정
|
||||||
|
deleteButton?: {
|
||||||
|
enabled: boolean; // 삭제 버튼 표시 여부 (기본: true)
|
||||||
|
buttonLabel?: string; // 버튼 라벨 (기본: "삭제")
|
||||||
|
buttonVariant?: "default" | "outline" | "ghost" | "destructive"; // 버튼 스타일 (기본: "ghost")
|
||||||
|
confirmMessage?: string; // 삭제 확인 메시지
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 레이아웃 설정
|
// 레이아웃 설정
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue