jskim-node #406
|
|
@ -929,6 +929,42 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
return result;
|
return result;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 프로그레스바 셀 렌더링 (부모 값 대비 자식 값 비율)
|
||||||
|
const renderProgressCell = useCallback(
|
||||||
|
(col: any, item: any, parentData: any) => {
|
||||||
|
const current = Number(item[col.numerator] || 0);
|
||||||
|
const max = Number(parentData?.[col.denominator] || item[col.denominator] || 0);
|
||||||
|
const percentage = max > 0 ? Math.round((current / max) * 100) : 0;
|
||||||
|
const barWidth = Math.min(percentage, 100);
|
||||||
|
const barColor =
|
||||||
|
percentage > 100
|
||||||
|
? "bg-red-600"
|
||||||
|
: percentage >= 90
|
||||||
|
? "bg-red-500"
|
||||||
|
: percentage >= 70
|
||||||
|
? "bg-amber-500"
|
||||||
|
: "bg-emerald-500";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-w-[120px] items-center gap-2">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="bg-muted h-2 w-full rounded-full">
|
||||||
|
<div
|
||||||
|
className={`h-2 rounded-full transition-all ${barColor}`}
|
||||||
|
style={{ width: `${barWidth}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground mt-0.5 text-[10px]">
|
||||||
|
{current.toLocaleString()} / {max.toLocaleString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="shrink-0 text-xs font-medium">{percentage}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// 셀 값 포맷팅 함수 (카테고리 타입 처리 + 날짜/숫자 포맷)
|
// 셀 값 포맷팅 함수 (카테고리 타입 처리 + 날짜/숫자 포맷)
|
||||||
const formatCellValue = useCallback(
|
const formatCellValue = useCallback(
|
||||||
(
|
(
|
||||||
|
|
@ -3950,12 +3986,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
>
|
>
|
||||||
{tabSummaryColumns.map((col: any) => (
|
{tabSummaryColumns.map((col: any) => (
|
||||||
<td key={col.name} className="px-3 py-2 text-xs">
|
<td key={col.name} className="px-3 py-2 text-xs">
|
||||||
{formatCellValue(
|
{col.type === "progress"
|
||||||
col.name,
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
getEntityJoinValue(item, col.name),
|
: formatCellValue(
|
||||||
rightCategoryMappings,
|
col.name,
|
||||||
col.format,
|
getEntityJoinValue(item, col.name),
|
||||||
)}
|
rightCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasTabActions && (
|
{hasTabActions && (
|
||||||
|
|
@ -4064,12 +4102,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
>
|
>
|
||||||
{listSummaryColumns.map((col: any) => (
|
{listSummaryColumns.map((col: any) => (
|
||||||
<td key={col.name} className="px-3 py-2 text-xs">
|
<td key={col.name} className="px-3 py-2 text-xs">
|
||||||
{formatCellValue(
|
{col.type === "progress"
|
||||||
col.name,
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
getEntityJoinValue(item, col.name),
|
: formatCellValue(
|
||||||
rightCategoryMappings,
|
col.name,
|
||||||
col.format,
|
getEntityJoinValue(item, col.name),
|
||||||
)}
|
rightCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{hasTabActions && (
|
{hasTabActions && (
|
||||||
|
|
@ -4486,12 +4526,14 @@ export const SplitPanelLayoutComponent: React.FC<SplitPanelLayoutComponentProps>
|
||||||
className="px-3 py-2 text-xs whitespace-nowrap"
|
className="px-3 py-2 text-xs whitespace-nowrap"
|
||||||
style={{ textAlign: col.align || "left" }}
|
style={{ textAlign: col.align || "left" }}
|
||||||
>
|
>
|
||||||
{formatCellValue(
|
{col.type === "progress"
|
||||||
col.name,
|
? renderProgressCell(col, item, selectedLeftItem)
|
||||||
getEntityJoinValue(item, col.name),
|
: formatCellValue(
|
||||||
rightCategoryMappings,
|
col.name,
|
||||||
col.format,
|
getEntityJoinValue(item, col.name),
|
||||||
)}
|
rightCategoryMappings,
|
||||||
|
col.format,
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
{/* 수정 또는 삭제 버튼이 하나라도 활성화되어 있을 때만 작업 셀 표시 */}
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,10 @@ import { CSS } from "@dnd-kit/utilities";
|
||||||
|
|
||||||
// 드래그 가능한 컬럼 아이템
|
// 드래그 가능한 컬럼 아이템
|
||||||
function SortableColumnRow({
|
function SortableColumnRow({
|
||||||
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, onShowInSummaryChange, onShowInDetailChange,
|
id, col, index, isNumeric, isEntityJoin, onLabelChange, onWidthChange, onFormatChange, onRemove, onShowInSummaryChange, onShowInDetailChange, onProgressChange, availableChildColumns, availableParentColumns,
|
||||||
}: {
|
}: {
|
||||||
id: string;
|
id: string;
|
||||||
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean };
|
col: { name: string; label: string; width?: number; format?: any; showInSummary?: boolean; showInDetail?: boolean; type?: string; numerator?: string; denominator?: string };
|
||||||
index: number;
|
index: number;
|
||||||
isNumeric: boolean;
|
isNumeric: boolean;
|
||||||
isEntityJoin?: boolean;
|
isEntityJoin?: boolean;
|
||||||
|
|
@ -41,6 +41,9 @@ function SortableColumnRow({
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onShowInSummaryChange?: (checked: boolean) => void;
|
onShowInSummaryChange?: (checked: boolean) => void;
|
||||||
onShowInDetailChange?: (checked: boolean) => void;
|
onShowInDetailChange?: (checked: boolean) => void;
|
||||||
|
onProgressChange?: (updates: { numerator?: string; denominator?: string }) => void;
|
||||||
|
availableChildColumns?: Array<{ columnName: string; columnLabel: string }>;
|
||||||
|
availableParentColumns?: Array<{ columnName: string; columnLabel: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
|
||||||
const style = { transform: CSS.Transform.toString(transform), transition };
|
const style = { transform: CSS.Transform.toString(transform), transition };
|
||||||
|
|
@ -53,12 +56,44 @@ function SortableColumnRow({
|
||||||
"flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5",
|
"flex items-center gap-1.5 rounded-md border bg-card px-2 py-1.5",
|
||||||
isDragging && "z-50 opacity-50 shadow-md",
|
isDragging && "z-50 opacity-50 shadow-md",
|
||||||
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
isEntityJoin && "border-blue-200 bg-blue-50/30",
|
||||||
|
col.type === "progress" && "border-emerald-200 bg-emerald-50/30",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
<div {...attributes} {...listeners} className="text-muted-foreground hover:text-foreground cursor-grab touch-none">
|
||||||
<GripVertical className="h-3 w-3" />
|
<GripVertical className="h-3 w-3" />
|
||||||
</div>
|
</div>
|
||||||
{isEntityJoin ? (
|
{col.type === "progress" ? (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button className="shrink-0 cursor-pointer rounded bg-emerald-100 px-1 text-[9px] font-medium text-emerald-700 hover:bg-emerald-200" title="클릭하여 설정 변경">BAR</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-56 space-y-2 p-3" align="start">
|
||||||
|
<p className="text-xs font-medium">프로그레스 설정</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">현재값 (자식 컬럼)</Label>
|
||||||
|
<Select value={col.numerator || ""} onValueChange={(v) => onProgressChange?.({ numerator: v })}>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]"><SelectValue placeholder="컬럼 선택" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(availableChildColumns || []).map((c) => (
|
||||||
|
<SelectItem key={c.columnName} value={c.columnName} className="text-xs">{c.columnLabel || c.columnName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">최대값 (부모 컬럼)</Label>
|
||||||
|
<Select value={col.denominator || ""} onValueChange={(v) => onProgressChange?.({ denominator: v })}>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]"><SelectValue placeholder="컬럼 선택" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(availableParentColumns || []).map((c) => (
|
||||||
|
<SelectItem key={c.columnName} value={c.columnName} className="text-xs">{c.columnLabel || c.columnName}</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : isEntityJoin ? (
|
||||||
<Link2 className="h-3 w-3 shrink-0 text-blue-500" title="Entity 조인 컬럼" />
|
<Link2 className="h-3 w-3 shrink-0 text-blue-500" title="Entity 조인 컬럼" />
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
<span className="text-muted-foreground w-5 shrink-0 text-center text-[10px] font-medium">#{index + 1}</span>
|
||||||
|
|
@ -656,6 +691,13 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
newColumns[index] = { ...newColumns[index], showInDetail: checked };
|
newColumns[index] = { ...newColumns[index], showInDetail: checked };
|
||||||
updateTab({ columns: newColumns });
|
updateTab({ columns: newColumns });
|
||||||
}}
|
}}
|
||||||
|
onProgressChange={(updates) => {
|
||||||
|
const newColumns = [...selectedColumns];
|
||||||
|
newColumns[index] = { ...newColumns[index], ...updates };
|
||||||
|
updateTab({ columns: newColumns });
|
||||||
|
}}
|
||||||
|
availableChildColumns={tabColumns.map((c) => ({ columnName: c.columnName, columnLabel: c.columnLabel || c.columnName }))}
|
||||||
|
availableParentColumns={leftTableColumns.map((c) => ({ columnName: c.columnName, columnLabel: c.columnLabel || c.columnName }))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -685,6 +727,104 @@ const AdditionalTabConfigPanel: React.FC<AdditionalTabConfigPanelProps> = ({
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 프로그레스 컬럼 추가 */}
|
||||||
|
{tab.tableName && (
|
||||||
|
<div className="border-border/60 my-2 border-t pt-2">
|
||||||
|
<details className="group">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center gap-2 select-none">
|
||||||
|
<ChevronRight className="h-3 w-3 shrink-0 text-emerald-500 transition-transform group-open:rotate-90" />
|
||||||
|
<span className="text-[10px] font-medium text-emerald-600">프로그레스 컬럼 추가</span>
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 space-y-2 rounded-md border border-emerald-200 bg-emerald-50/50 p-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">라벨</Label>
|
||||||
|
<Input
|
||||||
|
id={`tab-${tabIndex}-progress-label`}
|
||||||
|
placeholder="예: 샷수 현황"
|
||||||
|
className="h-7 text-xs"
|
||||||
|
defaultValue=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">현재값 (자식 컬럼)</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const el = document.getElementById(`tab-${tabIndex}-progress-numerator`) as HTMLInputElement;
|
||||||
|
if (el) el.value = v;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]">
|
||||||
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{tabColumns.map((col) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||||
|
{col.columnLabel || col.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<input type="hidden" id={`tab-${tabIndex}-progress-numerator`} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-[10px]">최대값 (부모 컬럼)</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(v) => {
|
||||||
|
const el = document.getElementById(`tab-${tabIndex}-progress-denominator`) as HTMLInputElement;
|
||||||
|
if (el) el.value = v;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-7 text-[10px]">
|
||||||
|
<SelectValue placeholder="컬럼 선택" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{leftTableColumns.map((col) => (
|
||||||
|
<SelectItem key={col.columnName} value={col.columnName} className="text-xs">
|
||||||
|
{col.columnLabel || col.columnName}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<input type="hidden" id={`tab-${tabIndex}-progress-denominator`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-full text-xs text-emerald-700 border-emerald-300 hover:bg-emerald-100"
|
||||||
|
onClick={() => {
|
||||||
|
const labelEl = document.getElementById(`tab-${tabIndex}-progress-label`) as HTMLInputElement;
|
||||||
|
const numEl = document.getElementById(`tab-${tabIndex}-progress-numerator`) as HTMLInputElement;
|
||||||
|
const denEl = document.getElementById(`tab-${tabIndex}-progress-denominator`) as HTMLInputElement;
|
||||||
|
const label = labelEl?.value || "프로그레스";
|
||||||
|
const numerator = numEl?.value;
|
||||||
|
const denominator = denEl?.value;
|
||||||
|
if (!numerator || !denominator) return;
|
||||||
|
updateTab({
|
||||||
|
columns: [
|
||||||
|
...selectedColumns,
|
||||||
|
{
|
||||||
|
name: `progress_${numerator}_${denominator}`,
|
||||||
|
label,
|
||||||
|
width: 200,
|
||||||
|
type: "progress",
|
||||||
|
numerator,
|
||||||
|
denominator,
|
||||||
|
} as any,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (labelEl) labelEl.value = "";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
추가
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */}
|
{/* Entity 조인 컬럼 - 아코디언 (접기/펼치기) */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null;
|
const joinData = tab.tableName ? entityJoinColumnsMap?.[tab.tableName] : null;
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,13 @@ export const StatusCountComponent: React.FC<StatusCountComponentProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseData = res.data?.data;
|
const responseData = res.data?.data;
|
||||||
const rows: any[] = Array.isArray(responseData)
|
let rows: any[] = [];
|
||||||
? responseData
|
if (Array.isArray(responseData)) {
|
||||||
: (responseData?.data || responseData?.rows || []);
|
rows = responseData;
|
||||||
|
} else if (responseData && typeof responseData === "object") {
|
||||||
|
rows = Array.isArray(responseData.data) ? responseData.data :
|
||||||
|
Array.isArray(responseData.rows) ? responseData.rows : [];
|
||||||
|
}
|
||||||
const grouped: Record<string, number> = {};
|
const grouped: Record<string, number> = {};
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue