feat(pop): pop-search status-chip 입력 타입 추가 + all_rows 이벤트 연동
pop-search 컴포넌트에 status-chip 입력 타입을 추가하여 연결된 카드의 전체 데이터를 구독하고 상태별 건수를 집계/표시한다. 칩 클릭 시 filter_value를 발행하여 카드 목록을 필터링한다. [status-chip 입력 타입] - types.ts: StatusChipStyle, StatusChipConfig, STATUS_CHIP_STYLE_LABELS - PopSearchComponent: StatusChipInput 컴포넌트 (allRows 구독 + 건수 집계) - PopSearchConfig: StatusChipDetailSettings 설정 패널 (칩 옵션/스타일) - index.tsx: receivable에 all_rows 이벤트 등록 [all_rows 이벤트] - pop-card-list-v2: 데이터 로드 시 all_rows publish + sendable 등록 - pop-card-list: 데이터 로드 시 all_rows publish + sendable 등록 - useConnectionResolver: all_rows 타입 자동 매칭 로직 추가 [pop-card-list-v2 개선] - 하위 테이블 필터 적용 시 __subStatus__ 가상 컬럼 주입 - externalFilters에 하위 테이블 조건 분리 처리
This commit is contained in:
parent
ed3707a681
commit
c17dd86859
|
|
@ -60,6 +60,9 @@ function getAutoMatchPairs(
|
|||
if (s.type === "filter_value" && r.type === "filter_value") {
|
||||
pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: true });
|
||||
}
|
||||
if (s.type === "all_rows" && r.type === "all_rows") {
|
||||
pairs.push({ sourceKey: s.key, targetKey: r.key, isFilter: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,11 +108,17 @@ export function useConnectionResolver({
|
|||
const fieldName = data?.fieldName as string | undefined;
|
||||
const filterColumns = data?.filterColumns as string[] | undefined;
|
||||
const filterMode = (data?.filterMode as string) || "contains";
|
||||
// conn.filterConfig에 targetColumn이 명시되어 있으면 우선 사용
|
||||
const effectiveColumn = conn.filterConfig?.targetColumn || fieldName;
|
||||
const effectiveMode = conn.filterConfig?.filterMode || filterMode;
|
||||
const baseFilterConfig = effectiveColumn
|
||||
? { targetColumn: effectiveColumn, targetColumns: conn.filterConfig?.targetColumns || (filterColumns?.length ? filterColumns : [effectiveColumn]), filterMode: effectiveMode }
|
||||
: conn.filterConfig;
|
||||
publish(targetEvent, {
|
||||
value: payload,
|
||||
filterConfig: fieldName
|
||||
? { targetColumn: fieldName, targetColumns: filterColumns?.length ? filterColumns : [fieldName], filterMode }
|
||||
: conn.filterConfig,
|
||||
filterConfig: conn.filterConfig?.isSubTable
|
||||
? { ...baseFilterConfig, isSubTable: true }
|
||||
: baseFilterConfig,
|
||||
_connectionId: conn.id,
|
||||
});
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ export function PopCardListV2Component({
|
|||
Map<string, {
|
||||
fieldName: string;
|
||||
value: unknown;
|
||||
filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string };
|
||||
filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string; isSubTable?: boolean };
|
||||
}>
|
||||
>(new Map());
|
||||
|
||||
|
|
@ -143,7 +143,7 @@ export function PopCardListV2Component({
|
|||
(payload: unknown) => {
|
||||
const data = payload as {
|
||||
value?: { fieldName?: string; value?: unknown };
|
||||
filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string };
|
||||
filterConfig?: { targetColumn: string; targetColumns?: string[]; filterMode: string; isSubTable?: boolean };
|
||||
_connectionId?: string;
|
||||
};
|
||||
const connId = data?._connectionId || "default";
|
||||
|
|
@ -165,6 +165,12 @@ export function PopCardListV2Component({
|
|||
return unsub;
|
||||
}, [componentId, subscribe]);
|
||||
|
||||
// 전체 rows 발행 (status-chip 등 연결된 컴포넌트에서 건수 집계용)
|
||||
useEffect(() => {
|
||||
if (!componentId || loading) return;
|
||||
publish(`__comp_output__${componentId}__all_rows`, rows);
|
||||
}, [componentId, rows, loading, publish]);
|
||||
|
||||
const cartRef = useRef(cart);
|
||||
cartRef.current = cart;
|
||||
|
||||
|
|
@ -235,31 +241,75 @@ export function PopCardListV2Component({
|
|||
const gridColumns = Math.max(1, Math.min(autoColumns, maxGridColumns, maxAllowedColumns));
|
||||
const gridRows = configGridRows;
|
||||
|
||||
// 외부 필터
|
||||
// 외부 필터 (메인 테이블 + 하위 테이블 분기)
|
||||
const filteredRows = useMemo(() => {
|
||||
if (externalFilters.size === 0) return rows;
|
||||
|
||||
const allFilters = [...externalFilters.values()];
|
||||
return rows.filter((row) =>
|
||||
allFilters.every((filter) => {
|
||||
const searchValue = String(filter.value).toLowerCase();
|
||||
if (!searchValue) return true;
|
||||
const fc = filter.filterConfig;
|
||||
const columns: string[] =
|
||||
fc?.targetColumns?.length ? fc.targetColumns
|
||||
: fc?.targetColumn ? [fc.targetColumn]
|
||||
: filter.fieldName ? [filter.fieldName] : [];
|
||||
if (columns.length === 0) return true;
|
||||
const mode = fc?.filterMode || "contains";
|
||||
return columns.some((col) => {
|
||||
const cellValue = String(row[col] ?? "").toLowerCase();
|
||||
switch (mode) {
|
||||
case "equals": return cellValue === searchValue;
|
||||
case "starts_with": return cellValue.startsWith(searchValue);
|
||||
default: return cellValue.includes(searchValue);
|
||||
}
|
||||
const mainFilters = allFilters.filter((f) => !f.filterConfig?.isSubTable);
|
||||
const subFilters = allFilters.filter((f) => f.filterConfig?.isSubTable);
|
||||
|
||||
return rows
|
||||
.map((row) => {
|
||||
// 1) 메인 테이블 필터
|
||||
const passMain = mainFilters.every((filter) => {
|
||||
const searchValue = String(filter.value).toLowerCase();
|
||||
if (!searchValue) return true;
|
||||
const fc = filter.filterConfig;
|
||||
const columns: string[] =
|
||||
fc?.targetColumns?.length ? fc.targetColumns
|
||||
: fc?.targetColumn ? [fc.targetColumn]
|
||||
: filter.fieldName ? [filter.fieldName] : [];
|
||||
if (columns.length === 0) return true;
|
||||
const mode = fc?.filterMode || "contains";
|
||||
return columns.some((col) => {
|
||||
const cellValue = String(row[col] ?? "").toLowerCase();
|
||||
switch (mode) {
|
||||
case "equals": return cellValue === searchValue;
|
||||
case "starts_with": return cellValue.startsWith(searchValue);
|
||||
default: return cellValue.includes(searchValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (!passMain) return null;
|
||||
|
||||
// 2) 하위 테이블 필터 없으면 그대로 반환
|
||||
if (subFilters.length === 0) return row;
|
||||
|
||||
// 3) __processFlow__에서 모든 하위 필터 조건을 만족하는 step 탐색
|
||||
const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined;
|
||||
if (!processFlow || processFlow.length === 0) return null;
|
||||
|
||||
const matchingSteps = processFlow.filter((step) =>
|
||||
subFilters.every((filter) => {
|
||||
const searchValue = String(filter.value).toLowerCase();
|
||||
if (!searchValue) return true;
|
||||
const fc = filter.filterConfig;
|
||||
const col = fc?.targetColumn || filter.fieldName || "";
|
||||
if (!col) return true;
|
||||
const cellValue = String(step.rawData?.[col] ?? "").toLowerCase();
|
||||
const mode = fc?.filterMode || "contains";
|
||||
switch (mode) {
|
||||
case "equals": return cellValue === searchValue;
|
||||
case "starts_with": return cellValue.startsWith(searchValue);
|
||||
default: return cellValue.includes(searchValue);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
if (matchingSteps.length === 0) return null;
|
||||
|
||||
// 매칭된 step 중 첫 번째의 상태를 __subStatus__/__subSemantic__으로 주입
|
||||
const matched = matchingSteps[0];
|
||||
return {
|
||||
...row,
|
||||
__subStatus__: matched.status,
|
||||
__subSemantic__: matched.semantic || "pending",
|
||||
__subProcessName__: matched.processName,
|
||||
__subSeqNo__: matched.seqNo,
|
||||
};
|
||||
})
|
||||
.filter((row): row is RowData => row !== null);
|
||||
}, [rows, externalFilters]);
|
||||
|
||||
const overflowCfg = effectiveConfig?.overflow;
|
||||
|
|
@ -367,6 +417,7 @@ export function PopCardListV2Component({
|
|||
status: normalizedStatus,
|
||||
semantic: semantic as "pending" | "active" | "done",
|
||||
isCurrent: semantic === "active",
|
||||
rawData: p as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ PopComponentRegistry.registerComponent({
|
|||
connectionMeta: {
|
||||
sendable: [
|
||||
{ key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" },
|
||||
{ key: "all_rows", label: "전체 데이터", type: "all_rows", category: "data", description: "필터 적용 전 전체 데이터 배열 (상태 칩 건수 등)" },
|
||||
{ key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "장바구니 변경 시 count/isDirty 전달" },
|
||||
{ key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "장바구니 DB 저장 완료 후 결과 전달" },
|
||||
{ key: "selected_items", label: "선택된 항목", type: "event", category: "event", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" },
|
||||
|
|
|
|||
|
|
@ -256,6 +256,12 @@ export function PopCardListComponent({
|
|||
return unsub;
|
||||
}, [componentId, subscribe]);
|
||||
|
||||
// 전체 rows 발행 (status-chip 등 연결된 컴포넌트에서 건수 집계용)
|
||||
useEffect(() => {
|
||||
if (!componentId || loading) return;
|
||||
publish(`__comp_output__${componentId}__all_rows`, rows);
|
||||
}, [componentId, rows, loading, publish]);
|
||||
|
||||
// cart를 ref로 유지: 이벤트 콜백에서 항상 최신 참조를 사용
|
||||
const cartRef = useRef(cart);
|
||||
cartRef.current = cart;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ PopComponentRegistry.registerComponent({
|
|||
connectionMeta: {
|
||||
sendable: [
|
||||
{ key: "selected_row", label: "선택된 행", type: "selected_row", category: "data", description: "사용자가 선택한 카드의 행 데이터를 전달" },
|
||||
{ key: "all_rows", label: "전체 데이터", type: "all_rows", category: "data", description: "필터 적용 전 전체 데이터 배열 (상태 칩 건수 등)" },
|
||||
{ key: "cart_updated", label: "장바구니 상태", type: "event", category: "event", description: "장바구니 변경 시 count/isDirty 전달" },
|
||||
{ key: "cart_save_completed", label: "저장 완료", type: "event", category: "event", description: "장바구니 DB 저장 완료 후 결과 전달" },
|
||||
{ key: "selected_items", label: "선택된 항목", type: "event", category: "event", description: "장바구니 모드에서 체크박스로 선택된 항목 배열" },
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ import type {
|
|||
ModalSelectConfig,
|
||||
ModalSearchMode,
|
||||
ModalFilterTab,
|
||||
SelectOption,
|
||||
StatusChipConfig,
|
||||
} from "./types";
|
||||
import {
|
||||
DATE_PRESET_LABELS,
|
||||
|
|
@ -147,6 +149,24 @@ export function PopSearchComponent({
|
|||
return unsub;
|
||||
}, [subscribe, emitFilterChanged, config.fieldName, config.filterColumns, isModalType]);
|
||||
|
||||
// status-chip: 연결된 카드 컴포넌트의 전체 rows 수신
|
||||
const [allRows, setAllRows] = useState<Record<string, unknown>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!componentId || normalizedType !== "status-chip") return;
|
||||
const unsub = subscribe(
|
||||
`__comp_input__${componentId}__all_rows`,
|
||||
(payload: unknown) => {
|
||||
const data = payload as { value?: unknown } | unknown;
|
||||
const rows = (typeof data === "object" && data && "value" in data)
|
||||
? (data as { value: unknown }).value
|
||||
: data;
|
||||
if (Array.isArray(rows)) setAllRows(rows);
|
||||
}
|
||||
);
|
||||
return unsub;
|
||||
}, [componentId, subscribe, normalizedType]);
|
||||
|
||||
const handleModalOpen = useCallback(() => {
|
||||
if (!config.modalConfig) return;
|
||||
setSimpleModalOpen(true);
|
||||
|
|
@ -189,6 +209,7 @@ export function PopSearchComponent({
|
|||
modalDisplayText={modalDisplayText}
|
||||
onModalOpen={handleModalOpen}
|
||||
onModalClear={handleModalClear}
|
||||
allRows={allRows}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -218,7 +239,11 @@ interface InputRendererProps {
|
|||
onModalClear?: () => void;
|
||||
}
|
||||
|
||||
function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModalOpen, onModalClear }: InputRendererProps) {
|
||||
interface InputRendererPropsExt extends InputRendererProps {
|
||||
allRows?: Record<string, unknown>[];
|
||||
}
|
||||
|
||||
function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModalOpen, onModalClear, allRows }: InputRendererPropsExt) {
|
||||
const normalized = normalizeInputType(config.inputType as string);
|
||||
switch (normalized) {
|
||||
case "text":
|
||||
|
|
@ -238,6 +263,8 @@ function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModa
|
|||
return <ToggleSearchInput value={Boolean(value)} onChange={onChange} />;
|
||||
case "modal":
|
||||
return <ModalSearchInput config={config} displayText={modalDisplayText || ""} onClick={onModalOpen} onClear={onModalClear} />;
|
||||
case "status-chip":
|
||||
return <StatusChipInput config={config} value={String(value ?? "")} onChange={onChange} allRows={allRows || []} />;
|
||||
default:
|
||||
return <PlaceholderInput inputType={config.inputType} />;
|
||||
}
|
||||
|
|
@ -651,6 +678,118 @@ function ModalSearchInput({ config, displayText, onClick, onClear }: { config: P
|
|||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// status-chip 서브타입
|
||||
// ========================================
|
||||
|
||||
function StatusChipInput({
|
||||
config,
|
||||
value,
|
||||
onChange,
|
||||
allRows,
|
||||
}: {
|
||||
config: PopSearchConfig;
|
||||
value: string;
|
||||
onChange: (v: unknown) => void;
|
||||
allRows: Record<string, unknown>[];
|
||||
}) {
|
||||
const chipCfg: StatusChipConfig = config.statusChipConfig || {};
|
||||
const chipStyle = chipCfg.chipStyle || "tab";
|
||||
const showCount = chipCfg.showCount !== false;
|
||||
const countColumn = chipCfg.countColumn || config.fieldName || "";
|
||||
const allowAll = chipCfg.allowAll !== false;
|
||||
const allLabel = chipCfg.allLabel || "전체";
|
||||
|
||||
const options: SelectOption[] = config.options || [];
|
||||
|
||||
const counts = useMemo(() => {
|
||||
if (!showCount || !countColumn || allRows.length === 0) return new Map<string, number>();
|
||||
const map = new Map<string, number>();
|
||||
for (const row of allRows) {
|
||||
const v = String(row[countColumn] ?? "");
|
||||
map.set(v, (map.get(v) || 0) + 1);
|
||||
}
|
||||
return map;
|
||||
}, [allRows, countColumn, showCount]);
|
||||
|
||||
const totalCount = allRows.length;
|
||||
|
||||
const chipItems: { value: string; label: string; count: number }[] = 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]);
|
||||
|
||||
if (chipStyle === "pill") {
|
||||
return (
|
||||
<div className="flex h-full flex-wrap items-center gap-1.5">
|
||||
{chipItems.map((item) => {
|
||||
const isActive = value === item.value;
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => onChange(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>
|
||||
);
|
||||
}
|
||||
|
||||
// tab 스타일 (기본)
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center gap-2">
|
||||
{chipItems.map((item) => {
|
||||
const isActive = value === item.value;
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
onClick={() => onChange(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>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// 미구현 서브타입 플레이스홀더
|
||||
// ========================================
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import type {
|
|||
ModalDisplayStyle,
|
||||
ModalSearchMode,
|
||||
ModalFilterTab,
|
||||
StatusChipStyle,
|
||||
StatusChipConfig,
|
||||
} from "./types";
|
||||
import {
|
||||
SEARCH_INPUT_TYPE_LABELS,
|
||||
|
|
@ -46,6 +48,7 @@ import {
|
|||
MODAL_DISPLAY_STYLE_LABELS,
|
||||
MODAL_SEARCH_MODE_LABELS,
|
||||
MODAL_FILTER_TAB_LABELS,
|
||||
STATUS_CHIP_STYLE_LABELS,
|
||||
normalizeInputType,
|
||||
} from "./types";
|
||||
import { tableManagementApi, getTableColumns } from "@/lib/api/tableManagement";
|
||||
|
|
@ -231,6 +234,8 @@ function StepDetailSettings({ cfg, update, allComponents, connections, component
|
|||
return <DatePresetDetailSettings cfg={cfg} update={update} allComponents={allComponents} connections={connections} componentId={componentId} />;
|
||||
case "modal":
|
||||
return <ModalDetailSettings cfg={cfg} update={update} />;
|
||||
case "status-chip":
|
||||
return <StatusChipDetailSettings cfg={cfg} update={update} allComponents={allComponents} connections={connections} componentId={componentId} />;
|
||||
case "toggle":
|
||||
return (
|
||||
<div className="rounded-lg bg-muted/50 p-3">
|
||||
|
|
@ -1066,3 +1071,128 @@ function ModalDetailSettings({ cfg, update }: StepProps) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// status-chip 상세 설정
|
||||
// ========================================
|
||||
|
||||
function StatusChipDetailSettings({ cfg, update, allComponents, connections, componentId }: StepProps) {
|
||||
const chipCfg: StatusChipConfig = cfg.statusChipConfig || {};
|
||||
const options = cfg.options || [];
|
||||
|
||||
const updateChip = (partial: Partial<StatusChipConfig>) => {
|
||||
update({ statusChipConfig: { ...chipCfg, ...partial } });
|
||||
};
|
||||
|
||||
const addOption = () => {
|
||||
update({
|
||||
options: [...options, { value: `status_${options.length + 1}`, label: `상태 ${options.length + 1}` }],
|
||||
});
|
||||
};
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
update({ options: options.filter((_, i) => i !== index) });
|
||||
};
|
||||
|
||||
const updateOption = (index: number, field: "value" | "label", val: string) => {
|
||||
update({ options: options.map((opt, i) => (i === index ? { ...opt, [field]: val } : opt)) });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* 칩 옵션 목록 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">칩 옵션 목록</Label>
|
||||
{options.length === 0 && (
|
||||
<p className="text-[9px] text-muted-foreground">옵션이 없습니다. 아래 버튼으로 추가하세요.</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>
|
||||
))}
|
||||
<Button variant="outline" size="sm" className="h-7 w-full text-[10px]" onClick={addOption}>
|
||||
<Plus className="mr-1 h-3 w-3" />
|
||||
옵션 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 전체 칩 자동 추가 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="allowAll"
|
||||
checked={chipCfg.allowAll !== false}
|
||||
onCheckedChange={(checked) => updateChip({ allowAll: Boolean(checked) })}
|
||||
/>
|
||||
<Label htmlFor="allowAll" className="text-[10px]">"전체" 칩 자동 추가</Label>
|
||||
</div>
|
||||
|
||||
{chipCfg.allowAll !== false && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">"전체" 라벨</Label>
|
||||
<Input
|
||||
value={chipCfg.allLabel || ""}
|
||||
onChange={(e) => updateChip({ allLabel: e.target.value })}
|
||||
placeholder="전체"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 건수 표시 */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="showCount"
|
||||
checked={chipCfg.showCount !== false}
|
||||
onCheckedChange={(checked) => updateChip({ showCount: Boolean(checked) })}
|
||||
/>
|
||||
<Label htmlFor="showCount" className="text-[10px]">건수 표시</Label>
|
||||
</div>
|
||||
|
||||
{chipCfg.showCount !== false && (
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">집계 컬럼</Label>
|
||||
<Input
|
||||
value={chipCfg.countColumn || ""}
|
||||
onChange={(e) => updateChip({ countColumn: e.target.value })}
|
||||
placeholder="예: status"
|
||||
className="h-8 text-xs"
|
||||
/>
|
||||
<p className="text-[9px] text-muted-foreground">
|
||||
연결된 카드의 이 컬럼 값으로 상태별 건수를 집계합니다
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 칩 스타일 */}
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px]">칩 스타일</Label>
|
||||
<Select
|
||||
value={chipCfg.chipStyle || "tab"}
|
||||
onValueChange={(v) => updateChip({ 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>
|
||||
|
||||
{/* 필터 연결 */}
|
||||
<FilterConnectionSection cfg={cfg} update={update} showFieldName fixedFilterMode="equals" allComponents={allComponents} connections={connections} componentId={componentId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ PopComponentRegistry.registerComponent({
|
|||
],
|
||||
receivable: [
|
||||
{ key: "set_value", label: "값 설정", type: "filter_value", category: "filter", description: "외부에서 값을 받아 검색 필드에 세팅 (스캔, 모달 선택 등)" },
|
||||
{ key: "all_rows", label: "전체 데이터", type: "all_rows", category: "data", description: "연결된 카드의 전체 데이터를 받아 상태 칩 건수 표시" },
|
||||
],
|
||||
},
|
||||
touchOptimized: true,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// ===== pop-search 전용 타입 =====
|
||||
// 단일 필드 검색 컴포넌트. 그리드 한 칸 = 검색 필드 하나.
|
||||
|
||||
/** 검색 필드 입력 타입 (9종) */
|
||||
/** 검색 필드 입력 타입 (10종) */
|
||||
export type SearchInputType =
|
||||
| "text"
|
||||
| "number"
|
||||
|
|
@ -11,7 +11,8 @@ export type SearchInputType =
|
|||
| "multi-select"
|
||||
| "combo"
|
||||
| "modal"
|
||||
| "toggle";
|
||||
| "toggle"
|
||||
| "status-chip";
|
||||
|
||||
/** 레거시 입력 타입 (DB에 저장된 기존 값 호환용) */
|
||||
export type LegacySearchInputType = "modal-table" | "modal-card" | "modal-icon-grid";
|
||||
|
|
@ -78,6 +79,18 @@ export interface ModalSelectConfig {
|
|||
distinct?: boolean;
|
||||
}
|
||||
|
||||
/** 상태 칩 표시 스타일 */
|
||||
export type StatusChipStyle = "tab" | "pill";
|
||||
|
||||
/** status-chip 전용 설정 */
|
||||
export interface StatusChipConfig {
|
||||
showCount?: boolean;
|
||||
countColumn?: string;
|
||||
allowAll?: boolean;
|
||||
allLabel?: string;
|
||||
chipStyle?: StatusChipStyle;
|
||||
}
|
||||
|
||||
/** pop-search 전체 설정 */
|
||||
export interface PopSearchConfig {
|
||||
inputType: SearchInputType | LegacySearchInputType;
|
||||
|
|
@ -103,6 +116,9 @@ export interface PopSearchConfig {
|
|||
// modal 전용
|
||||
modalConfig?: ModalSelectConfig;
|
||||
|
||||
// status-chip 전용
|
||||
statusChipConfig?: StatusChipConfig;
|
||||
|
||||
// 라벨
|
||||
labelText?: string;
|
||||
labelVisible?: boolean;
|
||||
|
|
@ -144,6 +160,13 @@ export const SEARCH_INPUT_TYPE_LABELS: Record<SearchInputType, string> = {
|
|||
combo: "자동완성",
|
||||
modal: "모달",
|
||||
toggle: "토글",
|
||||
"status-chip": "상태 칩 (대시보드)",
|
||||
};
|
||||
|
||||
/** 상태 칩 스타일 라벨 (설정 패널용) */
|
||||
export const STATUS_CHIP_STYLE_LABELS: Record<StatusChipStyle, string> = {
|
||||
tab: "탭 (큰 숫자)",
|
||||
pill: "알약 (작은 뱃지)",
|
||||
};
|
||||
|
||||
/** 모달 보여주기 방식 라벨 */
|
||||
|
|
|
|||
Loading…
Reference in New Issue