[agent-pipeline] pipe-20260309055714-23ry round-3
This commit is contained in:
parent
790592ec76
commit
e4de414dfb
|
|
@ -53,9 +53,7 @@ const statusTranslations: { [key: string]: string } = {
|
||||||
maintenance: "정비중",
|
maintenance: "정비중",
|
||||||
|
|
||||||
// 기사 관련 (존중하는 표현)
|
// 기사 관련 (존중하는 표현)
|
||||||
waiting: "대기중",
|
|
||||||
resting: "휴식중",
|
resting: "휴식중",
|
||||||
unavailable: "운행불가",
|
|
||||||
|
|
||||||
// 기사 평가
|
// 기사 평가
|
||||||
excellent: "우수",
|
excellent: "우수",
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
<div
|
<div
|
||||||
className={`rounded-lg p-2 ${
|
className={`rounded-lg p-2 ${
|
||||||
index === 0
|
index === 0
|
||||||
? "border-l-4 border-emerald-400 bg-emerald-50"
|
? "border-l-4 border-success/60 bg-success/10"
|
||||||
: index === 1
|
: index === 1
|
||||||
? "border-l-4 border-primary/60 bg-accent"
|
? "border-l-4 border-primary/60 bg-accent"
|
||||||
: "bg-muted"
|
: "bg-muted"
|
||||||
|
|
@ -73,7 +73,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
<div className="mb-1 flex items-center justify-between">
|
<div className="mb-1 flex items-center justify-between">
|
||||||
<div
|
<div
|
||||||
className={`text-xs font-medium ${
|
className={`text-xs font-medium ${
|
||||||
index === 0 ? "text-emerald-700" : index === 1 ? "text-primary" : "text-foreground"
|
index === 0 ? "text-success" : index === 1 ? "text-primary" : "text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{displayName}
|
{displayName}
|
||||||
|
|
@ -81,7 +81,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
||||||
{selectedNodes.length === 2 && (
|
{selectedNodes.length === 2 && (
|
||||||
<div
|
<div
|
||||||
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
|
className={`rounded-full px-2 py-0.5 text-xs font-bold ${
|
||||||
index === 0 ? "bg-emerald-200 text-emerald-800" : "bg-blue-200 text-primary"
|
index === 0 ? "bg-success/20 text-success" : "bg-primary/20 text-primary"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{index === 0 ? "FROM" : "TO"}
|
{index === 0 ? "FROM" : "TO"}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,6 @@ const initialState: DataConnectionState = {
|
||||||
|
|
||||||
export const DataConnectionDesigner: React.FC = () => {
|
export const DataConnectionDesigner: React.FC = () => {
|
||||||
const [state, setState] = useState<DataConnectionState>(initialState);
|
const [state, setState] = useState<DataConnectionState>(initialState);
|
||||||
const { isMobile, isTablet } = useResponsive();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-background">
|
<div className="h-screen bg-background">
|
||||||
|
|
@ -41,7 +40,7 @@ export const DataConnectionDesigner: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-[calc(100vh-80px)]">
|
<div className="flex h-[calc(100vh-80px)]">
|
||||||
<div className="w-[30%] bg-white border-r border-border flex flex-col">
|
<div className="w-[30%] bg-background border-r border-border flex flex-col">
|
||||||
<ConnectionTypeSelector
|
<ConnectionTypeSelector
|
||||||
connectionType={state.connectionType}
|
connectionType={state.connectionType}
|
||||||
onConnectionTypeChange={(type) => setState(prev => ({ ...prev, connectionType: type }))}
|
onConnectionTypeChange={(type) => setState(prev => ({ ...prev, connectionType: type }))}
|
||||||
|
|
|
||||||
|
|
@ -1196,7 +1196,7 @@ function ModalSizeSettingsPanel({
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",
|
"inline-block h-3.5 w-3.5 rounded-full bg-background transition-transform",
|
||||||
usePerMode ? "translate-x-4.5" : "translate-x-0.5"
|
usePerMode ? "translate-x-4.5" : "translate-x-0.5"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -308,6 +308,253 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
[codeOptions],
|
[codeOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 데이터 로드 함수 (useEffect보다 먼저 선언해야 함 - TS2448 방지)
|
||||||
|
const loadData = useCallback(
|
||||||
|
async (page: number = 1, searchParams: Record<string, any> = {}) => {
|
||||||
|
if (!component.tableName) return;
|
||||||
|
|
||||||
|
// 프리뷰 모드에서는 샘플 데이터만 표시
|
||||||
|
if (isPreviewMode) {
|
||||||
|
const sampleData = Array.from({ length: 3 }, (_, i) => {
|
||||||
|
const sample: Record<string, any> = { id: i + 1 };
|
||||||
|
component.columns.forEach((col) => {
|
||||||
|
if (col.widgetType === "number") {
|
||||||
|
sample[col.columnName] = Math.floor(Math.random() * 1000);
|
||||||
|
} else if (col.widgetType === "boolean") {
|
||||||
|
sample[col.columnName] = i % 2 === 0 ? "Y" : "N";
|
||||||
|
} else {
|
||||||
|
sample[col.columnName] = `샘플 ${col.label} ${i + 1}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return sample;
|
||||||
|
});
|
||||||
|
setData(sampleData);
|
||||||
|
setTotal(3);
|
||||||
|
setTotalPages(1);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
// 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때)
|
||||||
|
let linkedFilterValues: Record<string, any> = {};
|
||||||
|
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
|
||||||
|
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
|
||||||
|
|
||||||
|
if (splitPanelContext) {
|
||||||
|
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
|
||||||
|
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
|
||||||
|
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
|
||||||
|
(filter) =>
|
||||||
|
filter.targetColumn?.startsWith(component.tableName + ".") || filter.targetColumn === component.tableName,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 좌측 데이터 선택 여부 확인
|
||||||
|
hasSelectedLeftData =
|
||||||
|
splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0;
|
||||||
|
|
||||||
|
linkedFilterValues = splitPanelContext.getLinkedFilterValues();
|
||||||
|
// 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
|
||||||
|
const tableSpecificFilters: Record<string, any> = {};
|
||||||
|
for (const [key, value] of Object.entries(linkedFilterValues)) {
|
||||||
|
// key가 "테이블명.컬럼명" 형식인 경우
|
||||||
|
if (key.includes(".")) {
|
||||||
|
const [tableName, columnName] = key.split(".");
|
||||||
|
if (tableName === component.tableName) {
|
||||||
|
tableSpecificFilters[columnName] = value;
|
||||||
|
hasLinkedFiltersConfigured = true; // 이 테이블에 대한 필터가 있음
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 테이블명 없이 컬럼명만 있는 경우 그대로 사용
|
||||||
|
tableSpecificFilters[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
linkedFilterValues = tableSpecificFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
|
||||||
|
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
||||||
|
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
|
||||||
|
console.log("⚠️ [InteractiveDataTable] 연결 필터 설정됨 but 좌측 데이터 미선택 → 빈 데이터 표시");
|
||||||
|
setData([]);
|
||||||
|
setTotal(0);
|
||||||
|
setTotalPages(0);
|
||||||
|
setCurrentPage(1);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🆕 RelatedDataButtons 필터 적용
|
||||||
|
const relatedButtonFilterValues: Record<string, any> = {};
|
||||||
|
if (relatedButtonFilter) {
|
||||||
|
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 검색 파라미터와 연결 필터 병합
|
||||||
|
const mergedSearchParams = {
|
||||||
|
...searchParams,
|
||||||
|
...linkedFilterValues,
|
||||||
|
...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentPageSize = component.pagination?.pageSize || 10;
|
||||||
|
|
||||||
|
console.log("🔍 데이터 조회 시작:", {
|
||||||
|
tableName: component.tableName,
|
||||||
|
page,
|
||||||
|
pageSize: currentPageSize,
|
||||||
|
linkedFilterValues,
|
||||||
|
relatedButtonFilterValues,
|
||||||
|
mergedSearchParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await tableTypeApi.getTableData(component.tableName, {
|
||||||
|
page,
|
||||||
|
size: currentPageSize,
|
||||||
|
search: mergedSearchParams,
|
||||||
|
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("✅ 데이터 조회 완료:", {
|
||||||
|
tableName: component.tableName,
|
||||||
|
dataLength: result.data.length,
|
||||||
|
total: result.total,
|
||||||
|
page: result.page,
|
||||||
|
});
|
||||||
|
|
||||||
|
setData(result.data);
|
||||||
|
setTotal(result.total);
|
||||||
|
setTotalPages(result.totalPages);
|
||||||
|
setCurrentPage(result.page);
|
||||||
|
|
||||||
|
// 카테고리 코드 패턴(CATEGORY_*) 검출 및 라벨 조회
|
||||||
|
const detectAndLoadCategoryLabels = async () => {
|
||||||
|
const categoryCodes = new Set<string>();
|
||||||
|
result.data.forEach((row: Record<string, any>) => {
|
||||||
|
Object.values(row).forEach((value) => {
|
||||||
|
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
|
||||||
|
categoryCodes.add(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 감지된 카테고리 코드:", Array.from(categoryCodes));
|
||||||
|
|
||||||
|
// 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외)
|
||||||
|
const newCodes = Array.from(categoryCodes);
|
||||||
|
|
||||||
|
if (newCodes.length > 0) {
|
||||||
|
try {
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes);
|
||||||
|
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data);
|
||||||
|
if (response.data.success && response.data.data) {
|
||||||
|
setCategoryCodeLabels((prev) => {
|
||||||
|
const newLabels = {
|
||||||
|
...prev,
|
||||||
|
...response.data.data,
|
||||||
|
};
|
||||||
|
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 캐시 업데이트:", newLabels);
|
||||||
|
return newLabels;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("카테고리 라벨 조회 실패:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
detectAndLoadCategoryLabels();
|
||||||
|
|
||||||
|
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
|
||||||
|
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
|
||||||
|
const primaryKeyField = Object.keys(rowData)[0];
|
||||||
|
const recordId = rowData[primaryKeyField];
|
||||||
|
|
||||||
|
if (!recordId) return { rowKey: recordId, statuses: {} };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileResponse = await getLinkedFiles(component.tableName, recordId);
|
||||||
|
const allFiles = fileResponse.files || [];
|
||||||
|
|
||||||
|
// 전체 행에 대한 파일 상태
|
||||||
|
const rowStatus = {
|
||||||
|
hasFiles: allFiles.length > 0,
|
||||||
|
fileCount: allFiles.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 가상 파일 컬럼별 파일 상태
|
||||||
|
const columnStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {};
|
||||||
|
|
||||||
|
// 가상 파일 컬럼 찾기
|
||||||
|
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
|
||||||
|
|
||||||
|
virtualFileColumns.forEach((column) => {
|
||||||
|
// 해당 컬럼의 파일만 필터링 (targetObjid로 수정)
|
||||||
|
let columnFiles = allFiles.filter((file: any) => file.targetObjid?.endsWith(`:${column.columnName}`));
|
||||||
|
|
||||||
|
// fallback: 컬럼명으로 찾지 못한 경우 모든 파일 컬럼 파일 포함
|
||||||
|
if (columnFiles.length === 0) {
|
||||||
|
columnFiles = allFiles.filter((file: any) =>
|
||||||
|
file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const columnKey = `${recordId}_${column.columnName}`;
|
||||||
|
columnStatuses[columnKey] = {
|
||||||
|
hasFiles: columnFiles.length > 0,
|
||||||
|
fileCount: columnFiles.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
rowKey: recordId,
|
||||||
|
statuses: {
|
||||||
|
[recordId]: rowStatus, // 전체 행 상태
|
||||||
|
...columnStatuses, // 컬럼별 상태
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// 에러 시 기본값
|
||||||
|
const defaultStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {
|
||||||
|
[recordId]: { hasFiles: false, fileCount: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
// 가상 파일 컬럼에 대해서도 기본값 설정
|
||||||
|
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
|
||||||
|
virtualFileColumns.forEach((column) => {
|
||||||
|
const columnKey = `${recordId}_${column.columnName}`;
|
||||||
|
defaultStatuses[columnKey] = { hasFiles: false, fileCount: 0 };
|
||||||
|
});
|
||||||
|
|
||||||
|
return { rowKey: recordId, statuses: defaultStatuses };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 파일 상태 업데이트
|
||||||
|
Promise.all(fileStatusPromises).then((results) => {
|
||||||
|
const statusMap: Record<string, { hasFiles: boolean; fileCount: number }> = {};
|
||||||
|
|
||||||
|
results.forEach((result) => {
|
||||||
|
Object.assign(statusMap, result.statuses);
|
||||||
|
});
|
||||||
|
|
||||||
|
setFileStatusMap(statusMap);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// console.error("❌ 테이블 데이터 조회 실패:", error);
|
||||||
|
setData([]);
|
||||||
|
setTotal(0);
|
||||||
|
setTotalPages(1);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[component.tableName, component.pagination?.pageSize, component.autoFilter, splitPanelContext?.selectedLeftData, relatedButtonFilter],
|
||||||
|
);
|
||||||
|
|
||||||
// 🆕 전역 테이블 새로고침 이벤트 리스너
|
// 🆕 전역 테이블 새로고침 이벤트 리스너
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleRefreshTable = () => {
|
const handleRefreshTable = () => {
|
||||||
|
|
@ -695,251 +942,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
}
|
}
|
||||||
}, [visibleColumns]);
|
}, [visibleColumns]);
|
||||||
|
|
||||||
// 데이터 로드 함수
|
|
||||||
const loadData = useCallback(
|
|
||||||
async (page: number = 1, searchParams: Record<string, any> = {}) => {
|
|
||||||
if (!component.tableName) return;
|
|
||||||
|
|
||||||
// 프리뷰 모드에서는 샘플 데이터만 표시
|
|
||||||
if (isPreviewMode) {
|
|
||||||
const sampleData = Array.from({ length: 3 }, (_, i) => {
|
|
||||||
const sample: Record<string, any> = { id: i + 1 };
|
|
||||||
component.columns.forEach((col) => {
|
|
||||||
if (col.widgetType === "number") {
|
|
||||||
sample[col.columnName] = Math.floor(Math.random() * 1000);
|
|
||||||
} else if (col.widgetType === "boolean") {
|
|
||||||
sample[col.columnName] = i % 2 === 0 ? "Y" : "N";
|
|
||||||
} else {
|
|
||||||
sample[col.columnName] = `샘플 ${col.label} ${i + 1}`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return sample;
|
|
||||||
});
|
|
||||||
setData(sampleData);
|
|
||||||
setTotal(3);
|
|
||||||
setTotalPages(1);
|
|
||||||
setCurrentPage(1);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
try {
|
|
||||||
// 🆕 연결 필터 값 가져오기 (분할 패널 내부일 때)
|
|
||||||
let linkedFilterValues: Record<string, any> = {};
|
|
||||||
let hasLinkedFiltersConfigured = false; // 연결 필터가 설정되어 있는지 여부
|
|
||||||
let hasSelectedLeftData = false; // 좌측에서 데이터가 선택되었는지 여부
|
|
||||||
|
|
||||||
if (splitPanelContext) {
|
|
||||||
// 연결 필터 설정 여부 확인 (현재 테이블에 해당하는 필터가 있는지)
|
|
||||||
const linkedFiltersConfig = splitPanelContext.linkedFilters || [];
|
|
||||||
hasLinkedFiltersConfigured = linkedFiltersConfig.some(
|
|
||||||
(filter) =>
|
|
||||||
filter.targetColumn?.startsWith(component.tableName + ".") || filter.targetColumn === component.tableName,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 좌측 데이터 선택 여부 확인
|
|
||||||
hasSelectedLeftData =
|
|
||||||
splitPanelContext.selectedLeftData && Object.keys(splitPanelContext.selectedLeftData).length > 0;
|
|
||||||
|
|
||||||
linkedFilterValues = splitPanelContext.getLinkedFilterValues();
|
|
||||||
// 현재 테이블에 해당하는 필터만 추출 (테이블명.컬럼명 형식에서)
|
|
||||||
const tableSpecificFilters: Record<string, any> = {};
|
|
||||||
for (const [key, value] of Object.entries(linkedFilterValues)) {
|
|
||||||
// key가 "테이블명.컬럼명" 형식인 경우
|
|
||||||
if (key.includes(".")) {
|
|
||||||
const [tableName, columnName] = key.split(".");
|
|
||||||
if (tableName === component.tableName) {
|
|
||||||
tableSpecificFilters[columnName] = value;
|
|
||||||
hasLinkedFiltersConfigured = true; // 이 테이블에 대한 필터가 있음
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 테이블명 없이 컬럼명만 있는 경우 그대로 사용
|
|
||||||
tableSpecificFilters[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
linkedFilterValues = tableSpecificFilters;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 연결 필터가 설정되어 있지만 좌측에서 데이터가 선택되지 않은 경우
|
|
||||||
// → 빈 데이터 표시 (모든 데이터를 보여주지 않음)
|
|
||||||
if (hasLinkedFiltersConfigured && !hasSelectedLeftData) {
|
|
||||||
console.log("⚠️ [InteractiveDataTable] 연결 필터 설정됨 but 좌측 데이터 미선택 → 빈 데이터 표시");
|
|
||||||
setData([]);
|
|
||||||
setTotal(0);
|
|
||||||
setTotalPages(0);
|
|
||||||
setCurrentPage(1);
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🆕 RelatedDataButtons 필터 적용
|
|
||||||
const relatedButtonFilterValues: Record<string, any> = {};
|
|
||||||
if (relatedButtonFilter) {
|
|
||||||
relatedButtonFilterValues[relatedButtonFilter.filterColumn] = relatedButtonFilter.filterValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 검색 파라미터와 연결 필터 병합
|
|
||||||
const mergedSearchParams = {
|
|
||||||
...searchParams,
|
|
||||||
...linkedFilterValues,
|
|
||||||
...relatedButtonFilterValues, // 🆕 RelatedDataButtons 필터 추가
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("🔍 데이터 조회 시작:", {
|
|
||||||
tableName: component.tableName,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
linkedFilterValues,
|
|
||||||
relatedButtonFilterValues,
|
|
||||||
mergedSearchParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await tableTypeApi.getTableData(component.tableName, {
|
|
||||||
page,
|
|
||||||
size: pageSize,
|
|
||||||
search: mergedSearchParams,
|
|
||||||
autoFilter: component.autoFilter, // 🆕 자동 필터 설정 전달
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ 데이터 조회 완료:", {
|
|
||||||
tableName: component.tableName,
|
|
||||||
dataLength: result.data.length,
|
|
||||||
total: result.total,
|
|
||||||
page: result.page,
|
|
||||||
});
|
|
||||||
|
|
||||||
setData(result.data);
|
|
||||||
setTotal(result.total);
|
|
||||||
setTotalPages(result.totalPages);
|
|
||||||
setCurrentPage(result.page);
|
|
||||||
|
|
||||||
// 카테고리 코드 패턴(CATEGORY_*) 검출 및 라벨 조회
|
|
||||||
const detectAndLoadCategoryLabels = async () => {
|
|
||||||
const categoryCodes = new Set<string>();
|
|
||||||
result.data.forEach((row: Record<string, any>) => {
|
|
||||||
Object.values(row).forEach((value) => {
|
|
||||||
if (typeof value === "string" && value.startsWith("CATEGORY_")) {
|
|
||||||
categoryCodes.add(value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("🏷️ [InteractiveDataTable] 감지된 카테고리 코드:", Array.from(categoryCodes));
|
|
||||||
|
|
||||||
// 새로운 카테고리 코드만 필터링 (이미 캐시된 것 제외)
|
|
||||||
const newCodes = Array.from(categoryCodes);
|
|
||||||
|
|
||||||
if (newCodes.length > 0) {
|
|
||||||
try {
|
|
||||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 호출:", newCodes);
|
|
||||||
const response = await apiClient.post("/table-categories/labels-by-codes", { valueCodes: newCodes });
|
|
||||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 API 응답:", response.data);
|
|
||||||
if (response.data.success && response.data.data) {
|
|
||||||
setCategoryCodeLabels((prev) => {
|
|
||||||
const newLabels = {
|
|
||||||
...prev,
|
|
||||||
...response.data.data,
|
|
||||||
};
|
|
||||||
console.log("🏷️ [InteractiveDataTable] 카테고리 라벨 캐시 업데이트:", newLabels);
|
|
||||||
return newLabels;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("카테고리 라벨 조회 실패:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
detectAndLoadCategoryLabels();
|
|
||||||
|
|
||||||
// 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별)
|
|
||||||
const fileStatusPromises = result.data.map(async (rowData: Record<string, any>) => {
|
|
||||||
const primaryKeyField = Object.keys(rowData)[0];
|
|
||||||
const recordId = rowData[primaryKeyField];
|
|
||||||
|
|
||||||
if (!recordId) return { rowKey: recordId, statuses: {} };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const fileResponse = await getLinkedFiles(component.tableName, recordId);
|
|
||||||
const allFiles = fileResponse.files || [];
|
|
||||||
|
|
||||||
// 전체 행에 대한 파일 상태
|
|
||||||
const rowStatus = {
|
|
||||||
hasFiles: allFiles.length > 0,
|
|
||||||
fileCount: allFiles.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 가상 파일 컬럼별 파일 상태
|
|
||||||
const columnStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {};
|
|
||||||
|
|
||||||
// 가상 파일 컬럼 찾기
|
|
||||||
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
|
|
||||||
|
|
||||||
virtualFileColumns.forEach((column) => {
|
|
||||||
// 해당 컬럼의 파일만 필터링 (targetObjid로 수정)
|
|
||||||
let columnFiles = allFiles.filter((file: any) => file.targetObjid?.endsWith(`:${column.columnName}`));
|
|
||||||
|
|
||||||
// fallback: 컬럼명으로 찾지 못한 경우 모든 파일 컬럼 파일 포함
|
|
||||||
if (columnFiles.length === 0) {
|
|
||||||
columnFiles = allFiles.filter((file: any) =>
|
|
||||||
file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const columnKey = `${recordId}_${column.columnName}`;
|
|
||||||
columnStatuses[columnKey] = {
|
|
||||||
hasFiles: columnFiles.length > 0,
|
|
||||||
fileCount: columnFiles.length,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
rowKey: recordId,
|
|
||||||
statuses: {
|
|
||||||
[recordId]: rowStatus, // 전체 행 상태
|
|
||||||
...columnStatuses, // 컬럼별 상태
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
// 에러 시 기본값
|
|
||||||
const defaultStatuses: Record<string, { hasFiles: boolean; fileCount: number }> = {
|
|
||||||
[recordId]: { hasFiles: false, fileCount: 0 },
|
|
||||||
};
|
|
||||||
|
|
||||||
// 가상 파일 컬럼에 대해서도 기본값 설정
|
|
||||||
const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn);
|
|
||||||
virtualFileColumns.forEach((column) => {
|
|
||||||
const columnKey = `${recordId}_${column.columnName}`;
|
|
||||||
defaultStatuses[columnKey] = { hasFiles: false, fileCount: 0 };
|
|
||||||
});
|
|
||||||
|
|
||||||
return { rowKey: recordId, statuses: defaultStatuses };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 파일 상태 업데이트
|
|
||||||
Promise.all(fileStatusPromises).then((results) => {
|
|
||||||
const statusMap: Record<string, { hasFiles: boolean; fileCount: number }> = {};
|
|
||||||
|
|
||||||
results.forEach((result) => {
|
|
||||||
Object.assign(statusMap, result.statuses);
|
|
||||||
});
|
|
||||||
|
|
||||||
setFileStatusMap(statusMap);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
// console.error("❌ 테이블 데이터 조회 실패:", error);
|
|
||||||
setData([]);
|
|
||||||
setTotal(0);
|
|
||||||
setTotalPages(1);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[component.tableName, pageSize, component.autoFilter, splitPanelContext?.selectedLeftData, relatedButtonFilter], // 🆕 autoFilter, 연결필터, RelatedDataButtons 필터 추가
|
|
||||||
);
|
|
||||||
|
|
||||||
// 현재 사용자 정보 로드
|
// 현재 사용자 정보 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCurrentUser = async () => {
|
const fetchCurrentUser = async () => {
|
||||||
|
|
@ -2681,7 +2683,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
className="h-32 text-center"
|
className="h-32 text-center"
|
||||||
>
|
>
|
||||||
<div className="text-muted-foreground flex flex-col items-center gap-2">
|
<div className="text-muted-foreground flex flex-col items-center gap-2">
|
||||||
<Database className="h-6 w-6" />
|
<Database className="h-8 w-8 opacity-40" />
|
||||||
<p>검색 결과가 없습니다</p>
|
<p>검색 결과가 없습니다</p>
|
||||||
<p className="text-xs">검색 조건을 변경하거나 새로고침을 시도해보세요</p>
|
<p className="text-xs">검색 조건을 변경하거나 새로고침을 시도해보세요</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -2759,7 +2761,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 items-center justify-center">
|
||||||
<div className="text-muted-foreground flex flex-col items-center gap-2">
|
<div className="text-muted-foreground flex flex-col items-center gap-2">
|
||||||
<Database className="h-6 w-6" />
|
<Database className="h-8 w-8 opacity-40" />
|
||||||
<p className="text-sm">표시할 컬럼이 없습니다</p>
|
<p className="text-sm">표시할 컬럼이 없습니다</p>
|
||||||
<p className="text-xs">테이블 설정에서 컬럼을 추가해주세요</p>
|
<p className="text-xs">테이블 설정에서 컬럼을 추가해주세요</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -231,17 +231,17 @@ export function ComponentsPanel({
|
||||||
const getCategoryColor = (category: string) => {
|
const getCategoryColor = (category: string) => {
|
||||||
switch (category) {
|
switch (category) {
|
||||||
case "data":
|
case "data":
|
||||||
return "from-primary/50/10 to-primary/10 text-primary group-hover:from-primary/50/20 group-hover:to-primary/20";
|
return "bg-primary/10 text-primary group-hover:bg-primary/20";
|
||||||
case "display":
|
case "display":
|
||||||
return "from-emerald-500/10 to-emerald-600/10 text-emerald-600 group-hover:from-emerald-500/20 group-hover:to-emerald-600/20";
|
return "bg-emerald-500/10 text-emerald-600 group-hover:bg-emerald-500/20";
|
||||||
case "input":
|
case "input":
|
||||||
return "from-violet-500/10 to-violet-600/10 text-violet-600 group-hover:from-violet-500/20 group-hover:to-violet-600/20";
|
return "bg-violet-500/10 text-violet-600 group-hover:bg-violet-500/20";
|
||||||
case "layout":
|
case "layout":
|
||||||
return "from-amber-500/10 to-amber-600/10 text-amber-600 group-hover:from-amber-500/20 group-hover:to-amber-600/20";
|
return "bg-amber-500/10 text-amber-600 group-hover:bg-amber-500/20";
|
||||||
case "action":
|
case "action":
|
||||||
return "from-rose-500/10 to-rose-600/10 text-rose-600 group-hover:from-rose-500/20 group-hover:to-rose-600/20";
|
return "bg-rose-500/10 text-rose-600 group-hover:bg-rose-500/20";
|
||||||
default:
|
default:
|
||||||
return "from-slate-500/10 to-slate-600/10 text-slate-600 group-hover:from-slate-500/20 group-hover:to-slate-600/20";
|
return "bg-muted text-muted-foreground group-hover:bg-muted/80";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -263,7 +263,7 @@ export function ComponentsPanel({
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<div
|
<div
|
||||||
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-gradient-to-br transition-all duration-200 ${getCategoryColor(component.category)}`}
|
className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-md transition-all duration-200 ${getCategoryColor(component.category)}`}
|
||||||
>
|
>
|
||||||
{getCategoryIcon(component.category)}
|
{getCategoryIcon(component.category)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -813,7 +813,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
|
|
||||||
if (!selectedComponent) {
|
if (!selectedComponent) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col border-r border-border/60 bg-gradient-to-br from-slate-50 to-orange-50/30 shadow-sm">
|
<div className="flex h-full flex-col border-r border-border/60 bg-background shadow-sm">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -1122,7 +1122,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col border-r border-border/60 bg-gradient-to-br from-slate-50 to-orange-50/30 shadow-sm">
|
<div className="flex h-full flex-col border-r border-border/60 bg-background shadow-sm">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ export default function LayoutsPanel({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`layouts-panel h-full bg-gradient-to-br from-slate-50 to-indigo-50/30 border-r border-border/60 shadow-sm ${className || ""}`}>
|
<div className={`layouts-panel h-full bg-background border-r border-border/60 shadow-sm ${className || ""}`}>
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="border-b p-4">
|
<div className="border-b p-4">
|
||||||
|
|
|
||||||
|
|
@ -487,7 +487,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col bg-slate-50 p-6 border-r border-border/60 shadow-sm">
|
<div className="flex h-full flex-col bg-background p-6 border-r border-border/60 shadow-sm">
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h2 className="text-lg font-semibold text-foreground mb-1">템플릿</h2>
|
<h2 className="text-lg font-semibold text-foreground mb-1">템플릿</h2>
|
||||||
|
|
@ -570,7 +570,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
|
||||||
className="group cursor-grab rounded-lg border border-border/40 bg-white/90 backdrop-blur-sm p-6 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-blue-500/15 hover:scale-[1.02] hover:border-primary/40/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0"
|
className="group cursor-grab rounded-lg border border-border/40 bg-white/90 backdrop-blur-sm p-6 shadow-sm transition-all duration-300 hover:bg-white hover:shadow-lg hover:shadow-blue-500/15 hover:scale-[1.02] hover:border-primary/40/60 hover:-translate-y-1 active:cursor-grabbing active:scale-[0.98] active:translate-y-0"
|
||||||
>
|
>
|
||||||
<div className="flex items-start space-x-4">
|
<div className="flex items-start space-x-4">
|
||||||
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-gradient-to-br from-primary/10 to-primary/5 text-primary shadow-md group-hover:shadow-lg group-hover:scale-110 transition-all duration-300">
|
<div className="flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10 text-primary group-hover:bg-primary/15 group-hover:scale-110 transition-all duration-300">
|
||||||
{template.icon}
|
{template.icon}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|
@ -583,11 +583,11 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
|
||||||
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed mb-3">{template.description}</p>
|
<p className="text-xs text-muted-foreground line-clamp-2 leading-relaxed mb-3">{template.description}</p>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-2 text-xs text-muted-foreground/70">
|
<div className="flex items-center space-x-2 text-xs text-muted-foreground/70">
|
||||||
<span className="bg-gradient-to-r from-primary/10 to-primary/5 px-3 py-1 rounded-full font-medium text-primary shadow-sm">
|
<span className="bg-primary/10 px-3 py-1 rounded-full font-medium text-primary">
|
||||||
{template.defaultSize.width}×{template.defaultSize.height}
|
{template.defaultSize.width}×{template.defaultSize.height}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium text-primary capitalize bg-gradient-to-r from-primary/5 to-indigo-50 px-3 py-1 rounded-full border border-primary/20/50">
|
<span className="text-xs font-medium text-primary capitalize bg-primary/5 px-3 py-1 rounded-full border border-primary/20">
|
||||||
{template.category}
|
{template.category}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -599,7 +599,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 도움말 */}
|
{/* 도움말 */}
|
||||||
<div className="rounded-xl bg-gradient-to-r from-primary/5 to-indigo-50 border border-primary/10/60 p-4 mt-6">
|
<div className="rounded-lg bg-primary/5 border border-primary/10 p-4 mt-6">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/20 text-primary">
|
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/20 text-primary">
|
||||||
<Info className="h-4 w-4" />
|
<Info className="h-4 w-4" />
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
|
||||||
}, [component.id]);
|
}, [component.id]);
|
||||||
|
|
||||||
const { fileConfig = {} } = component;
|
const { fileConfig = {} } = component;
|
||||||
const { user: authUser, isLoading, isLoggedIn } = useAuth(); // 인증 상태도 함께 가져오기
|
const { user: authUser, loading: isLoading, isLoggedIn } = useAuth(); // 인증 상태도 함께 가져오기
|
||||||
|
|
||||||
// props로 받은 userInfo를 우선 사용, 없으면 useAuth에서 가져온 user 사용
|
// props로 받은 userInfo를 우선 사용, 없으면 useAuth에서 가져온 user 사용
|
||||||
const user = userInfo || authUser;
|
const user = userInfo || authUser;
|
||||||
|
|
|
||||||
|
|
@ -62,9 +62,9 @@ const statusLabels: Record<string, string> = {
|
||||||
// 상태 색상
|
// 상태 색상
|
||||||
const statusColors: Record<string, string> = {
|
const statusColors: Record<string, string> = {
|
||||||
draft: "bg-muted text-foreground",
|
draft: "bg-muted text-foreground",
|
||||||
issued: "bg-emerald-100 text-emerald-800",
|
issued: "bg-success/10 text-success",
|
||||||
sent: "bg-primary/10 text-primary",
|
sent: "bg-primary/10 text-primary",
|
||||||
cancelled: "bg-destructive/10 text-red-800",
|
cancelled: "bg-destructive/10 text-destructive",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function TaxInvoiceDetail({ open, onClose, invoiceId }: TaxInvoiceDetailProps) {
|
export function TaxInvoiceDetail({ open, onClose, invoiceId }: TaxInvoiceDetailProps) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
#!/bin/bash
|
||||||
|
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$HOME/.nvm/versions/node/$(ls $HOME/.nvm/versions/node/ 2>/dev/null | tail -1)/bin"
|
||||||
|
|
||||||
|
cd /Users/gbpark/ERP-node
|
||||||
|
|
||||||
|
# Node 경로 찾기
|
||||||
|
NODE_BIN=""
|
||||||
|
if command -v node &>/dev/null; then
|
||||||
|
NODE_BIN=$(command -v node)
|
||||||
|
elif [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
source "$HOME/.nvm/nvm.sh"
|
||||||
|
NODE_BIN=$(command -v node)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$NODE_BIN" ]; then
|
||||||
|
echo "BROWSER_TEST_RESULT: FAIL - node not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using node: $NODE_BIN"
|
||||||
|
|
||||||
|
# playwright가 루트 node_modules에 있으므로 그걸로 실행
|
||||||
|
PLAYWRIGHT_BIN="/Users/gbpark/ERP-node/node_modules/.bin/playwright"
|
||||||
|
|
||||||
|
if [ -f "$PLAYWRIGHT_BIN" ]; then
|
||||||
|
echo "playwright binary found: $PLAYWRIGHT_BIN"
|
||||||
|
"$PLAYWRIGHT_BIN" test .agent-pipeline/browser-tests/e2e-test.spec.ts \
|
||||||
|
--config=.agent-pipeline/browser-tests/playwright.config.ts \
|
||||||
|
--reporter=line
|
||||||
|
EXIT=$?
|
||||||
|
if [ $EXIT -eq 0 ]; then
|
||||||
|
echo "BROWSER_TEST_RESULT: PASS"
|
||||||
|
else
|
||||||
|
echo "BROWSER_TEST_RESULT: FAIL - test failed with exit code $EXIT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "playwright binary not found, falling back to mjs runner"
|
||||||
|
$NODE_BIN /Users/gbpark/ERP-node/run-e2e-runtime-test.mjs
|
||||||
|
fi
|
||||||
Loading…
Reference in New Issue