From 17fb815513b6db4e71ac79f08a74e25614ac913e Mon Sep 17 00:00:00 2001 From: SeongHyun Kim Date: Thu, 19 Mar 2026 18:01:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20MES=20=EC=83=81=ED=83=9C=20=ED=83=AD?= =?UTF-8?q?=EC=9D=84=20=EC=B9=B4=EB=93=9C=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EC=97=90=20=EB=82=B4=EC=9E=A5=20=EB=B3=84=EB=8F=84=20?= =?UTF-8?q?pop-status-bar=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=B0=EC=B9=98/=EC=97=B0=EA=B2=B0=20=EC=97=86=EC=9D=B4=20?= =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EC=83=81=EB=8B=A8=EC=97=90=20MES=20?= =?UTF-8?q?=EA=B3=A0=EC=A0=95=20=EC=83=81=ED=83=9C=20=ED=83=AD(=EC=A0=84?= =?UTF-8?q?=EC=B2=B4/=EB=8C=80=EA=B8=B0/=EC=A0=91=EC=88=98=EA=B0=80?= =?UTF-8?q?=EB=8A=A5/=EC=A7=84=ED=96=89/=EC=99=84=EB=A3=8C)=EC=9D=84=20?= =?UTF-8?q?=EB=82=B4=EC=9E=A5=ED=95=9C=EB=8B=A4.=20[=EB=82=B4=EC=9E=A5=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=ED=83=AD=20UI]=20-=20MES=5FSTATUS=5FTABS?= =?UTF-8?q?=20=EC=83=81=EC=88=98:=20waiting/acceptable/in=5Fprogress/compl?= =?UTF-8?q?eted=20=EA=B3=A0=EC=A0=95=20-=20=EC=B9=B4=EB=93=9C=20=EA=B7=B8?= =?UTF-8?q?=EB=A6=AC=EB=93=9C=20=EC=83=81=EB=8B=A8=EC=97=90=20pill=20?= =?UTF-8?q?=ED=98=95=ED=83=9C=20=ED=83=AD=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?-=20=EC=83=81=ED=83=9C=EB=B3=84=20=EC=B9=B4=EC=9A=B4=ED=8A=B8?= =?UTF-8?q?=20+=20"=EC=A0=84=EC=B2=B4"=EB=8A=94=20=ED=81=B4=EB=A1=A0=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20originalCount=20=ED=91=9C=EC=8B=9C=20-=20s?= =?UTF-8?q?howStatusTabs=20&&=20hasProcessFlow=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=EB=B6=80=20=ED=91=9C=EC=8B=9C=20[=EC=9E=90=EC=B2=B4=20?= =?UTF-8?q?=EC=B9=B4=EC=9A=B4=ED=8A=B8/=ED=95=84=ED=84=B0]=20-=20statusCou?= =?UTF-8?q?nts=20useMemo:=20filteredRows=20=EA=B8=B0=EC=A4=80=20VIRTUAL=5F?= =?UTF-8?q?SUB=5FSTATUS=20=EC=A7=91=EA=B3=84=20-=20statusFilteredRows=20us?= =?UTF-8?q?eMemo:=20=EC=84=A0=ED=83=9D=20=ED=83=AD=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=82=B4=EB=B6=80=20=ED=95=84=ED=84=B0=20-=20displayCards/hasM?= =?UTF-8?q?oreCards/totalPages=EA=B0=80=20statusFilteredRows=20=EA=B8=B0?= =?UTF-8?q?=EC=A4=80=EC=9C=BC=EB=A1=9C=20=EB=8F=99=EC=9E=91=20-=20?= =?UTF-8?q?=ED=83=AD=20=EC=A0=84=ED=99=98=20=EC=8B=9C=20currentPage=3D1=20?= =?UTF-8?q?=EB=A6=AC=EC=85=8B=20[=EC=99=B8=EB=B6=80=20status-bar=20?= =?UTF-8?q?=ED=95=98=EC=9C=84=20=ED=98=B8=ED=99=98]=20-=20effectiveExterna?= =?UTF-8?q?lFilters:=20showStatusTabs=EC=9D=BC=20=EB=95=8C=20=5Fsource=3D"?= =?UTF-8?q?status-bar"=20=20=20=ED=95=84=ED=84=B0=20=EB=AC=B4=EC=8B=9C=20(?= =?UTF-8?q?=EB=82=B4=EC=9E=A5=20=ED=83=AD=EC=9C=BC=EB=A1=9C=20=EB=8C=80?= =?UTF-8?q?=EC=B2=B4,=20=EC=B6=A9=EB=8F=8C=20=EB=B0=A9=EC=A7=80)=20-=20all?= =?UTF-8?q?=5Frows=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=9C=ED=96=89=20?= =?UTF-8?q?=EC=9C=A0=EC=A7=80=20(=EA=B8=B0=EC=A1=B4=20pop-status-bar=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=20=EC=95=88=20=EA=B9=A8=EC=A7=90)=20-=20rows?= =?UTF-8?q?ForStatusCount:=20showStatusTabs=EC=9D=BC=20=EB=95=8C=20filtere?= =?UTF-8?q?dRows=20=EC=A7=81=EC=A0=91=20=EB=B0=98=ED=99=98=20[=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=ED=8C=A8=EB=84=90]=20-=20=EA=B3=A0=EA=B8=89=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=EC=97=90=20"=EC=83=81=ED=83=9C=20=ED=83=AD?= =?UTF-8?q?=20=EB=82=B4=EC=9E=A5"=20Switch=20=ED=86=A0=EA=B8=80=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20-=20showStatusTabs=3F:=20boolean=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80=20(=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20false)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopCardListV2Component.tsx | 111 +++++++++++++++--- .../pop-card-list-v2/PopCardListV2Config.tsx | 15 +++ frontend/lib/registry/pop-components/types.ts | 1 + 3 files changed, 112 insertions(+), 15 deletions(-) diff --git a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx index 56296c87..075e436b 100644 --- a/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx +++ b/frontend/lib/registry/pop-components/pop-card-list-v2/PopCardListV2Component.tsx @@ -61,6 +61,14 @@ const LazyPopWorkDetail = dynamic( type RowData = Record; +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(); + 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({

데이터 소스를 설정해주세요.

- ) : effectiveConfig?.hideUntilFiltered && externalFilters.size === 0 ? ( + ) : effectiveConfig?.hideUntilFiltered && effectiveExternalFilters.size === 0 ? (

필터를 선택하면 데이터가 표시됩니다.

@@ -1290,10 +1336,10 @@ export function PopCardListV2Component({
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({
)} + {/* 내장 MES 상태 탭 */} + {config?.showStatusTabs && statusCounts && hasProcessFlow && !selectMode && ( +
+ {MES_STATUS_TABS.map((tab) => { + const isActive = selectedStatusTab === tab.value; + const count = tab.value === "" + ? statusCounts.total + : (statusCounts.counts.get(tab.value) || 0); + return ( + + ); + })} +
+ )} +
{advancedOpen && (
+ {/* 내장 상태 탭 */} +
+ + onUpdate({ showStatusTabs: checked })} + /> +
+ {cfg.showStatusTabs && ( +

+ 카드 상단에 MES 상태 탭(전체/대기/접수가능/진행/완료)이 표시됩니다. + 별도 상태 바 컴포넌트가 필요 없습니다. +

+ )} + {/* 필터 전 비표시 */}
diff --git a/frontend/lib/registry/pop-components/types.ts b/frontend/lib/registry/pop-components/types.ts index e89165fe..8a1341df 100644 --- a/frontend/lib/registry/pop-components/types.ts +++ b/frontend/lib/registry/pop-components/types.ts @@ -1006,6 +1006,7 @@ export interface PopCardListV2Config { ownerSortColumn?: string; ownerFilterMode?: "priority" | "only"; workDetailConfig?: PopWorkDetailConfig; + showStatusTabs?: boolean; } /** 카드 컴포넌트가 하위 필터 적용 시 주입하는 가상 컬럼 키 */