[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: "정비중",
|
||||
|
||||
// 기사 관련 (존중하는 표현)
|
||||
waiting: "대기중",
|
||||
resting: "휴식중",
|
||||
unavailable: "운행불가",
|
||||
|
||||
// 기사 평가
|
||||
excellent: "우수",
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
<div
|
||||
className={`rounded-lg p-2 ${
|
||||
index === 0
|
||||
? "border-l-4 border-emerald-400 bg-emerald-50"
|
||||
? "border-l-4 border-success/60 bg-success/10"
|
||||
: index === 1
|
||||
? "border-l-4 border-primary/60 bg-accent"
|
||||
: "bg-muted"
|
||||
|
|
@ -73,7 +73,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
<div className="mb-1 flex items-center justify-between">
|
||||
<div
|
||||
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}
|
||||
|
|
@ -81,7 +81,7 @@ export const SelectedTablesPanel: React.FC<SelectedTablesPanelProps> = ({
|
|||
{selectedNodes.length === 2 && (
|
||||
<div
|
||||
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"}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ const initialState: DataConnectionState = {
|
|||
|
||||
export const DataConnectionDesigner: React.FC = () => {
|
||||
const [state, setState] = useState<DataConnectionState>(initialState);
|
||||
const { isMobile, isTablet } = useResponsive();
|
||||
|
||||
return (
|
||||
<div className="h-screen bg-background">
|
||||
|
|
@ -41,7 +40,7 @@ export const DataConnectionDesigner: React.FC = () => {
|
|||
</div>
|
||||
|
||||
<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
|
||||
connectionType={state.connectionType}
|
||||
onConnectionTypeChange={(type) => setState(prev => ({ ...prev, connectionType: type }))}
|
||||
|
|
|
|||
|
|
@ -1196,7 +1196,7 @@ function ModalSizeSettingsPanel({
|
|||
>
|
||||
<span
|
||||
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"
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -308,6 +308,253 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
[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(() => {
|
||||
const handleRefreshTable = () => {
|
||||
|
|
@ -695,251 +942,6 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
}
|
||||
}, [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(() => {
|
||||
const fetchCurrentUser = async () => {
|
||||
|
|
@ -2681,7 +2683,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
className="h-32 text-center"
|
||||
>
|
||||
<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 className="text-xs">검색 조건을 변경하거나 새로고침을 시도해보세요</p>
|
||||
</div>
|
||||
|
|
@ -2759,7 +2761,7 @@ export const InteractiveDataTable: React.FC<InteractiveDataTableProps> = ({
|
|||
) : (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<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-xs">테이블 설정에서 컬럼을 추가해주세요</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -231,17 +231,17 @@ export function ComponentsPanel({
|
|||
const getCategoryColor = (category: string) => {
|
||||
switch (category) {
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
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:
|
||||
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 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)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -813,7 +813,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
|
||||
if (!selectedComponent) {
|
||||
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="mb-6">
|
||||
|
|
@ -1122,7 +1122,7 @@ export const DetailSettingsPanel: React.FC<DetailSettingsPanelProps> = ({
|
|||
};
|
||||
|
||||
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="mb-6">
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ export default function LayoutsPanel({
|
|||
};
|
||||
|
||||
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="border-b p-4">
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
|
|||
});
|
||||
|
||||
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">
|
||||
<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"
|
||||
>
|
||||
<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}
|
||||
</div>
|
||||
<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>
|
||||
<div className="flex items-center justify-between">
|
||||
<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}
|
||||
</span>
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -599,7 +599,7 @@ export const TemplatesPanel: React.FC<TemplatesPanelProps> = ({ onDragStart }) =
|
|||
</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 h-8 w-8 items-center justify-center rounded-lg bg-primary/20 text-primary">
|
||||
<Info className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export function FileUpload({ component, onUpdateComponent, onFileUpload, userInf
|
|||
}, [component.id]);
|
||||
|
||||
const { fileConfig = {} } = component;
|
||||
const { user: authUser, isLoading, isLoggedIn } = useAuth(); // 인증 상태도 함께 가져오기
|
||||
const { user: authUser, loading: isLoading, isLoggedIn } = useAuth(); // 인증 상태도 함께 가져오기
|
||||
|
||||
// props로 받은 userInfo를 우선 사용, 없으면 useAuth에서 가져온 user 사용
|
||||
const user = userInfo || authUser;
|
||||
|
|
|
|||
|
|
@ -62,9 +62,9 @@ const statusLabels: Record<string, string> = {
|
|||
// 상태 색상
|
||||
const statusColors: Record<string, string> = {
|
||||
draft: "bg-muted text-foreground",
|
||||
issued: "bg-emerald-100 text-emerald-800",
|
||||
issued: "bg-success/10 text-success",
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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