= {
};
function StatusBadgeCell({ cell, row }: CellRendererProps) {
- const value = cell.statusColumn ? row[cell.statusColumn] : (cell.columnName ? row[cell.columnName] : "");
- const strValue = String(value || "");
+ const hasSubStatus = row[VIRTUAL_SUB_STATUS] !== undefined;
+ const effectiveValue = hasSubStatus
+ ? row[VIRTUAL_SUB_STATUS]
+ : (cell.statusColumn ? row[cell.statusColumn] : (cell.columnName ? row[cell.columnName] : ""));
+ const strValue = String(effectiveValue || "");
const mapped = cell.statusMap?.find((m) => m.value === strValue);
- // 접수가능 자동 판별: 하위 데이터 기반
- // 직전 항목이 done이고 현재 항목이 pending이면 "접수가능"
- const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined;
- const isAcceptable = useMemo(() => {
- if (!processFlow || strValue !== "waiting") return false;
- const currentIdx = processFlow.findIndex((s) => s.isCurrent);
- if (currentIdx < 0) return false;
- if (currentIdx === 0) return true;
- const prevStep = processFlow[currentIdx - 1];
- const prevSem = prevStep?.semantic || LEGACY_STATUS_TO_SEMANTIC[prevStep?.status || ""] || "pending";
- return prevSem === "done";
- }, [processFlow, strValue]);
-
- if (isAcceptable) {
- return (
-
-
- 접수가능
-
- );
- }
-
if (mapped) {
return (
- {formatValue(value)}
+ {formatValue(effectiveValue)}
);
}
@@ -614,66 +592,23 @@ function TimelineCell({ cell, row }: CellRendererProps) {
// ===== 11. action-buttons =====
function ActionButtonsCell({ cell, row, onActionButtonClick }: CellRendererProps) {
- const statusValue = cell.statusColumn ? String(row[cell.statusColumn] || "") : (cell.columnName ? String(row[cell.columnName] || "") : "");
+ const hasSubStatus = row[VIRTUAL_SUB_STATUS] !== undefined;
+ const statusValue = hasSubStatus
+ ? String(row[VIRTUAL_SUB_STATUS] || "")
+ : (cell.statusColumn ? String(row[cell.statusColumn] || "") : (cell.columnName ? String(row[cell.columnName] || "") : ""));
const rules = cell.actionRules || [];
- // 접수가능 자동 판별
- const processFlow = row.__processFlow__ as TimelineProcessStep[] | undefined;
- const isAcceptable = useMemo(() => {
- if (!processFlow || statusValue !== "waiting") return false;
- const currentIdx = processFlow.findIndex((s) => s.isCurrent);
- if (currentIdx < 0) return false;
- if (currentIdx === 0) return true;
- const prevStep = processFlow[currentIdx - 1];
- const prevSem = prevStep?.semantic || LEGACY_STATUS_TO_SEMANTIC[prevStep?.status || ""] || "pending";
- return prevSem === "done";
- }, [processFlow, statusValue]);
+ const matchedRule = rules.find((r) => r.whenStatus === statusValue);
- const effectiveStatus = isAcceptable ? "acceptable" : statusValue;
- const matchedRule = rules.find((r) => r.whenStatus === effectiveStatus)
- || rules.find((r) => r.whenStatus === statusValue);
-
- // 매칭 규칙이 없을 때 기본 동작
if (!matchedRule) {
- if (isAcceptable) {
- return (
-
-
-
- );
- }
- if (statusValue === "in_progress") {
- return (
-
-
-
- );
- }
return null;
}
+ // __processFlow__에서 isCurrent 공정의 processId 추출
+ const processFlow = row.__processFlow__ as { isCurrent: boolean; processId?: string | number }[] | undefined;
+ const currentProcess = processFlow?.find((s) => s.isCurrent);
+ const currentProcessId = currentProcess?.processId;
+
return (
{matchedRule.buttons.map((btn, idx) => (
@@ -684,7 +619,11 @@ function ActionButtonsCell({ cell, row, onActionButtonClick }: CellRendererProps
className="h-7 text-[10px]"
onClick={(e) => {
e.stopPropagation();
- onActionButtonClick?.(btn.taskPreset, row, btn as Record);
+ const config = { ...(btn as Record) };
+ if (currentProcessId !== undefined) {
+ config.__processId = currentProcessId;
+ }
+ onActionButtonClick?.(btn.taskPreset, row, config);
}}
>
{btn.label}
diff --git a/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx b/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx
index 7db10988..a878bb2b 100644
--- a/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx
+++ b/frontend/lib/registry/pop-components/pop-search/PopSearchComponent.tsx
@@ -89,13 +89,23 @@ export function PopSearchComponent({
return "contains";
}, [config.filterMode, config.dateSelectionMode, normalizedType]);
+ // status-chip: 연결된 카드 컴포넌트의 전체 rows + 메타 수신
+ const [allRows, setAllRows] = useState[]>([]);
+ const [autoSubStatusColumn, setAutoSubStatusColumn] = useState(null);
+
const emitFilterChanged = useCallback(
(newValue: unknown) => {
setValue(newValue);
setSharedData(`search_${fieldKey}`, newValue);
if (componentId) {
- const filterColumns = config.filterColumns?.length ? config.filterColumns : [fieldKey];
+ const baseColumns = config.filterColumns?.length ? config.filterColumns : [fieldKey];
+ const chipCfg = config.statusChipConfig;
+ // 카드가 전달한 subStatusColumn이 있으면 자동으로 하위 필터 컬럼 추가
+ const subActive = chipCfg?.useSubCount && !!autoSubStatusColumn;
+ const filterColumns = subActive
+ ? [...new Set([...baseColumns, autoSubStatusColumn!])]
+ : baseColumns;
publish(`__comp_output__${componentId}__filter_value`, {
fieldName: fieldKey,
filterColumns,
@@ -106,7 +116,7 @@ export function PopSearchComponent({
publish("filter_changed", { [fieldKey]: newValue });
},
- [fieldKey, publish, setSharedData, componentId, resolveFilterMode, config.filterColumns]
+ [fieldKey, publish, setSharedData, componentId, resolveFilterMode, config.filterColumns, config.statusChipConfig, autoSubStatusColumn]
);
useEffect(() => {
@@ -149,19 +159,25 @@ export function PopSearchComponent({
return unsub;
}, [subscribe, emitFilterChanged, config.fieldName, config.filterColumns, isModalType]);
- // status-chip: 연결된 카드 컴포넌트의 전체 rows 수신
- const [allRows, setAllRows] = useState[]>([]);
-
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)
+ const inner = (typeof data === "object" && data && "value" in data)
? (data as { value: unknown }).value
: data;
- if (Array.isArray(rows)) setAllRows(rows);
+
+ // 카드가 { rows, subStatusColumn } 형태로 발행하는 경우 메타 추출
+ if (typeof inner === "object" && inner && !Array.isArray(inner) && "rows" in inner) {
+ const envelope = inner as { rows?: unknown; subStatusColumn?: string | null };
+ if (Array.isArray(envelope.rows)) setAllRows(envelope.rows as Record[]);
+ setAutoSubStatusColumn(envelope.subStatusColumn ?? null);
+ } else if (Array.isArray(inner)) {
+ setAllRows(inner as Record[]);
+ setAutoSubStatusColumn(null);
+ }
}
);
return unsub;
@@ -210,6 +226,7 @@ export function PopSearchComponent({
onModalOpen={handleModalOpen}
onModalClear={handleModalClear}
allRows={allRows}
+ autoSubStatusColumn={autoSubStatusColumn}
/>
@@ -241,9 +258,10 @@ interface InputRendererProps {
interface InputRendererPropsExt extends InputRendererProps {
allRows?: Record[];
+ autoSubStatusColumn?: string | null;
}
-function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModalOpen, onModalClear, allRows }: InputRendererPropsExt) {
+function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModalOpen, onModalClear, allRows, autoSubStatusColumn }: InputRendererPropsExt) {
const normalized = normalizeInputType(config.inputType as string);
switch (normalized) {
case "text":
@@ -264,7 +282,7 @@ function SearchInputRenderer({ config, value, onChange, modalDisplayText, onModa
case "modal":
return ;
case "status-chip":
- return ;
+ return ;
default:
return ;
}
@@ -687,30 +705,36 @@ function StatusChipInput({
value,
onChange,
allRows,
+ autoSubStatusColumn,
}: {
config: PopSearchConfig;
value: string;
onChange: (v: unknown) => void;
allRows: Record[];
+ autoSubStatusColumn: string | null;
}) {
const chipCfg: StatusChipConfig = config.statusChipConfig || {};
const chipStyle = chipCfg.chipStyle || "tab";
const showCount = chipCfg.showCount !== false;
- const countColumn = chipCfg.countColumn || config.fieldName || "";
+ const baseCountColumn = chipCfg.countColumn || config.fieldName || "";
+ const useSubCount = chipCfg.useSubCount || false;
const allowAll = chipCfg.allowAll !== false;
const allLabel = chipCfg.allLabel || "전체";
const options: SelectOption[] = config.options || [];
+ // 카드가 전달한 가상 컬럼명이 있으면 자동 사용
+ const effectiveCountColumn = (useSubCount && autoSubStatusColumn) ? autoSubStatusColumn : baseCountColumn;
+
const counts = useMemo(() => {
- if (!showCount || !countColumn || allRows.length === 0) return new Map();
+ if (!showCount || !effectiveCountColumn || allRows.length === 0) return new Map();
const map = new Map();
for (const row of allRows) {
- const v = String(row[countColumn] ?? "");
+ const v = String(row[effectiveCountColumn] ?? "");
map.set(v, (map.get(v) || 0) + 1);
}
return map;
- }, [allRows, countColumn, showCount]);
+ }, [allRows, effectiveCountColumn, showCount]);
const totalCount = allRows.length;
diff --git a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx
index eac031c3..7c6b98c2 100644
--- a/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx
+++ b/frontend/lib/registry/pop-components/pop-search/PopSearchConfig.tsx
@@ -1168,6 +1168,26 @@ function StatusChipDetailSettings({ cfg, update, allComponents, connections, com