[agent-pipeline] pipe-20260309055714-23ry round-3

This commit is contained in:
DDD1542 2026-03-09 17:18:45 +09:00
parent 790592ec76
commit e4de414dfb
12 changed files with 311 additions and 273 deletions

View File

@ -53,9 +53,7 @@ const statusTranslations: { [key: string]: string } = {
maintenance: "정비중",
// 기사 관련 (존중하는 표현)
waiting: "대기중",
resting: "휴식중",
unavailable: "운행불가",
// 기사 평가
excellent: "우수",

View File

@ -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"}

View File

@ -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 }))}

View File

@ -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"
)}
/>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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">

View File

@ -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" />

View File

@ -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;

View File

@ -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) {

39
run-e2e-new-spec.sh Normal file
View File

@ -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