refactor(pop): status-chip을 pop-status-bar 독립 컴포넌트로 분리 + 카운트 순환 문제 수정
pop-search에 내장되어 있던 status-chip 기능을 pop-status-bar라는
독립 컴포넌트로 분리하여 재사용성과 설정 유연성을 높인다.
상태 칩 클릭 시 카운트가 왜곡되던 순환 의존 문제를 해결한다.
[pop-status-bar 신규 컴포넌트]
- types.ts: StatusBarConfig, StatusChipOption, hiddenMessage 등 타입 정의
- PopStatusBarComponent: all_rows 구독 + 카운트 집계 + filter_value 발행
_source: "status-bar" 마커로 자신의 필터를 식별
hideUntilSubFilter: 하위 필터 선택 전 칩 숨김 + 설정 가능 안내 문구
- PopStatusBarConfig: 설정 패널 (DB 자동 채우기, 고유값 클릭 추가,
숨김 문구 설정, 하위 필터 가상 컬럼 안내)
- index.tsx: 레지스트리 등록, connectionMeta(filter_value/all_rows/set_value)
[카운트 순환 문제 수정]
- PopCardListV2Component: externalFilters에 _source 필드 저장
all_rows 발행 시 status-bar 소스 필터를 제외한 rowsForStatusCount 계산
상태 칩 클릭해도 전체 카운트가 유지됨
[pop-search에서 status-chip 제거]
- PopSearchComponent: StatusChipInput, allRows 구독, autoSubStatusColumn 제거
- PopSearchConfig: StatusChipDetailSettings 제거, 분리 안내 메시지로 대체
- index.tsx: receivable에서 all_rows 제거
- types.ts: StatusChipStyle, StatusChipConfig에 @deprecated 주석 추가
[설정 UX 개선]
- "전체 칩 자동 추가" → "전체 보기 칩 표시" + 설명 문구 추가
- hiddenMessage: 숨김 상태 안내 문구 설정 가능 (하드코딩 제거)
- useSubCount 활성 시 가상 컬럼 안내 경고 표시
2026-03-11 16:35:49 +09:00
|
|
|
"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;
|
refactor: POP 그리드 시스템 명칭 통일 + Dead Code 제거
V5→V6 전환 과정에서 누적된 버전 접미사, 미사용 함수, 레거시 잔재를
정리하여 코드 관리성을 확보한다. 14개 파일 수정, 365줄 순감.
[타입 리네이밍] (14개 파일)
- PopLayoutDataV5 → PopLayoutData
- PopComponentDefinitionV5 → PopComponentDefinition
- PopGlobalSettingsV5 → PopGlobalSettings
- PopModeOverrideV5 → PopModeOverride
- createEmptyPopLayoutV5 → createEmptyLayout
- isV5Layout → isPopLayout
- addComponentToV5Layout → addComponentToLayout
- createComponentDefinitionV5 → createComponentDefinition
- 구 이름은 deprecated 별칭으로 유지 (하위 호환)
[Dead Code 삭제] (gridUtils.ts -350줄)
- getAdjustedBreakpoint, convertPositionToMode, isOutOfBounds,
mouseToGridPosition, gridToPixelPosition, isValidPosition,
clampPosition, autoLayoutComponents (전부 외부 사용 0건)
- needsReview + ReviewPanel/ReviewItem (항상 false, 미사용)
- getEffectiveComponentPosition export → 내부 함수로 전환
[레거시 로더 분리] (신규 legacyLoader.ts)
- convertV5LayoutToV6 → loadLegacyLayout (legacyLoader.ts)
- V5 변환 상수/함수를 gridUtils에서 분리
[주석 정리]
- "v5 그리드" → "POP 블록 그리드"
- "하위 호환용" → "뷰포트 프리셋" / "레이아웃 설정용"
- 파일 헤더, 섹션 구분, 함수 JSDoc 정리
기능 변경 0건. DB 변경 0건. 백엔드 변경 0건.
2026-03-13 16:32:20 +09:00
|
|
|
allComponents?: import("@/components/pop/designer/types/pop-layout").PopComponentDefinition[];
|
refactor(pop): status-chip을 pop-status-bar 독립 컴포넌트로 분리 + 카운트 순환 문제 수정
pop-search에 내장되어 있던 status-chip 기능을 pop-status-bar라는
독립 컴포넌트로 분리하여 재사용성과 설정 유연성을 높인다.
상태 칩 클릭 시 카운트가 왜곡되던 순환 의존 문제를 해결한다.
[pop-status-bar 신규 컴포넌트]
- types.ts: StatusBarConfig, StatusChipOption, hiddenMessage 등 타입 정의
- PopStatusBarComponent: all_rows 구독 + 카운트 집계 + filter_value 발행
_source: "status-bar" 마커로 자신의 필터를 식별
hideUntilSubFilter: 하위 필터 선택 전 칩 숨김 + 설정 가능 안내 문구
- PopStatusBarConfig: 설정 패널 (DB 자동 채우기, 고유값 클릭 추가,
숨김 문구 설정, 하위 필터 가상 컬럼 안내)
- index.tsx: 레지스트리 등록, connectionMeta(filter_value/all_rows/set_value)
[카운트 순환 문제 수정]
- PopCardListV2Component: externalFilters에 _source 필드 저장
all_rows 발행 시 status-bar 소스 필터를 제외한 rowsForStatusCount 계산
상태 칩 클릭해도 전체 카운트가 유지됨
[pop-search에서 status-chip 제거]
- PopSearchComponent: StatusChipInput, allRows 구독, autoSubStatusColumn 제거
- PopSearchConfig: StatusChipDetailSettings 제거, 분리 안내 메시지로 대체
- index.tsx: receivable에서 all_rows 제거
- types.ts: StatusChipStyle, StatusChipConfig에 @deprecated 주석 추가
[설정 UX 개선]
- "전체 칩 자동 추가" → "전체 보기 칩 표시" + 설명 문구 추가
- hiddenMessage: 숨김 상태 안내 문구 설정 가능 (하드코딩 제거)
- useSubCount 활성 시 가상 컬럼 안내 경고 표시
2026-03-11 16:35:49 +09:00
|
|
|
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]">
|
|
|
|
|
"전체" 보기 칩 표시
|
|
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
}
|