OverView/제조지시현황/작업탐색기 팝업 디자인 및 속성 변경

This commit is contained in:
SOOBEEN HAN
2026-03-23 16:56:29 +09:00
parent d07dc8b4f7
commit 81e6c19484
9 changed files with 830 additions and 668 deletions

View File

@@ -1,206 +1,254 @@
/* ────────────────────────────────────────────────────────── /* ============================================================
EWLKMfgOrderModalContent — 제조지시현황 모달 EWLKMfgOrderModalContent — 제조지시현황 (라이트 테마)
────────────────────────────────────────────────────────── */ 탭 + 요약 바 + 11컬럼 테이블
============================================================ */
/* ── 루트 컨테이너 ──────────────────────────────────────── */ /* ── 루트 ──────────────────────────────────────────────── */
.ewlk-mfgorder { .ewlk-mfgorder {
flex-direction: column;
flex-grow: 1; flex-grow: 1;
background-color: rgb(18, 22, 36); flex-direction: column;
padding: 0; background-color: rgb(255, 255, 255);
min-height: 400px;
} }
/* ── 탭 바 ─────────────────────────────────────────────── */ /* ── 탭 바 ─────────────────────────────────────────────── */
.ewlk-mfgorder__tab-bar { .ewlk-mfgorder__tab-bar {
flex-direction: row; flex-direction: row;
border-bottom-width: 2px; padding-left: 12px;
border-bottom-color: rgb(50, 60, 90); padding-top: 8px;
margin-bottom: 0;
} }
.ewlk-mfgorder__tab { .ewlk-mfgorder__tab {
flex-shrink: 0; min-width: 0;
padding: 8px 20px; padding: 6px 16px;
background-color: rgb(34, 42, 66); margin-right: 4px;
color: rgb(140, 150, 175); background-color: rgba(0, 0, 0, 0);
border-width: 0; border-width: 0;
border-bottom-width: 2px; border-bottom-width: 2px;
border-bottom-color: rgba(0, 0, 0, 0); border-color: rgba(0, 0, 0, 0);
border-radius: 0; border-radius: 0;
font-size: 13px; font-size: 12px;
-unity-font-style: normal; color: rgb(120, 120, 120);
margin-bottom: -2px;
}
.ewlk-mfgorder__tab:hover {
background-color: rgb(40, 50, 78);
color: rgb(220, 225, 240);
} }
.ewlk-mfgorder__tab--active { .ewlk-mfgorder__tab--active {
color: rgb(220, 225, 240); color: rgb(30, 100, 220);
border-bottom-color: rgb(0, 140, 255); border-bottom-color: rgb(30, 100, 220);
-unity-font-style: bold; -unity-font-style: bold;
} }
/* ── 요약 바 ─────────────────────────────────────────────── */ .ewlk-mfgorder__tab:hover {
color: rgb(60, 60, 60);
}
/* ── 요약 바 ────────────────────────────────────────────── */
.ewlk-mfgorder__summary-bar { .ewlk-mfgorder__summary-bar {
flex-direction: row; flex-direction: row;
background-color: rgb(26, 32, 50); padding: 12px 16px;
padding: 10px 16px; border-width: 1px;
border-bottom-width: 1px; border-color: rgb(230, 230, 230);
border-bottom-color: rgb(50, 60, 90); 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; flex-direction: row;
align-items: center; align-items: center;
flex-grow: 1; margin-bottom: 8px;
margin-right: 16px;
} }
.ewlk-mfgorder__summary-title { .ewlk-mfgorder__bar-item-title {
color: rgb(140, 150, 175);
font-size: 11px; font-size: 11px;
margin-right: 8px; color: rgb(100, 100, 100);
width: 110px;
flex-shrink: 0; flex-shrink: 0;
-unity-text-align: middle-left;
} }
.ewlk-mfgorder__summary-value { .ewlk-mfgorder__bar-item-value {
color: rgb(220, 225, 240);
font-size: 13px; font-size: 13px;
-unity-font-style: bold; -unity-font-style: bold;
color: rgb(33, 33, 33);
width: 120px;
flex-shrink: 0; flex-shrink: 0;
-unity-text-align: middle-left;
} }
/* ── 설비가동률 행 ───────────────────────────────────────── */ .ewlk-mfgorder__bar-item .ewlk-mfgorder__progress-wrap {
.ewlk-mfgorder__equip-row { flex-grow: 1;
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__equip-title { /* ── 설비 가동율 값 세로 스택 (%+대수) ──────────────────── */
color: rgb(140, 150, 175); .ewlk-mfgorder__equip-value-stack {
font-size: 11px; flex-direction: column;
width: 120px;
flex-shrink: 0; 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 { .ewlk-mfgorder__count {
color: rgb(140, 150, 175); font-size: 11px;
font-size: 12px; color: rgb(30, 100, 220);
flex-shrink: 0; -unity-font-style: bold;
margin-left: 12px; -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 { .ewlk-mfgorder__progress-wrap {
flex-grow: 1; height: 12px;
height: 10px; background-color: rgb(240, 240, 240);
background-color: rgb(50, 60, 90); border-radius: 2px;
border-radius: 4px;
overflow: hidden; overflow: hidden;
max-width: 300px;
} }
.ewlk-mfgorder__progress-fill { .ewlk-mfgorder__progress-fill {
height: 100%; height: 100%;
background-color: rgb(0, 200, 100); background-color: rgb(180, 200, 230);
border-radius: 4px; border-radius: 2px;
width: 0%; width: 0;
} }
/* ── 테이블 헤더 ─────────────────────────────────────────── */ /* ── 테이블 헤더 ──────────────────────────────────────── */
.ewlk-mfgorder__header { .ewlk-mfgorder__header {
flex-direction: row; flex-direction: row;
background-color: rgb(34, 42, 66); min-height: 32px;
background-color: rgb(245, 245, 250);
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-color: rgb(50, 60, 90); border-color: rgb(220, 220, 220);
padding: 0 4px; margin-left: 12px;
margin-right: 12px;
} }
.ewlk-mfgorder__header-cell { .ewlk-mfgorder__header-cell {
color: rgb(140, 150, 175); -unity-text-align: middle-center;
font-size: 11px; font-size: 11px;
-unity-font-style: bold; -unity-font-style: bold;
-unity-text-align: middle-center; color: rgb(80, 80, 80);
padding: 6px 4px; padding: 4px 2px;
border-right-width: 1px; 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 { .ewlk-mfgorder__list {
flex-grow: 1; flex-grow: 1;
background-color: rgb(18, 22, 36); margin-left: 12px;
margin-right: 12px;
} }
/* ── 데이터 행 ───────────────────────────────────────────── */ /* ── 데이터 행 ─────────────────────────────────────────── */
.ewlk-mfgorder__row { .ewlk-mfgorder__row {
flex-direction: row; flex-direction: row;
align-items: center; min-height: 28px;
height: 28px;
padding: 0 4px;
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-color: rgb(50, 60, 90); border-color: rgb(240, 240, 240);
} }
/* ── 셀 공통 ──────────────────────────────────────────── */
.ewlk-mfgorder__cell { .ewlk-mfgorder__cell {
height: 100%;
justify-content: center; justify-content: center;
padding: 0 4px; align-items: center;
padding: 2px 4px;
border-right-width: 1px; border-right-width: 1px;
border-right-color: rgb(50, 60, 90); border-color: rgb(245, 245, 245);
overflow: hidden; }
.ewlk-mfgorder__cell:last-child {
border-right-width: 0;
} }
.ewlk-mfgorder__cell-label { .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; font-size: 11px;
color: rgb(50, 50, 50);
-unity-text-align: middle-center; -unity-text-align: middle-center;
background-color: rgb(60, 65, 85); overflow: hidden;
padding: 2px 6px; white-space: nowrap;
border-radius: 3px;
display: none;
} }
/* 품목명 줄임표시 */
.ewlk-mfgorder__cell-label--ellipsis {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* ── 진척률 셀 ────────────────────────────────────────── */
.ewlk-mfgorder__pct { .ewlk-mfgorder__pct {
color: rgb(220, 225, 240);
font-size: 11px; font-size: 11px;
-unity-text-align: middle-right; color: rgb(50, 50, 50);
-unity-text-align: middle-center;
min-width: 30px;
flex-shrink: 0; flex-shrink: 0;
margin-left: 4px;
width: 36px;
display: none;
} }
/* ── 컬럼 너비 ───────────────────────────────────────────── */ /* ── 컬럼 너비 ────────────────────────────────────────── */
.col-equip { width: 100px; flex-shrink: 0; } .col-no { width: 35px; flex-shrink: 0; }
.col-order { width: 120px; flex-shrink: 0; } .col-equip { width: 110px; flex-shrink: 0; }
.col-item-code { width: 110px; flex-shrink: 0; } .col-order { width: 100px; flex-shrink: 0; }
.col-item-name { flex-grow: 1; } .col-item-code { width: 100px; flex-shrink: 0; }
.col-time { width: 80px; flex-shrink: 0; } .col-item-name { width: 90px; flex-shrink: 0; }
.col-target { width: 90px; flex-shrink: 0; } .col-start-time { width: 90px; flex-shrink: 0; }
.col-achievement { width: 150px; flex-shrink: 0; flex-direction: row; align-items: center; } .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;
}

View File

@@ -1,128 +1,150 @@
/* ============================================================ /* ============================================================
EWLKOverViewModalContent — OverView 모달 테이블 스타일 EWLKOverViewModalContent — OverView 컨텐츠 스타일
3개 컬럼 (작업장별), 각 컬럼에 월간/일간 섹션 + 프로그레스 바
레이아웃 구조:
.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
============================================================ */ ============================================================ */
/* ── 테이블 루트 ─────────────────────────────────────────── */ /* ── 루트 ──────────────────────────────────────────────── */
.ewlk-overview { .ewlk-overview {
flex-direction: column; flex-grow: 1;
background-color: rgb(255, 255, 255); background-color: rgb(255, 255, 255);
border-width: 1px; padding: 16px;
border-color: rgb(140, 140, 140);
} }
/* ── 헤더 행 ─────────────────────────────────────────────── */ /* ── 3컬럼 컨테이너 ────────────────────────────────────── */
.ewlk-overview__header { .ewlk-overview__columns {
flex-direction: row; 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; flex-grow: 1;
align-items: stretch;
}
/* ── 작업장 컬럼 ────────────────────────────────────────── */
.ewlk-overview__column {
flex-grow: 1;
flex-basis: 0;
flex-direction: column; 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__column:last-child {
.ewlk-overview__row { margin-right: 0;
flex-direction: row;
min-height: 36px;
border-bottom-width: 1px;
border-color: rgb(200, 200, 200);
} }
.ewlk-overview__rows .ewlk-overview__row:last-child { /* ── 컬럼 타이틀 ────────────────────────────────────────── */
border-bottom-width: 0; .ewlk-overview__column-title {
} font-size: 13px;
/* ── 공통 셀 ─────────────────────────────────────────────── */
.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;
-unity-font-style: bold; -unity-font-style: bold;
background-color: rgb(232, 235, 248); color: rgb(33, 33, 33);
border-right-color: rgb(140, 140, 140); -unity-text-align: middle-center;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom-width: 1px;
border-color: rgb(220, 220, 220);
} }
/* 월간/일간 구분 열 */ /* ── 기간 섹션 (월간/일간) — 각각 50% 높이 ──────────────── */
.ewlk-overview__cell--period { .ewlk-overview__section {
width: 50px; flex-direction: column;
flex-shrink: 0;
background-color: rgb(245, 246, 252);
border-right-color: rgb(180, 180, 180);
}
/* 목표수량·현시점계획·실적수량 (동등 너비 분할) */
.ewlk-overview__cell--data {
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-basis: 0;
} }
/* 구분 (제조/생산) — 마지막 열, 오른쪽 테두리 제거 */ /* ── 기간 라벨 ──────────────────────────────────────────── */
.ewlk-overview__cell--type { .ewlk-overview__period-label {
width: 60px; 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; flex-shrink: 0;
border-right-width: 0; width: 55%;
} }
/* 헤더의 name/period/type 셀도 동일 너비 클래스 공유 */ .ewlk-overview__data-name {
.ewlk-overview__header .ewlk-overview__cell--name { font-size: 11px;
background-color: rgb(200, 206, 224); color: rgb(100, 100, 100);
border-right-color: rgb(140, 140, 140); width: 80px;
flex-shrink: 0;
-unity-text-align: middle-left;
} }
.ewlk-overview__header .ewlk-overview__cell--period { .ewlk-overview__data-value {
background-color: rgb(200, 206, 224); 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);
} }

View File

@@ -4,7 +4,10 @@
============================================================ */ ============================================================ */
.ewlk-work-explorer { .ewlk-work-explorer {
flex-grow: 1;
flex-direction: column;
background-color: rgb(255, 255, 255); background-color: rgb(255, 255, 255);
padding: 12px 16px;
} }
/* ── 섹션 타이틀 ──────────────────────────────────────── */ /* ── 섹션 타이틀 ──────────────────────────────────────── */

View File

@@ -3,82 +3,94 @@ using System.Collections.Generic;
namespace UVC.EnglewoodLAB.Data namespace UVC.EnglewoodLAB.Data
{ {
/// <summary>제조지시현황 요약 데이터 (라인별)</summary> /// <summary>제조지시현황 요약 데이터</summary>
public class EWLKMfgOrderSummaryData public class EWLKMfgOrderSummaryData
{ {
/// <summary>공장구분 (탭 레이블로 사용)</summary> /// <summary>공장구분</summary>
public string FactoryName { get; set; } = string.Empty; public string FactoryName { get; internal set; } = string.Empty;
/// <summary>전체 제조 목표량</summary> /// <summary>전체 제조 목표량</summary>
public string TotalTarget { get; set; } = string.Empty; public string TotalTarget { get; internal set; } = string.Empty;
/// <summary>실제 실적량</summary> /// <summary>실제 실적량</summary>
public string TotalActual { get; set; } = string.Empty; public string TotalActual { get; internal set; } = string.Empty;
/// <summary>전체 제조 진척률 (0~100)</summary> /// <summary>전체 제조 진척률 (0~100)</summary>
public float TotalProgress { get; set; } public float TotalProgress { get; internal set; }
/// <summary>일 설비가동률 가중대수% (0~100)</summary> /// <summary>일 설비가동률 가중대수% (0~100)</summary>
public float EquipRate { get; set; } public float EquipRate { get; internal set; }
/// <summary>가동 중 설비 수</summary> /// <summary>가동 중 설비 수</summary>
public int ActiveCount { get; set; } public int ActiveCount { get; internal set; }
/// <summary>전체 설비 수</summary> /// <summary>전체 설비 수</summary>
public int TotalCount { get; set; } public int TotalCount { get; internal set; }
} }
/// <summary>설비별 제조지시 행 데이터</summary> /// <summary>설비별 제조지시 행 데이터</summary>
public class EWLKMfgOrderRowData public class EWLKMfgOrderRowData
{ {
/// <summary>설비명</summary> /// <summary>설비명</summary>
public string EquipName { get; set; } = string.Empty; public string EquipName { get; internal set; } = string.Empty;
/// <summary>지시번호</summary> /// <summary>지시번호</summary>
public string OrderNo { get; set; } = string.Empty; public string OrderNo { get; internal set; } = string.Empty;
/// <summary>품목코드</summary> /// <summary>품목코드</summary>
public string ItemCode { get; set; } = string.Empty; public string ItemCode { get; internal set; } = string.Empty;
/// <summary>품목명</summary> /// <summary>품목명</summary>
public string ItemName { get; set; } = string.Empty; public string ItemName { get; internal set; } = string.Empty;
/// <summary>시작시간</summary> /// <summary>시작시간</summary>
public string StartTime { get; set; } = string.Empty; public string StartTime { get; internal set; } = string.Empty;
/// <summary>목표량</summary> /// <summary>종료시간</summary>
public string Target { get; set; } = string.Empty; public string EndTime { get; internal set; } = string.Empty;
/// <summary>달성률 (0~100). IsStopped가 true면 사용되지 않음</summary> /// <summary>총 작업 시간</summary>
public float Achievement { get; set; } public string TotalWorkTime { get; internal set; } = string.Empty;
/// <summary>계획 정지 여부 (true면 달성률 대신 "계획 정지" 표시)</summary> /// <summary>계획 생산량(kg)</summary>
public bool IsStopped { get; set; } public string PlanProduction { get; internal set; } = string.Empty;
/// <summary>실 생산량(kg)</summary>
public string ActualProduction { get; internal set; } = string.Empty;
/// <summary>달성률/진척률 (0~100)</summary>
public float Achievement { get; internal set; }
/// <summary>계획 정지 여부</summary>
public bool IsStopped { get; internal set; }
/// <summary>플레이스홀더 행을 생성합니다.</summary>
public static EWLKMfgOrderRowData CreatePlaceholder()
{
return new EWLKMfgOrderRowData
{
EquipName = "-",
OrderNo = "-",
ItemCode = "-",
ItemName = "-",
StartTime = "-",
EndTime = "-",
TotalWorkTime = "-",
PlanProduction = "-",
ActualProduction = "-",
Achievement = 0f,
IsStopped = false,
};
}
} }
/// <summary>공장 라인별 제조지시 데이터 (요약 + 행 목록)</summary> /// <summary>제조지시현황 데이터 (요약 + 행 목록)</summary>
public class EWLKMfgOrderLineData public class EWLKMfgOrderLineData
{ {
/// <summary>기본 라인 이름 (MQTT summary 수신 전 초기값)</summary>
public string DefaultName { get; }
/// <summary>요약 데이터</summary> /// <summary>요약 데이터</summary>
public EWLKMfgOrderSummaryData Summary { get; } = new(); public EWLKMfgOrderSummaryData Summary { get; } = new();
/// <summary>설비별 행 목록 (ListView itemsSource)</summary> /// <summary>설비별 행 목록</summary>
public List<EWLKMfgOrderRowData> Rows { get; } = new(); public List<EWLKMfgOrderRowData> Rows { get; } = new();
/// <param name="defaultName">MQTT 수신 전 초기 탭 레이블</param>
public EWLKMfgOrderLineData(string defaultName) => DefaultName = defaultName;
}
/// <summary>제조지시현황 전체 데이터 (2개 라인)</summary>
public class EWLKMfgOrderData
{
/// <summary>라인 1</summary>
public EWLKMfgOrderLineData Line1 { get; } = new("라인 1");
/// <summary>라인 2</summary>
public EWLKMfgOrderLineData Line2 { get; } = new("라인 2");
} }
} }

View File

@@ -8,105 +8,53 @@ namespace UVC.EnglewoodLAB.Data
{ {
/// <summary> /// <summary>
/// 제조지시현황 데이터를 MQTT로 수신하는 서비스. /// 제조지시현황 데이터를 MQTT로 수신하는 서비스.
/// 2개 라인의 요약 및 설비 행 데이터를 구독합니다. /// 요약 및 설비 행 데이터를 구독합니다.
/// DataRepository.Instance.MqttReceiver(공유 수신기)를 사용합니다.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// MQTT 토픽 구조: /// MQTT 토픽:
/// ewlk/mfgorder/line1/summary — 라인1 요약 (DataObject) /// ewlk/mfgorder/line1/summary 요약 (DataObject)
/// ewlk/mfgorder/line1/rows — 라인1 설비 행 배열 (DataArray) /// ewlk/mfgorder/line1/rows 설비 행 배열 (DataArray)
/// ewlk/mfgorder/line2/summary — 라인2 요약 (DataObject)
/// ewlk/mfgorder/line2/rows — 라인2 설비 행 배열 (DataArray)
///
/// Summary JSON 스키마:
/// <code>
/// {
/// "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
/// }
/// </code>
///
/// Rows JSON 스키마 (배열):
/// <code>
/// [
/// {
/// "equip_name": "UHM50",
/// "order_no": "-",
/// "item_code": "-",
/// "item_name": "-",
/// "start_time": "-",
/// "target": "-",
/// "achievement": 0.0,
/// "status": "stopped"
/// },
/// ...
/// ]
/// </code>
/// </remarks> /// </remarks>
public class EWLKMfgOrderMqttService : IDisposable public class EWLKMfgOrderMqttService : IDisposable
{ {
// ── MQTT 토픽 ────────────────────────────────────────────── private const string TopicSummary = "ewlk/mfgorder/line1/summary";
private const string TopicLine1Summary = "ewlk/mfgorder/line1/summary"; private const string TopicRows = "ewlk/mfgorder/line1/rows";
private const string TopicLine1Rows = "ewlk/mfgorder/line1/rows";
private const string TopicLine2Summary = "ewlk/mfgorder/line2/summary";
private const string TopicLine2Rows = "ewlk/mfgorder/line2/rows";
// ── DataMask ──────────────────────────────────────────────
private static readonly DataMask s_SummaryMask = CreateSummaryMask(); private static readonly DataMask s_SummaryMask = CreateSummaryMask();
private static readonly DataMask s_RowMask = CreateRowMask(); private static readonly DataMask s_RowMask = CreateRowMask();
private bool _subscribed; private bool _subscribed;
private bool _disposed; private bool _disposed;
// ── 공개 API ──────────────────────────────────────────────
/// <summary>가장 최근에 수신한 제조지시 데이터</summary> /// <summary>가장 최근에 수신한 제조지시 데이터</summary>
public EWLKMfgOrderData CurrentData { get; } = new(); public EWLKMfgOrderLineData CurrentData { get; } = new();
/// <summary>라인 데이터(요약 또는 행)가 갱신될 때 발생 — 메인 스레드에서 호출됩니다.</summary> /// <summary>데이터 갱신 시 발생 — 메인 스레드에서 호출됩니다.</summary>
public event Action<EWLKMfgOrderData>? OnDataUpdated; public event Action<EWLKMfgOrderLineData>? OnDataUpdated;
// ── 구독 관리 ────────────────────────────────────────────── /// <summary>MQTT 토픽을 구독합니다.</summary>
/// <summary>
/// DataRepository 공유 수신기에 4개 토픽을 등록합니다.
/// MqttReceiver.Start()는 EWLKSceneMain에서 호출합니다.
/// </summary>
public void Subscribe() public void Subscribe()
{ {
if (_subscribed || _disposed) return; if (_subscribed || _disposed) return;
_subscribed = true; _subscribed = true;
var receiver = DataRepository.Instance.MqttReceiver; var receiver = DataRepository.Instance.MqttReceiver;
receiver.Add(BuildSummaryConfig(TopicLine1Summary, receiver.Add(BuildSummaryConfig(TopicSummary, UpdateSummary));
data => UpdateSummary(CurrentData.Line1, data))); receiver.Add(BuildRowsConfig(TopicRows, UpdateRows));
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)));
} }
/// <summary>DataRepository 공유 수신기에서 4개 토픽 구독을 해제합니다.</summary> /// <summary>MQTT 토픽 구독을 해제합니다.</summary>
public void Unsubscribe() public void Unsubscribe()
{ {
if (!_subscribed) return; if (!_subscribed) return;
_subscribed = false; _subscribed = false;
var receiver = DataRepository.Instance.MqttReceiver; var receiver = DataRepository.Instance.MqttReceiver;
receiver.Remove(TopicLine1Summary); receiver.Remove(TopicSummary);
receiver.Remove(TopicLine1Rows); receiver.Remove(TopicRows);
receiver.Remove(TopicLine2Summary);
receiver.Remove(TopicLine2Rows);
} }
// ── 내부 구현 ────────────────────────────────────────────── // ── 내부 구현 ──
private static DataMask CreateSummaryMask() private static DataMask CreateSummaryMask()
{ {
@@ -129,9 +77,12 @@ namespace UVC.EnglewoodLAB.Data
mask["item_code"] = ""; mask["item_code"] = "";
mask["item_name"] = ""; mask["item_name"] = "";
mask["start_time"] = ""; mask["start_time"] = "";
mask["target"] = ""; mask["end_time"] = "";
mask["total_work_time"] = "";
mask["plan_production"] = "";
mask["actual_production"] = "";
mask["achievement"] = 0.0f; mask["achievement"] = 0.0f;
mask["status"] = ""; // "running" | "stopped" mask["status"] = "";
return mask; return mask;
} }
@@ -147,12 +98,11 @@ namespace UVC.EnglewoodLAB.Data
.SetDataMapper(new DataMapper(s_RowMask)) .SetDataMapper(new DataMapper(s_RowMask))
.SetHandler(handler); .SetHandler(handler);
/// <summary>요약 DataObject를 SummaryData에 반영합니다.</summary> private void UpdateSummary(IDataObject? data)
private void UpdateSummary(EWLKMfgOrderLineData line, IDataObject? data)
{ {
if (data is not DataObject obj) return; if (data is not DataObject obj) return;
var s = line.Summary; var s = CurrentData.Summary;
s.FactoryName = obj.GetString("factory_name") ?? string.Empty; s.FactoryName = obj.GetString("factory_name") ?? string.Empty;
s.TotalTarget = obj.GetString("total_target") ?? string.Empty; s.TotalTarget = obj.GetString("total_target") ?? string.Empty;
s.TotalActual = obj.GetString("total_actual") ?? string.Empty; s.TotalActual = obj.GetString("total_actual") ?? string.Empty;
@@ -164,25 +114,24 @@ namespace UVC.EnglewoodLAB.Data
OnDataUpdated?.Invoke(CurrentData); OnDataUpdated?.Invoke(CurrentData);
} }
/// <summary> private void UpdateRows(IDataObject? data)
/// 행 DataArray를 파싱하여 Rows를 교체합니다.
/// DataArray는 List&lt;DataObject&gt;를 상속합니다.
/// </summary>
private void UpdateRows(EWLKMfgOrderLineData line, IDataObject? data)
{ {
if (data is not DataArray arr) return; if (data is not DataArray arr) return;
line.Rows.Clear(); CurrentData.Rows.Clear();
foreach (var obj in arr) foreach (var obj in arr)
{ {
line.Rows.Add(new EWLKMfgOrderRowData CurrentData.Rows.Add(new EWLKMfgOrderRowData
{ {
EquipName = obj.GetString("equip_name") ?? string.Empty, EquipName = obj.GetString("equip_name") ?? string.Empty,
OrderNo = obj.GetString("order_no") ?? string.Empty, OrderNo = obj.GetString("order_no") ?? string.Empty,
ItemCode = obj.GetString("item_code") ?? string.Empty, ItemCode = obj.GetString("item_code") ?? string.Empty,
ItemName = obj.GetString("item_name") ?? string.Empty, ItemName = obj.GetString("item_name") ?? string.Empty,
StartTime = obj.GetString("start_time") ?? string.Empty, StartTime = obj.GetString("start_time") ?? string.Empty,
Target = obj.GetString("target") ?? 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, Achievement = obj.GetFloat("achievement") ?? 0f,
IsStopped = (obj.GetString("status") ?? "") == "stopped", IsStopped = (obj.GetString("status") ?? "") == "stopped",
}); });
@@ -191,8 +140,6 @@ namespace UVC.EnglewoodLAB.Data
OnDataUpdated?.Invoke(CurrentData); OnDataUpdated?.Invoke(CurrentData);
} }
// ── IDisposable ────────────────────────────────────────────
public void Dispose() public void Dispose()
{ {
if (_disposed) return; if (_disposed) return;

View File

@@ -482,7 +482,7 @@ namespace UVC.EnglewoodLAB
BindMenuPopupContent(menuId, _menuPopup); BindMenuPopupContent(menuId, _menuPopup);
// 메뉴별 사이즈 조정 (기본: 전체 폭) // 메뉴별 사이즈 조정 (기본: 전체 폭)
if (menuId == "work_explorer") if (menuId is "work_explorer" or "equip_list")
{ {
_menuPopup.style.right = StyleKeyword.Auto; _menuPopup.style.right = StyleKeyword.Auto;
_menuPopup.style.width = new Length(40, LengthUnit.Percent); _menuPopup.style.width = new Length(40, LengthUnit.Percent);
@@ -558,6 +558,14 @@ namespace UVC.EnglewoodLAB
popup.AddContent(packContent); popup.AddContent(packContent);
break; 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": case "work_explorer":
var workExplorerContent = new EWLKWorkExplorerContent(); var workExplorerContent = new EWLKWorkExplorerContent();
// TODO: MQTT 서비스 연결 시 아래 패턴 적용 // TODO: MQTT 서비스 연결 시 아래 패턴 적용

View File

@@ -4,172 +4,237 @@ using System.Collections.Generic;
using UnityEngine; using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
using UVC.EnglewoodLAB.Data; using UVC.EnglewoodLAB.Data;
using UVC.UIToolkit;
namespace UVC.EnglewoodLAB.UIToolkit namespace UVC.EnglewoodLAB.UIToolkit
{ {
/// <summary> /// <summary>
/// 제조지시현황 모달 콘텐츠. /// 제조지시현황 텐츠.
/// 2개 라인 탭, 요약 바, 설비 행 ListView로 구성됩니다. /// 2개 라인 탭, 요약 바 (목표량/실적량 + 진척률/가동률), 11컬럼 테이블.
/// MQTT 데이터가 없는 경우 플레이스홀더 행을 표시합니다.
/// </summary> /// </summary>
[UxmlElement] [UxmlElement]
public partial class EWLKMfgOrderModalContent : VisualElement, IDisposable public partial class EWLKMfgOrderModalContent : VisualElement, IDisposable
{ {
private const string UssPath = "EWLK/UIToolkit/Main/EWLKMfgOrderModalContentUss"; private const string UssPath = "EWLK/UIToolkit/Main/EWLKMfgOrderModalContentUss";
private const int ItemNameMaxChars = 5;
// ── 탭 ────────────────────────────────────────────── // ── 탭 ──
private readonly Button _tab1Btn; private readonly Button _tab1Btn;
private readonly Button _tab2Btn; private readonly Button _tab2Btn;
// ── 요약 바 ───────────────────────────────────────── // ── 요약 바 (좌측) ──
private readonly Label _totalTargetLabel; private readonly Label _totalTargetLabel;
private readonly VisualElement _totalTargetFill;
private readonly Label _totalActualLabel; private readonly Label _totalActualLabel;
private readonly VisualElement _totalActualFill;
// ── 요약 바 (우측) ──
private readonly Label _totalProgressLabel; private readonly Label _totalProgressLabel;
private readonly VisualElement _totalProgressFill; private readonly VisualElement _totalProgressFill;
private readonly Label _equipRateLabel; private readonly Label _equipRateLabel;
private readonly VisualElement _equipRateFill; private readonly VisualElement _equipRateFill;
private readonly Label _countLabel; 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 EWLKMfgOrderLineData? _currentLine;
private int _activeTabIndex; private List<EWLKMfgOrderRowData> _allRows = new();
private List<EWLKMfgOrderRowData> _displayRows = new(); private List<EWLKMfgOrderRowData> _displayRows = new();
// ── 플레이스홀더 (MQTT 연결 전 기본 표시용) ──────────
private static readonly List<EWLKMfgOrderRowData> s_PlaceholderRows = CreatePlaceholderRows(); private static readonly List<EWLKMfgOrderRowData> 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() public EWLKMfgOrderModalContent()
{ {
styleSheets.Add(Resources.Load<StyleSheet>(UssPath)); var uss = Resources.Load<StyleSheet>(UssPath);
if (uss != null) styleSheets.Add(uss);
AddToClassList("ewlk-mfgorder"); AddToClassList("ewlk-mfgorder");
// ── 탭 바 ──────────────────────────────────────── // ── 탭 바 ──
var tabBar = new VisualElement(); var tabBar = new VisualElement();
tabBar.AddToClassList("ewlk-mfgorder__tab-bar"); 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"); _tab1Btn.AddToClassList("ewlk-mfgorder__tab");
_tab2Btn = new Button(() => SelectTab(1)) { text = "라인 2" }; _tab1Btn.AddToClassList("ewlk-mfgorder__tab--active");
_tab2Btn.AddToClassList("ewlk-mfgorder__tab"); _tab2Btn = _tab1Btn; // 단일 탭 (호환성 유지)
tabBar.Add(_tab1Btn); tabBar.Add(_tab1Btn);
tabBar.Add(_tab2Btn);
Add(tabBar); Add(tabBar);
// ── 요약 바 ────────────────────────────────────── // ── 요약 바 (좌측: 목표량+실적량, 우측: 진척률+가동률) ──
var summaryBar = new VisualElement(); var summaryBar = new VisualElement();
summaryBar.AddToClassList("ewlk-mfgorder__summary-bar"); 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(); var equipRow = new VisualElement();
equipRow.AddToClassList("ewlk-mfgorder__equip-row"); equipRow.AddToClassList("ewlk-mfgorder__bar-item");
var equipTitle = new Label("일 설비가동률(가중대수%)"); var equipTitle = new Label("일 설비 가동");
equipTitle.AddToClassList("ewlk-mfgorder__equip-title"); equipTitle.AddToClassList("ewlk-mfgorder__bar-item-title");
equipRow.Add(equipTitle); 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 = new VisualElement();
_equipRateFill.AddToClassList("ewlk-mfgorder__progress-fill"); _equipRateFill.AddToClassList("ewlk-mfgorder__progress-fill");
equipBarWrap.Add(_equipRateFill); equipBarBg.Add(_equipRateFill);
equipRow.Add(equipBarWrap); equipRow.Add(equipBarBg);
_equipRateLabel = new Label("-"); summaryRight.Add(equipRow);
_equipRateLabel.AddToClassList("ewlk-mfgorder__equip-rate");
equipRow.Add(_equipRateLabel);
_countLabel = new Label("- / -"); summaryBar.Add(summaryRight);
_countLabel.AddToClassList("ewlk-mfgorder__count"); Add(summaryBar);
equipRow.Add(_countLabel);
Add(equipRow);
// ── 테이블 헤더 ─────────────────────────────────── // ── 검색창 ──
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<FocusInEvent>(_ =>
{
if (_searchField.value == SearchPlaceholder)
{
_searchField.value = string.Empty;
_searchField.RemoveFromClassList("ewlk-mfgorder__search-field--placeholder");
}
});
_searchField.RegisterCallback<FocusOutEvent>(_ =>
{
if (string.IsNullOrEmpty(_searchField.value))
{
_searchField.value = SearchPlaceholder;
_searchField.AddToClassList("ewlk-mfgorder__search-field--placeholder");
}
});
_searchField.RegisterCallback<ChangeEvent<string>>(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()); Add(BuildTableHeader());
// ── ListView ───────────────────────────────────── // ── ScrollView + 행 컨테이너 ──
_listView = new ListView _scrollView = new ScrollView(ScrollViewMode.Vertical);
{ _scrollView.AddToClassList("ewlk-mfgorder__list");
makeItem = MakeRowElement, _rowContainer = new VisualElement();
bindItem = BindRowElement, _scrollView.Add(_rowContainer);
fixedItemHeight = 28, Add(_scrollView);
selectionType = SelectionType.None,
};
_listView.AddToClassList("ewlk-mfgorder__list");
Add(_listView);
SetTabActive(0);
// 데이터가 없어도 플레이스홀더로 레이아웃 표시
RefreshView(); RefreshView();
} }
// ── 공개 API ────────────────────────────────────────── // ── 공개 API ──
/// <summary> /// <summary>MQTT 데이터 갱신.</summary>
/// 데이터를 갱신합니다. MQTT 수신 시 호출됩니다 (메인 스레드 보장). public void UpdateData(EWLKMfgOrderLineData data)
/// </summary>
public void UpdateData(EWLKMfgOrderData data)
{ {
_data = data; if (!string.IsNullOrEmpty(data.Summary.FactoryName))
_tab1Btn.text = data.Summary.FactoryName;
// 공장구분이 수신되면 탭 레이블 갱신 _currentLine = data;
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;
RefreshView(); RefreshView();
} }
public void Dispose() { } public void Dispose() { }
// ── 탭 선택 ─────────────────────────────────────────── // ── 탭 (단일 탭, 호환성 유지) ──
private void SelectTab(int index) private void SelectTab(int index)
{ {
_activeTabIndex = index;
SetTabActive(index);
if (_data != null)
_currentLine = index == 0 ? _data.Line1 : _data.Line2;
RefreshView(); 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() private void RefreshView()
{ {
if (_currentLine == null) if (_currentLine == null)
{ {
// 데이터 없음 — 플레이스홀더 표시
_totalTargetLabel.text = "-"; _totalTargetLabel.text = "-";
_totalActualLabel.text = "-"; _totalActualLabel.text = "-";
_totalProgressLabel.text = "-"; _totalProgressLabel.text = "-";
_totalProgressFill.style.width = Length.Percent(0f); _totalProgressFill.style.width = Length.Percent(0f);
_totalTargetFill.style.width = Length.Percent(0f);
_totalActualFill.style.width = Length.Percent(0f);
_equipRateLabel.text = "-"; _equipRateLabel.text = "-";
_equipRateFill.style.width = Length.Percent(0f); _equipRateFill.style.width = Length.Percent(0f);
_countLabel.text = "- / -"; _countLabel.text = "- / -";
_displayRows = s_PlaceholderRows; _displayRows = s_PlaceholderRows;
_listView.itemsSource = _displayRows; RebuildRows();
_listView.Rebuild();
return; return;
} }
@@ -178,50 +243,89 @@ namespace UVC.EnglewoodLAB.UIToolkit
_totalTargetLabel.text = Or(s.TotalTarget, "-"); _totalTargetLabel.text = Or(s.TotalTarget, "-");
_totalActualLabel.text = Or(s.TotalActual, "-"); _totalActualLabel.text = Or(s.TotalActual, "-");
_totalProgressLabel.text = $"{s.TotalProgress:F1}%"; _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}%"; _equipRateLabel.text = $"{s.EquipRate:F1}%";
_equipRateFill.style.width = _equipRateFill.style.width = Length.Percent(Mathf.Clamp(s.EquipRate, 0f, 100f));
Length.Percent(Mathf.Clamp(s.EquipRate, 0f, 100f)); _countLabel.text = $"{s.ActiveCount} / {s.TotalCount} 대";
_countLabel.text = $"{s.ActiveCount} / {s.TotalCount}";
_displayRows = _currentLine.Rows; _allRows = _currentLine.Rows;
_listView.itemsSource = _displayRows; ApplyFilter();
_listView.Rebuild();
} }
// ── ListView 바인딩 ─────────────────────────────────── // ── 검색 ──
private const string SearchPlaceholder = "설비명, 지시번호, 품목 코드, 품목명을 검색해 보세요.";
private void OnSearchChanged(ChangeEvent<string> evt)
{
var val = evt.newValue ?? string.Empty;
_searchText = val == SearchPlaceholder ? string.Empty : val;
ApplyFilter();
}
/// <summary>검색어로 행을 필터링하고 재생성합니다.</summary>
private void ApplyFilter()
{
if (string.IsNullOrWhiteSpace(_searchText))
{
_displayRows = _allRows;
}
else
{
var keyword = _searchText.Trim();
_displayRows = new List<EWLKMfgOrderRowData>();
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;
/// <summary>ScrollView 내 행을 재생성합니다.</summary>
private void RebuildRows()
{
_rowContainer.Clear();
for (int i = 0; i < _displayRows.Count; i++)
{
var row = MakeRowElement();
BindRowElement(row, i);
_rowContainer.Add(row);
}
}
// ── ListView 바인딩 ──
/// <summary>
/// 행 요소를 생성합니다. 모든 셀과 서브 요소를 미리 생성합니다.
/// </summary>
private static VisualElement MakeRowElement() private static VisualElement MakeRowElement()
{ {
var row = new VisualElement(); var row = new VisualElement();
row.AddToClassList("ewlk-mfgorder__row"); row.AddToClassList("ewlk-mfgorder__row");
// 문자열 셀 6개 (설비명~목표량) foreach (var (cls, _) in s_Columns)
var textCols = new[] {
"col-equip", "col-order", "col-item-code",
"col-item-name", "col-time", "col-target",
};
foreach (var cls in textCols)
{ {
var cell = new VisualElement(); var cell = new VisualElement();
cell.AddToClassList("ewlk-mfgorder__cell"); cell.AddToClassList("ewlk-mfgorder__cell");
cell.AddToClassList(cls); cell.AddToClassList(cls);
var lbl = new Label();
lbl.AddToClassList("ewlk-mfgorder__cell-label");
cell.Add(lbl);
row.Add(cell);
}
// 달성률 셀 (progress + stopped-label + pct-label)
var achieveCell = new VisualElement();
achieveCell.AddToClassList("ewlk-mfgorder__cell");
achieveCell.AddToClassList("col-achievement");
if (cls == "col-achievement")
{
// 진척률: 프로그레스 바 + 퍼센트 라벨
var barWrap = new VisualElement(); var barWrap = new VisualElement();
barWrap.name = "bar-wrap"; barWrap.name = "bar-wrap";
barWrap.AddToClassList("ewlk-mfgorder__progress-wrap"); barWrap.AddToClassList("ewlk-mfgorder__progress-wrap");
@@ -229,133 +333,123 @@ namespace UVC.EnglewoodLAB.UIToolkit
fill.name = "achieve-fill"; fill.name = "achieve-fill";
fill.AddToClassList("ewlk-mfgorder__progress-fill"); fill.AddToClassList("ewlk-mfgorder__progress-fill");
barWrap.Add(fill); barWrap.Add(fill);
cell.Add(barWrap);
var stoppedLbl = new Label("계획 정지");
stoppedLbl.name = "stopped-label";
stoppedLbl.AddToClassList("ewlk-mfgorder__stopped");
var pctLbl = new Label(); var pctLbl = new Label();
pctLbl.name = "pct-label"; pctLbl.name = "pct-label";
pctLbl.AddToClassList("ewlk-mfgorder__pct"); pctLbl.AddToClassList("ewlk-mfgorder__pct");
cell.Add(pctLbl);
}
else
{
var lbl = new Label();
lbl.AddToClassList("ewlk-mfgorder__cell-label");
achieveCell.Add(barWrap); // 품목명: 텍스트 줄임 + 툴팁
achieveCell.Add(stoppedLbl); if (cls == "col-item-name")
achieveCell.Add(pctLbl); lbl.AddToClassList("ewlk-mfgorder__cell-label--ellipsis");
row.Add(achieveCell);
cell.Add(lbl);
}
row.Add(cell);
}
return row; return row;
} }
/// <summary>
/// ListView 바인딩. _displayRows(실데이터 또는 플레이스홀더)를 사용합니다.
/// </summary>
private void BindRowElement(VisualElement element, int index) private void BindRowElement(VisualElement element, int index)
{ {
if (index >= _displayRows.Count) return; if (index >= _displayRows.Count) return;
var d = _displayRows[index]; var d = _displayRows[index];
// No (1-based)
SetLabel(element, "col-no", (index + 1).ToString());
SetLabel(element, "col-equip", d.EquipName); SetLabel(element, "col-equip", d.EquipName);
SetLabel(element, "col-order", d.OrderNo); SetLabel(element, "col-order", d.OrderNo);
SetLabel(element, "col-item-code", d.ItemCode); 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<VisualElement>("bar-wrap"); // 품목명: 5글자 초과 시 줄임 + 툴팁
SetItemNameLabel(element, d.ItemName);
SetLabel(element, "col-start-time", d.StartTime);
SetLabel(element, "col-end-time", d.EndTime);
SetLabel(element, "col-work-time", d.TotalWorkTime);
SetLabel(element, "col-plan-prod", d.PlanProduction);
SetLabel(element, "col-actual-prod", d.ActualProduction);
// 진척률
var fill = element.Q<VisualElement>("achieve-fill"); var fill = element.Q<VisualElement>("achieve-fill");
var stoppedLbl = element.Q<Label>("stopped-label");
var pctLbl = element.Q<Label>("pct-label"); var pctLbl = element.Q<Label>("pct-label");
if (d.IsStopped)
{
if (barWrap != null) barWrap.style.display = DisplayStyle.None;
if (stoppedLbl != null) stoppedLbl.style.display = DisplayStyle.Flex;
if (pctLbl != null) pctLbl.style.display = DisplayStyle.None;
}
else
{
float pct = Mathf.Clamp(d.Achievement, 0f, 100f); float pct = Mathf.Clamp(d.Achievement, 0f, 100f);
if (barWrap != null)
{
barWrap.style.display = DisplayStyle.Flex;
if (fill != null) fill.style.width = Length.Percent(pct); if (fill != null) fill.style.width = Length.Percent(pct);
} if (pctLbl != null) pctLbl.text = $"{d.Achievement:F0}";
if (stoppedLbl != null) stoppedLbl.style.display = DisplayStyle.None;
if (pctLbl != null)
{
pctLbl.style.display = DisplayStyle.Flex;
pctLbl.text = $"{d.Achievement:F1}%";
}
}
} }
// ── 헬퍼 ────────────────────────────────────────────── // ── 헬퍼 ──
private static void SetLabel(VisualElement row, string cellClass, string text) private static void SetLabel(VisualElement row, string cellClass, string text)
{ {
var cell = row.Q(className: cellClass); var cell = row.Q(className: cellClass);
var lbl = cell?.Q<Label>(); var lbl = cell?.Q<Label>(className: "ewlk-mfgorder__cell-label");
if (lbl != null) lbl.text = Or(text, "-"); if (lbl != null) lbl.text = Or(text, "-");
} }
private static void SetItemNameLabel(VisualElement row, string itemName)
{
var cell = row.Q(className: "col-item-name");
var lbl = cell?.Q<Label>(className: "ewlk-mfgorder__cell-label");
if (lbl == null) return;
string displayText = Or(itemName, "-");
if (displayText.Length > ItemNameMaxChars)
{
lbl.text = displayText[..ItemNameMaxChars] + "...";
lbl.tooltip = displayText;
}
else
{
lbl.text = displayText;
lbl.tooltip = string.Empty;
}
}
private static string Or(string value, string fallback) => private static string Or(string value, string fallback) =>
string.IsNullOrEmpty(value) ? fallback : value; string.IsNullOrEmpty(value) ? fallback : value;
/// <summary>
/// MQTT 연결 전에 레이아웃을 미리 확인하기 위한 플레이스홀더 행 생성.
/// Achievement = 10f 로 달성률 바 그래프를 기본 표시합니다.
/// </summary>
private static List<EWLKMfgOrderRowData> CreatePlaceholderRows() private static List<EWLKMfgOrderRowData> CreatePlaceholderRows()
{ {
var rows = new List<EWLKMfgOrderRowData>(); var rows = new List<EWLKMfgOrderRowData>();
for (int i = 0; i < 5; i++) for (int i = 0; i < 5; i++)
{ rows.Add(EWLKMfgOrderRowData.CreatePlaceholder());
rows.Add(new EWLKMfgOrderRowData
{
Achievement = 10f,
IsStopped = false,
});
}
return rows; return rows;
} }
private static VisualElement MakeStaticItem(string title, out Label valueLabel) /// <summary>가로 배치 바 아이템: [라벨] [값] [프로그레스 바]</summary>
private static VisualElement MakeBarItem(string title, out Label valueLabel, out VisualElement fill)
{ {
var wrap = new VisualElement(); var wrap = new VisualElement();
wrap.AddToClassList("ewlk-mfgorder__summary-item"); wrap.AddToClassList("ewlk-mfgorder__bar-item");
var t = new Label(title); // 라벨
t.AddToClassList("ewlk-mfgorder__summary-title"); var titleLbl = new Label(title);
wrap.Add(t); titleLbl.AddToClassList("ewlk-mfgorder__bar-item-title");
wrap.Add(titleLbl);
// 값
valueLabel = new Label("-"); valueLabel = new Label("-");
valueLabel.AddToClassList("ewlk-mfgorder__summary-value"); valueLabel.AddToClassList("ewlk-mfgorder__bar-item-value");
wrap.Add(valueLabel); wrap.Add(valueLabel);
return wrap;
}
private static VisualElement MakeProgressItem( // 프로그레스 바
string title, var barBg = new VisualElement();
out VisualElement fill, barBg.AddToClassList("ewlk-mfgorder__progress-wrap");
out Label valueLabel)
{
var wrap = new VisualElement();
wrap.AddToClassList("ewlk-mfgorder__summary-item");
var t = new Label(title);
t.AddToClassList("ewlk-mfgorder__summary-title");
wrap.Add(t);
var barWrap = new VisualElement();
barWrap.AddToClassList("ewlk-mfgorder__progress-wrap");
fill = new VisualElement(); fill = new VisualElement();
fill.AddToClassList("ewlk-mfgorder__progress-fill"); fill.AddToClassList("ewlk-mfgorder__progress-fill");
barWrap.Add(fill); barBg.Add(fill);
wrap.Add(barWrap); wrap.Add(barBg);
valueLabel = new Label("-");
valueLabel.AddToClassList("ewlk-mfgorder__summary-value");
wrap.Add(valueLabel);
return wrap; return wrap;
} }
@@ -364,22 +458,14 @@ namespace UVC.EnglewoodLAB.UIToolkit
var header = new VisualElement(); var header = new VisualElement();
header.AddToClassList("ewlk-mfgorder__header"); header.AddToClassList("ewlk-mfgorder__header");
var cols = new[] { foreach (var (cls, text) in s_Columns)
("설비명", "col-equip"),
("지시번호", "col-order"),
("품목코드", "col-item-code"),
("품목명", "col-item-name"),
("시작시간", "col-time"),
("목표량", "col-target"),
("달성률", "col-achievement"),
};
foreach (var (text, cls) in cols)
{ {
var cell = new Label(text); var cell = new Label(text);
cell.AddToClassList("ewlk-mfgorder__header-cell"); cell.AddToClassList("ewlk-mfgorder__header-cell");
cell.AddToClassList(cls); cell.AddToClassList(cls);
header.Add(cell); header.Add(cell);
} }
return header; return header;
} }
} }

View File

@@ -7,33 +7,28 @@ using UVC.EnglewoodLAB.Data;
namespace UVC.EnglewoodLAB.UIToolkit namespace UVC.EnglewoodLAB.UIToolkit
{ {
/// <summary> /// <summary>
/// OverView 모달 내부 테이블 컨텐츠. /// OverView 컨텐츠.
/// 3개 작업장의 월간/일간 생산 실적(목표수량·현시점 계획·실적수량·구분)을 표시합니다. /// 3개 작업장 컬럼, 각 컬럼에 월간/일간 섹션, 프로그레스 바 포함.
/// </summary> /// </summary>
/// <example> /// <example>
/// <code>
/// var content = new EWLKOverViewModalContent(); /// var content = new EWLKOverViewModalContent();
/// content.UpdateData(mqttService.CurrentData); /// content.UpdateData(mqttService.CurrentData);
/// mqttService.OnDataUpdated += content.UpdateData; /// mqttService.OnDataUpdated += content.UpdateData;
///
/// var modal = UTKModal.Create("OVERVIEW", UTKModal.ModalSize.Large);
/// modal.Add(content);
/// await modal.ShowAsync();
///
/// mqttService.OnDataUpdated -= content.UpdateData;
/// content.Dispose();
/// </code>
/// </example> /// </example>
[UxmlElement] [UxmlElement]
public partial class EWLKOverViewModalContent : VisualElement, IDisposable public partial class EWLKOverViewModalContent : VisualElement, IDisposable
{ {
private const string USS_PATH = "EWLK/UIToolkit/Main/EWLKOverViewModalContentUss"; private const string USS_PATH = "EWLK/UIToolkit/Main/EWLKOverViewModalContentUss";
// 데이터 셀 참조 캐시 [작업장, 기간, 컬럼] // 작업장 이름 / 구분
// 작업장: 0=제조작업장, 1=충포장(3F), 2=충포장(4F) private static readonly string[] WorkshopNames = { "제조 작업장 · 제조", "충/포장 작업장(3F) · 생산", "충/포장 작업장(4F) · 생산" };
// 데이터 셀 캐시 [작업장, 기간, 행]
// 작업장: 0=제조, 1=충포장3F, 2=충포장4F
// 기간: 0=월간, 1=일간 // 기간: 0=월간, 1=일간
// 컬럼: 0=목표수량, 1=현시점계획, 2=실적수량, 3=구분 // 행: 0=목표수량, 1=현시점계획, 2=실적수량
private readonly Label[,,] _cells = new Label[3, 2, 4]; private readonly Label[,,] _valueCells = new Label[3, 2, 3];
private readonly VisualElement[,,] _progressBars = new VisualElement[3, 2, 3];
public EWLKOverViewModalContent() public EWLKOverViewModalContent()
{ {
@@ -44,98 +39,106 @@ namespace UVC.EnglewoodLAB.UIToolkit
BuildUI(); BuildUI();
} }
// ── UI 구성 ───────────────────────────────────────────────
private void BuildUI() private void BuildUI()
{ {
Add(BuildHeader()); // 3개 작업장 컬럼을 가로로 배치
var columnsContainer = new VisualElement();
columnsContainer.AddToClassList("ewlk-overview__columns");
string[] names = { "제조작업장", "충포장작업장(3F)", "충포장작업장(4F)" };
for (int wi = 0; wi < 3; wi++) for (int wi = 0; wi < 3; wi++)
Add(BuildWorkshopGroup(wi, names[wi])); {
var column = BuildWorkshopColumn(wi);
columnsContainer.Add(column);
} }
/// <summary>헤더 행을 생성합니다.</summary> Add(columnsContainer);
private static VisualElement BuildHeader()
{
var header = new VisualElement();
header.AddToClassList("ewlk-overview__header");
header.Add(MakeHeaderCell("구분", "ewlk-overview__cell--name"));
header.Add(MakeHeaderCell("", "ewlk-overview__cell--period"));
header.Add(MakeHeaderCell("목표수량", "ewlk-overview__cell--data"));
header.Add(MakeHeaderCell("현시점 계획", "ewlk-overview__cell--data"));
header.Add(MakeHeaderCell("실적수량", "ewlk-overview__cell--data"));
header.Add(MakeHeaderCell("구분", "ewlk-overview__cell--type"));
return header;
} }
/// <summary> /// <summary>작업장 컬럼 1개를 생성합니다.</summary>
/// 작업장 행 그룹을 생성합니다. private VisualElement BuildWorkshopColumn(int wi)
/// 왼쪽에 작업장 이름이 2행(월간+일간)에 걸쳐 표시됩니다.
/// </summary>
private VisualElement BuildWorkshopGroup(int wi, string name)
{ {
var group = new VisualElement(); var column = new VisualElement();
group.AddToClassList("ewlk-overview__group"); column.AddToClassList("ewlk-overview__column");
// 왼쪽: 작업장 이름 셀 (2행 height 걸쳐 세로 중앙 정렬) // 컬럼 제목
var nameCell = new Label(name); var title = new Label(WorkshopNames[wi]);
nameCell.AddToClassList("ewlk-overview__cell"); title.AddToClassList("ewlk-overview__column-title");
nameCell.AddToClassList("ewlk-overview__cell--name"); column.Add(title);
group.Add(nameCell);
// 오른쪽: 월간/일간 행 컨테이너 // 월간 섹션
var rows = new VisualElement(); column.Add(BuildPeriodSection(wi, 0, "월간"));
rows.AddToClassList("ewlk-overview__rows");
string[] periodLabels = { "월간", "일간" }; // 일간 섹션
for (int pi = 0; pi < 2; pi++) column.Add(BuildPeriodSection(wi, 1, "일간"));
return column;
}
/// <summary>월간/일간 섹션을 생성합니다.</summary>
private VisualElement BuildPeriodSection(int wi, int pi, string periodName)
{
var section = new VisualElement();
section.AddToClassList("ewlk-overview__section");
// 기간 라벨
var periodLabel = new Label(periodName);
periodLabel.AddToClassList("ewlk-overview__period-label");
section.Add(periodLabel);
// 구분선
var divider = new VisualElement();
divider.AddToClassList("ewlk-overview__divider");
section.Add(divider);
// 3행: 목표수량, 현시점 계획, 실적수량
string[] rowNames = { "목표 수량", "현시점 계획", "실적 수량" };
string[] barClasses = { "ewlk-overview__bar--target", "ewlk-overview__bar--plan", "ewlk-overview__bar--actual" };
string[] valueClasses = { "ewlk-overview__value--target", "ewlk-overview__value--plan", "ewlk-overview__value--actual" };
for (int ri = 0; ri < 3; ri++)
{ {
var row = new VisualElement(); var row = new VisualElement();
row.AddToClassList("ewlk-overview__row"); row.AddToClassList("ewlk-overview__data-row");
// 기간 레이블 (월간/일간) // 라벨 + 값
var periodLabel = new Label(periodLabels[pi]); var labelRow = new VisualElement();
periodLabel.AddToClassList("ewlk-overview__cell"); labelRow.AddToClassList("ewlk-overview__label-row");
periodLabel.AddToClassList("ewlk-overview__cell--period");
row.Add(periodLabel);
// 데이터 셀 4개: 목표수량·현시점계획·실적수량·구분 var nameLabel = new Label(rowNames[ri]);
for (int ci = 0; ci < 4; ci++) nameLabel.AddToClassList("ewlk-overview__data-name");
{ labelRow.Add(nameLabel);
var cell = new Label();
cell.AddToClassList("ewlk-overview__cell"); var valueLabel = new Label("0");
cell.AddToClassList(ci < 3 valueLabel.AddToClassList("ewlk-overview__data-value");
? "ewlk-overview__cell--data" valueLabel.AddToClassList(valueClasses[ri]);
: "ewlk-overview__cell--type"); _valueCells[wi, pi, ri] = valueLabel;
_cells[wi, pi, ci] = cell; labelRow.Add(valueLabel);
row.Add(cell);
row.Add(labelRow);
// 프로그레스 바
var barBg = new VisualElement();
barBg.AddToClassList("ewlk-overview__bar-bg");
var barFill = new VisualElement();
barFill.AddToClassList("ewlk-overview__bar-fill");
barFill.AddToClassList(barClasses[ri]);
_progressBars[wi, pi, ri] = barFill;
barBg.Add(barFill);
row.Add(barBg);
section.Add(row);
} }
rows.Add(row); return section;
}
group.Add(rows);
return group;
}
private static Label MakeHeaderCell(string text, string sizeClass)
{
var label = new Label(text);
label.AddToClassList("ewlk-overview__header-cell");
label.AddToClassList(sizeClass);
return label;
} }
// ── 데이터 갱신 ─────────────────────────────────────────── // ── 데이터 갱신 ───────────────────────────────────────────
/// <summary> /// <summary>
/// OverView 데이터로 테이블을 갱신합니다. /// OverView 데이터로 테이블을 갱신합니다.
/// MQTT 데이터 수신 시 또는 모달 열릴 때 호출합니다.
/// </summary> /// </summary>
/// <param name="data">갱신할 OverView 데이터</param>
public void UpdateData(EWLKOverViewData data) public void UpdateData(EWLKOverViewData data)
{ {
var workshops = new[] var workshops = new[]
@@ -154,13 +157,52 @@ namespace UVC.EnglewoodLAB.UIToolkit
private void ApplyPeriod(int wi, int pi, EWLKOverViewPeriodData period) private void ApplyPeriod(int wi, int pi, EWLKOverViewPeriodData period)
{ {
_cells[wi, pi, 0].text = period.Target; _valueCells[wi, pi, 0].text = period.Target;
_cells[wi, pi, 1].text = period.Plan; _valueCells[wi, pi, 1].text = period.Plan;
_cells[wi, pi, 2].text = period.Actual; _valueCells[wi, pi, 2].text = period.Actual;
_cells[wi, pi, 3].text = period.Type;
// 프로그레스 바 비율 계산
UpdateProgressBar(wi, pi, 0, period.Target, period.Target); // 목표 = 100%
UpdateProgressBar(wi, pi, 1, period.Plan, period.Target); // 계획/목표
UpdateProgressBar(wi, pi, 2, period.Actual, period.Target); // 실적/목표
} }
// ── IDisposable ─────────────────────────────────────────── /// <summary>프로그레스 바 비율을 업데이트합니다.</summary>
private void UpdateProgressBar(int wi, int pi, int ri, string valueStr, string maxStr)
{
float ratio = ParseRatio(valueStr, maxStr);
// 최소 3%로 색상이 보이도록
float percent = Mathf.Max(ratio * 100f, 3f);
_progressBars[wi, pi, ri].style.width = new Length(percent, LengthUnit.Percent);
}
/// <summary>문자열에서 숫자를 추출하여 비율을 계산합니다.</summary>
private static float ParseRatio(string valueStr, string maxStr)
{
float value = ExtractNumber(valueStr);
float max = ExtractNumber(maxStr);
if (max <= 0) return 0f;
return Mathf.Clamp01(value / max);
}
/// <summary>문자열에서 첫 번째 숫자를 추출합니다.</summary>
private static float ExtractNumber(string str)
{
if (string.IsNullOrEmpty(str)) return 0f;
// 숫자와 소수점, 콤마만 남기기
var sb = new System.Text.StringBuilder();
foreach (char c in str)
{
if (char.IsDigit(c) || c == '.') sb.Append(c);
else if (c == ',') continue; // 천단위 구분자 제거
else if (sb.Length > 0) break; // 숫자 끝
}
if (float.TryParse(sb.ToString(), out float result))
return result;
return 0f;
}
public void Dispose() { } public void Dispose() { }
} }

View File

@@ -36,12 +36,6 @@ namespace UVC.EnglewoodLAB.UIToolkit
if (uss != null) styleSheets.Add(uss); if (uss != null) styleSheets.Add(uss);
AddToClassList("ewlk-work-explorer"); AddToClassList("ewlk-work-explorer");
style.flexGrow = 1;
style.flexDirection = FlexDirection.Column;
style.paddingLeft = 16;
style.paddingRight = 16;
style.paddingTop = 12;
style.paddingBottom = 12;
// 섹션 1: 작업장별 설비 상태 // 섹션 1: 작업장별 설비 상태
Add(CreateSectionTitle("작업장별 설비 상태")); Add(CreateSectionTitle("작업장별 설비 상태"));