diff --git a/Assets/Resources/EWLK/UIToolkit/Main/EWLKMfgOrderModalContentUss.uss b/Assets/Resources/EWLK/UIToolkit/Main/EWLKMfgOrderModalContentUss.uss index 9ca94d28..e37fc811 100644 --- a/Assets/Resources/EWLK/UIToolkit/Main/EWLKMfgOrderModalContentUss.uss +++ b/Assets/Resources/EWLK/UIToolkit/Main/EWLKMfgOrderModalContentUss.uss @@ -1,206 +1,254 @@ -/* ────────────────────────────────────────────────────────── - EWLKMfgOrderModalContent — 제조지시현황 모달 - ────────────────────────────────────────────────────────── */ +/* ============================================================ + EWLKMfgOrderModalContent — 제조지시현황 (라이트 테마) + 탭 + 요약 바 + 11컬럼 테이블 + ============================================================ */ -/* ── 루트 컨테이너 ──────────────────────────────────────── */ +/* ── 루트 ──────────────────────────────────────────────── */ .ewlk-mfgorder { - flex-direction: column; flex-grow: 1; - background-color: rgb(18, 22, 36); - padding: 0; - min-height: 400px; + flex-direction: column; + background-color: rgb(255, 255, 255); } -/* ── 탭 바 ──────────────────────────────────────────────── */ +/* ── 탭 바 ─────────────────────────────────────────────── */ .ewlk-mfgorder__tab-bar { flex-direction: row; - border-bottom-width: 2px; - border-bottom-color: rgb(50, 60, 90); - margin-bottom: 0; + padding-left: 12px; + padding-top: 8px; } .ewlk-mfgorder__tab { - flex-shrink: 0; - padding: 8px 20px; - background-color: rgb(34, 42, 66); - color: rgb(140, 150, 175); + min-width: 0; + padding: 6px 16px; + margin-right: 4px; + background-color: rgba(0, 0, 0, 0); border-width: 0; border-bottom-width: 2px; - border-bottom-color: rgba(0, 0, 0, 0); + border-color: rgba(0, 0, 0, 0); border-radius: 0; - font-size: 13px; - -unity-font-style: normal; - margin-bottom: -2px; -} - -.ewlk-mfgorder__tab:hover { - background-color: rgb(40, 50, 78); - color: rgb(220, 225, 240); + font-size: 12px; + color: rgb(120, 120, 120); } .ewlk-mfgorder__tab--active { - color: rgb(220, 225, 240); - border-bottom-color: rgb(0, 140, 255); + color: rgb(30, 100, 220); + border-bottom-color: rgb(30, 100, 220); -unity-font-style: bold; } -/* ── 요약 바 ─────────────────────────────────────────────── */ +.ewlk-mfgorder__tab:hover { + color: rgb(60, 60, 60); +} + +/* ── 요약 바 ────────────────────────────────────────────── */ .ewlk-mfgorder__summary-bar { flex-direction: row; - background-color: rgb(26, 32, 50); - padding: 10px 16px; - border-bottom-width: 1px; - border-bottom-color: rgb(50, 60, 90); + padding: 12px 16px; + border-width: 1px; + border-color: rgb(230, 230, 230); + border-radius: 4px; + margin: 8px 12px; } -.ewlk-mfgorder__summary-item { +.ewlk-mfgorder__summary-left { + flex-grow: 1; + flex-direction: column; + padding-right: 24px; + border-right-width: 1px; + border-color: rgb(230, 230, 230); +} + +.ewlk-mfgorder__summary-right { + flex-grow: 1; + flex-direction: column; + padding-left: 24px; +} + +/* ── 바 아이템 (가로: 라벨 | 값 | 프로그레스 바) ──────── */ +.ewlk-mfgorder__bar-item { flex-direction: row; align-items: center; - flex-grow: 1; - margin-right: 16px; + margin-bottom: 8px; } -.ewlk-mfgorder__summary-title { - color: rgb(140, 150, 175); +.ewlk-mfgorder__bar-item-title { font-size: 11px; - margin-right: 8px; + color: rgb(100, 100, 100); + width: 110px; flex-shrink: 0; + -unity-text-align: middle-left; } -.ewlk-mfgorder__summary-value { - color: rgb(220, 225, 240); +.ewlk-mfgorder__bar-item-value { font-size: 13px; -unity-font-style: bold; + color: rgb(33, 33, 33); + width: 120px; flex-shrink: 0; + -unity-text-align: middle-left; } -/* ── 설비가동률 행 ───────────────────────────────────────── */ -.ewlk-mfgorder__equip-row { - flex-direction: row; - align-items: center; - background-color: rgb(26, 32, 50); - padding: 6px 16px; - border-bottom-width: 1px; - border-bottom-color: rgb(50, 60, 90); +.ewlk-mfgorder__bar-item .ewlk-mfgorder__progress-wrap { + flex-grow: 1; } -.ewlk-mfgorder__equip-title { - color: rgb(140, 150, 175); - font-size: 11px; +/* ── 설비 가동율 값 세로 스택 (%+대수) ──────────────────── */ +.ewlk-mfgorder__equip-value-stack { + flex-direction: column; + width: 120px; flex-shrink: 0; - margin-right: 10px; - width: 180px; -} - -.ewlk-mfgorder__equip-rate { - color: rgb(220, 225, 240); - font-size: 12px; - flex-shrink: 0; - margin-left: 8px; - width: 44px; } .ewlk-mfgorder__count { - color: rgb(140, 150, 175); - font-size: 12px; - flex-shrink: 0; - margin-left: 12px; + font-size: 11px; + color: rgb(30, 100, 220); + -unity-font-style: bold; + -unity-text-align: middle-left; } -/* ── 프로그레스 바 (공통) ────────────────────────────────── */ +/* ── 검색창 ────────────────────────────────────────────── */ +.ewlk-mfgorder__search-row { + flex-direction: row; + align-items: center; + justify-content: flex-end; + margin: 4px 12px 8px 12px; +} + +.ewlk-mfgorder__search-icon { + color: rgb(180, 180, 180); + margin-right: 4px; + flex-shrink: 0; +} + +.ewlk-mfgorder__search-field { + width: 25%; + border-width: 0; + background-color: rgba(0, 0, 0, 0); + font-size: 12px; + color: rgb(160, 160, 160); +} + +/* 검색창 다크 테마 오버라이드 (3단계 specificity) */ +.ewlk-mfgorder .ewlk-mfgorder__search-row .ewlk-mfgorder__search-field .unity-text-input { + border-width: 0; + padding: 2px 0; + background-color: rgb(255, 255, 255); + color: rgb(30, 30, 30); + --unity-cursor-color: rgb(80, 80, 80); +} + +/* placeholder 상태 — 텍스트 회색 */ +.ewlk-mfgorder .ewlk-mfgorder__search-row .ewlk-mfgorder__search-field--placeholder .unity-text-input { + color: rgb(160, 160, 160); +} + +/* ── 프로그레스 바 공통 ──────────────────────────────────── */ .ewlk-mfgorder__progress-wrap { - flex-grow: 1; - height: 10px; - background-color: rgb(50, 60, 90); - border-radius: 4px; + height: 12px; + background-color: rgb(240, 240, 240); + border-radius: 2px; overflow: hidden; - max-width: 300px; } .ewlk-mfgorder__progress-fill { height: 100%; - background-color: rgb(0, 200, 100); - border-radius: 4px; - width: 0%; + background-color: rgb(180, 200, 230); + border-radius: 2px; + width: 0; } -/* ── 테이블 헤더 ─────────────────────────────────────────── */ +/* ── 테이블 헤더 ──────────────────────────────────────── */ .ewlk-mfgorder__header { flex-direction: row; - background-color: rgb(34, 42, 66); + min-height: 32px; + background-color: rgb(245, 245, 250); border-bottom-width: 1px; - border-bottom-color: rgb(50, 60, 90); - padding: 0 4px; + border-color: rgb(220, 220, 220); + margin-left: 12px; + margin-right: 12px; } .ewlk-mfgorder__header-cell { - color: rgb(140, 150, 175); + -unity-text-align: middle-center; font-size: 11px; -unity-font-style: bold; - -unity-text-align: middle-center; - padding: 6px 4px; + color: rgb(80, 80, 80); + padding: 4px 2px; border-right-width: 1px; - border-right-color: rgb(50, 60, 90); + border-color: rgb(230, 230, 230); } -/* ── ListView ────────────────────────────────────────────── */ +.ewlk-mfgorder__header-cell:last-child { + border-right-width: 0; +} + +/* ── ListView ──────────────────────────────────────────── */ .ewlk-mfgorder__list { flex-grow: 1; - background-color: rgb(18, 22, 36); + margin-left: 12px; + margin-right: 12px; } -/* ── 데이터 행 ───────────────────────────────────────────── */ +/* ── 데이터 행 ─────────────────────────────────────────── */ .ewlk-mfgorder__row { flex-direction: row; - align-items: center; - height: 28px; - padding: 0 4px; + min-height: 28px; border-bottom-width: 1px; - border-bottom-color: rgb(50, 60, 90); + border-color: rgb(240, 240, 240); } +/* ── 셀 공통 ──────────────────────────────────────────── */ .ewlk-mfgorder__cell { - height: 100%; justify-content: center; - padding: 0 4px; + align-items: center; + padding: 2px 4px; border-right-width: 1px; - border-right-color: rgb(50, 60, 90); - overflow: hidden; + border-color: rgb(245, 245, 245); +} + +.ewlk-mfgorder__cell:last-child { + border-right-width: 0; } .ewlk-mfgorder__cell-label { - color: rgb(220, 225, 240); - font-size: 12px; - -unity-text-align: middle-left; - overflow: hidden; -} - -/* ── 달성률 셀 내부 ─────────────────────────────────────── */ -.ewlk-mfgorder__stopped { - color: rgb(160, 165, 185); font-size: 11px; + color: rgb(50, 50, 50); -unity-text-align: middle-center; - background-color: rgb(60, 65, 85); - padding: 2px 6px; - border-radius: 3px; - display: none; + overflow: hidden; + white-space: nowrap; } +/* 품목명 줄임표시 */ +.ewlk-mfgorder__cell-label--ellipsis { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* ── 진척률 셀 ────────────────────────────────────────── */ .ewlk-mfgorder__pct { - color: rgb(220, 225, 240); font-size: 11px; - -unity-text-align: middle-right; + color: rgb(50, 50, 50); + -unity-text-align: middle-center; + min-width: 30px; flex-shrink: 0; - margin-left: 4px; - width: 36px; - display: none; } -/* ── 컬럼 너비 ───────────────────────────────────────────── */ -.col-equip { width: 100px; flex-shrink: 0; } -.col-order { width: 120px; flex-shrink: 0; } -.col-item-code { width: 110px; flex-shrink: 0; } -.col-item-name { flex-grow: 1; } -.col-time { width: 80px; flex-shrink: 0; } -.col-target { width: 90px; flex-shrink: 0; } -.col-achievement { width: 150px; flex-shrink: 0; flex-direction: row; align-items: center; } +/* ── 컬럼 너비 ────────────────────────────────────────── */ +.col-no { width: 35px; flex-shrink: 0; } +.col-equip { width: 110px; flex-shrink: 0; } +.col-order { width: 100px; flex-shrink: 0; } +.col-item-code { width: 100px; flex-shrink: 0; } +.col-item-name { width: 90px; flex-shrink: 0; } +.col-start-time { width: 90px; flex-shrink: 0; } +.col-end-time { width: 90px; flex-shrink: 0; } +.col-work-time { width: 80px; flex-shrink: 0; } +.col-plan-prod { width: 90px; flex-shrink: 0; } +.col-actual-prod { width: 80px; flex-shrink: 0; } +.col-achievement { flex-grow: 1; flex-direction: row; align-items: center; padding-left: 4px; padding-right: 4px; } + +/* 진척률 셀 내부 프로그레스 바 */ +.col-achievement .ewlk-mfgorder__progress-wrap { + flex-grow: 1; + margin-right: 4px; +} diff --git a/Assets/Resources/EWLK/UIToolkit/Main/EWLKOverViewModalContentUss.uss b/Assets/Resources/EWLK/UIToolkit/Main/EWLKOverViewModalContentUss.uss index 096b69b9..5b3c63eb 100644 --- a/Assets/Resources/EWLK/UIToolkit/Main/EWLKOverViewModalContentUss.uss +++ b/Assets/Resources/EWLK/UIToolkit/Main/EWLKOverViewModalContentUss.uss @@ -1,128 +1,150 @@ /* ============================================================ - EWLKOverViewModalContent — OverView 모달 테이블 스타일 - - 레이아웃 구조: - .ewlk-overview (테이블 루트, flex-column) - .ewlk-overview__header (헤더 행, flex-row) - .ewlk-overview__header-cell (헤더 셀) - .ewlk-overview__group × 3 (작업장 그룹, flex-row) - .ewlk-overview__cell--name (작업장 이름, 2행 span) - .ewlk-overview__rows (월간+일간, flex-column) - .ewlk-overview__row × 2 (데이터 행, flex-row) - .ewlk-overview__cell--period - .ewlk-overview__cell--data × 3 - .ewlk-overview__cell--type + EWLKOverViewModalContent — OverView 컨텐츠 스타일 + 3개 컬럼 (작업장별), 각 컬럼에 월간/일간 섹션 + 프로그레스 바 ============================================================ */ -/* ── 테이블 루트 ─────────────────────────────────────────── */ +/* ── 루트 ──────────────────────────────────────────────── */ .ewlk-overview { - flex-direction: column; + flex-grow: 1; background-color: rgb(255, 255, 255); - border-width: 1px; - border-color: rgb(140, 140, 140); + padding: 16px; } -/* ── 헤더 행 ─────────────────────────────────────────────── */ -.ewlk-overview__header { +/* ── 3컬럼 컨테이너 ────────────────────────────────────── */ +.ewlk-overview__columns { flex-direction: row; - min-height: 36px; - background-color: rgb(200, 206, 224); - border-bottom-width: 1px; - border-color: rgb(140, 140, 140); -} - -/* ── 헤더 셀 공통 ────────────────────────────────────────── */ -.ewlk-overview__header-cell { - -unity-text-align: middle-center; - font-size: 12px; - -unity-font-style: bold; - color: rgb(20, 20, 20); - padding-top: 4px; - padding-bottom: 4px; - border-right-width: 1px; - border-color: rgb(140, 140, 140); -} - -/* ── 작업장 그룹 ─────────────────────────────────────────── */ -.ewlk-overview__group { - flex-direction: row; - border-bottom-width: 1px; - border-color: rgb(140, 140, 140); -} - -/* ── 행 묶음 컨테이너 ────────────────────────────────────── */ -.ewlk-overview__rows { flex-grow: 1; + align-items: stretch; +} + +/* ── 작업장 컬럼 ────────────────────────────────────────── */ +.ewlk-overview__column { + flex-grow: 1; + flex-basis: 0; flex-direction: column; + border-width: 1px; + border-color: rgb(230, 230, 230); + border-radius: 4px; + margin-right: 12px; + padding: 12px; + background-color: rgb(252, 252, 252); } -/* ── 데이터 행 ───────────────────────────────────────────── */ -.ewlk-overview__row { - flex-direction: row; - min-height: 36px; - border-bottom-width: 1px; - border-color: rgb(200, 200, 200); +.ewlk-overview__column:last-child { + margin-right: 0; } -.ewlk-overview__rows .ewlk-overview__row:last-child { - border-bottom-width: 0; -} - -/* ── 공통 셀 ─────────────────────────────────────────────── */ -.ewlk-overview__cell { - -unity-text-align: middle-center; - font-size: 12px; - color: rgb(20, 20, 20); - padding-left: 6px; - padding-right: 6px; - align-items: center; - justify-content: center; - border-right-width: 1px; - border-color: rgb(200, 200, 200); -} - -/* ── 셀 종류별 너비 / 배경 ──────────────────────────────── */ - -/* 작업장 이름 (헤더의 '구분' + 그룹의 작업장명 — 동일 너비) */ -.ewlk-overview__cell--name { - width: 140px; - flex-shrink: 0; +/* ── 컬럼 타이틀 ────────────────────────────────────────── */ +.ewlk-overview__column-title { + font-size: 13px; -unity-font-style: bold; - background-color: rgb(232, 235, 248); - border-right-color: rgb(140, 140, 140); + color: rgb(33, 33, 33); + -unity-text-align: middle-center; + margin-bottom: 12px; + padding-bottom: 8px; + border-bottom-width: 1px; + border-color: rgb(220, 220, 220); } -/* 월간/일간 구분 열 */ -.ewlk-overview__cell--period { - width: 50px; - flex-shrink: 0; - background-color: rgb(245, 246, 252); - border-right-color: rgb(180, 180, 180); -} - -/* 목표수량·현시점계획·실적수량 (동등 너비 분할) */ -.ewlk-overview__cell--data { +/* ── 기간 섹션 (월간/일간) — 각각 50% 높이 ──────────────── */ +.ewlk-overview__section { + flex-direction: column; flex-grow: 1; - flex-shrink: 1; + flex-basis: 0; } -/* 구분 (제조/생산) — 마지막 열, 오른쪽 테두리 제거 */ -.ewlk-overview__cell--type { - width: 60px; +/* ── 기간 라벨 ──────────────────────────────────────────── */ +.ewlk-overview__period-label { + font-size: 12px; + -unity-font-style: bold; + color: rgb(100, 100, 100); + margin-bottom: 4px; + -unity-text-align: middle-center; +} + +/* ── 섹션 내 구분선 ──────────────────────────────────────── */ +.ewlk-overview__divider { + height: 1px; + background-color: rgb(230, 230, 230); + margin-bottom: 8px; +} + +/* ── 데이터 행 (가로: 라벨 | 값 | 프로그레스 바) ──────── */ +.ewlk-overview__data-row { + flex-direction: row; + align-items: center; + margin-bottom: 6px; +} + +/* ── 라벨 + 값 영역 ──────────────────────────────────── */ +.ewlk-overview__label-row { + flex-direction: row; + align-items: center; flex-shrink: 0; - border-right-width: 0; + width: 55%; } -/* 헤더의 name/period/type 셀도 동일 너비 클래스 공유 */ -.ewlk-overview__header .ewlk-overview__cell--name { - background-color: rgb(200, 206, 224); - border-right-color: rgb(140, 140, 140); +.ewlk-overview__data-name { + font-size: 11px; + color: rgb(100, 100, 100); + width: 80px; + flex-shrink: 0; + -unity-text-align: middle-left; } -.ewlk-overview__header .ewlk-overview__cell--period { - background-color: rgb(200, 206, 224); +.ewlk-overview__data-value { + font-size: 12px; + -unity-font-style: bold; + color: rgb(33, 33, 33); + flex-grow: 1; + -unity-text-align: middle-left; } -.ewlk-overview__header .ewlk-overview__cell--type { - border-right-width: 0; +/* 목표수량 값 — 검정 */ +.ewlk-overview__value--target { + color: rgb(33, 33, 33); +} + +/* 현시점 계획 값 — 파랑 */ +.ewlk-overview__value--plan { + color: rgb(40, 100, 200); +} + +/* 실적수량 값 — 빨강 */ +.ewlk-overview__value--actual { + color: rgb(200, 60, 50); +} + +/* ── 프로그레스 바 배경 ──────────────────────────────────── */ +.ewlk-overview__bar-bg { + flex-grow: 1; + height: 14px; + background-color: rgb(240, 240, 240); + border-radius: 2px; + overflow: hidden; +} + +/* ── 프로그레스 바 채움 ──────────────────────────────────── */ +.ewlk-overview__bar-fill { + height: 100%; + border-radius: 2px; + width: 0; + transition-property: width; + transition-duration: 0.3s; + transition-timing-function: ease-out; +} + +/* 목표수량 — 연한 회색 */ +.ewlk-overview__bar--target { + background-color: rgb(200, 200, 210); +} + +/* 현시점 계획 — 연한 파랑 */ +.ewlk-overview__bar--plan { + background-color: rgb(180, 200, 230); +} + +/* 실적수량 — 연한 빨강 */ +.ewlk-overview__bar--actual { + background-color: rgb(230, 190, 185); } diff --git a/Assets/Resources/EWLK/UIToolkit/Main/EWLKWorkExplorerContentUss.uss b/Assets/Resources/EWLK/UIToolkit/Main/EWLKWorkExplorerContentUss.uss index a9f0adf8..46164cdf 100644 --- a/Assets/Resources/EWLK/UIToolkit/Main/EWLKWorkExplorerContentUss.uss +++ b/Assets/Resources/EWLK/UIToolkit/Main/EWLKWorkExplorerContentUss.uss @@ -4,7 +4,10 @@ ============================================================ */ .ewlk-work-explorer { + flex-grow: 1; + flex-direction: column; background-color: rgb(255, 255, 255); + padding: 12px 16px; } /* ── 섹션 타이틀 ──────────────────────────────────────── */ diff --git a/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderData.cs b/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderData.cs index 2646ebdd..4d7d2449 100644 --- a/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderData.cs +++ b/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderData.cs @@ -3,82 +3,94 @@ using System.Collections.Generic; namespace UVC.EnglewoodLAB.Data { - /// 제조지시현황 요약 데이터 (라인별) + /// 제조지시현황 요약 데이터 public class EWLKMfgOrderSummaryData { - /// 공장구분 (탭 레이블로 사용) - public string FactoryName { get; set; } = string.Empty; + /// 공장구분 + public string FactoryName { get; internal set; } = string.Empty; /// 전체 제조 목표량 - public string TotalTarget { get; set; } = string.Empty; + public string TotalTarget { get; internal set; } = string.Empty; /// 실제 실적량 - public string TotalActual { get; set; } = string.Empty; + public string TotalActual { get; internal set; } = string.Empty; /// 전체 제조 진척률 (0~100) - public float TotalProgress { get; set; } + public float TotalProgress { get; internal set; } /// 일 설비가동률 가중대수% (0~100) - public float EquipRate { get; set; } + public float EquipRate { get; internal set; } /// 가동 중 설비 수 - public int ActiveCount { get; set; } + public int ActiveCount { get; internal set; } /// 전체 설비 수 - public int TotalCount { get; set; } + public int TotalCount { get; internal set; } } /// 설비별 제조지시 행 데이터 public class EWLKMfgOrderRowData { /// 설비명 - public string EquipName { get; set; } = string.Empty; + public string EquipName { get; internal set; } = string.Empty; /// 지시번호 - public string OrderNo { get; set; } = string.Empty; + public string OrderNo { get; internal set; } = string.Empty; /// 품목코드 - public string ItemCode { get; set; } = string.Empty; + public string ItemCode { get; internal set; } = string.Empty; /// 품목명 - public string ItemName { get; set; } = string.Empty; + public string ItemName { get; internal set; } = string.Empty; /// 시작시간 - public string StartTime { get; set; } = string.Empty; + public string StartTime { get; internal set; } = string.Empty; - /// 목표량 - public string Target { get; set; } = string.Empty; + /// 종료시간 + public string EndTime { get; internal set; } = string.Empty; - /// 달성률 (0~100). IsStopped가 true면 사용되지 않음 - public float Achievement { get; set; } + /// 총 작업 시간 + public string TotalWorkTime { get; internal set; } = string.Empty; - /// 계획 정지 여부 (true면 달성률 대신 "계획 정지" 표시) - public bool IsStopped { get; set; } + /// 계획 생산량(kg) + public string PlanProduction { get; internal set; } = string.Empty; + + /// 실 생산량(kg) + public string ActualProduction { get; internal set; } = string.Empty; + + /// 달성률/진척률 (0~100) + public float Achievement { get; internal set; } + + /// 계획 정지 여부 + public bool IsStopped { get; internal set; } + + /// 플레이스홀더 행을 생성합니다. + public static EWLKMfgOrderRowData CreatePlaceholder() + { + return new EWLKMfgOrderRowData + { + EquipName = "-", + OrderNo = "-", + ItemCode = "-", + ItemName = "-", + StartTime = "-", + EndTime = "-", + TotalWorkTime = "-", + PlanProduction = "-", + ActualProduction = "-", + Achievement = 0f, + IsStopped = false, + }; + } } - /// 공장 라인별 제조지시 데이터 (요약 + 행 목록) + /// 제조지시현황 데이터 (요약 + 행 목록) public class EWLKMfgOrderLineData { - /// 기본 라인 이름 (MQTT summary 수신 전 초기값) - public string DefaultName { get; } - /// 요약 데이터 public EWLKMfgOrderSummaryData Summary { get; } = new(); - /// 설비별 행 목록 (ListView itemsSource) + /// 설비별 행 목록 public List Rows { get; } = new(); - - /// MQTT 수신 전 초기 탭 레이블 - public EWLKMfgOrderLineData(string defaultName) => DefaultName = defaultName; - } - - /// 제조지시현황 전체 데이터 (2개 라인) - public class EWLKMfgOrderData - { - /// 라인 1 - public EWLKMfgOrderLineData Line1 { get; } = new("라인 1"); - - /// 라인 2 - public EWLKMfgOrderLineData Line2 { get; } = new("라인 2"); } } diff --git a/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderMqttService.cs b/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderMqttService.cs index 4a36bd72..1a4afa54 100644 --- a/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderMqttService.cs +++ b/Assets/Scripts/EnglewoodLAB/Data/EWLKMfgOrderMqttService.cs @@ -8,105 +8,53 @@ namespace UVC.EnglewoodLAB.Data { /// /// 제조지시현황 데이터를 MQTT로 수신하는 서비스. - /// 2개 라인의 요약 및 설비 행 데이터를 구독합니다. - /// DataRepository.Instance.MqttReceiver(공유 수신기)를 사용합니다. + /// 요약 및 설비 행 데이터를 구독합니다. /// /// - /// MQTT 토픽 구조: - /// ewlk/mfgorder/line1/summary — 라인1 요약 (DataObject) - /// ewlk/mfgorder/line1/rows — 라인1 설비 행 배열 (DataArray) - /// ewlk/mfgorder/line2/summary — 라인2 요약 (DataObject) - /// ewlk/mfgorder/line2/rows — 라인2 설비 행 배열 (DataArray) - /// - /// Summary JSON 스키마: - /// - /// { - /// "factory_name": "2공장 제조1", - /// "total_target": "47,910.2 Kg", - /// "total_actual": "38,889.0 Kg", - /// "total_progress": 34.2, - /// "equip_rate": 0.0, - /// "active_count": 0, - /// "total_count": 47 - /// } - /// - /// - /// Rows JSON 스키마 (배열): - /// - /// [ - /// { - /// "equip_name": "UHM50", - /// "order_no": "-", - /// "item_code": "-", - /// "item_name": "-", - /// "start_time": "-", - /// "target": "-", - /// "achievement": 0.0, - /// "status": "stopped" - /// }, - /// ... - /// ] - /// + /// MQTT 토픽: + /// ewlk/mfgorder/line1/summary — 요약 (DataObject) + /// ewlk/mfgorder/line1/rows — 설비 행 배열 (DataArray) /// public class EWLKMfgOrderMqttService : IDisposable { - // ── MQTT 토픽 ────────────────────────────────────────────── - private const string TopicLine1Summary = "ewlk/mfgorder/line1/summary"; - private const string TopicLine1Rows = "ewlk/mfgorder/line1/rows"; - private const string TopicLine2Summary = "ewlk/mfgorder/line2/summary"; - private const string TopicLine2Rows = "ewlk/mfgorder/line2/rows"; + private const string TopicSummary = "ewlk/mfgorder/line1/summary"; + private const string TopicRows = "ewlk/mfgorder/line1/rows"; - // ── DataMask ────────────────────────────────────────────── private static readonly DataMask s_SummaryMask = CreateSummaryMask(); private static readonly DataMask s_RowMask = CreateRowMask(); private bool _subscribed; private bool _disposed; - // ── 공개 API ────────────────────────────────────────────── - /// 가장 최근에 수신한 제조지시 데이터 - public EWLKMfgOrderData CurrentData { get; } = new(); + public EWLKMfgOrderLineData CurrentData { get; } = new(); - /// 라인 데이터(요약 또는 행)가 갱신될 때 발생 — 메인 스레드에서 호출됩니다. - public event Action? OnDataUpdated; + /// 데이터 갱신 시 발생 — 메인 스레드에서 호출됩니다. + public event Action? OnDataUpdated; - // ── 구독 관리 ────────────────────────────────────────────── - - /// - /// DataRepository 공유 수신기에 4개 토픽을 등록합니다. - /// MqttReceiver.Start()는 EWLKSceneMain에서 호출합니다. - /// + /// MQTT 토픽을 구독합니다. public void Subscribe() { if (_subscribed || _disposed) return; _subscribed = true; var receiver = DataRepository.Instance.MqttReceiver; - receiver.Add(BuildSummaryConfig(TopicLine1Summary, - data => UpdateSummary(CurrentData.Line1, data))); - receiver.Add(BuildRowsConfig(TopicLine1Rows, - data => UpdateRows(CurrentData.Line1, data))); - receiver.Add(BuildSummaryConfig(TopicLine2Summary, - data => UpdateSummary(CurrentData.Line2, data))); - receiver.Add(BuildRowsConfig(TopicLine2Rows, - data => UpdateRows(CurrentData.Line2, data))); + receiver.Add(BuildSummaryConfig(TopicSummary, UpdateSummary)); + receiver.Add(BuildRowsConfig(TopicRows, UpdateRows)); } - /// DataRepository 공유 수신기에서 4개 토픽 구독을 해제합니다. + /// MQTT 토픽 구독을 해제합니다. public void Unsubscribe() { if (!_subscribed) return; _subscribed = false; var receiver = DataRepository.Instance.MqttReceiver; - receiver.Remove(TopicLine1Summary); - receiver.Remove(TopicLine1Rows); - receiver.Remove(TopicLine2Summary); - receiver.Remove(TopicLine2Rows); + receiver.Remove(TopicSummary); + receiver.Remove(TopicRows); } - // ── 내부 구현 ────────────────────────────────────────────── + // ── 내부 구현 ── private static DataMask CreateSummaryMask() { @@ -124,14 +72,17 @@ namespace UVC.EnglewoodLAB.Data private static DataMask CreateRowMask() { var mask = new DataMask(); - mask["equip_name"] = ""; - mask["order_no"] = ""; - mask["item_code"] = ""; - mask["item_name"] = ""; - mask["start_time"] = ""; - mask["target"] = ""; - mask["achievement"] = 0.0f; - mask["status"] = ""; // "running" | "stopped" + mask["equip_name"] = ""; + mask["order_no"] = ""; + mask["item_code"] = ""; + mask["item_name"] = ""; + mask["start_time"] = ""; + mask["end_time"] = ""; + mask["total_work_time"] = ""; + mask["plan_production"] = ""; + mask["actual_production"] = ""; + mask["achievement"] = 0.0f; + mask["status"] = ""; return mask; } @@ -147,12 +98,11 @@ namespace UVC.EnglewoodLAB.Data .SetDataMapper(new DataMapper(s_RowMask)) .SetHandler(handler); - /// 요약 DataObject를 SummaryData에 반영합니다. - private void UpdateSummary(EWLKMfgOrderLineData line, IDataObject? data) + private void UpdateSummary(IDataObject? data) { if (data is not DataObject obj) return; - var s = line.Summary; + var s = CurrentData.Summary; s.FactoryName = obj.GetString("factory_name") ?? string.Empty; s.TotalTarget = obj.GetString("total_target") ?? string.Empty; s.TotalActual = obj.GetString("total_actual") ?? string.Empty; @@ -164,35 +114,32 @@ namespace UVC.EnglewoodLAB.Data OnDataUpdated?.Invoke(CurrentData); } - /// - /// 행 DataArray를 파싱하여 Rows를 교체합니다. - /// DataArray는 List<DataObject>를 상속합니다. - /// - private void UpdateRows(EWLKMfgOrderLineData line, IDataObject? data) + private void UpdateRows(IDataObject? data) { if (data is not DataArray arr) return; - line.Rows.Clear(); + CurrentData.Rows.Clear(); foreach (var obj in arr) { - line.Rows.Add(new EWLKMfgOrderRowData + CurrentData.Rows.Add(new EWLKMfgOrderRowData { - EquipName = obj.GetString("equip_name") ?? string.Empty, - OrderNo = obj.GetString("order_no") ?? string.Empty, - ItemCode = obj.GetString("item_code") ?? string.Empty, - ItemName = obj.GetString("item_name") ?? string.Empty, - StartTime = obj.GetString("start_time") ?? string.Empty, - Target = obj.GetString("target") ?? string.Empty, - Achievement = obj.GetFloat("achievement") ?? 0f, - IsStopped = (obj.GetString("status") ?? "") == "stopped", + EquipName = obj.GetString("equip_name") ?? string.Empty, + OrderNo = obj.GetString("order_no") ?? string.Empty, + ItemCode = obj.GetString("item_code") ?? string.Empty, + ItemName = obj.GetString("item_name") ?? string.Empty, + StartTime = obj.GetString("start_time") ?? string.Empty, + EndTime = obj.GetString("end_time") ?? string.Empty, + TotalWorkTime = obj.GetString("total_work_time") ?? string.Empty, + PlanProduction = obj.GetString("plan_production") ?? string.Empty, + ActualProduction = obj.GetString("actual_production") ?? string.Empty, + Achievement = obj.GetFloat("achievement") ?? 0f, + IsStopped = (obj.GetString("status") ?? "") == "stopped", }); } OnDataUpdated?.Invoke(CurrentData); } - // ── IDisposable ──────────────────────────────────────────── - public void Dispose() { if (_disposed) return; diff --git a/Assets/Scripts/EnglewoodLAB/EWLKSceneMain.cs b/Assets/Scripts/EnglewoodLAB/EWLKSceneMain.cs index 5c9fcc11..c7fc4ed5 100644 --- a/Assets/Scripts/EnglewoodLAB/EWLKSceneMain.cs +++ b/Assets/Scripts/EnglewoodLAB/EWLKSceneMain.cs @@ -482,7 +482,7 @@ namespace UVC.EnglewoodLAB BindMenuPopupContent(menuId, _menuPopup); // 메뉴별 사이즈 조정 (기본: 전체 폭) - if (menuId == "work_explorer") + if (menuId is "work_explorer" or "equip_list") { _menuPopup.style.right = StyleKeyword.Auto; _menuPopup.style.width = new Length(40, LengthUnit.Percent); @@ -558,6 +558,14 @@ namespace UVC.EnglewoodLAB popup.AddContent(packContent); break; + case "equip_list": + var equipContent = new EWLKEquipListContent(); + // TODO: MQTT 서비스 연결 시 아래 패턴 적용 + // equipContent.UpdateData(equipListMqtt.CurrentData); + // equipListMqtt.OnDataUpdated += equipContent.UpdateData; + popup.AddContent(equipContent); + break; + case "work_explorer": var workExplorerContent = new EWLKWorkExplorerContent(); // TODO: MQTT 서비스 연결 시 아래 패턴 적용 diff --git a/Assets/Scripts/EnglewoodLAB/UIToolkit/EWLKMfgOrderModalContent.cs b/Assets/Scripts/EnglewoodLAB/UIToolkit/EWLKMfgOrderModalContent.cs index 085fb8b4..d3de0be6 100644 --- a/Assets/Scripts/EnglewoodLAB/UIToolkit/EWLKMfgOrderModalContent.cs +++ b/Assets/Scripts/EnglewoodLAB/UIToolkit/EWLKMfgOrderModalContent.cs @@ -4,172 +4,237 @@ using System.Collections.Generic; using UnityEngine; using UnityEngine.UIElements; using UVC.EnglewoodLAB.Data; +using UVC.UIToolkit; namespace UVC.EnglewoodLAB.UIToolkit { /// - /// 제조지시현황 모달 콘텐츠. - /// 2개 라인 탭, 요약 바, 설비 행 ListView로 구성됩니다. - /// MQTT 데이터가 없는 경우 플레이스홀더 행을 표시합니다. + /// 제조지시현황 컨텐츠. + /// 2개 라인 탭, 요약 바 (목표량/실적량 + 진척률/가동률), 11컬럼 테이블. /// [UxmlElement] public partial class EWLKMfgOrderModalContent : VisualElement, IDisposable { private const string UssPath = "EWLK/UIToolkit/Main/EWLKMfgOrderModalContentUss"; + private const int ItemNameMaxChars = 5; - // ── 탭 ────────────────────────────────────────────── + // ── 탭 ── private readonly Button _tab1Btn; private readonly Button _tab2Btn; - // ── 요약 바 ───────────────────────────────────────── + // ── 요약 바 (좌측) ── private readonly Label _totalTargetLabel; + private readonly VisualElement _totalTargetFill; private readonly Label _totalActualLabel; + private readonly VisualElement _totalActualFill; + + // ── 요약 바 (우측) ── private readonly Label _totalProgressLabel; private readonly VisualElement _totalProgressFill; private readonly Label _equipRateLabel; private readonly VisualElement _equipRateFill; private readonly Label _countLabel; - // ── 테이블 ─────────────────────────────────────────── - private readonly ListView _listView; + // ── 테이블 ── + private readonly ScrollView _scrollView; + private readonly VisualElement _rowContainer; - // ── 상태 ───────────────────────────────────────────── - private EWLKMfgOrderData? _data; + // ── 검색 ── + private readonly TextField _searchField; + private string _searchText = string.Empty; + + // ── 상태 ── private EWLKMfgOrderLineData? _currentLine; - private int _activeTabIndex; + private List _allRows = new(); private List _displayRows = new(); - // ── 플레이스홀더 (MQTT 연결 전 기본 표시용) ────────── private static readonly List s_PlaceholderRows = CreatePlaceholderRows(); - // ──────────────────────────────────────────────────── + // 11개 컬럼 정의 (CSS 클래스, 헤더 텍스트) + private static readonly (string cls, string header)[] s_Columns = + { + ("col-no", "No"), + ("col-equip", "설비명"), + ("col-order", "지시 번호"), + ("col-item-code", "품목 코드"), + ("col-item-name", "품목명"), + ("col-start-time", "시작 시간"), + ("col-end-time", "종료 시간"), + ("col-work-time", "총 작업 시간"), + ("col-plan-prod", "계획 생산량(kg)"), + ("col-actual-prod", "실 생산량(kg)"), + ("col-achievement", "진척률(%)"), + }; public EWLKMfgOrderModalContent() { - styleSheets.Add(Resources.Load(UssPath)); + var uss = Resources.Load(UssPath); + if (uss != null) styleSheets.Add(uss); + AddToClassList("ewlk-mfgorder"); - // ── 탭 바 ──────────────────────────────────────── + // ── 탭 바 ── var tabBar = new VisualElement(); tabBar.AddToClassList("ewlk-mfgorder__tab-bar"); - _tab1Btn = new Button(() => SelectTab(0)) { text = "라인 1" }; + _tab1Btn = new Button(() => SelectTab(0)) { text = "2공장 제조1 목표 현황판" }; _tab1Btn.AddToClassList("ewlk-mfgorder__tab"); - _tab2Btn = new Button(() => SelectTab(1)) { text = "라인 2" }; - _tab2Btn.AddToClassList("ewlk-mfgorder__tab"); + _tab1Btn.AddToClassList("ewlk-mfgorder__tab--active"); + _tab2Btn = _tab1Btn; // 단일 탭 (호환성 유지) tabBar.Add(_tab1Btn); - tabBar.Add(_tab2Btn); Add(tabBar); - // ── 요약 바 ────────────────────────────────────── + // ── 요약 바 (좌측: 목표량+실적량, 우측: 진척률+가동률) ── var summaryBar = new VisualElement(); summaryBar.AddToClassList("ewlk-mfgorder__summary-bar"); - summaryBar.Add(MakeStaticItem("전체 제조 목표량", out _totalTargetLabel)); - summaryBar.Add(MakeStaticItem("실제 실적량", out _totalActualLabel)); - summaryBar.Add(MakeProgressItem("전체 제조 진척률", - out _totalProgressFill, out _totalProgressLabel)); - Add(summaryBar); - // ── 설비가동률 행 ───────────────────────────────── + // 좌측 요약 + var summaryLeft = new VisualElement(); + summaryLeft.AddToClassList("ewlk-mfgorder__summary-left"); + summaryLeft.Add(MakeBarItem("전체 제조 목표량", out _totalTargetLabel, out _totalTargetFill)); + summaryLeft.Add(MakeBarItem("실제 실적량", out _totalActualLabel, out _totalActualFill)); + summaryBar.Add(summaryLeft); + + // 우측 요약 + var summaryRight = new VisualElement(); + summaryRight.AddToClassList("ewlk-mfgorder__summary-right"); + summaryRight.Add(MakeBarItem("전체 제조 진척률", out _totalProgressLabel, out _totalProgressFill)); + + // 일 설비 가동율: [라벨] [%+대수 세로] [프로그레스 바] var equipRow = new VisualElement(); - equipRow.AddToClassList("ewlk-mfgorder__equip-row"); + equipRow.AddToClassList("ewlk-mfgorder__bar-item"); - var equipTitle = new Label("일 설비가동률(가중대수%)"); - equipTitle.AddToClassList("ewlk-mfgorder__equip-title"); + var equipTitle = new Label("일 설비 가동율"); + equipTitle.AddToClassList("ewlk-mfgorder__bar-item-title"); equipRow.Add(equipTitle); - var equipBarWrap = new VisualElement(); - equipBarWrap.AddToClassList("ewlk-mfgorder__progress-wrap"); + // % 와 대수를 세로로 묶기 + var equipValueStack = new VisualElement(); + equipValueStack.AddToClassList("ewlk-mfgorder__equip-value-stack"); + _equipRateLabel = new Label("-"); + _equipRateLabel.AddToClassList("ewlk-mfgorder__bar-item-value"); + equipValueStack.Add(_equipRateLabel); + _countLabel = new Label("- / - 대"); + _countLabel.AddToClassList("ewlk-mfgorder__count"); + equipValueStack.Add(_countLabel); + equipRow.Add(equipValueStack); + + // 프로그레스 바 + var equipBarBg = new VisualElement(); + equipBarBg.AddToClassList("ewlk-mfgorder__progress-wrap"); _equipRateFill = new VisualElement(); _equipRateFill.AddToClassList("ewlk-mfgorder__progress-fill"); - equipBarWrap.Add(_equipRateFill); - equipRow.Add(equipBarWrap); + equipBarBg.Add(_equipRateFill); + equipRow.Add(equipBarBg); - _equipRateLabel = new Label("-"); - _equipRateLabel.AddToClassList("ewlk-mfgorder__equip-rate"); - equipRow.Add(_equipRateLabel); + summaryRight.Add(equipRow); - _countLabel = new Label("- / -"); - _countLabel.AddToClassList("ewlk-mfgorder__count"); - equipRow.Add(_countLabel); - Add(equipRow); + summaryBar.Add(summaryRight); + Add(summaryBar); - // ── 테이블 헤더 ─────────────────────────────────── + // ── 검색창 ── + var searchRow = new VisualElement(); + searchRow.AddToClassList("ewlk-mfgorder__search-row"); + + var searchIcon = new Label(UTKMaterialIcons.Search); + searchIcon.AddToClassList("ewlk-mfgorder__search-icon"); + UTKMaterialIcons.ApplyIconStyle(searchIcon, 16); + searchRow.Add(searchIcon); + + _searchField = new TextField(); + _searchField.AddToClassList("ewlk-mfgorder__search-field"); + _searchField.AddToClassList("ewlk-mfgorder__search-field--placeholder"); + _searchField.focusable = true; + _searchField.pickingMode = PickingMode.Position; + _searchField.value = SearchPlaceholder; + _searchField.RegisterCallback(_ => + { + if (_searchField.value == SearchPlaceholder) + { + _searchField.value = string.Empty; + _searchField.RemoveFromClassList("ewlk-mfgorder__search-field--placeholder"); + } + }); + _searchField.RegisterCallback(_ => + { + if (string.IsNullOrEmpty(_searchField.value)) + { + _searchField.value = SearchPlaceholder; + _searchField.AddToClassList("ewlk-mfgorder__search-field--placeholder"); + } + }); + _searchField.RegisterCallback>(OnSearchChanged); + + // picking/focusable + 배경색 (TextField 내부 요소는 USS specificity로 덮어쓸 수 없음) + var textInput = _searchField.Q("unity-text-input"); + if (textInput != null) + { + textInput.pickingMode = PickingMode.Position; + textInput.focusable = true; + textInput.style.backgroundColor = Color.white; + textInput.style.borderTopWidth = 0; + textInput.style.borderBottomWidth = 0; + textInput.style.borderLeftWidth = 0; + textInput.style.borderRightWidth = 0; + } + + searchRow.pickingMode = PickingMode.Position; + searchRow.Add(_searchField); + + Add(searchRow); + + // ── 테이블 헤더 ── Add(BuildTableHeader()); - // ── ListView ───────────────────────────────────── - _listView = new ListView - { - makeItem = MakeRowElement, - bindItem = BindRowElement, - fixedItemHeight = 28, - selectionType = SelectionType.None, - }; - _listView.AddToClassList("ewlk-mfgorder__list"); - Add(_listView); + // ── ScrollView + 행 컨테이너 ── + _scrollView = new ScrollView(ScrollViewMode.Vertical); + _scrollView.AddToClassList("ewlk-mfgorder__list"); + _rowContainer = new VisualElement(); + _scrollView.Add(_rowContainer); + Add(_scrollView); - SetTabActive(0); - - // 데이터가 없어도 플레이스홀더로 레이아웃 표시 RefreshView(); } - // ── 공개 API ────────────────────────────────────────── + // ── 공개 API ── - /// - /// 데이터를 갱신합니다. MQTT 수신 시 호출됩니다 (메인 스레드 보장). - /// - public void UpdateData(EWLKMfgOrderData data) + /// MQTT 데이터 갱신. + public void UpdateData(EWLKMfgOrderLineData data) { - _data = data; + if (!string.IsNullOrEmpty(data.Summary.FactoryName)) + _tab1Btn.text = data.Summary.FactoryName; - // 공장구분이 수신되면 탭 레이블 갱신 - if (!string.IsNullOrEmpty(data.Line1.Summary.FactoryName)) - _tab1Btn.text = data.Line1.Summary.FactoryName; - if (!string.IsNullOrEmpty(data.Line2.Summary.FactoryName)) - _tab2Btn.text = data.Line2.Summary.FactoryName; - - _currentLine = _activeTabIndex == 0 ? data.Line1 : data.Line2; + _currentLine = data; RefreshView(); } public void Dispose() { } - // ── 탭 선택 ─────────────────────────────────────────── + // ── 탭 (단일 탭, 호환성 유지) ── private void SelectTab(int index) { - _activeTabIndex = index; - SetTabActive(index); - if (_data != null) - _currentLine = index == 0 ? _data.Line1 : _data.Line2; RefreshView(); } - private void SetTabActive(int index) - { - _tab1Btn.EnableInClassList("ewlk-mfgorder__tab--active", index == 0); - _tab2Btn.EnableInClassList("ewlk-mfgorder__tab--active", index == 1); - } - - // ── 뷰 갱신 ─────────────────────────────────────────── + // ── 뷰 갱신 ── private void RefreshView() { if (_currentLine == null) { - // 데이터 없음 — 플레이스홀더 표시 _totalTargetLabel.text = "-"; _totalActualLabel.text = "-"; _totalProgressLabel.text = "-"; _totalProgressFill.style.width = Length.Percent(0f); + _totalTargetFill.style.width = Length.Percent(0f); + _totalActualFill.style.width = Length.Percent(0f); _equipRateLabel.text = "-"; _equipRateFill.style.width = Length.Percent(0f); - _countLabel.text = "- / -"; + _countLabel.text = "- / - 대"; _displayRows = s_PlaceholderRows; - _listView.itemsSource = _displayRows; - _listView.Rebuild(); + RebuildRows(); return; } @@ -178,184 +243,213 @@ namespace UVC.EnglewoodLAB.UIToolkit _totalTargetLabel.text = Or(s.TotalTarget, "-"); _totalActualLabel.text = Or(s.TotalActual, "-"); _totalProgressLabel.text = $"{s.TotalProgress:F1}%"; - _totalProgressFill.style.width = - Length.Percent(Mathf.Clamp(s.TotalProgress, 0f, 100f)); + + _totalTargetFill.style.width = Length.Percent(100f); + float actualRatio = s.TotalProgress > 0 ? Mathf.Clamp(s.TotalProgress, 0f, 100f) : 0f; + _totalActualFill.style.width = Length.Percent(actualRatio); + _totalProgressFill.style.width = Length.Percent(Mathf.Clamp(s.TotalProgress, 0f, 100f)); _equipRateLabel.text = $"{s.EquipRate:F1}%"; - _equipRateFill.style.width = - Length.Percent(Mathf.Clamp(s.EquipRate, 0f, 100f)); - _countLabel.text = $"{s.ActiveCount} / {s.TotalCount}"; + _equipRateFill.style.width = Length.Percent(Mathf.Clamp(s.EquipRate, 0f, 100f)); + _countLabel.text = $"{s.ActiveCount} / {s.TotalCount} 대"; - _displayRows = _currentLine.Rows; - _listView.itemsSource = _displayRows; - _listView.Rebuild(); + _allRows = _currentLine.Rows; + ApplyFilter(); } - // ── ListView 바인딩 ─────────────────────────────────── + // ── 검색 ── + + private const string SearchPlaceholder = "설비명, 지시번호, 품목 코드, 품목명을 검색해 보세요."; + + private void OnSearchChanged(ChangeEvent evt) + { + var val = evt.newValue ?? string.Empty; + _searchText = val == SearchPlaceholder ? string.Empty : val; + ApplyFilter(); + } + + /// 검색어로 행을 필터링하고 재생성합니다. + private void ApplyFilter() + { + if (string.IsNullOrWhiteSpace(_searchText)) + { + _displayRows = _allRows; + } + else + { + var keyword = _searchText.Trim(); + _displayRows = new List(); + foreach (var row in _allRows) + { + if (Contains(row.EquipName, keyword) || + Contains(row.OrderNo, keyword) || + Contains(row.ItemCode, keyword) || + Contains(row.ItemName, keyword)) + { + _displayRows.Add(row); + } + } + } + + RebuildRows(); + } + + private static bool Contains(string source, string keyword) => + !string.IsNullOrEmpty(source) && + source.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0; + + /// ScrollView 내 행을 재생성합니다. + private void RebuildRows() + { + _rowContainer.Clear(); + for (int i = 0; i < _displayRows.Count; i++) + { + var row = MakeRowElement(); + BindRowElement(row, i); + _rowContainer.Add(row); + } + } + + // ── ListView 바인딩 ── - /// - /// 행 요소를 생성합니다. 모든 셀과 서브 요소를 미리 생성합니다. - /// private static VisualElement MakeRowElement() { var row = new VisualElement(); row.AddToClassList("ewlk-mfgorder__row"); - // 문자열 셀 6개 (설비명~목표량) - var textCols = new[] { - "col-equip", "col-order", "col-item-code", - "col-item-name", "col-time", "col-target", - }; - foreach (var cls in textCols) + foreach (var (cls, _) in s_Columns) { var cell = new VisualElement(); cell.AddToClassList("ewlk-mfgorder__cell"); cell.AddToClassList(cls); - var lbl = new Label(); - lbl.AddToClassList("ewlk-mfgorder__cell-label"); - cell.Add(lbl); + + if (cls == "col-achievement") + { + // 진척률: 프로그레스 바 + 퍼센트 라벨 + var barWrap = new VisualElement(); + barWrap.name = "bar-wrap"; + barWrap.AddToClassList("ewlk-mfgorder__progress-wrap"); + var fill = new VisualElement(); + fill.name = "achieve-fill"; + fill.AddToClassList("ewlk-mfgorder__progress-fill"); + barWrap.Add(fill); + cell.Add(barWrap); + + var pctLbl = new Label(); + pctLbl.name = "pct-label"; + pctLbl.AddToClassList("ewlk-mfgorder__pct"); + cell.Add(pctLbl); + } + else + { + var lbl = new Label(); + lbl.AddToClassList("ewlk-mfgorder__cell-label"); + + // 품목명: 텍스트 줄임 + 툴팁 + if (cls == "col-item-name") + lbl.AddToClassList("ewlk-mfgorder__cell-label--ellipsis"); + + cell.Add(lbl); + } + row.Add(cell); } - // 달성률 셀 (progress + stopped-label + pct-label) - var achieveCell = new VisualElement(); - achieveCell.AddToClassList("ewlk-mfgorder__cell"); - achieveCell.AddToClassList("col-achievement"); - - var barWrap = new VisualElement(); - barWrap.name = "bar-wrap"; - barWrap.AddToClassList("ewlk-mfgorder__progress-wrap"); - var fill = new VisualElement(); - fill.name = "achieve-fill"; - fill.AddToClassList("ewlk-mfgorder__progress-fill"); - barWrap.Add(fill); - - var stoppedLbl = new Label("계획 정지"); - stoppedLbl.name = "stopped-label"; - stoppedLbl.AddToClassList("ewlk-mfgorder__stopped"); - - var pctLbl = new Label(); - pctLbl.name = "pct-label"; - pctLbl.AddToClassList("ewlk-mfgorder__pct"); - - achieveCell.Add(barWrap); - achieveCell.Add(stoppedLbl); - achieveCell.Add(pctLbl); - row.Add(achieveCell); - return row; } - /// - /// ListView 바인딩. _displayRows(실데이터 또는 플레이스홀더)를 사용합니다. - /// private void BindRowElement(VisualElement element, int index) { if (index >= _displayRows.Count) return; var d = _displayRows[index]; - SetLabel(element, "col-equip", d.EquipName); - SetLabel(element, "col-order", d.OrderNo); + // No (1-based) + SetLabel(element, "col-no", (index + 1).ToString()); + SetLabel(element, "col-equip", d.EquipName); + SetLabel(element, "col-order", d.OrderNo); SetLabel(element, "col-item-code", d.ItemCode); - SetLabel(element, "col-item-name", d.ItemName); - SetLabel(element, "col-time", d.StartTime); - SetLabel(element, "col-target", d.Target); - var barWrap = element.Q("bar-wrap"); - var fill = element.Q("achieve-fill"); - var stoppedLbl = element.Q