feat: MES 상태 탭을 카드 컴포넌트에 내장

별도 pop-status-bar 컴포넌트 배치/연결 없이 카드 상단에
MES 고정 상태 탭(전체/대기/접수가능/진행/완료)을 내장한다.
[내장 상태 탭 UI]
- MES_STATUS_TABS 상수: waiting/acceptable/in_progress/completed 고정
- 카드 그리드 상단에 pill 형태 탭 렌더링
- 상태별 카운트 + "전체"는 클론 제외 originalCount 표시
- showStatusTabs && hasProcessFlow 조건부 표시
[자체 카운트/필터]
- statusCounts useMemo: filteredRows 기준 VIRTUAL_SUB_STATUS 집계
- statusFilteredRows useMemo: 선택 탭으로 내부 필터
- displayCards/hasMoreCards/totalPages가 statusFilteredRows 기준으로 동작
- 탭 전환 시 currentPage=1 리셋
[외부 status-bar 하위 호환]
- effectiveExternalFilters: showStatusTabs일 때 _source="status-bar"
  필터 무시 (내장 탭으로 대체, 충돌 방지)
- all_rows 이벤트 발행 유지 (기존 pop-status-bar 연결 안 깨짐)
- rowsForStatusCount: showStatusTabs일 때 filteredRows 직접 반환
[설정 패널]
- 고급 설정에 "상태 탭 내장" Switch 토글 추가
- showStatusTabs?: boolean 타입 추가 (기본 false)
This commit is contained in:
SeongHyun Kim 2026-03-19 18:01:57 +09:00
parent 1d85de8bf6
commit 17fb815513
3 changed files with 112 additions and 15 deletions

View File

@ -61,6 +61,14 @@ const LazyPopWorkDetail = dynamic(
type RowData = Record<string, unknown>;
const MES_STATUS_TABS = [
{ value: "", label: "전체" },
{ value: "waiting", label: "대기", color: "#94a3b8", bg: "#f8fafc" },
{ value: "acceptable", label: "접수가능", color: "#2563eb", bg: "#eff6ff" },
{ value: "in_progress", label: "진행중", color: "#d97706", bg: "#fffbeb" },
{ value: "completed", label: "완료", color: "#059669", bg: "#ecfdf5" },
] as const;
function calculateMaxQty(
row: RowData,
processId: string | number | undefined,
@ -357,6 +365,9 @@ export function PopCardListV2Component({
return true;
}, [selectMode, selectModeStatus]);
// 내장 상태 탭
const [selectedStatusTab, setSelectedStatusTab] = useState("");
// 확장/페이지네이션
const [isExpanded, setIsExpanded] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
@ -585,23 +596,57 @@ export function PopCardListV2Component({
return col ? subTableKeys.has(col) : false;
}, [subTableKeys]);
// showStatusTabs일 때 외부 status-bar 필터 무시 (내장 탭으로 대체)
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]);
// 외부 필터 (자동 분류: 컬럼이 processFlow에 있으면 subFilter)
const filteredRows = useMemo(() => {
if (externalFilters.size === 0) return duplicateAcceptableCards(rows);
if (effectiveExternalFilters.size === 0) return duplicateAcceptableCards(rows);
const allFilters = [...externalFilters.values()];
const allFilters = [...effectiveExternalFilters.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, externalFilters, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
}, [rows, effectiveExternalFilters, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
// 하위 필터 활성 여부
const hasActiveSubFilter = useMemo(() => {
if (externalFilters.size === 0) return false;
return [...externalFilters.values()].some((f) => isSubTableColumn(f));
}, [externalFilters, isSubTableColumn]);
if (effectiveExternalFilters.size === 0) return false;
return [...effectiveExternalFilters.values()].some((f) => isSubTableColumn(f));
}, [effectiveExternalFilters, isSubTableColumn]);
// 내장 상태 탭: 카운트 계산 (filteredRows 기준, 상태 필터 적용 전)
const hasProcessFlow = useMemo(
() => rows.some((r) => r[VIRTUAL_SUB_STATUS] !== undefined),
[rows],
);
const statusCounts = useMemo(() => {
if (!config?.showStatusTabs || !hasProcessFlow) return null;
const map = new Map<string, number>();
let originalTotal = 0;
for (const row of filteredRows) {
const v = String(row[VIRTUAL_SUB_STATUS] ?? "");
if (v) map.set(v, (map.get(v) || 0) + 1);
if (!row.__isAcceptClone) originalTotal++;
}
return { counts: map, total: originalTotal };
}, [filteredRows, config?.showStatusTabs, hasProcessFlow]);
// 내장 상태 탭: 필터 적용
const statusFilteredRows = useMemo(() => {
if (!config?.showStatusTabs || !selectedStatusTab) return filteredRows;
return filteredRows.filter((row) =>
String(row[VIRTUAL_SUB_STATUS] ?? "") === selectedStatusTab
);
}, [filteredRows, selectedStatusTab, config?.showStatusTabs]);
// 선택 모드 일괄 처리
const handleSelectModeAction = useCallback(async (btnConfig: SelectModeButtonConfig) => {
@ -680,8 +725,9 @@ export function PopCardListV2Component({
}
}, [selectedRowIds, filteredRows, exitSelectMode]);
// status-bar 필터를 제외한 rows (카운트 집계용)
// status-bar 필터를 제외한 rows (외부 status-bar 카운트 집계용, 하위 호환)
const rowsForStatusCount = useMemo(() => {
if (config?.showStatusTabs) return filteredRows;
const hasStatusBarFilter = [...externalFilters.values()].some((f) => f._source === "status-bar");
if (!hasStatusBarFilter) return filteredRows;
@ -696,7 +742,7 @@ export function PopCardListV2Component({
const afterDuplicate = applySubFilterAndDuplicate(rows, subFilters);
return applyMainFilters(afterDuplicate, mainFilters, subFilters.length > 0);
}, [rows, filteredRows, externalFilters, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
}, [rows, filteredRows, externalFilters, config?.showStatusTabs, duplicateAcceptableCards, applySubFilterAndDuplicate, applyMainFilters, isSubTableColumn]);
// 카운트 집계용 rows 발행 (status-bar 필터 제외)
// originalCount: 복제 카드를 제외한 원본 카드 수
@ -713,7 +759,7 @@ export function PopCardListV2Component({
const overflowCfg = effectiveConfig?.overflow;
const baseVisibleCount = overflowCfg?.visibleCount ?? gridColumns * gridRows;
const visibleCardCount = useMemo(() => Math.max(1, baseVisibleCount), [baseVisibleCount]);
const hasMoreCards = filteredRows.length > visibleCardCount;
const hasMoreCards = statusFilteredRows.length > visibleCardCount;
const expandedCardsPerPage = useMemo(() => {
if (overflowCfg?.mode === "pagination" && overflowCfg.pageSize) return overflowCfg.pageSize;
if (overflowCfg?.mode === "loadMore" && overflowCfg.loadMoreCount) return overflowCfg.loadMoreCount + visibleCardCount;
@ -726,7 +772,7 @@ export function PopCardListV2Component({
const ownerFilterMode = config?.ownerFilterMode || "priority";
const displayCards = useMemo(() => {
let source = filteredRows;
let source = statusFilteredRows;
if (ownerSortColumn && currentUserId) {
const mine: RowData[] = [];
@ -744,9 +790,9 @@ export function PopCardListV2Component({
if (!isExpanded) return source.slice(0, visibleCardCount);
const start = (currentPage - 1) * expandedCardsPerPage;
return source.slice(start, start + expandedCardsPerPage);
}, [filteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage, ownerSortColumn, ownerFilterMode, currentUserId]);
}, [statusFilteredRows, isExpanded, visibleCardCount, currentPage, expandedCardsPerPage, ownerSortColumn, ownerFilterMode, currentUserId]);
const totalPages = isExpanded ? Math.ceil(filteredRows.length / expandedCardsPerPage) : 1;
const totalPages = isExpanded ? Math.ceil(statusFilteredRows.length / expandedCardsPerPage) : 1;
const needsPagination = isExpanded && totalPages > 1;
const toggleExpand = () => {
@ -1244,7 +1290,7 @@ export function PopCardListV2Component({
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
<p className="text-sm text-muted-foreground"> .</p>
</div>
) : effectiveConfig?.hideUntilFiltered && externalFilters.size === 0 ? (
) : effectiveConfig?.hideUntilFiltered && effectiveExternalFilters.size === 0 ? (
<div className="flex flex-1 items-center justify-center rounded-md border border-dashed bg-muted/30 p-4">
<p className="text-sm text-muted-foreground"> .</p>
</div>
@ -1290,10 +1336,10 @@ export function PopCardListV2Component({
<div className="flex shrink-0 items-center gap-3 border-b px-3 py-2">
<input
type="checkbox"
checked={selectedKeys.size === filteredRows.length && filteredRows.length > 0}
checked={selectedKeys.size === statusFilteredRows.length && statusFilteredRows.length > 0}
onChange={(e) => {
if (e.target.checked) {
setSelectedKeys(new Set(filteredRows.map((r) => String(r.__cart_id ?? ""))));
setSelectedKeys(new Set(statusFilteredRows.map((r) => String(r.__cart_id ?? ""))));
} else {
setSelectedKeys(new Set());
}
@ -1306,6 +1352,41 @@ export function PopCardListV2Component({
</div>
)}
{/* 내장 MES 상태 탭 */}
{config?.showStatusTabs && statusCounts && hasProcessFlow && !selectMode && (
<div className="flex shrink-0 items-center gap-2 border-b bg-background px-4 py-2">
{MES_STATUS_TABS.map((tab) => {
const isActive = selectedStatusTab === tab.value;
const count = tab.value === ""
? statusCounts.total
: (statusCounts.counts.get(tab.value) || 0);
return (
<button
key={tab.value}
type="button"
onClick={() => { setSelectedStatusTab(tab.value); setCurrentPage(1); }}
className={cn(
"flex items-center gap-1.5 rounded-full px-4 py-2 text-sm font-semibold transition-colors",
isActive
? "bg-primary text-primary-foreground shadow-sm"
: "bg-muted text-muted-foreground hover:bg-accent"
)}
>
{tab.label}
<span className={cn(
"min-w-[20px] rounded-full px-1.5 py-0.5 text-center text-xs font-bold leading-none",
isActive
? "bg-primary-foreground/20 text-primary-foreground"
: "bg-background text-foreground"
)}>
{count}
</span>
</button>
);
})}
</div>
)}
<div
ref={scrollAreaRef}
className={`min-h-0 flex-1 grid ${scrollClassName}`}

View File

@ -3235,6 +3235,21 @@ function TabActions({
</button>
{advancedOpen && (
<div className="mt-2 space-y-3 rounded border bg-muted/10 p-2">
{/* 내장 상태 탭 */}
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>
<Switch
checked={!!cfg.showStatusTabs}
onCheckedChange={(checked) => onUpdate({ showStatusTabs: checked })}
/>
</div>
{cfg.showStatusTabs && (
<p className="text-[9px] text-muted-foreground -mt-2 pl-1">
MES (////) .
.
</p>
)}
{/* 필터 전 비표시 */}
<div className="flex items-center justify-between">
<Label className="text-xs"> </Label>

View File

@ -1006,6 +1006,7 @@ export interface PopCardListV2Config {
ownerSortColumn?: string;
ownerFilterMode?: "priority" | "only";
workDetailConfig?: PopWorkDetailConfig;
showStatusTabs?: boolean;
}
/** 카드 컴포넌트가 하위 필터 적용 시 주입하는 가상 컬럼 키 */