diff --git a/Assets/Resources/EWLK/UIToolkit/Main/EWLKEquipListContentUss.uss b/Assets/Resources/EWLK/UIToolkit/Main/EWLKEquipListContentUss.uss new file mode 100644 index 00000000..2db4ed4b --- /dev/null +++ b/Assets/Resources/EWLK/UIToolkit/Main/EWLKEquipListContentUss.uss @@ -0,0 +1,238 @@ +/* ============================================================ + EWLKEquipListContent — 설비 목록 + 필터 바 + 8컬럼 테이블 + ============================================================ */ + +.ewlk-equip { + flex-grow: 1; + flex-direction: column; + background-color: rgb(255, 255, 255); +} + +/* ── 필터 바 ────────────────────────────────────────────── */ +.ewlk-equip__filter-bar { + flex-direction: row; + align-items: center; + justify-content: flex-end; + padding: 8px 12px; + flex-shrink: 0; +} + +/* ── 검색 ────────────────────────────────────────────── */ +.ewlk-equip__search-wrap { + flex-direction: row; + align-items: center; + flex-grow: 1; + margin-right: 12px; +} + +.ewlk-equip__search-icon { + color: rgb(180, 180, 180); + margin-right: 4px; + flex-shrink: 0; +} + +.ewlk-equip__search-field { + width: 300px; + border-width: 0; + background-color: rgba(0, 0, 0, 0); + font-size: 12px; + color: rgb(160, 160, 160); +} + +.ewlk-equip .ewlk-equip__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); +} + +.ewlk-equip .ewlk-equip__search-field--placeholder .unity-text-input { + color: rgb(160, 160, 160); +} + +/* ── 필터 드롭다운 ──────────────────────────────────────── */ +.ewlk-equip__filter-dropdown { + width: 100px; + margin-left: 8px; + font-size: 12px; +} + +.ewlk-equip .ewlk-equip__filter-dropdown .unity-base-popup-field__input { + background-color: rgb(255, 255, 255); + border-width: 1px; + border-color: rgb(210, 210, 210); + border-radius: 4px; +} + +.ewlk-equip .ewlk-equip__filter-dropdown .unity-base-popup-field__text { + color: rgb(77, 77, 77); + margin-right: 0; + padding-right: 0; + font-size: 12px; +} + +.ewlk-equip .ewlk-equip__filter-dropdown .unity-base-popup-field__arrow { + margin-left: 2px; + padding-left: 0; +} + +/* ── 테이블 헤더 ──────────────────────────────────────── */ +.ewlk-equip__header { + flex-direction: row; + height: 32px; + background-color: rgb(245, 245, 250); + border-bottom-width: 1px; + border-color: rgb(220, 220, 220); + margin-left: 12px; + margin-right: 12px; + flex-shrink: 0; +} + +.ewlk-equip__header-cell { + background-color: rgb(245, 245, 250); +} + +.ewlk-equip__header-cell .ewlk-equip__cell-label { + -unity-font-style: bold; + color: rgb(80, 80, 80); +} + +/* ── ScrollView ────────────────────────────────────────── */ +.ewlk-equip__list { + flex-grow: 1; + margin-left: 12px; + margin-right: 12px; +} + +/* ── 데이터 행 ─────────────────────────────────────────── */ +.ewlk-equip__row { + flex-direction: row; + height: 32px; + border-bottom-width: 1px; + border-color: rgb(240, 240, 240); + flex-shrink: 0; +} + +/* ── 셀 공통 ──────────────────────────────────────────── */ +.ewlk-equip__cell { + justify-content: center; + align-items: center; + padding: 0 2px; + border-right-width: 0; + overflow: hidden; +} + +.ewlk-equip__cell-label { + font-size: 11px; + color: rgb(50, 50, 50); + -unity-text-align: middle-center; + overflow: hidden; + white-space: nowrap; +} + +/* ── 컬럼 너비 ────────────────────────────────────────── */ +.ewlk-equip .col-el-no { width: 35px; flex-shrink: 0; flex-grow: 0; } +.ewlk-equip .col-el-process { width: 100px; flex-shrink: 0; flex-grow: 0; } +.ewlk-equip .col-el-line { width: 70px; flex-shrink: 0; flex-grow: 0; } +.ewlk-equip .col-el-category { width: 80px; flex-shrink: 0; flex-grow: 0; } +.ewlk-equip .col-el-equipno { width: 80px; flex-shrink: 0; flex-grow: 0; } +.ewlk-equip .col-el-name { flex-grow: 1; } +.ewlk-equip .col-el-status { width: 70px; flex-shrink: 0; flex-grow: 0; } +.ewlk-equip .col-el-control { width: 80px; flex-shrink: 0; flex-grow: 0; flex-direction: row; } + +/* ── 상태 뱃지 (타원) ─────────────────────────────────── */ +.ewlk-equip__badge { + font-size: 10px; + -unity-text-align: middle-center; + padding: 2px 10px; + border-radius: 10px; + overflow: hidden; + white-space: nowrap; +} + +.ewlk-equip__badge--ready { + color: rgb(30, 100, 200); + background-color: rgb(210, 230, 255); + border-width: 1px; + border-color: rgb(100, 160, 230); +} + +.ewlk-equip__badge--error { + color: rgb(200, 50, 40); + background-color: rgb(255, 220, 215); + border-width: 1px; + border-color: rgb(230, 100, 90); +} + +.ewlk-equip__badge--rest { + color: rgb(160, 120, 20); + background-color: rgb(255, 240, 200); + border-width: 1px; + border-color: rgb(220, 180, 60); +} + +.ewlk-equip__badge--planned { + color: rgb(100, 100, 100); + background-color: rgb(230, 230, 230); + border-width: 1px; + border-color: rgb(180, 180, 180); +} + +/* ── 제어 토글 (읽기전용) ─────────────────────────────── */ +.ewlk-equip__toggle { + margin: 0; + flex-shrink: 0; +} + +.ewlk-equip__toggle-label { + font-size: 10px; + color: rgb(80, 80, 80); + margin-left: 4px; + -unity-text-align: middle-center; +} + +/* ── 설비명 래퍼 (이름 + +n + ∧∨) ────────────────────── */ +.ewlk-equip__name-wrap { + flex-direction: row; + align-items: center; + flex-grow: 1; + overflow: hidden; +} + +/* +n 뱃지 (파란색 작은 글씨) */ +.ewlk-equip__sub-count { + font-size: 10px; + color: rgb(30, 100, 220); + -unity-font-style: bold; + margin-left: 2px; + flex-shrink: 0; +} + +/* 아코디언 토글 아이콘 */ +.ewlk-equip__expand-icon { + color: rgb(120, 120, 120); + flex-shrink: 0; + margin-left: 2px; +} + +/* ── 세부 설비 아코디언 ──────────────────────────────── */ +.ewlk-equip__accordion { + flex-direction: column; + margin-left: 12px; + border-left-width: 1px; + border-color: rgb(230, 230, 230); +} + +.ewlk-equip__sub-row { + flex-direction: row; + align-items: center; + height: 28px; + padding-left: 390px; +} + +.ewlk-equip__sub-label { + font-size: 11px; + color: rgb(60, 60, 60); +} diff --git a/Assets/Resources/EWLK/UIToolkit/Main/EWLKEquipListContentUss.uss.meta b/Assets/Resources/EWLK/UIToolkit/Main/EWLKEquipListContentUss.uss.meta new file mode 100644 index 00000000..6da002da --- /dev/null +++ b/Assets/Resources/EWLK/UIToolkit/Main/EWLKEquipListContentUss.uss.meta @@ -0,0 +1,12 @@ +fileFormatVersion: 2 +guid: 1d49b12c27bdf8b409c0af4d005531bc +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 + unsupportedSelectorAction: 0 diff --git a/Assets/Scripts/EnglewoodLAB/Data/EWLKEquipListData.cs b/Assets/Scripts/EnglewoodLAB/Data/EWLKEquipListData.cs new file mode 100644 index 00000000..d09631e6 --- /dev/null +++ b/Assets/Scripts/EnglewoodLAB/Data/EWLKEquipListData.cs @@ -0,0 +1,71 @@ +#nullable enable +using System.Collections.Generic; + +namespace UVC.EnglewoodLAB.Data +{ + /// 설비 상태 + public enum EWLKEquipStatus + { + /// 준비 (파랑) + Ready, + /// 이상 (빨강) + Error, + /// 휴식 (노랑) + Rest, + /// 계획정지 (회색) + PlannedStop + } + + /// 설비 목록 행 데이터 + public class EWLKEquipListRowData + { + /// 공정 구분 + public string Process { get; internal set; } = string.Empty; + + /// 설비 라인 + public string Line { get; internal set; } = string.Empty; + + /// 설비 분류 + public string Category { get; internal set; } = string.Empty; + + /// 설비 번호 + public string EquipNo { get; internal set; } = string.Empty; + + /// 설비명 + public string EquipName { get; internal set; } = string.Empty; + + /// 상태 + public EWLKEquipStatus Status { get; internal set; } = EWLKEquipStatus.Ready; + + /// 가동 여부 (제어 토글) + public bool IsRunning { get; internal set; } + + /// 세부 설비명 목록 (충/포장설비에 연결된 하위 설비) + public List SubEquipments { get; internal set; } = new(); + + /// 세부 설비 존재 여부 + public bool HasSubEquipments => SubEquipments.Count > 0; + + /// 플레이스홀더 행 생성 + public static EWLKEquipListRowData CreatePlaceholder() + { + return new EWLKEquipListRowData + { + Process = "-", + Line = "-", + Category = "-", + EquipNo = "-", + EquipName = "-", + Status = EWLKEquipStatus.Ready, + IsRunning = false, + }; + } + } + + /// 설비 목록 데이터 + public class EWLKEquipListData + { + /// 설비 행 목록 + public List Rows { get; } = new(); + } +} diff --git a/Assets/Scripts/EnglewoodLAB/Data/EWLKEquipListData.cs.meta b/Assets/Scripts/EnglewoodLAB/Data/EWLKEquipListData.cs.meta new file mode 100644 index 00000000..83d77b71 --- /dev/null +++ b/Assets/Scripts/EnglewoodLAB/Data/EWLKEquipListData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0c70eeb78b5b62e4998d2a7bf6676e1d \ No newline at end of file diff --git a/Assets/Scripts/EnglewoodLAB/UIToolkit/EWLKEquipListContent.cs b/Assets/Scripts/EnglewoodLAB/UIToolkit/EWLKEquipListContent.cs new file mode 100644 index 00000000..76e49927 --- /dev/null +++ b/Assets/Scripts/EnglewoodLAB/UIToolkit/EWLKEquipListContent.cs @@ -0,0 +1,496 @@ +#nullable enable +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UIElements; +using UVC.EnglewoodLAB.Data; +using UVC.UIToolkit; + +namespace UVC.EnglewoodLAB.UIToolkit +{ + /// + /// 설비 목록 컨텐츠. + /// 검색창 + 공정/상태 필터 + 8컬럼 테이블 (ScrollView). + /// 상태: 타원 뱃지, 제어: 가동/정지 읽기전용 토글. + /// + [UxmlElement] + public partial class EWLKEquipListContent : VisualElement, IDisposable + { + private const string UssPath = "EWLK/UIToolkit/Main/EWLKEquipListContentUss"; + private const string SearchPlaceholder = "공정, 라인, 설비 번호, 설비명을 검색해 보세요."; + + // 8컬럼 (상태/제어는 특수 처리) + private static readonly (string cls, string header)[] s_Columns = + { + ("col-el-no", "No"), + ("col-el-process", "공정 구분"), + ("col-el-line", "설비 라인"), + ("col-el-category", "설비 분류"), + ("col-el-equipno", "설비 번호"), + ("col-el-name", "설비명"), + ("col-el-status", "상태"), + ("col-el-control", "제어"), + }; + + // 상태 → (텍스트, CSS modifier) + private static readonly Dictionary s_StatusMap = new() + { + { EWLKEquipStatus.Ready, ("준비", "ewlk-equip__badge--ready") }, + { EWLKEquipStatus.Error, ("이상", "ewlk-equip__badge--error") }, + { EWLKEquipStatus.Rest, ("휴식", "ewlk-equip__badge--rest") }, + { EWLKEquipStatus.PlannedStop, ("계획정지", "ewlk-equip__badge--planned") }, + }; + + private readonly TextField _searchField; + private readonly DropdownField _processFilter; + private readonly DropdownField _statusFilter; + private readonly ScrollView _scrollView; + private readonly VisualElement _rowContainer; + + private string _searchText = string.Empty; + private string _selectedProcess = "공정 전체"; + private string _selectedStatus = "상태 전체"; + private List _allRows = new(); + private List _displayRows = new(); + + private static readonly List s_PlaceholderRows = CreatePlaceholderRows(); + + public EWLKEquipListContent() + { + var uss = Resources.Load(UssPath); + if (uss != null) styleSheets.Add(uss); + + AddToClassList("ewlk-equip"); + + // ── 필터 바 ── + var filterBar = new VisualElement(); + filterBar.AddToClassList("ewlk-equip__filter-bar"); + filterBar.pickingMode = PickingMode.Position; + + // 검색창 + var searchWrap = new VisualElement(); + searchWrap.AddToClassList("ewlk-equip__search-wrap"); + searchWrap.pickingMode = PickingMode.Position; + + var searchIcon = new Label(UTKMaterialIcons.Search); + searchIcon.AddToClassList("ewlk-equip__search-icon"); + UTKMaterialIcons.ApplyIconStyle(searchIcon, 16); + searchWrap.Add(searchIcon); + + _searchField = new TextField(); + _searchField.AddToClassList("ewlk-equip__search-field"); + _searchField.AddToClassList("ewlk-equip__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-equip__search-field--placeholder"); + } + }); + _searchField.RegisterCallback(_ => + { + if (string.IsNullOrEmpty(_searchField.value)) + { + _searchField.value = SearchPlaceholder; + _searchField.AddToClassList("ewlk-equip__search-field--placeholder"); + } + }); + _searchField.RegisterCallback>(evt => + { + var val = evt.newValue ?? string.Empty; + _searchText = val == SearchPlaceholder ? string.Empty : val; + ApplyFilter(); + }); + + // TextField 내부 요소 (USS로 덮어쓸 수 없으므로 인라인 필수) + 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; + } + + searchWrap.Add(_searchField); + filterBar.Add(searchWrap); + + // 공정 필터 + var processChoices = new List + { + "공정 전체", "1층 제조", "3층 생산 충/포장", "4층 생산 충/포장" + }; + _processFilter = new DropdownField(processChoices, 0) { label = string.Empty }; + _processFilter.AddToClassList("ewlk-equip__filter-dropdown"); + _processFilter.pickingMode = PickingMode.Position; + _processFilter.focusable = true; + ApplyDropdownPicking(_processFilter); + _processFilter.RegisterCallback>(evt => + { + _selectedProcess = evt.newValue ?? "공정 전체"; + ApplyFilter(); + }); + filterBar.Add(_processFilter); + + // 상태 필터 + var statusChoices = new List + { + "상태 전체", "준비", "이상", "휴식", "계획정지" + }; + _statusFilter = new DropdownField(statusChoices, 0) { label = string.Empty }; + _statusFilter.AddToClassList("ewlk-equip__filter-dropdown"); + _statusFilter.pickingMode = PickingMode.Position; + _statusFilter.focusable = true; + ApplyDropdownPicking(_statusFilter); + _statusFilter.RegisterCallback>(evt => + { + _selectedStatus = evt.newValue ?? "상태 전체"; + ApplyFilter(); + }); + filterBar.Add(_statusFilter); + + Add(filterBar); + + // ── 테이블 헤더 ── + Add(BuildTableHeader()); + + // ── ScrollView ── + _scrollView = new ScrollView(ScrollViewMode.Vertical); + _scrollView.AddToClassList("ewlk-equip__list"); + _rowContainer = new VisualElement(); + _scrollView.Add(_rowContainer); + Add(_scrollView); + + RefreshView(); + } + + // ── 공개 API ── + + /// MQTT 데이터 갱신. + public void UpdateData(EWLKEquipListData data) + { + _allRows = data.Rows; + ApplyFilter(); + } + + public void Dispose() { } + + // ── 뷰 갱신 ── + + private void RefreshView() + { + _allRows = s_PlaceholderRows; + ApplyFilter(); + } + + private void ApplyFilter() + { + _displayRows = new List(); + + foreach (var row in _allRows) + { + // 공정 필터 + if (_selectedProcess != "공정 전체" && !Contains(row.Process, _selectedProcess)) + continue; + + // 상태 필터 + if (_selectedStatus != "상태 전체") + { + var statusText = s_StatusMap.TryGetValue(row.Status, out var info) ? info.text : ""; + if (statusText != _selectedStatus) continue; + } + + // 검색어 필터 + if (!string.IsNullOrWhiteSpace(_searchText)) + { + if (!Contains(row.Process, _searchText) && + !Contains(row.Line, _searchText) && + !Contains(row.EquipNo, _searchText) && + !Contains(row.EquipName, _searchText)) + continue; + } + + _displayRows.Add(row); + } + + RebuildRows(); + } + + private void RebuildRows() + { + _rowContainer.Clear(); + for (int i = 0; i < _displayRows.Count; i++) + { + var row = MakeRowElement(); + BindRowElement(row, i); + _rowContainer.Add(row); + + // 세부 설비 아코디언 컨테이너 + var d = _displayRows[i]; + if (d.HasSubEquipments) + { + var accordion = new VisualElement(); + accordion.name = "sub-accordion"; + accordion.AddToClassList("ewlk-equip__accordion"); + accordion.style.display = DisplayStyle.None; + + foreach (var subName in d.SubEquipments) + { + var subRow = new VisualElement(); + subRow.AddToClassList("ewlk-equip__sub-row"); + + var subLabel = new Label(subName); + subLabel.AddToClassList("ewlk-equip__sub-label"); + subRow.Add(subLabel); + + accordion.Add(subRow); + } + + _rowContainer.Add(accordion); + + // 클릭 토글 + var capturedAccordion = accordion; + var expandIcon = row.Q