Compare commits

...

10 Commits

Author SHA1 Message Date
SeongHyun Kim 660f81edbc fix: table-list 컴포넌트 크기 조절 및 동기화 문제 해결
- 기본 너비 120px → 1000px 변경
- 선택 영역과 컴포넌트 영역 크기 동기화
- 편집 패널에서 너비/높이 조절 시 즉시 반영되도록 개선
2025-11-17 10:01:09 +09:00
SeongHyun Kim e2a4df575c Merge branch 'ksh' into main - 거래처별 품목정보 컴포넌트 추가 2025-11-14 17:41:03 +09:00
hyeonsu 0c1292c55b Merge pull request '대시보드 수정사항 적용' (#207) from common/feat/dashboard-map into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/207
2025-11-14 12:11:07 +09:00
dohyeons 02d4a3a3d3 리스크 알림 위젯 날짜 포맷 변경 2025-11-14 12:10:10 +09:00
dohyeons 05273daa92 커스텀 통계 카드 위젯을 수정 2025-11-14 11:35:16 +09:00
dohyeons 2eb8c3a61b 리스트 카드 렌더링 문제 해결 2025-11-14 11:16:03 +09:00
dohyeons a491f08337 지도 위젯에서도 마커 종류 선택 가능하게 구현 2025-11-14 10:49:11 +09:00
dohyeons a3503c0b9f 지도 위젯 별 polling 설정 구현 2025-11-14 10:26:09 +09:00
dohyeons b3e217c1de 빌드 에러 해결 2025-11-13 18:09:54 +09:00
dohyeons 50410475c0 배포 오류 해결 2025-11-13 18:06:11 +09:00
18 changed files with 321 additions and 192 deletions

View File

@ -17,6 +17,7 @@ import {
} from "../types/screen";
import { generateId } from "../utils/generateId";
import logger from "../utils/logger";
// 화면 복사 요청 인터페이스
interface CopyScreenRequest {
@ -24,7 +25,7 @@ interface CopyScreenRequest {
screenCode: string;
description?: string;
companyCode: string; // 요청한 사용자의 회사 코드 (인증용)
userId: string;
createdBy?: string; // 생성자 ID
targetCompanyCode?: string; // 복사 대상 회사 코드 (최고 관리자 전용)
}
@ -2153,9 +2154,9 @@ export class ScreenManagementService {
targetCompanyCode, // 대상 회사 코드 사용
sourceScreen.table_name,
sourceScreen.is_active,
copyData.userId,
copyData.createdBy,
new Date(),
copyData.userId,
copyData.createdBy,
new Date(),
]
);
@ -2267,7 +2268,7 @@ export class ScreenManagementService {
screenCode: data.mainScreen.screenCode,
description: data.mainScreen.description || "",
companyCode: data.companyCode,
userId: data.userId,
createdBy: data.userId,
targetCompanyCode: data.targetCompanyCode, // 대상 회사 코드 전달
});
@ -2283,7 +2284,7 @@ export class ScreenManagementService {
screenCode: modalData.screenCode,
description: "",
companyCode: data.companyCode,
userId: data.userId,
createdBy: data.userId,
targetCompanyCode: data.targetCompanyCode, // 대상 회사 코드 전달
});

View File

@ -26,16 +26,31 @@ export interface DataFilterConfig {
*/
export function buildDataFilterWhereClause(
dataFilter: DataFilterConfig | undefined,
tableAlias?: string,
tableAliasOrStartIndex?: string | number,
startParamIndex: number = 1
): { whereClause: string; params: any[] } {
if (!dataFilter || !dataFilter.enabled || !dataFilter.filters || dataFilter.filters.length === 0) {
return { whereClause: "", params: [] };
}
// 파라미터 처리: 첫 번째 파라미터가 숫자면 startParamIndex, 문자열이면 tableAlias
let tableAlias: string | undefined;
let actualStartIndex: number;
if (typeof tableAliasOrStartIndex === "number") {
actualStartIndex = tableAliasOrStartIndex;
tableAlias = undefined;
} else if (typeof tableAliasOrStartIndex === "string") {
tableAlias = tableAliasOrStartIndex;
actualStartIndex = startParamIndex;
} else {
actualStartIndex = startParamIndex;
tableAlias = undefined;
}
const conditions: string[] = [];
const params: any[] = [];
let paramIndex = startParamIndex;
let paramIndex = actualStartIndex;
// 테이블 별칭이 있으면 "alias."를 붙이고, 없으면 그냥 컬럼명만
const getColumnRef = (colName: string) => {

View File

@ -17,6 +17,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { DatabaseConfig } from "./data-sources/DatabaseConfig";
import { ApiConfig } from "./data-sources/ApiConfig";
import { QueryEditor } from "./QueryEditor";
@ -146,6 +147,9 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
// 커스텀 메트릭 설정
const [customMetricConfig, setCustomMetricConfig] = useState<CustomMetricConfig>({});
// 자동 새로고침 간격 (지도 위젯용)
const [refreshInterval, setRefreshInterval] = useState<number>(5);
// 사이드바 열릴 때 초기화
useEffect(() => {
if (isOpen && element) {
@ -155,6 +159,8 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
// dataSources는 element.dataSources 또는 chartConfig.dataSources에서 가져옴
setDataSources(element.dataSources || element.chartConfig?.dataSources || []);
setQueryResult(null);
// 자동 새로고침 간격 초기화
setRefreshInterval(element.chartConfig?.refreshInterval ?? 5);
// 리스트 위젯 설정 초기화
if (element.subtype === "list-v2" && element.listConfig) {
@ -290,6 +296,24 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
element.subtype === "custom-metric-v2" ||
element.subtype === "risk-alert-v2";
// chartConfig 구성 (위젯 타입별로 다르게 처리)
let finalChartConfig = { ...chartConfig };
if (isMultiDataSourceWidget) {
finalChartConfig = {
...finalChartConfig,
dataSources: dataSources,
};
}
// 지도 위젯인 경우 refreshInterval 추가
if (element.subtype === "map-summary-v2") {
finalChartConfig = {
...finalChartConfig,
refreshInterval,
};
}
const updatedElement: DashboardElement = {
...element,
customTitle: customTitle.trim() || undefined,
@ -302,8 +326,6 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
...(isMultiDataSourceWidget
? {
dataSources: dataSources,
// chartConfig에도 dataSources 포함 (일부 위젯은 chartConfig에서 읽음)
chartConfig: { ...chartConfig, dataSources: dataSources },
}
: {}),
}
@ -314,21 +336,10 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
listConfig,
}
: {}),
// 차트 설정 (차트 타입이거나 차트 기능이 있는 위젯)
...(element.type === "chart" ||
element.subtype === "chart" ||
["bar", "horizontal-bar", "pie", "line", "area", "stacked-bar", "donut", "combo"].includes(element.subtype)
// 차트 설정 (모든 위젯 공통)
...(needsDataSource(element.subtype)
? {
// 다중 데이터 소스 위젯은 chartConfig에 dataSources 포함 (빈 배열도 허용 - 연결 해제)
chartConfig: isMultiDataSourceWidget
? { ...chartConfig, dataSources: dataSources }
: chartConfig,
// 프론트엔드 호환성을 위해 dataSources도 element에 직접 포함 (빈 배열도 허용 - 연결 해제)
...(isMultiDataSourceWidget
? {
dataSources: dataSources,
}
: {}),
chartConfig: finalChartConfig,
}
: {}),
// 커스텀 메트릭 설정
@ -341,6 +352,10 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
console.log("🔧 [WidgetConfigSidebar] handleApply 호출:", {
subtype: element.subtype,
isMultiDataSourceWidget,
dataSources,
listConfig,
finalChartConfig,
customMetricConfig,
updatedElement,
});
@ -356,6 +371,7 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
listConfig,
chartConfig,
customMetricConfig,
refreshInterval,
onApply,
onClose,
]);
@ -432,6 +448,40 @@ export function WidgetConfigSidebar({ element, isOpen, onClose, onApply }: Widge
<Switch id="show-header" checked={showHeader} onCheckedChange={setShowHeader} />
</div>
</div>
{/* 자동 새로고침 설정 (지도 위젯 전용) */}
{element.subtype === "map-summary-v2" && (
<div className="bg-background rounded-lg p-3 shadow-sm">
<Label htmlFor="refresh-interval" className="mb-2 block text-xs font-semibold">
</Label>
<Select value={refreshInterval.toString()} onValueChange={(value) => setRefreshInterval(parseInt(value))}>
<SelectTrigger id="refresh-interval" className="h-9 text-sm">
<SelectValue placeholder="간격 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0" className="text-sm">
</SelectItem>
<SelectItem value="5" className="text-sm">
5
</SelectItem>
<SelectItem value="10" className="text-sm">
10
</SelectItem>
<SelectItem value="30" className="text-sm">
30
</SelectItem>
<SelectItem value="60" className="text-sm">
1
</SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground mt-1.5 text-xs">
</p>
</div>
)}
</div>
</TabsContent>

View File

@ -530,30 +530,6 @@ export default function MultiApiConfig({ dataSource, onChange, onTestResult }: M
))}
</div>
{/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */}
<div className="space-y-2">
<Label htmlFor="marker-refresh-interval" className="text-xs">
</Label>
<Select
value={(dataSource.refreshInterval ?? 5).toString()}
onValueChange={(value) => onChange({ refreshInterval: parseInt(value) })}
>
<SelectTrigger id="marker-refresh-interval" className="h-9 text-xs">
<SelectValue placeholder="간격 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0" className="text-xs"></SelectItem>
<SelectItem value="5" className="text-xs">5</SelectItem>
<SelectItem value="10" className="text-xs">10</SelectItem>
<SelectItem value="30" className="text-xs">30</SelectItem>
<SelectItem value="60" className="text-xs">1</SelectItem>
</SelectContent>
</Select>
<p className="text-[10px] text-muted-foreground">
</p>
</div>
{/* 마커 종류 선택 (MapTestWidgetV2 전용) */}
<div className="space-y-2">

View File

@ -321,37 +321,28 @@ ORDER BY 하위부서수 DESC`,
/>
</div>
{/* 마커 polling 간격 설정 (MapTestWidgetV2 전용) */}
{/* 마커 종류 선택 (MapTestWidgetV2 전용) */}
<div className="space-y-2">
<Label htmlFor="marker-refresh-interval" className="text-xs">
<Label htmlFor="marker-type" className="text-xs">
</Label>
<Select
value={(dataSource.refreshInterval ?? 5).toString()}
onValueChange={(value) => onChange({ refreshInterval: parseInt(value) })}
value={dataSource.markerType || "circle"}
onValueChange={(value) => onChange({ markerType: value })}
>
<SelectTrigger id="marker-refresh-interval" className="h-8 text-xs">
<SelectValue placeholder="간격 선택" />
<SelectTrigger id="marker-type" className="h-8 text-xs">
<SelectValue placeholder="마커 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="0" className="text-xs">
<SelectItem value="circle" className="text-xs">
</SelectItem>
<SelectItem value="5" className="text-xs">
5
</SelectItem>
<SelectItem value="10" className="text-xs">
10
</SelectItem>
<SelectItem value="30" className="text-xs">
30
</SelectItem>
<SelectItem value="60" className="text-xs">
1
<SelectItem value="arrow" className="text-xs">
</SelectItem>
</SelectContent>
</Select>
<p className="text-muted-foreground text-[10px]"> </p>
<p className="text-muted-foreground text-[10px]"> </p>
</div>
{/* 지도 색상 설정 (MapTestWidgetV2 전용) */}

View File

@ -155,7 +155,6 @@ export interface ChartDataSource {
jsonPath?: string; // JSON 응답에서 데이터 추출 경로 (예: "data.results")
// 공통
refreshInterval?: number; // 자동 새로고침 (초, 0이면 수동)
lastExecuted?: string; // 마지막 실행 시간
lastError?: string; // 마지막 오류 메시지
mapDisplayType?: "auto" | "marker" | "polygon"; // 지도 표시 방식 (auto: 자동, marker: 마커, polygon: 영역)
@ -184,6 +183,9 @@ export interface ChartConfig {
// 다중 데이터 소스 (테스트 위젯용)
dataSources?: ChartDataSource[]; // 여러 데이터 소스 (REST API + Database 혼합 가능)
// 위젯 레벨 설정 (MapTestWidgetV2용)
refreshInterval?: number; // 위젯 전체 자동 새로고침 간격 (초, 0이면 수동)
// 멀티 차트 설정 (ChartTestWidget용)
chartType?: string; // 차트 타입 (line, bar, pie, etc.)
mergeMode?: boolean; // 데이터 병합 모드 (여러 데이터 소스를 하나의 라인/바로 합침)

View File

@ -209,10 +209,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
if (loading) {
return (
<div className="flex h-full items-center justify-center bg-background">
<div className="bg-background flex h-full items-center justify-center">
<div className="text-center">
<div className="border-primary mx-auto h-8 w-8 animate-spin rounded-full border-2 border-t-transparent" />
<p className="mt-2 text-sm text-muted-foreground"> ...</p>
<p className="text-muted-foreground mt-2 text-sm"> ...</p>
</div>
</div>
);
@ -220,12 +220,12 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
if (error) {
return (
<div className="flex h-full items-center justify-center bg-background p-4">
<div className="bg-background flex h-full items-center justify-center p-4">
<div className="text-center">
<p className="text-sm text-destructive"> {error}</p>
<p className="text-destructive text-sm"> {error}</p>
<button
onClick={loadData}
className="mt-2 rounded bg-destructive/10 px-3 py-1 text-xs text-destructive hover:bg-destructive/20"
className="bg-destructive/10 text-destructive hover:bg-destructive/20 mt-2 rounded px-3 py-1 text-xs"
>
</button>
@ -244,10 +244,10 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
// 설정이 없으면 안내 화면
if (!hasDataSource || !hasConfig) {
return (
<div className="flex h-full items-center justify-center bg-background p-4">
<div className="bg-background flex h-full items-center justify-center p-4">
<div className="max-w-xs space-y-2 text-center">
<h3 className="text-sm font-bold text-foreground"> </h3>
<div className="space-y-1.5 text-xs text-foreground">
<h3 className="text-foreground text-sm font-bold"> </h3>
<div className="text-foreground space-y-1.5 text-xs">
<p className="font-medium">📊 </p>
<ul className="space-y-0.5 text-left">
<li> </li>
@ -256,7 +256,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
<li> COUNT, SUM, AVG, MIN, MAX </li>
</ul>
</div>
<div className="mt-2 rounded-lg bg-primary/10 p-2 text-[10px] text-primary">
<div className="bg-primary/10 text-primary mt-2 rounded-lg p-2 text-[10px]">
<p className="font-medium"> </p>
<p>1. </p>
<p>2. ()</p>
@ -274,7 +274,7 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
// 통계 카드 렌더링 (전체 크기 꽉 차게)
return (
<div className="flex h-full w-full flex-col items-center justify-center rounded-lg border bg-card p-6 text-center shadow-sm">
<div className="bg-card flex h-full w-full flex-col items-center justify-center rounded-lg p-6 text-center">
{/* 제목 */}
<div className="text-muted-foreground mb-2 text-sm font-medium">{config?.title || "통계"}</div>
@ -283,11 +283,6 @@ export default function CustomMetricTestWidget({ element }: CustomMetricTestWidg
<span className="text-primary text-4xl font-bold">{formattedValue}</span>
{config?.unit && <span className="text-muted-foreground text-lg">{config.unit}</span>}
</div>
{/* 필터 표시 (디버깅용, 작게) */}
{config?.filters && config.filters.length > 0 && (
<div className="text-muted-foreground mt-2 text-xs">: {config.filters.length} </div>
)}
</div>
);
}

View File

@ -268,23 +268,14 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
// 통계 카드 렌더링
return (
<div className="flex h-full w-full items-center justify-center bg-background p-4">
<div className="flex flex-col items-center justify-center rounded-lg border bg-card p-6 text-center shadow-sm">
{/* 제목 */}
<div className="text-muted-foreground mb-2 text-sm font-medium">{config?.title || "통계"}</div>
<div className="flex h-full w-full flex-col items-center justify-center bg-card p-6 text-center">
{/* 제목 */}
<div className="text-muted-foreground mb-2 text-sm font-medium">{config?.title || "통계"}</div>
{/* 값 */}
<div className="flex items-baseline gap-1">
<span className="text-primary text-4xl font-bold">{formattedValue}</span>
{config?.unit && <span className="text-muted-foreground text-lg">{config.unit}</span>}
</div>
{/* 필터 표시 (디버깅용, 작게) */}
{config?.filters && config.filters.length > 0 && (
<div className="text-muted-foreground mt-2 text-xs">
: {config.filters.length}
</div>
)}
{/* 값 */}
<div className="flex items-baseline gap-1">
<span className="text-primary text-4xl font-bold">{formattedValue}</span>
{config?.unit && <span className="text-muted-foreground text-lg">{config.unit}</span>}
</div>
</div>
);

View File

@ -37,8 +37,19 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
// // console.log("🧪 ListTestWidget 렌더링!", element);
const dataSources = useMemo(() => {
return element?.dataSources || element?.chartConfig?.dataSources;
}, [element?.dataSources, element?.chartConfig?.dataSources]);
// 다중 데이터 소스 우선
const multiSources = element?.dataSources || element?.chartConfig?.dataSources;
if (multiSources && multiSources.length > 0) {
return multiSources;
}
// 단일 데이터 소스 fallback (배열로 변환)
if (element?.dataSource) {
return [element.dataSource];
}
return [];
}, [element?.dataSources, element?.chartConfig?.dataSources, element?.dataSource]);
// // console.log("📊 dataSources 확인:", {
// hasDataSources: !!dataSources,
@ -58,6 +69,27 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
cardColumns: 3,
};
// visible 컬럼 설정 객체 배열 (field + label)
const visibleColumnConfigs = useMemo(() => {
if (config.columns && config.columns.length > 0 && typeof config.columns[0] === "object") {
return config.columns.filter((col: any) => col.visible !== false);
}
return [];
}, [config.columns]);
// 표시할 컬럼 필드명 (데이터 접근용)
const displayColumns = useMemo(() => {
if (!data?.columns) return [];
// 컬럼 설정이 있으면 field 사용
if (visibleColumnConfigs.length > 0) {
return visibleColumnConfigs.map((col: any) => col.field);
}
// 자동 모드: 모든 컬럼 표시
return data.columns;
}, [data?.columns, visibleColumnConfigs]);
// 다중 데이터 소스 로딩
const loadMultipleDataSources = useCallback(async () => {
if (!dataSources || dataSources.length === 0) {
@ -313,50 +345,66 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
const paginatedRows = data?.rows.slice(startIndex, endIndex) || [];
// 테이블 뷰
const renderTable = () => (
<div className="overflow-auto">
<Table>
{config.showHeader && (
<TableHeader>
<TableRow>
{data?.columns.map((col) => (
<TableHead key={col} className="whitespace-nowrap">
{col}
</TableHead>
))}
</TableRow>
</TableHeader>
)}
<TableBody>
{paginatedRows.map((row, idx) => (
<TableRow key={idx} className={config.stripedRows && idx % 2 === 0 ? "bg-muted/50" : ""}>
{data?.columns.map((col) => (
<TableCell key={col} className="whitespace-nowrap">
{String(row[col] ?? "")}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
const renderTable = () => {
// 헤더명 가져오기 (label 우선, 없으면 field 그대로)
const getHeaderLabel = (field: string) => {
const colConfig = visibleColumnConfigs.find((col: any) => col.field === field);
return colConfig?.label || field;
};
return (
<div className="overflow-auto">
<Table>
{config.showHeader && (
<TableHeader>
<TableRow>
{displayColumns.map((field) => (
<TableHead key={field} className="whitespace-nowrap">
{getHeaderLabel(field)}
</TableHead>
))}
</TableRow>
</TableHeader>
)}
<TableBody>
{paginatedRows.map((row, idx) => (
<TableRow key={idx} className={config.stripedRows && idx % 2 === 0 ? "bg-muted/50" : ""}>
{displayColumns.map((field) => (
<TableCell key={field} className="whitespace-nowrap">
{String(row[field] ?? "")}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
// 카드 뷰
const renderCards = () => (
<div className={`grid gap-4 grid-cols-1 md:grid-cols-${config.cardColumns || 3}`}>
{paginatedRows.map((row, idx) => (
<Card key={idx} className="p-4">
{data?.columns.map((col) => (
<div key={col} className="mb-2">
<span className="font-semibold">{col}: </span>
<span>{String(row[col] ?? "")}</span>
</div>
))}
</Card>
))}
</div>
);
const renderCards = () => {
// 헤더명 가져오기 (label 우선, 없으면 field 그대로)
const getLabel = (field: string) => {
const colConfig = visibleColumnConfigs.find((col: any) => col.field === field);
return colConfig?.label || field;
};
return (
<div className={`grid gap-4 grid-cols-1 md:grid-cols-${config.cardColumns || 3}`}>
{paginatedRows.map((row, idx) => (
<Card key={idx} className="p-4">
{displayColumns.map((field) => (
<div key={field} className="mb-2">
<span className="font-semibold">{getLabel(field)}: </span>
<span>{String(row[field] ?? "")}</span>
</div>
))}
</Card>
))}
</div>
);
};
return (
<div className="flex h-full flex-col bg-card shadow-sm">
@ -396,7 +444,7 @@ export function ListTestWidget({ element }: ListTestWidgetProps) {
<div className="flex h-full items-center justify-center">
<p className="text-sm text-destructive">{error}</p>
</div>
) : !(element?.dataSources || element?.chartConfig?.dataSources) || (element?.dataSources || element?.chartConfig?.dataSources)?.length === 0 ? (
) : !dataSources || dataSources.length === 0 ? (
<div className="flex h-full items-center justify-center">
<p className="text-sm text-muted-foreground">

View File

@ -916,9 +916,8 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
// 즉시 첫 로드 (마커 데이터)
loadMultipleDataSources();
// 첫 번째 데이터 소스의 새로고침 간격 사용 (초)
const firstDataSource = dataSources[0];
const refreshInterval = firstDataSource?.refreshInterval ?? 5;
// 위젯 레벨의 새로고침 간격 사용 (초)
const refreshInterval = element?.chartConfig?.refreshInterval ?? 5;
// 0이면 자동 새로고침 비활성화
if (refreshInterval === 0) {
@ -933,7 +932,7 @@ export default function MapTestWidgetV2({ element }: MapTestWidgetV2Props) {
clearInterval(intervalId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dataSources]);
}, [dataSources, element?.chartConfig?.refreshInterval]);
// 타일맵 URL (chartConfig에서 가져오기)
const tileMapUrl =

View File

@ -634,7 +634,26 @@ export default function RiskAlertTestWidget({ element }: RiskAlertTestWidgetProp
)}
<p className="text-[10px] text-foreground mt-0.5 line-clamp-2">{alert.description}</p>
<div className="mt-1 flex items-center gap-2 text-[9px] text-muted-foreground">
<span>{new Date(alert.timestamp).toLocaleString("ko-KR")}</span>
<span>
{(() => {
const ts = String(alert.timestamp);
// yyyyMMddHHmm 형식 감지 (예: 20251114 1000)
if (/^\d{12}$/.test(ts)) {
const year = ts.substring(0, 4);
const month = ts.substring(4, 6);
const day = ts.substring(6, 8);
const hour = ts.substring(8, 10);
const minute = ts.substring(10, 12);
const date = new Date(`${year}-${month}-${day}T${hour}:${minute}:00`);
return isNaN(date.getTime()) ? ts : date.toLocaleString("ko-KR");
}
// ISO 형식 또는 일반 날짜 형식
const date = new Date(ts);
return isNaN(date.getTime()) ? ts : date.toLocaleString("ko-KR");
})()}
</span>
{alert.source && <span>· {alert.source}</span>}
</div>
</div>

View File

@ -212,14 +212,7 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
// 컴포넌트 기본 스타일 - 레이아웃은 항상 맨 아래
// 🔥 모든 컴포넌트를 픽셀 기준으로 통일 (스케일로만 조정)
const getWidth = () => {
// table-list는 화면 너비 전체 사용
if (component.componentConfig?.type === "table-list") {
// 디자인 해상도 기준으로 픽셀 반환
const screenWidth = 1920; // 기본 디자인 해상도
return `${screenWidth}px`;
}
// 모든 컴포넌트는 size.width 픽셀 사용
// 모든 컴포넌트는 size.width 픽셀 사용 (table-list 포함)
const width = `${size?.width || 100}px`;
return width;
};
@ -259,19 +252,30 @@ export const RealtimePreviewDynamic: React.FC<RealtimePreviewProps> = ({
}
: component;
// componentStyle에서 width, height 제거 (size.width, size.height만 사용)
const { width: _styleWidth, height: _styleHeight, ...restComponentStyle } = componentStyle || {};
const baseStyle = {
left: `${position.x}px`,
top: `${position.y}px`,
...restComponentStyle, // width/height 제외한 스타일 먼저 적용
width: getWidth(), // size.width로 덮어쓰기
height: getHeight(), // size.height로 덮어쓰기
...componentStyle, // componentStyle 전체 적용 (DynamicComponentRenderer에서 이미 size가 변환됨)
width: getWidth(), // getWidth() 우선 (table-list 등 특수 케이스)
height: getHeight(), // getHeight() 우선 (flow-widget 등 특수 케이스)
zIndex: component.type === "layout" ? 1 : position.z || 2,
right: undefined,
};
// 디버깅: 크기 정보 로그
if (component.id && isSelected) {
console.log("📐 RealtimePreview baseStyle:", {
componentId: component.id,
componentType: (component as any).componentType || component.type,
sizeWidth: size?.width,
sizeHeight: size?.height,
styleWidth: componentStyle?.width,
styleHeight: componentStyle?.height,
baseStyleWidth: baseStyle.width,
baseStyleHeight: baseStyle.height,
});
}
// 🔍 DOM 렌더링 후 실제 크기 측정
const innerDivRef = React.useRef<HTMLDivElement>(null);
const outerDivRef = React.useRef<HTMLDivElement>(null);

View File

@ -525,6 +525,34 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
const finalKey = pathParts[pathParts.length - 1];
current[finalKey] = value;
// 🆕 size 변경 시 style도 함께 업데이트 (파란 테두리와 실제 크기 동기화)
if (path === "size.width" || path === "size.height" || path === "size") {
if (!newComp.style) {
newComp.style = {};
}
if (path === "size.width") {
newComp.style.width = `${value}px`;
} else if (path === "size.height") {
newComp.style.height = `${value}px`;
} else if (path === "size") {
// size 객체 전체가 변경된 경우
if (value.width !== undefined) {
newComp.style.width = `${value.width}px`;
}
if (value.height !== undefined) {
newComp.style.height = `${value.height}px`;
}
}
console.log("🔄 size 변경 → style 동기화:", {
componentId: newComp.id,
path,
value,
updatedStyle: newComp.style,
});
}
// gridColumns 변경 시 크기 자동 업데이트 제거 (격자 시스템 제거됨)
// if (path === "gridColumns" && prevLayout.gridSettings) {
// const updatedSize = updateSizeFromGridColumns(newComp, prevLayout.gridSettings as GridUtilSettings);
@ -2217,7 +2245,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
labelColor: "#212121",
labelFontWeight: "500",
labelMarginBottom: "4px",
width: `${widthPercent}%`, // gridColumns에 맞춘 퍼센트 너비
width: `${componentSize.width}px`, // size와 동기화 (픽셀 단위)
height: `${componentSize.height}px`, // size와 동기화 (픽셀 단위)
},
};

View File

@ -287,18 +287,16 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
};
// 렌더러 props 구성
// component.style에서 height 제거 (RealtimePreviewDynamic에서 size.height로 처리)
// 단, layout 타입 컴포넌트(split-panel-layout 등)는 height 유지
const isLayoutComponent =
component.type === "layout" ||
componentType === "split-panel-layout" ||
componentType?.includes("layout");
const { height: _height, ...styleWithoutHeight } = component.style || {};
// 숨김 값 추출
const hiddenValue = component.hidden || component.componentConfig?.hidden;
// size.width와 size.height를 style.width와 style.height로 변환
const finalStyle: React.CSSProperties = {
...component.style,
width: component.size?.width ? `${component.size.width}px` : component.style?.width,
height: component.size?.height ? `${component.size.height}px` : component.style?.height,
};
const rendererProps = {
component,
isSelected,
@ -307,7 +305,7 @@ export const DynamicComponentRenderer: React.FC<DynamicComponentRendererProps> =
onDragEnd,
size: component.size || newComponent.defaultSize,
position: component.position,
style: isLayoutComponent ? component.style : styleWithoutHeight, // 레이아웃은 height 유지
style: finalStyle, // size를 포함한 최종 style
config: component.componentConfig,
componentConfig: component.componentConfig,
value: currentValue, // formData에서 추출한 현재 값 전달

View File

@ -75,13 +75,15 @@ export const CustomerItemMappingComponent: React.FC<CustomerItemMappingComponent
// 스타일 계산
const componentStyle: React.CSSProperties = {
position: "relative",
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
backgroundColor: "hsl(var(--background))",
overflow: "hidden",
boxSizing: "border-box",
width: "100%",
height: "100%",
minHeight: isDesignMode ? "300px" : "100%",
...style, // style prop이 위의 기본값들을 덮어씀
};
// 이벤트 핸들러

View File

@ -61,16 +61,15 @@ export const SingleTableWithSticky: React.FC<SingleTableWithStickyProps> = ({
className="relative flex h-full flex-col overflow-hidden bg-background shadow-sm"
style={{
width: "100%",
maxWidth: "100%",
height: "100%",
boxSizing: "border-box",
}}
>
<div className="relative flex-1 overflow-x-auto overflow-y-auto">
<div className="relative flex-1 overflow-auto">
<Table
className="w-full"
style={{
width: "100%",
minWidth: "100%",
tableLayout: "auto", // 테이블 크기 자동 조정
boxSizing: "border-box",
}}

View File

@ -230,16 +230,16 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
}
const componentStyle: React.CSSProperties = {
width: calculatedWidth,
height: isDesignMode ? "auto" : "100%",
minHeight: isDesignMode ? "300px" : "100%",
position: "relative",
display: "flex",
flexDirection: "column",
backgroundColor: "hsl(var(--background))",
overflow: "hidden",
...style,
// width는 항상 100%로 고정 (부모 컨테이너가 gridColumns로 크기 제어)
boxSizing: "border-box",
width: "100%",
height: "100%",
minHeight: isDesignMode ? "300px" : "100%",
...style, // style prop이 위의 기본값들을 덮어씀
};
// ========================================
@ -1875,7 +1875,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onDragStart: isDesignMode ? onDragStart : undefined,
onDragEnd: isDesignMode ? onDragEnd : undefined,
draggable: isDesignMode,
className: cn(className, isDesignMode && "cursor-move"),
className: cn("w-full h-full", className, isDesignMode && "cursor-move"), // customer-item-mapping과 동일
style: componentStyle,
};
@ -1992,17 +1992,27 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
{/* 테이블 컨테이너 */}
<div
className="flex w-full max-w-full flex-1 flex-col overflow-hidden"
style={{ marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px` }}
className="flex flex-1 flex-col"
style={{
marginTop: `${tableConfig.filter?.bottomSpacing ?? 8}px`,
width: "100%",
height: "100%",
overflow: "hidden",
}}
>
{/* 스크롤 영역 */}
<div
className="bg-background flex-1 w-full max-w-full overflow-x-auto overflow-y-auto"
style={{ position: "relative" }}
className="bg-background flex-1"
style={{
position: "relative",
width: "100%",
height: "100%",
overflow: "auto",
}}
>
{/* 테이블 */}
<table
className={cn("table-mobile-fixed w-full max-w-full", !showGridLines && "hide-grid")}
className={cn("table-mobile-fixed", !showGridLines && "hide-grid")}
style={{
borderCollapse: "collapse",
width: "100%",

View File

@ -96,7 +96,7 @@ export const TableListDefinition = createComponentDefinition({
// 데이터 로딩
autoLoad: true,
},
defaultSize: { width: 120, height: 600 }, // 테이블 리스트 기본 높이
defaultSize: { width: 1000, height: 600 }, // 테이블 리스트 기본 크기 (너비 1000px, 높이 600px)
configPanel: TableListConfigPanel,
icon: "Table",
tags: ["테이블", "데이터", "목록", "그리드"],