refactor: pop-status-bar 컴포넌트 완전 삭제

사용하지 않는 pop-status-bar 컴포넌트를 코드에서 완전히 제거
- 컴포넌트 폴더 삭제 (4파일)
- 레지스트리, 디자이너, 타입 정의에서 참조 제거 (6파일)
- DB 화면 3개(입고목록/설비점검/작업지시)에서도 별도 제거 완료
This commit is contained in:
SeongHyun Kim 2026-03-31 13:50:45 +09:00
parent cd106c7499
commit 290275c27d
10 changed files with 9 additions and 908 deletions

View File

@ -76,7 +76,6 @@ const COMPONENT_TYPE_LABELS: Record<string, string> = {
"pop-button": "버튼",
"pop-string-list": "리스트 목록",
"pop-search": "검색",
"pop-status-bar": "상태 바",
"pop-list": "리스트",
"pop-indicator": "인디케이터",
"pop-scanner": "스캐너",

View File

@ -3,7 +3,7 @@
import { useDrag } from "react-dnd";
import { cn } from "@/lib/utils";
import { PopComponentType } from "../types/pop-layout";
import { Square, FileText, MousePointer, BarChart3, LayoutGrid, MousePointerClick, List, Search, TextCursorInput, ScanLine, UserCircle, BarChart2, ClipboardCheck } from "lucide-react";
import { Square, FileText, MousePointer, BarChart3, LayoutGrid, MousePointerClick, List, Search, TextCursorInput, ScanLine, UserCircle, ClipboardCheck } from "lucide-react";
import { DND_ITEM_TYPES } from "../constants";
// 컴포넌트 정의
@ -69,12 +69,6 @@ const PALETTE_ITEMS: PaletteItem[] = [
icon: Search,
description: "조건 입력 (텍스트/날짜/선택/모달)",
},
{
type: "pop-status-bar",
label: "상태 바",
icon: BarChart2,
description: "상태별 건수 대시보드 + 필터",
},
{
type: "pop-field",
label: "입력 필드",

View File

@ -80,7 +80,6 @@ const COMPONENT_TYPE_LABELS: Record<PopComponentType, string> = {
"pop-button": "버튼",
"pop-string-list": "리스트 목록",
"pop-search": "검색",
"pop-status-bar": "상태 바",
"pop-field": "입력",
"pop-scanner": "스캐너",
"pop-profile": "프로필",

View File

@ -7,7 +7,7 @@
/**
* POP
*/
export type PopComponentType = "pop-sample" | "pop-text" | "pop-icon" | "pop-dashboard" | "pop-card-list" | "pop-card-list-v2" | "pop-button" | "pop-string-list" | "pop-search" | "pop-status-bar" | "pop-field" | "pop-scanner" | "pop-profile" | "pop-work-detail";
export type PopComponentType = "pop-sample" | "pop-text" | "pop-icon" | "pop-dashboard" | "pop-card-list" | "pop-card-list-v2" | "pop-button" | "pop-string-list" | "pop-search" | "pop-field" | "pop-scanner" | "pop-profile" | "pop-work-detail";
/**
*
@ -373,7 +373,6 @@ export const DEFAULT_COMPONENT_GRID_SIZE: Record<PopComponentType, { colSpan: nu
"pop-button": { colSpan: 8, rowSpan: 4 },
"pop-string-list": { colSpan: 19, rowSpan: 10 },
"pop-search": { colSpan: 8, rowSpan: 4 },
"pop-status-bar": { colSpan: 19, rowSpan: 4 },
"pop-field": { colSpan: 19, rowSpan: 6 },
"pop-scanner": { colSpan: 2, rowSpan: 2 },
"pop-profile": { colSpan: 2, rowSpan: 2 },

View File

@ -21,7 +21,6 @@ import "./pop-card-list-v2";
import "./pop-button";
import "./pop-string-list";
import "./pop-search";
import "./pop-status-bar";
import "./pop-field";
import "./pop-scanner";

View File

@ -527,14 +527,10 @@ export function PopCardListV2Component({
return col ? subTableKeys.has(col) : false;
}, [subTableKeys]);
// showStatusTabs일 때 외부 status-bar 필터 무시 (내장 탭으로 대체)
// showStatusTabs일 때 외부 필터 그대로 사용 (내장 탭으로 상태 필터링)
const effectiveExternalFilters = useMemo(() => {
if (!config?.showStatusTabs) return externalFilters;
const filtered = new Map(
[...externalFilters.entries()].filter(([, f]) => f._source !== "status-bar")
);
return filtered;
}, [externalFilters, config?.showStatusTabs]);
return externalFilters;
}, [externalFilters]);
// 외부 필터 (자동 분류: 컬럼이 processFlow에 있으면 subFilter)
const filteredRows = useMemo(() => {
@ -656,26 +652,12 @@ export function PopCardListV2Component({
}
}, [selectedRowIds, filteredRows, exitSelectMode]);
// status-bar 필터를 제외한 rows (외부 status-bar 카운트 집계용, 하위 호환)
// 카운트 집계용 rows (필터 적용된 결과를 그대로 사용)
const rowsForStatusCount = useMemo(() => {
if (config?.showStatusTabs) return filteredRows;
const hasStatusBarFilter = [...externalFilters.values()].some((f) => f._source === "status-bar");
if (!hasStatusBarFilter) return filteredRows;
return filteredRows;
}, [filteredRows]);
const nonStatusFilters = new Map(
[...externalFilters.entries()].filter(([, f]) => f._source !== "status-bar")
);
if (nonStatusFilters.size === 0) return duplicateAcceptableCards(rows);
const allFilters = [...nonStatusFilters.values()];
const mainFilters = allFilters.filter((f) => !isSubTableColumn(f));
const subFilters = allFilters.filter((f) => isSubTableColumn(f));
const afterDuplicate = applySubFilterAndDuplicate(rows, subFilters);
return applyMainFilters(afterDuplicate, mainFilters, subFilters.length > 0);
}, [rows, filteredRows, externalFilters, config?.showStatusTabs, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
// 카운트 집계용 rows 발행 (status-bar 필터 제외)
// 카운트 집계용 rows 발행
// originalCount: 복제 카드를 제외한 원본 카드 수
useEffect(() => {
if (!componentId || loading) return;

View File

@ -1,247 +0,0 @@
"use client";
import { useState, useCallback, useEffect, useMemo } from "react";
import { cn } from "@/lib/utils";
import { usePopEvent } from "@/hooks/pop";
import type { StatusBarConfig, StatusChipOption } from "./types";
import { DEFAULT_STATUS_BAR_CONFIG } from "./types";
interface PopStatusBarComponentProps {
config: StatusBarConfig;
label?: string;
screenId?: string;
componentId?: string;
}
export function PopStatusBarComponent({
config: rawConfig,
label,
screenId,
componentId,
}: PopStatusBarComponentProps) {
const config = { ...DEFAULT_STATUS_BAR_CONFIG, ...(rawConfig || {}) };
const { publish, subscribe } = usePopEvent(screenId || "");
const [selectedValue, setSelectedValue] = useState<string>("");
const [allRows, setAllRows] = useState<Record<string, unknown>[]>([]);
const [autoSubStatusColumn, setAutoSubStatusColumn] = useState<string | null>(null);
const [originalCount, setOriginalCount] = useState<number | null>(null);
// all_rows 이벤트 구독
useEffect(() => {
if (!componentId) return;
const unsub = subscribe(
`__comp_input__${componentId}__all_rows`,
(payload: unknown) => {
const data = payload as { value?: unknown } | unknown;
const inner =
typeof data === "object" && data && "value" in data
? (data as { value: unknown }).value
: data;
if (
typeof inner === "object" &&
inner &&
!Array.isArray(inner) &&
"rows" in inner
) {
const envelope = inner as {
rows?: unknown;
subStatusColumn?: string | null;
originalCount?: number;
};
if (Array.isArray(envelope.rows))
setAllRows(envelope.rows as Record<string, unknown>[]);
setAutoSubStatusColumn(envelope.subStatusColumn ?? null);
setOriginalCount(envelope.originalCount ?? null);
} else if (Array.isArray(inner)) {
setAllRows(inner as Record<string, unknown>[]);
setAutoSubStatusColumn(null);
setOriginalCount(null);
}
}
);
return unsub;
}, [componentId, subscribe]);
// 외부에서 값 설정 이벤트 구독
useEffect(() => {
if (!componentId) return;
const unsub = subscribe(
`__comp_input__${componentId}__set_value`,
(payload: unknown) => {
const data = payload as { value?: unknown } | unknown;
const incoming =
typeof data === "object" && data && "value" in data
? (data as { value: unknown }).value
: data;
setSelectedValue(String(incoming ?? ""));
}
);
return unsub;
}, [componentId, subscribe]);
const emitFilter = useCallback(
(newValue: string) => {
setSelectedValue(newValue);
if (!componentId) return;
const baseColumn = config.filterColumn || config.countColumn || "";
const subActive = config.useSubCount && !!autoSubStatusColumn;
const filterColumns = subActive
? [...new Set([baseColumn, autoSubStatusColumn!].filter(Boolean))]
: [baseColumn].filter(Boolean);
publish(`__comp_output__${componentId}__filter_value`, {
fieldName: baseColumn,
filterColumns,
value: newValue,
filterMode: "equals",
_source: "status-bar",
});
},
[componentId, publish, config.filterColumn, config.countColumn, config.useSubCount, autoSubStatusColumn]
);
const chipCfg = config;
const showCount = chipCfg.showCount !== false;
const baseCountColumn = chipCfg.countColumn || "";
const useSubCount = chipCfg.useSubCount || false;
const hideUntilSubFilter = chipCfg.hideUntilSubFilter || false;
const allowAll = chipCfg.allowAll !== false;
const allLabel = chipCfg.allLabel || "전체";
const chipStyle = chipCfg.chipStyle || "tab";
const options: StatusChipOption[] = chipCfg.options || [];
// 하위 필터(공정) 활성 여부
const subFilterActive = useSubCount && !!autoSubStatusColumn;
// hideUntilSubFilter가 켜져있으면서 아직 공정 선택이 안 된 경우 숨김
const shouldHide = hideUntilSubFilter && !subFilterActive;
const effectiveCountColumn =
subFilterActive ? autoSubStatusColumn : baseCountColumn;
const counts = useMemo(() => {
if (!showCount || !effectiveCountColumn || allRows.length === 0)
return new Map<string, number>();
const map = new Map<string, number>();
for (const row of allRows) {
if (row == null || typeof row !== "object") continue;
const v = String(row[effectiveCountColumn] ?? "");
map.set(v, (map.get(v) || 0) + 1);
}
return map;
}, [allRows, effectiveCountColumn, showCount]);
const totalCount = originalCount ?? allRows.length;
const chipItems = useMemo(() => {
const items: { value: string; label: string; count: number }[] = [];
if (allowAll) {
items.push({ value: "", label: allLabel, count: totalCount });
}
for (const opt of options) {
items.push({
value: opt.value,
label: opt.label,
count: counts.get(opt.value) || 0,
});
}
return items;
}, [options, counts, totalCount, allowAll, allLabel]);
const showLabel = !!label;
if (shouldHide) {
return (
<div className="flex h-full w-full items-center justify-center p-1.5">
<span className="text-[10px] text-muted-foreground/50">
{chipCfg.hiddenMessage || "조건을 선택하면 상태별 현황이 표시됩니다"}
</span>
</div>
);
}
if (chipStyle === "pill") {
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-0.5 overflow-hidden p-1.5">
{showLabel && (
<span className="w-full shrink-0 truncate text-[10px] font-medium text-muted-foreground">
{label}
</span>
)}
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
{chipItems.map((item) => {
const isActive = selectedValue === item.value;
return (
<button
key={item.value}
type="button"
onClick={() => emitFilter(item.value)}
className={cn(
"flex items-center gap-1 rounded-full px-3 py-1 text-xs font-medium transition-colors",
isActive
? "bg-primary text-primary-foreground"
: "bg-muted text-muted-foreground hover:bg-accent hover:text-foreground"
)}
>
{item.label}
{showCount && (
<span
className={cn(
"ml-0.5 min-w-[18px] rounded-full px-1 py-0.5 text-center text-[10px] font-bold leading-none",
isActive
? "bg-primary-foreground/20 text-primary-foreground"
: "bg-background text-foreground"
)}
>
{item.count}
</span>
)}
</button>
);
})}
</div>
</div>
);
}
// tab 스타일 (기본)
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-0.5 overflow-hidden p-1.5">
{showLabel && (
<span className="w-full shrink-0 truncate text-[10px] font-medium text-muted-foreground">
{label}
</span>
)}
<div className="flex min-w-0 flex-1 items-center justify-center gap-2">
{chipItems.map((item) => {
const isActive = selectedValue === item.value;
return (
<button
key={item.value}
type="button"
onClick={() => emitFilter(item.value)}
className={cn(
"flex min-w-[60px] flex-col items-center justify-center rounded-lg px-3 py-1.5 transition-colors",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "bg-muted/60 text-muted-foreground hover:bg-accent"
)}
>
{showCount && (
<span className="text-lg font-bold leading-tight">
{item.count}
</span>
)}
<span className="text-[10px] font-medium leading-tight">
{item.label}
</span>
</button>
);
})}
</div>
</div>
);
}

View File

@ -1,489 +0,0 @@
"use client";
import { useState, useEffect, useMemo, useCallback } from "react";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Plus, Trash2, Loader2, AlertTriangle, RefreshCw } from "lucide-react";
import { getTableColumns } from "@/lib/api/tableManagement";
import { dataApi } from "@/lib/api/data";
import type { ColumnTypeInfo } from "@/lib/api/tableManagement";
import type { StatusBarConfig, StatusChipStyle, StatusChipOption } from "./types";
import { DEFAULT_STATUS_BAR_CONFIG, STATUS_CHIP_STYLE_LABELS } from "./types";
interface ConfigPanelProps {
config: StatusBarConfig | undefined;
onUpdate: (config: StatusBarConfig) => void;
allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[];
connections?: import("@/components/pop/designer/types/pop-layout").PopDataConnection[];
componentId?: string;
}
export function PopStatusBarConfigPanel({
config: rawConfig,
onUpdate,
allComponents,
connections,
componentId,
}: ConfigPanelProps) {
const cfg = { ...DEFAULT_STATUS_BAR_CONFIG, ...(rawConfig || {}) };
const update = (partial: Partial<StatusBarConfig>) => {
onUpdate({ ...cfg, ...partial });
};
const options = cfg.options || [];
const removeOption = (index: number) => {
update({ options: options.filter((_, i) => i !== index) });
};
const updateOption = (
index: number,
field: keyof StatusChipOption,
val: string
) => {
update({
options: options.map((opt, i) =>
i === index ? { ...opt, [field]: val } : opt
),
});
};
// 연결된 카드 컴포넌트의 테이블 컬럼 가져오기
const connectedTableName = useMemo(() => {
if (!componentId || !connections || !allComponents) return null;
const targetIds = connections
.filter((c) => c.sourceComponent === componentId)
.map((c) => c.targetComponent);
const sourceIds = connections
.filter((c) => c.targetComponent === componentId)
.map((c) => c.sourceComponent);
const peerIds = [...new Set([...targetIds, ...sourceIds])];
for (const pid of peerIds) {
const comp = allComponents.find((c) => c.id === pid);
if (!comp?.config) continue;
const compCfg = comp.config as Record<string, unknown>;
const ds = compCfg.dataSource as { tableName?: string } | undefined;
if (ds?.tableName) return ds.tableName;
}
return null;
}, [componentId, connections, allComponents]);
const [targetColumns, setTargetColumns] = useState<ColumnTypeInfo[]>([]);
const [columnsLoading, setColumnsLoading] = useState(false);
// 집계 컬럼의 고유값 (옵션 선택용)
const [distinctValues, setDistinctValues] = useState<string[]>([]);
const [distinctLoading, setDistinctLoading] = useState(false);
useEffect(() => {
if (!connectedTableName) {
setTargetColumns([]);
return;
}
let cancelled = false;
setColumnsLoading(true);
getTableColumns(connectedTableName)
.then((res) => {
if (cancelled) return;
if (res.success && res.data?.columns) {
setTargetColumns(res.data.columns);
}
})
.finally(() => {
if (!cancelled) setColumnsLoading(false);
});
return () => {
cancelled = true;
};
}, [connectedTableName]);
const fetchDistinctValues = useCallback(async (tableName: string, column: string) => {
setDistinctLoading(true);
try {
const res = await dataApi.getTableData(tableName, { page: 1, size: 9999 });
const vals = new Set<string>();
for (const row of res.data) {
const v = row[column];
if (v != null && String(v).trim() !== "") {
vals.add(String(v));
}
}
const sorted = [...vals].sort();
setDistinctValues(sorted);
return sorted;
} catch {
setDistinctValues([]);
return [];
} finally {
setDistinctLoading(false);
}
}, []);
// 집계 컬럼 변경 시 고유값 새로 가져오기
useEffect(() => {
const col = cfg.countColumn;
if (!connectedTableName || !col) {
setDistinctValues([]);
return;
}
fetchDistinctValues(connectedTableName, col);
}, [connectedTableName, cfg.countColumn, fetchDistinctValues]);
const handleAutoFill = useCallback(async () => {
if (!connectedTableName || !cfg.countColumn) return;
const vals = await fetchDistinctValues(connectedTableName, cfg.countColumn);
if (vals.length === 0) return;
const newOptions: StatusChipOption[] = vals.map((v) => {
const existing = options.find((o) => o.value === v);
return { value: v, label: existing?.label || v };
});
update({ options: newOptions });
}, [connectedTableName, cfg.countColumn, options, fetchDistinctValues]);
const addOptionFromValue = (value: string) => {
if (options.some((o) => o.value === value)) return;
update({
options: [...options, { value, label: value }],
});
};
return (
<div className="space-y-4">
{/* --- 칩 옵션 목록 --- */}
<div className="space-y-1">
<div className="flex items-center justify-between">
<Label className="text-[10px]"> </Label>
{connectedTableName && cfg.countColumn && (
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-[9px]"
onClick={handleAutoFill}
disabled={distinctLoading}
>
{distinctLoading ? (
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
) : (
<RefreshCw className="mr-1 h-3 w-3" />
)}
DB에서
</Button>
)}
</div>
{cfg.useSubCount && (
<div className="flex items-start gap-1.5 rounded border border-amber-200 bg-amber-50 p-2">
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
<p className="text-[9px] text-amber-700">
. DB .
</p>
</div>
)}
{options.length === 0 && (
<p className="text-[9px] text-muted-foreground">
{connectedTableName && cfg.countColumn
? "\"DB에서 자동 채우기\"를 클릭하거나 아래에서 추가하세요."
: "옵션이 없습니다. 먼저 집계 컬럼을 선택한 후 추가하세요."}
</p>
)}
{options.map((opt, i) => (
<div key={i} className="flex items-center gap-1">
<Input
value={opt.value}
onChange={(e) => updateOption(i, "value", e.target.value)}
placeholder="DB 값"
className="h-7 flex-1 text-[10px]"
/>
<Input
value={opt.label}
onChange={(e) => updateOption(i, "label", e.target.value)}
placeholder="표시 라벨"
className="h-7 flex-1 text-[10px]"
/>
<button
type="button"
onClick={() => removeOption(i)}
className="flex h-7 w-7 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-3 w-3" />
</button>
</div>
))}
{/* 고유값에서 추가 */}
{distinctValues.length > 0 && (
<div className="space-y-1">
<Label className="text-[9px] text-muted-foreground">
(DB에서 )
</Label>
<div className="flex flex-wrap gap-1">
{distinctValues
.filter((dv) => !options.some((o) => o.value === dv))
.map((dv) => (
<button
key={dv}
type="button"
onClick={() => addOptionFromValue(dv)}
className="flex h-6 items-center gap-1 rounded-full border border-dashed px-2 text-[9px] text-muted-foreground transition-colors hover:border-primary hover:text-primary"
>
<Plus className="h-2.5 w-2.5" />
{dv}
</button>
))}
{distinctValues.every((dv) => options.some((o) => o.value === dv)) && (
<p className="text-[9px] text-muted-foreground"> </p>
)}
</div>
</div>
)}
{/* 수동 추가 */}
<Button
variant="outline"
size="sm"
className="h-7 w-full text-[10px]"
onClick={() => {
update({
options: [
...options,
{ value: "", label: "" },
],
});
}}
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{/* --- 전체 보기 칩 --- */}
<div className="space-y-1">
<div className="flex items-center gap-2">
<Checkbox
id="allowAll"
checked={cfg.allowAll !== false}
onCheckedChange={(checked) => update({ allowAll: Boolean(checked) })}
/>
<Label htmlFor="allowAll" className="text-[10px]">
&quot;&quot;
</Label>
</div>
<p className="pl-5 text-[9px] text-muted-foreground">
</p>
{cfg.allowAll !== false && (
<div className="space-y-1 pl-5">
<Label className="text-[9px] text-muted-foreground">
</Label>
<Input
value={cfg.allLabel || ""}
onChange={(e) => update({ allLabel: e.target.value })}
placeholder="전체"
className="h-7 text-[10px]"
/>
</div>
)}
</div>
{/* --- 건수 표시 --- */}
<div className="flex items-center gap-2">
<Checkbox
id="showCount"
checked={cfg.showCount !== false}
onCheckedChange={(checked) => update({ showCount: Boolean(checked) })}
/>
<Label htmlFor="showCount" className="text-[10px]">
</Label>
</div>
{cfg.showCount !== false && (
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
{columnsLoading ? (
<div className="flex h-8 items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
) : targetColumns.length > 0 ? (
<Select
value={cfg.countColumn || ""}
onValueChange={(v) => update({ countColumn: v })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="집계 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{targetColumns.map((col) => (
<SelectItem
key={col.columnName}
value={col.columnName}
className="text-xs"
>
{col.displayName || col.columnName}
<span className="ml-1 text-muted-foreground">
({col.columnName})
</span>
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={cfg.countColumn || ""}
onChange={(e) => update({ countColumn: e.target.value })}
placeholder="예: status"
className="h-8 text-xs"
/>
)}
<p className="text-[9px] text-muted-foreground">
</p>
</div>
)}
{cfg.showCount !== false && (
<div className="space-y-2 rounded bg-muted/30 p-2">
<div className="flex items-center gap-2">
<Checkbox
id="useSubCount"
checked={cfg.useSubCount || false}
onCheckedChange={(checked) =>
update({ useSubCount: Boolean(checked) })
}
/>
<Label htmlFor="useSubCount" className="text-[10px]">
</Label>
</div>
{cfg.useSubCount && (
<>
<p className="pl-5 text-[9px] text-muted-foreground">
</p>
<div className="mt-1 flex items-center gap-2 pl-5">
<Checkbox
id="hideUntilSubFilter"
checked={cfg.hideUntilSubFilter || false}
onCheckedChange={(checked) =>
update({ hideUntilSubFilter: Boolean(checked) })
}
/>
<Label htmlFor="hideUntilSubFilter" className="text-[10px]">
</Label>
</div>
{cfg.hideUntilSubFilter && (
<div className="space-y-1 pl-5">
<Label className="text-[9px] text-muted-foreground">
</Label>
<Input
value={cfg.hiddenMessage || ""}
onChange={(e) => update({ hiddenMessage: e.target.value })}
placeholder="조건을 선택하면 상태별 현황이 표시됩니다"
className="h-7 text-[10px]"
/>
</div>
)}
</>
)}
</div>
)}
{/* --- 칩 스타일 --- */}
<div className="space-y-1">
<Label className="text-[10px]"> </Label>
<Select
value={cfg.chipStyle || "tab"}
onValueChange={(v) => update({ chipStyle: v as StatusChipStyle })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
{Object.entries(STATUS_CHIP_STYLE_LABELS).map(([key, label]) => (
<SelectItem key={key} value={key} className="text-xs">
{label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-[9px] text-muted-foreground">
: + / 알약: 작은
</p>
</div>
{/* --- 필터 컬럼 --- */}
<div className="space-y-1 border-t pt-3">
<Label className="text-[10px]"> </Label>
{!connectedTableName && (
<div className="flex items-start gap-1.5 rounded border border-amber-200 bg-amber-50 p-2">
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-amber-500" />
<p className="text-[9px] text-amber-700">
.
</p>
</div>
)}
{connectedTableName && (
<>
{columnsLoading ? (
<div className="flex h-8 items-center gap-2 text-xs text-muted-foreground">
<Loader2 className="h-3 w-3 animate-spin" />
...
</div>
) : targetColumns.length > 0 ? (
<Select
value={cfg.filterColumn || cfg.countColumn || ""}
onValueChange={(v) => update({ filterColumn: v })}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="필터 컬럼 선택" />
</SelectTrigger>
<SelectContent>
{targetColumns.map((col) => (
<SelectItem
key={col.columnName}
value={col.columnName}
className="text-xs"
>
{col.displayName || col.columnName}
<span className="ml-1 text-muted-foreground">
({col.columnName})
</span>
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
value={cfg.filterColumn || ""}
onChange={(e) => update({ filterColumn: e.target.value })}
placeholder="예: status"
className="h-8 text-xs"
/>
)}
<p className="text-[9px] text-muted-foreground">
(
)
</p>
</>
)}
</div>
</div>
);
}

View File

@ -1,87 +0,0 @@
"use client";
import { PopComponentRegistry } from "../../PopComponentRegistry";
import { PopStatusBarComponent } from "./PopStatusBarComponent";
import { PopStatusBarConfigPanel } from "./PopStatusBarConfig";
import type { StatusBarConfig } from "./types";
import { DEFAULT_STATUS_BAR_CONFIG } from "./types";
function PopStatusBarPreviewComponent({
config,
label,
}: {
config?: StatusBarConfig;
label?: string;
}) {
const cfg = config || DEFAULT_STATUS_BAR_CONFIG;
const options = cfg.options || [];
const displayLabel = label || "상태 바";
return (
<div className="flex h-full w-full flex-col items-center justify-center gap-1 p-2">
<span className="text-[10px] font-medium text-muted-foreground">
{displayLabel}
</span>
<div className="flex items-center gap-1">
{options.length === 0 ? (
<span className="text-[9px] text-muted-foreground">
</span>
) : (
options.slice(0, 4).map((opt) => (
<div
key={opt.value}
className="flex flex-col items-center rounded bg-muted/60 px-2 py-0.5"
>
<span className="text-[10px] font-bold leading-tight">0</span>
<span className="text-[8px] leading-tight text-muted-foreground">
{opt.label}
</span>
</div>
))
)}
</div>
</div>
);
}
PopComponentRegistry.registerComponent({
id: "pop-status-bar",
name: "상태 바",
description: "상태별 건수 대시보드 + 필터",
category: "display",
icon: "BarChart3",
component: PopStatusBarComponent,
configPanel: PopStatusBarConfigPanel,
preview: PopStatusBarPreviewComponent,
defaultProps: DEFAULT_STATUS_BAR_CONFIG,
connectionMeta: {
sendable: [
{
key: "filter_value",
label: "필터 값",
type: "filter_value",
category: "filter",
description: "선택한 상태 칩 값을 카드에 필터로 전달",
},
],
receivable: [
{
key: "all_rows",
label: "전체 데이터",
type: "all_rows",
category: "data",
description: "연결된 카드의 전체 데이터를 받아 상태별 건수 집계",
},
{
key: "set_value",
label: "값 설정",
type: "filter_value",
category: "filter",
description: "외부에서 선택 값 설정",
},
],
},
touchOptimized: true,
supportedDevices: ["mobile", "tablet"],
});

View File

@ -1,48 +0,0 @@
// ===== pop-status-bar 전용 타입 =====
// 상태 칩 대시보드 컴포넌트. 카드 데이터를 집계하여 상태별 건수 표시 + 필터 발행.
/** 상태 칩 표시 스타일 */
export type StatusChipStyle = "tab" | "pill";
/** 개별 옵션 */
export interface StatusChipOption {
value: string;
label: string;
}
/** status-bar 전용 설정 */
export interface StatusBarConfig {
showCount?: boolean;
countColumn?: string;
allowAll?: boolean;
allLabel?: string;
chipStyle?: StatusChipStyle;
/** 하위 필터 적용 시 집계 컬럼 자동 전환 (카드가 전달하는 가상 컬럼 사용) */
useSubCount?: boolean;
/** 하위 필터(공정 선택 등)가 활성화되기 전까지 칩을 숨김 */
hideUntilSubFilter?: boolean;
/** 칩 숨김 상태일 때 표시할 안내 문구 */
hiddenMessage?: string;
options?: StatusChipOption[];
/** 필터 대상 컬럼명 (기본: countColumn) */
filterColumn?: string;
/** 추가 필터 대상 컬럼 (하위 테이블 등) */
filterColumns?: string[];
}
/** 기본 설정값 */
export const DEFAULT_STATUS_BAR_CONFIG: StatusBarConfig = {
showCount: true,
allowAll: true,
allLabel: "전체",
chipStyle: "tab",
options: [],
};
/** 칩 스타일 라벨 (설정 패널용) */
export const STATUS_CHIP_STYLE_LABELS: Record<StatusChipStyle, string> = {
tab: "탭 (큰 숫자)",
pill: "알약 (작은 뱃지)",
};