3 Commits

Author SHA1 Message Date
SOOBEEN HAN
43ab917db0 OverView 메뉴의 모달창 테스트
- 데이터 없이 테스트
2026-03-12 15:21:45 +09:00
SOOBEEN HAN
5712da17d7 leftSideBar 구현
- 버튼 클릭 시 열고 닫히는 애니메이션 작동
- 열었을 때 메뉴 상세이름 보이도록
- 메뉴 눌렀을 때 동작은 아직 없는 상태
2026-03-12 13:53:08 +09:00
SOOBEEN HAN
cfba7fdf53 document내 상위 Element 이름 변경
- 'loadingProgress'가 하위에도 동일 이름이 있어서 발생할 수 있는 문제 방지
- 'loadingBar'로 변경
2026-03-12 13:51:01 +09:00
28 changed files with 1894 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<ui:VisualElement name="loadingProgress" class="utk-sample-container" style="height: 1075px;">
<ui:VisualElement name="loadingBar" class="utk-sample-container" style="height: 1075px;">
<ui:VisualElement class="utk-sample-section" style="translate: 0 1000px; transform-origin: bottom;">
<ui:Label text="로딩 중..." name="statusLabel" class="utk-sample-section__title"/>
<ui:VisualElement class="utk-sample-row">

View File

@@ -1,4 +1,2 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
<ui:Template name="UTKProgressBarSample" src="project://database/Assets/Resources/UIToolkit/Sample/Slider/UTKProgressBarSample.uxml?fileID=9197481963319205126&amp;guid=3c87ecf76a8c9fd4486ed612975878e8&amp;type=3#UTKProgressBarSample"/>
<ui:Instance template="UTKProgressBarSample"/>
</ui:UXML>

View File

@@ -1,9 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<ui:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ui="UnityEngine.UIElements"
xmlns:uie="UnityEditor.UIElements"
xsi:noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd"
>
</ui:UXML>
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="False">
</ui:UXML>

View File

@@ -3,7 +3,11 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ui="UnityEngine.UIElements"
xmlns:uie="UnityEditor.UIElements"
xmlns:ewlk="UVC.EnglewoodLAB.UIToolkit"
xsi:noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd"
>
<!-- 좌측 네비게이션 사이드바 -->
<ewlk:EWLKNavSideBar
name="navSideBar"
style="position: absolute; left: 0; top: 0; bottom: 0;" />
</ui:UXML>

View File

@@ -0,0 +1,166 @@
/* ============================================================
EWLKNavSideBar — 메인 씬 좌측 네비게이션 사이드바 스타일
네이비 배경, 아이콘 + 텍스트 레이아웃
수정: color:inherit → 제거 (Unity USS는 inherit 키워드 미지원,
부모 color가 자식에게 자동 전파됨)
cursor:link → 제거 (Unity USS 미지원)
============================================================ */
/* ── 사이드바 루트 ────────────────────────────────────────── */
.ewlk-nav-sidebar {
flex-direction: column;
background-color: rgb(26, 34, 64);
overflow: hidden;
}
/* ── 헤더 ─────────────────────────────────────────────────── */
.ewlk-nav__header {
flex-direction: row;
align-items: center;
height: 48px;
padding-left: 8px;
padding-right: 4px;
flex-shrink: 0;
}
.ewlk-nav__logo-icon {
width: 24px;
height: 24px;
background-image: resource("EWLK/Images/logo-white");
background-size: contain;
flex-shrink: 0;
}
.ewlk-nav__logo-text {
flex-grow: 1;
color: rgb(255, 255, 255);
font-size: 13px;
-unity-font-style: bold;
overflow: hidden;
white-space: nowrap;
margin-left: 8px;
margin-right: 4px;
}
.ewlk-nav__close-btn {
width: 28px;
height: 28px;
min-width: 0;
padding: 0;
background-color: rgba(0, 0, 0, 0);
border-width: 0;
border-radius: 4px;
flex-shrink: 0;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.6);
}
.ewlk-nav__close-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
color: rgb(255, 255, 255);
}
/* ── 아이콘 레이블 공통 ───────────────────────────────────── */
/* color는 부모(버튼)에서 자동 전파되므로 별도 지정 불필요 */
.ewlk-nav__icon-label {
-unity-text-align: middle-center;
flex-shrink: 0;
}
/* ── 구분선 ───────────────────────────────────────────────── */
.ewlk-nav__divider {
height: 1px;
background-color: rgba(255, 255, 255, 0.1);
margin-left: 8px;
margin-right: 8px;
flex-shrink: 0;
}
/* ── 메뉴 영역 ────────────────────────────────────────────── */
.ewlk-nav__menu {
flex-grow: 1;
flex-direction: column;
padding-top: 4px;
padding-bottom: 4px;
overflow: hidden;
}
/* ── 메뉴 항목 (Button) ────────────────────────────────────── */
.ewlk-nav__item {
flex-direction: row;
align-items: center;
height: 40px;
min-height: 0;
padding-left: 8px;
padding-right: 8px;
padding-top: 0;
padding-bottom: 0;
background-color: rgba(0, 0, 0, 0);
border-width: 0;
border-radius: 0;
color: rgba(255, 255, 255, 0.65);
-unity-text-align: middle-left;
margin: 0;
}
.ewlk-nav__item:hover {
background-color: rgba(255, 255, 255, 0.08);
color: rgb(255, 255, 255);
}
.ewlk-nav__item--active {
background-color: rgba(255, 255, 255, 0.14);
color: rgb(255, 255, 255);
}
.ewlk-nav__item--active:hover {
background-color: rgba(255, 255, 255, 0.18);
}
/* ── 메뉴 항목 아이콘 / 레이블 ─────────────────────────────── */
/* color는 부모(.ewlk-nav__item)에서 자동 전파 */
.ewlk-nav__item-icon {
width: 24px;
height: 24px;
-unity-text-align: middle-center;
flex-shrink: 0;
}
.ewlk-nav__item-label {
flex-grow: 1;
font-size: 12px;
overflow: hidden;
white-space: nowrap;
margin-left: 8px;
-unity-text-align: middle-left;
}
/* ── 푸터 ─────────────────────────────────────────────────── */
.ewlk-nav__footer {
flex-direction: column;
flex-shrink: 0;
padding-top: 4px;
padding-bottom: 4px;
}
/* ── 토글 버튼 (열기/닫기) ────────────────────────────────── */
.ewlk-nav__toggle-btn {
flex-direction: row;
align-items: center;
justify-content: center;
height: 40px;
min-height: 0;
padding: 0;
margin: 0;
background-color: rgba(0, 0, 0, 0);
border-width: 0;
border-radius: 0;
color: rgba(255, 255, 255, 0.5);
}
.ewlk-nav__toggle-btn:hover {
background-color: rgba(255, 255, 255, 0.08);
color: rgb(255, 255, 255);
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 2773d1950271a4b478656767cf6763d2
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
unsupportedSelectorAction: 0

View File

@@ -0,0 +1,128 @@
/* ============================================================
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
============================================================ */
/* ── 테이블 루트 ─────────────────────────────────────────── */
.ewlk-overview {
flex-direction: column;
background-color: rgb(255, 255, 255);
border-width: 1px;
border-color: rgb(140, 140, 140);
}
/* ── 헤더 행 ─────────────────────────────────────────────── */
.ewlk-overview__header {
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-direction: column;
}
/* ── 데이터 행 ───────────────────────────────────────────── */
.ewlk-overview__row {
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__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;
background-color: rgb(232, 235, 248);
border-right-color: rgb(140, 140, 140);
}
/* 월간/일간 구분 열 */
.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 {
flex-grow: 1;
flex-shrink: 1;
}
/* 구분 (제조/생산) — 마지막 열, 오른쪽 테두리 제거 */
.ewlk-overview__cell--type {
width: 60px;
flex-shrink: 0;
border-right-width: 0;
}
/* 헤더의 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__header .ewlk-overview__cell--period {
background-color: rgb(200, 206, 224);
}
.ewlk-overview__header .ewlk-overview__cell--type {
border-right-width: 0;
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 96af6e40133939f47bca856a7bdc2eaa
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
unsupportedSelectorAction: 0

View File

@@ -279,6 +279,7 @@ GameObject:
- component: {fileID: 672992130}
- component: {fileID: 672992129}
- component: {fileID: 672992128}
- component: {fileID: 672992131}
m_Layer: 0
m_Name: Main Camera
m_TagString: MainCamera
@@ -360,6 +361,50 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &672992131
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 672992127}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalCameraData
m_RenderShadows: 1
m_RequiresDepthTextureOption: 2
m_RequiresOpaqueTextureOption: 2
m_CameraType: 0
m_Cameras: []
m_RendererIndex: -1
m_VolumeLayerMask:
serializedVersion: 2
m_Bits: 1
m_VolumeTrigger: {fileID: 0}
m_VolumeFrameworkUpdateModeOption: 2
m_RenderPostProcessing: 0
m_Antialiasing: 0
m_AntialiasingQuality: 2
m_StopNaN: 0
m_Dithering: 0
m_ClearDepth: 1
m_AllowXRRendering: 1
m_AllowHDROutput: 1
m_UseScreenCoordOverride: 0
m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0}
m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0}
m_RequiresDepthTexture: 0
m_RequiresColorTexture: 0
m_TaaSettings:
m_Quality: 3
m_FrameInfluence: 0.1
m_JitterScale: 1
m_MipBias: 0
m_VarianceClampScale: 0.9
m_ContrastAdaptiveSharpening: 0
m_Version: 2
--- !u!1 &774244994
GameObject:
m_ObjectHideFlags: 0

View File

@@ -129,6 +129,7 @@ GameObject:
m_Component:
- component: {fileID: 39254822}
- component: {fileID: 39254821}
- component: {fileID: 39254823}
m_Layer: 0
m_Name: Directional Light
m_TagString: Untagged
@@ -216,6 +217,134 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 50, y: -30, z: 0}
--- !u!114 &39254823
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 39254820}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 474bcb49853aa07438625e644c072ee6, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalAdditionalLightData
m_UsePipelineSettings: 1
m_AdditionalLightsShadowResolutionTier: 2
m_CustomShadowLayers: 0
m_LightCookieSize: {x: 1, y: 1}
m_LightCookieOffset: {x: 0, y: 0}
m_SoftShadowQuality: 0
m_RenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_ShadowRenderingLayersMask:
serializedVersion: 0
m_Bits: 1
m_Version: 4
m_LightLayerMask: 1
m_ShadowLayerMask: 1
m_RenderingLayers: 1
m_ShadowRenderingLayers: 1
--- !u!1 &557669013
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 557669015}
- component: {fileID: 557669014}
m_Layer: 0
m_Name: StaticDocument
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &557669014
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 557669013}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 5ad7007b08a97b54d927c352279a18b6, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: 3e76559fee54d2040be8ca19a74c70e3, type: 3}
m_SortingOrder: 0
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!4 &557669015
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 557669013}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &883201336
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 883201337}
- component: {fileID: 883201338}
m_Layer: 0
m_Name: SceneMain
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &883201337
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 883201336}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 897.09546, y: 577.4398, z: 32.3774}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &883201338
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 883201336}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 85492bb0e96aa6946b45bd6bd762a556, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UVC.EnglewoodLAB.EWLKSceneMain
--- !u!1 &1079771970
GameObject:
m_ObjectHideFlags: 0
@@ -308,9 +437,174 @@ Transform:
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1306527161
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1306527163}
- component: {fileID: 1306527162}
m_Layer: 0
m_Name: DynamicDocument
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &1306527162
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1306527161}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 5ad7007b08a97b54d927c352279a18b6, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: 0cf46852e8559c8439acec80a2f14fc3, type: 3}
m_SortingOrder: 1000
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!4 &1306527163
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1306527161}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1664424032
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1664424033}
- component: {fileID: 1664424034}
m_Layer: 0
m_Name: SceneContext
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1664424033
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1664424032}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 897.09546, y: 577.4398, z: 32.3774}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1664424034
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1664424032}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 7bcd3c6511d97a94d8da2de5051ba292, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::UVC.EnglewoodLAB.EWLKSceneContext
scenePrefabs: []
autoInjectSceneObjects: 1
targetObjects: []
staticUI: {fileID: 557669014}
dynamicUI: {fileID: 1306527162}
modalUI: {fileID: 1840260740}
--- !u!1 &1840260738
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1840260739}
- component: {fileID: 1840260740}
m_Layer: 0
m_Name: ModalDocument
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &1840260739
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1840260738}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 897.09546, y: 577.4398, z: 32.3774}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &1840260740
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1840260738}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
m_Name:
m_EditorClassIdentifier: UnityEngine.dll::UnityEngine.UIElements.UIDocument
m_PanelSettings: {fileID: 11400000, guid: 5ad7007b08a97b54d927c352279a18b6, type: 2}
m_ParentUI: {fileID: 0}
sourceAsset: {fileID: 9197481963319205126, guid: 4f0c5b331c9f51f4387dadbbafc03e0b, type: 3}
m_SortingOrder: 2000
m_Position: 0
m_WorldSpaceSizeMode: 1
m_WorldSpaceWidth: 1920
m_WorldSpaceHeight: 1080
m_PivotReferenceSize: 0
m_Pivot: 0
m_WorldSpaceCollider: {fileID: 0}
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
m_Roots:
- {fileID: 1079771973}
- {fileID: 39254822}
- {fileID: 557669015}
- {fileID: 1306527163}
- {fileID: 1840260739}
- {fileID: 883201337}
- {fileID: 1664424033}

View File

@@ -0,0 +1,63 @@
#nullable enable
using Newtonsoft.Json;
using System.IO;
using UnityEngine;
namespace UVC.EnglewoodLAB.Config
{
/// <summary>
/// StreamingAssets/EWLKAppConfig.json에서 앱 설정을 로드합니다.
/// </summary>
public class EWLKAppConfig
{
/// <summary>로드된 설정 인스턴스 (로드 전에는 null)</summary>
public static EWLKAppConfig? Config { get; private set; }
/// <summary>
/// StreamingAssets 폴더의 EWLKAppConfig.json에서 설정을 로드합니다.
/// </summary>
/// <returns>로드 성공 여부</returns>
public static bool LoadConfig()
{
string path = Path.Combine(Application.streamingAssetsPath, "EWLKAppConfig.json");
if (!File.Exists(path))
{
Debug.LogError($"[EWLKAppConfig] 설정 파일을 찾을 수 없습니다: {path}");
return false;
}
string json = File.ReadAllText(path);
Config = JsonConvert.DeserializeObject<EWLKAppConfig>(json);
Debug.Log($"[EWLKAppConfig] 설정 로드 완료: {path}");
return Config != null;
}
/// <summary>목표 프레임 레이트</summary>
[JsonProperty("targetFrameRate")]
public int TargetFrameRate { get; set; } = 60;
/// <summary>MQTT 브로커 설정</summary>
[JsonProperty("mqtt")]
public EWLKMqttConfig? Mqtt { get; set; }
}
/// <summary>MQTT 브로커 연결 설정</summary>
public class EWLKMqttConfig
{
/// <summary>브로커 도메인</summary>
[JsonProperty("host")]
public string Host { get; set; } = "localhost";
/// <summary>브로커 포트</summary>
[JsonProperty("port")]
public int Port { get; set; } = 1883;
/// <summary>JSON 페이로드에서 데이터를 담고 있는 키</summary>
[JsonProperty("dataKey")]
public string DataKey { get; set; } = "data";
/// <summary>MessagePack 인코딩 사용 여부</summary>
[JsonProperty("messagePack")]
public bool MessagePack { get; set; } = false;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 453b4a2183338f94986518da60656b9b

View File

@@ -0,0 +1,21 @@
namespace UVC.EnglewoodLAB.Config
{
/// <summary>
/// 앱 설정에서 로드된 런타임 상수.
/// EWLKAppMain.Init()에서 EWLKAppConfig 로드 후 설정됩니다.
/// </summary>
public static class EWLKConstants
{
/// <summary>MQTT 브로커 도메인</summary>
public static string MQTT_DOMAIN = "localhost";
/// <summary>MQTT 브로커 포트</summary>
public static int MQTT_PORT = 1883;
/// <summary>MQTT JSON에서 데이터를 담고 있는 키</summary>
public static string MQTT_DATA_KEY = "data";
/// <summary>MQTT MessagePack 인코딩 사용 여부</summary>
public static bool MQTT_MESSAGEPACK_ENABLED = false;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0f9b7dbf83096114da5ae5d71bbcc9ff

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 24792ae178280d9449c2151aa8e38e8c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
#nullable enable
namespace UVC.EnglewoodLAB.Data
{
/// <summary>월간/일간 단위 생산 실적 데이터</summary>
public class EWLKOverViewPeriodData
{
/// <summary>목표수량</summary>
public string Target { get; set; } = string.Empty;
/// <summary>현시점 계획</summary>
public string Plan { get; set; } = string.Empty;
/// <summary>실적수량</summary>
public string Actual { get; set; } = string.Empty;
/// <summary>구분 (제조/생산 등)</summary>
public string Type { get; set; } = string.Empty;
}
/// <summary>작업장별 OverView 데이터 (월간 + 일간)</summary>
public class EWLKOverViewWorkshopData
{
/// <summary>작업장 이름 (예: 제조작업장, 충포장작업장(3F))</summary>
public string Name { get; }
/// <summary>월간 실적</summary>
public EWLKOverViewPeriodData Monthly { get; } = new();
/// <summary>일간 실적</summary>
public EWLKOverViewPeriodData Daily { get; } = new();
/// <param name="name">작업장 이름</param>
public EWLKOverViewWorkshopData(string name) => Name = name;
}
/// <summary>OverView 화면 전체 데이터 (3개 작업장)</summary>
public class EWLKOverViewData
{
/// <summary>제조작업장</summary>
public EWLKOverViewWorkshopData Manufacture { get; } = new("제조작업장");
/// <summary>충포장작업장(3F)</summary>
public EWLKOverViewWorkshopData Packing3F { get; } = new("충포장작업장(3F)");
/// <summary>충포장작업장(4F)</summary>
public EWLKOverViewWorkshopData Packing4F { get; } = new("충포장작업장(4F)");
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 677e0385b85098c499cbc1178b672f39

View File

@@ -0,0 +1,133 @@
#nullable enable
using System;
using UVC.Data;
using UVC.Data.Core;
using UVC.Data.Mqtt;
namespace UVC.EnglewoodLAB.Data
{
/// <summary>
/// MES/ERP OverView 데이터를 MQTT로 수신하는 서비스.
/// 3개 작업장(제조작업장, 충포장3F/4F)의 월간·일간 생산 실적을 구독합니다.
/// DataRepository.Instance.MqttReceiver(공유 수신기)를 사용합니다.
/// </summary>
/// <remarks>
/// MQTT JSON 페이로드 스키마:
/// <code>
/// {
/// "monthly_target": "25,000 kg(15 BT)",
/// "monthly_plan": "20,000 kg(10 BT)",
/// "monthly_actual": "20,000 kg(10 BT)",
/// "daily_target": "3,000 kg(2BT)",
/// "daily_plan": "1,000 kg(1 BT)",
/// "daily_actual": "1,000 kg(1 BT)",
/// "type": "제조"
/// }
/// </code>
/// </remarks>
public class EWLKOverViewMqttService : IDisposable
{
// ── MQTT 토픽 (MES/ERP 연동) ──────────────────────────────
private const string TopicManufacture = "ewlk/overview/manufacture";
private const string TopicPacking3F = "ewlk/overview/packing3f";
private const string TopicPacking4F = "ewlk/overview/packing4f";
// ── DataMask (공통 스키마) ─────────────────────────────────
private static readonly DataMask s_Mask = CreateMask();
private bool _subscribed;
private bool _disposed;
// ── 공개 API ──────────────────────────────────────────────
/// <summary>가장 최근에 수신한 OverView 데이터 (초기값: 빈 문자열)</summary>
public EWLKOverViewData CurrentData { get; } = new();
/// <summary>작업장 데이터가 갱신될 때 발생 — 메인 스레드에서 호출됩니다.</summary>
public event Action<EWLKOverViewData>? OnDataUpdated;
// ── 초기화 ────────────────────────────────────────────────
/// <summary>
/// DataRepository 공유 수신기에 3개 작업장 토픽을 등록합니다.
/// MqttReceiver.Start()는 EWLKSceneMain에서 호출합니다.
/// </summary>
public void Subscribe()
{
if (_subscribed || _disposed) return;
_subscribed = true;
var receiver = DataRepository.Instance.MqttReceiver;
receiver.Add(BuildConfig(TopicManufacture,
data => UpdateWorkshop(CurrentData.Manufacture, data)));
receiver.Add(BuildConfig(TopicPacking3F,
data => UpdateWorkshop(CurrentData.Packing3F, data)));
receiver.Add(BuildConfig(TopicPacking4F,
data => UpdateWorkshop(CurrentData.Packing4F, data)));
}
/// <summary>DataRepository 공유 수신기에서 토픽 구독을 해제합니다.</summary>
public void Unsubscribe()
{
if (!_subscribed) return;
_subscribed = false;
var receiver = DataRepository.Instance.MqttReceiver;
receiver.Remove(TopicManufacture);
receiver.Remove(TopicPacking3F);
receiver.Remove(TopicPacking4F);
}
// ── 내부 구현 ─────────────────────────────────────────────
private static DataMask CreateMask()
{
var mask = new DataMask();
mask["monthly_target"] = "";
mask["monthly_plan"] = "";
mask["monthly_actual"] = "";
mask["daily_target"] = "";
mask["daily_plan"] = "";
mask["daily_actual"] = "";
mask["type"] = "";
return mask;
}
private static MqttSubscriptionConfig BuildConfig(string topic, Action<IDataObject?> handler) =>
new MqttSubscriptionConfig(topic)
.SetDataMapper(new DataMapper(s_Mask))
.SetHandler(handler);
/// <summary>
/// 수신된 IDataObject를 DataObject로 캐스팅하여 작업장 데이터를 갱신합니다.
/// 이 메서드는 메인 스레드에서 호출됩니다 (MqttDataReceiver가 UniTask.Post로 보장).
/// </summary>
private void UpdateWorkshop(EWLKOverViewWorkshopData workshop, IDataObject? data)
{
if (data is not DataObject obj) return;
workshop.Monthly.Target = obj.GetString("monthly_target") ?? string.Empty;
workshop.Monthly.Plan = obj.GetString("monthly_plan") ?? string.Empty;
workshop.Monthly.Actual = obj.GetString("monthly_actual") ?? string.Empty;
workshop.Monthly.Type = obj.GetString("type") ?? string.Empty;
workshop.Daily.Target = obj.GetString("daily_target") ?? string.Empty;
workshop.Daily.Plan = obj.GetString("daily_plan") ?? string.Empty;
workshop.Daily.Actual = obj.GetString("daily_actual") ?? string.Empty;
workshop.Daily.Type = obj.GetString("type") ?? string.Empty;
OnDataUpdated?.Invoke(CurrentData);
}
// ── IDisposable ───────────────────────────────────────────
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Unsubscribe();
OnDataUpdated = null;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 695c3a78b9151da4cb71ca0a276e7c87

View File

@@ -1,6 +1,79 @@
#nullable enable
using Cysharp.Threading.Tasks;
using UnityEngine;
using UVC.Core;
using UVC.Data;
using UVC.Data.Mqtt;
using UVC.EnglewoodLAB.Config;
public class EWLKAppMain
namespace UVC.EnglewoodLAB
{
/// <summary>
/// EnglewoodLAB 앱의 진입점 (싱글톤).
/// 설정 로드, MQTT 브로커 설정을 담당합니다.
/// [DefaultExecutionOrder(100)]: SceneContext/SceneMain의 Awake보다 나중에 실행됩니다.
/// </summary>
[DefaultExecutionOrder(100)]
public class EWLKAppMain : SingletonApp<EWLKAppMain>
{
/// <summary>초기화 완료 후 발생하는 이벤트</summary>
public System.Action? Initialized;
/// <summary>
/// SingletonApp Awake 시 호출됩니다.
/// 설정을 로드하고 네트워크(MQTT)를 구성합니다.
/// </summary>
protected override async void Init()
{
if (!Application.isPlaying) return;
await SetupConfigAsync();
SetNetworkConfig();
Initialized?.Invoke();
}
// ── 설정 로드 ─────────────────────────────────────────────
private UniTask SetupConfigAsync()
{
if (EWLKAppConfig.LoadConfig())
{
var cfg = EWLKAppConfig.Config!;
Application.targetFrameRate = cfg.TargetFrameRate;
if (cfg.Mqtt != null)
{
EWLKConstants.MQTT_DOMAIN = cfg.Mqtt.Host;
EWLKConstants.MQTT_PORT = cfg.Mqtt.Port;
EWLKConstants.MQTT_DATA_KEY = cfg.Mqtt.DataKey;
EWLKConstants.MQTT_MESSAGEPACK_ENABLED = cfg.Mqtt.MessagePack;
}
}
return UniTask.CompletedTask;
}
// ── 네트워크 설정 ─────────────────────────────────────────
private void SetNetworkConfig()
{
bool useWebSocket = false;
#if UNITY_WEBGL && !UNITY_EDITOR
EWLKConstants.MQTT_PORT = 8083;
useWebSocket = true;
#endif
DataRepository.Instance.MqttReceiver
.SetDataPicker(new MqttDataPicker(
EWLKConstants.MQTT_DATA_KEY,
EWLKConstants.MQTT_MESSAGEPACK_ENABLED));
DataRepository.Instance.MqttReceiver
.SetDomainPort(EWLKConstants.MQTT_DOMAIN, EWLKConstants.MQTT_PORT);
DataRepository.Instance.MqttReceiver
.SetUseWebSocket(useWebSocket);
}
}
}

View File

@@ -1,6 +1,98 @@
#nullable enable
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Core;
using UVC.EnglewoodLAB.Data;
using UVC.EnglewoodLAB.UIToolkit;
using UVC.UIToolkit;
public class EWLKSceneContext
namespace UVC.EnglewoodLAB
{
/// <summary>UIDocument 타입 충돌 방지용 마커 래퍼 — Modal UI Document</summary>
public sealed class ModalUIDocument { public readonly UIDocument Value; public ModalUIDocument(UIDocument v) { Value = v; } }
/// <summary>
/// 메인 씬의 DI 컨텍스트.
/// UIDocument 세 개, NavSideBar, OverView MQTT 서비스를 등록합니다.
/// </summary>
public class EWLKSceneContext : InjectorSceneContext
{
/// <summary>EWLKSceneContext 인스턴스에 접근하기 위한 편의 프로퍼티</summary>
public static new EWLKSceneContext? Instance => InjectorSceneContext.Instance as EWLKSceneContext;
[SerializeField] private UIDocument? staticUI;
[SerializeField] private UIDocument? dynamicUI;
[SerializeField] private UIDocument? modalUI;
/// <summary>
/// 메인 씬 서비스를 등록합니다.
/// 씬 로드 시 자동 호출되며, 씬 전환 시 자동으로 정리됩니다.
/// </summary>
protected override void RegisterSceneServices()
{
base.RegisterSceneServices();
Injector.RegisterSingleton<EWLKSceneMain>();
if (staticUI != null)
{
staticUI.name = "StaticUI";
Injector.RegisterInstance(new StaticUIDocument(staticUI), ServiceLifetime.Scene);
}
if (dynamicUI != null)
{
dynamicUI.name = "DynamicUI";
Injector.RegisterInstance(new DynamicUIDocument(dynamicUI), ServiceLifetime.Scene);
}
if (modalUI != null)
{
modalUI.name = "ModalUI";
Injector.RegisterInstance(new ModalUIDocument(modalUI), ServiceLifetime.Scene);
}
// OverView MQTT 서비스 등록 (토픽 구독 — 브로커 연결은 EWLKSceneMain.Initialize()에서)
var overViewMqtt = new EWLKOverViewMqttService();
overViewMqtt.Subscribe();
Injector.RegisterInstance<EWLKOverViewMqttService>(overViewMqtt, ServiceLifetime.Scene);
}
/// <summary>
/// 비동기 서비스 등록.
/// UIDocument의 rootVisualElement가 준비된 후 NavSideBar를 추출하고,
/// Modal UI 루트를 UTK 컴포넌트에 설정합니다.
/// RegisterSceneServices()는 Awake 동 프레임에 실행되므로 rootVisualElement가
/// null일 수 있습니다. 여기서는 한 프레임 대기 후 안전하게 접근합니다.
/// </summary>
protected override async UniTask RegisterSceneServicesAsync()
{
await base.RegisterSceneServicesAsync();
// UIDocument 패널 초기화 대기 (Awake → Start 이후 준비됨)
await UniTask.WaitUntil(
() => staticUI == null || staticUI.rootVisualElement != null
);
// NavSideBar를 UXML에서 추출하여 DI에 등록
if (staticUI != null)
{
var navSideBar = staticUI.rootVisualElement?.Q<EWLKNavSideBar>("navSideBar");
if (navSideBar != null)
Injector.RegisterInstance<EWLKNavSideBar>(navSideBar, ServiceLifetime.Scene);
else
Debug.LogWarning("[EWLKSceneContext] StaticUI에서 EWLKNavSideBar(navSideBar)를 찾지 못했습니다.");
}
// 모달 UI를 사용하는 UTK 컴포넌트에 루트 설정
if (modalUI?.rootVisualElement != null)
{
UTKAlert.SetRoot(modalUI.rootVisualElement);
UTKModal.SetRoot(modalUI.rootVisualElement);
UTKNotification.SetRoot(modalUI.rootVisualElement);
UTKToast.SetRoot(modalUI.rootVisualElement);
}
}
}
}

View File

@@ -1,9 +1,151 @@
#nullable enable
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Core;
using UVC.Data;
using UVC.EnglewoodLAB.Data;
using UVC.EnglewoodLAB.UIToolkit;
using UVC.UIToolkit;
namespace UVC.EnglewoodLAB
{
public class EWLKSceneMain
/// <summary>
/// 메인 씬의 초기화 컨트롤러.
/// NavSideBar 설정, 콘텐츠 뷰 전환, OverView 모달 표시를 담당합니다.
/// </summary>
[DefaultExecutionOrder(90)]
public class EWLKSceneMain : SingletonScene<EWLKSceneMain>
{
// ── DI 주입 ─────────────────────────────────────
[Inject] private StaticUIDocument? _staticUIDoc;
[Inject] private DynamicUIDocument? _dynamicUIDoc;
[Inject] private ModalUIDocument? _modalUIDoc;
[Inject] private EWLKNavSideBar? _navSideBar;
[Inject] private EWLKOverViewMqttService? _overViewMqtt;
// ── UIDocument 편의 접근자 ────────────────────────
private UIDocument? StaticUI => _staticUIDoc?.Value;
private UIDocument? DynamicUI => _dynamicUIDoc?.Value;
private UIDocument? ModalUI => _modalUIDoc?.Value;
/// <summary>씬 초기화 완료 이벤트</summary>
public Action? Initialized;
// ────────────────────────────────────────────────
/// <summary>
/// SingletonScene Awake 시 호출됩니다.
/// InjectorAppContext 초기화 완료 후 씬을 세팅합니다.
/// </summary>
protected override void Init()
{
InitializeAsync()
.Forget(ex => Debug.LogError(ex));
}
protected override void OnDestroy()
{
if (_navSideBar != null)
_navSideBar.OnNavItemSelected -= OnNavItemSelected;
}
// ── 초기화 ──────────────────────────────────────
/// <summary>
/// AppContext DI 완료를 기다린 후 씬을 초기화합니다.
/// InjectorSceneContext.PerformSceneInjection()은 Start()에서 실행되므로
/// WaitForInitializationAsync()로 [Inject] 필드 주입 완료를 보장합니다.
/// </summary>
private async UniTask InitializeAsync()
{
await InjectorAppContext.Instance.WaitForInitializationAsync();
Initialize();
}
private void Initialize()
{
DataRepository.Instance.MqttReceiver.Start();
SetupNavSideBar();
Initialized?.Invoke();
}
// ── NavSideBar 설정 ──────────────────────────────
/// <summary>
/// NavSideBar에 메뉴 항목을 설정하고 이벤트를 구독합니다.
/// </summary>
private void SetupNavSideBar()
{
if (_navSideBar == null)
{
Debug.LogWarning("[EWLKSceneMain] EWLKNavSideBar가 주입되지 않았습니다. " +
"EWLKSceneContext에서 StaticUI UXML을 확인하세요.");
return;
}
_navSideBar.SetMenuItems(new[]
{
new EWLKNavSideBar.NavItemData("설비목록", UTKMaterialIcons.List),
new EWLKNavSideBar.NavItemData("OverView", UTKMaterialIcons.BarChart),
new EWLKNavSideBar.NavItemData("제조지시현황", UTKMaterialIcons.Factory),
new EWLKNavSideBar.NavItemData("충포장지시현황", UTKMaterialIcons.Inventory2),
});
_navSideBar.OnNavItemSelected += OnNavItemSelected;
}
// ── 이벤트 처리 ──────────────────────────────────
/// <summary>
/// NavSideBar 항목 선택 시 호출됩니다.
/// </summary>
/// <param name="index">선택된 항목 인덱스</param>
private void OnNavItemSelected(int index)
{
switch (index)
{
case 1: // OverView
ShowOverViewModalAsync()
.Forget(ex => Debug.LogError(ex));
break;
default:
// TODO: 인덱스별 콘텐츠 뷰(DynamicUI) 전환 구현 예정
Debug.Log($"[EWLKSceneMain] 메뉴 {index} 선택됨");
break;
}
}
// ── OverView 모달 ────────────────────────────────
/// <summary>
/// OverView 모달을 열고 MQTT 실시간 데이터를 바인딩합니다.
/// 모달이 닫히면 이벤트 바인딩을 해제합니다.
/// </summary>
private async UniTask ShowOverViewModalAsync()
{
var content = new EWLKOverViewModalContent();
// 현재 캐시된 데이터로 즉시 표시 (MQTT 미연결 시 빈 테이블)
if (_overViewMqtt != null)
{
content.UpdateData(_overViewMqtt.CurrentData);
_overViewMqtt.OnDataUpdated += content.UpdateData;
}
var modal = UTKModal.Create("OVERVIEW", UTKModal.ModalSize.Large);
modal.Add(content);
await modal.ShowAsync();
// 모달 닫힌 후 정리
if (_overViewMqtt != null)
_overViewMqtt.OnDataUpdated -= content.UpdateData;
content.Dispose();
// 메뉴 선택 상태 해제 (모달이 닫히면 활성화 항목 없음)
_navSideBar?.SetActiveItem(-1);
}
}
}
}

View File

@@ -0,0 +1,446 @@
#nullable enable
using System;
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit;
namespace UVC.EnglewoodLAB.UIToolkit
{
/// <summary>
/// EnglewoodLAB 메인 씬의 좌측 네비게이션 사이드바.
/// 열기/닫기 애니메이션과 항목 선택 이벤트를 지원합니다.
/// </summary>
/// <example>
/// <code>
/// var sidebar = root.Q&lt;EWLKNavSideBar&gt;("navSideBar");
/// sidebar.SetMenuItems(new[]
/// {
/// new EWLKNavSideBar.NavItemData("설비목록", UTKMaterialIcons.List),
/// new EWLKNavSideBar.NavItemData("OverView", UTKMaterialIcons.BarChart),
/// });
/// sidebar.OnNavItemSelected += index => Debug.Log($"메뉴 {index} 선택");
/// </code>
/// </example>
[UxmlElement]
public partial class EWLKNavSideBar : VisualElement, IDisposable
{
#region (Constants)
private const string USS_PATH = "EWLK/UIToolkit/Main/EWLKNavSideBarUss";
private const float ExpandedWidth = 160f;
private const float CollapsedWidth = 40f;
private const float AnimDuration = 0.25f;
#endregion
#region (Inner Types)
/// <summary>내비게이션 메뉴 항목 데이터</summary>
public sealed class NavItemData
{
/// <summary>표시 이름</summary>
public string Label { get; }
/// <summary>Material Icons 유니코드 문자 (예: UTKMaterialIcons.List)</summary>
public string IconChar { get; }
/// <param name="label">표시 이름</param>
/// <param name="iconChar">Material Icons 유니코드 문자</param>
public NavItemData(string label, string iconChar)
{
Label = label;
IconChar = iconChar;
}
}
/// <summary>
/// 클릭 콜백을 대칭적으로 등록/해제하기 위한 내비게이션 항목 래퍼.
/// RegisterCallback ↔ UnregisterCallback 대칭 유지.
/// </summary>
private sealed class NavItemButton : Button, IDisposable
{
private readonly EventCallback<ClickEvent> _onClick;
private bool _disposed;
public NavItemButton(EventCallback<ClickEvent> onClick)
{
_onClick = onClick;
RegisterCallback<ClickEvent>(_onClick);
}
public new void Dispose()
{
if (_disposed) return;
_disposed = true;
UnregisterCallback<ClickEvent>(_onClick);
}
}
#endregion
#region (Fields)
private bool _disposed;
private bool _isExpanded = true;
private Tweener? _animTween;
// ── 헤더 요소 ─────────────────────────────────
private Label? _logoText;
private Button? _closeBtn;
private EventCallback<ClickEvent>? _onCloseBtnClick;
// ── 메뉴 컨테이너 ─────────────────────────────
private VisualElement? _menuContainer;
// ── 푸터 요소 ─────────────────────────────────
private Label? _toggleIcon;
private Button? _toggleBtn;
private EventCallback<ClickEvent>? _onToggleBtnClick;
// ── 아이템 목록 ────────────────────────────────
private readonly List<NavItemButton> _navItemBtns = new();
private readonly List<Label> _itemLabels = new();
#endregion
#region (Events)
/// <summary>내비게이션 항목 선택 이벤트 (인덱스 전달)</summary>
public event Action<int>? OnNavItemSelected;
#endregion
#region (Properties)
/// <summary>현재 메뉴 확장 여부</summary>
public bool IsExpanded => _isExpanded;
/// <summary>현재 활성화된 항목 인덱스. 없으면 -1.</summary>
public int ActiveIndex { get; private set; } = -1;
#endregion
#region (Constructor)
public EWLKNavSideBar()
{
// 1. USS 로드
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null)
styleSheets.Add(uss);
// 2. 기본 클래스 설정
AddToClassList("ewlk-nav-sidebar");
style.width = ExpandedWidth;
// 3. UI 구성
BuildUI();
// 4. 패널 생명주기 등록
RegisterCallback<AttachToPanelEvent>(OnAttachToPanel);
RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
}
#endregion
#region UI (Build UI)
private void BuildUI()
{
// ── 헤더 ─────────────────────────────────────
var header = new VisualElement { name = "nav-header" };
header.AddToClassList("ewlk-nav__header");
// 로고 아이콘 (항상 표시)
var logoIcon = new VisualElement { name = "logo-icon" };
logoIcon.AddToClassList("ewlk-nav__logo-icon");
// 로고 텍스트 (확장 시만 표시)
_logoText = new Label("Englewood LAB") { name = "logo-text" };
_logoText.AddToClassList("ewlk-nav__logo-text");
// 닫기 버튼 (확장 시만 표시)
_closeBtn = new Button { name = "close-btn" };
_closeBtn.AddToClassList("ewlk-nav__close-btn");
var closeBtnIcon = new Label(UTKMaterialIcons.Close);
closeBtnIcon.AddToClassList("ewlk-nav__icon-label");
UTKMaterialIcons.ApplyIconStyle(closeBtnIcon, 16);
_closeBtn.Add(closeBtnIcon);
header.Add(logoIcon);
header.Add(_logoText);
header.Add(_closeBtn);
Add(header);
// ── 구분선 ───────────────────────────────────
var divider = new VisualElement { name = "nav-divider" };
divider.AddToClassList("ewlk-nav__divider");
Add(divider);
// ── 메뉴 영역 ────────────────────────────────
_menuContainer = new VisualElement { name = "nav-menu" };
_menuContainer.AddToClassList("ewlk-nav__menu");
Add(_menuContainer);
// ── 푸터 ─────────────────────────────────────
var footer = new VisualElement { name = "nav-footer" };
footer.AddToClassList("ewlk-nav__footer");
// 구분선
var footerDivider = new VisualElement();
footerDivider.AddToClassList("ewlk-nav__divider");
footer.Add(footerDivider);
// 설정 버튼 (고정 Tail 항목)
var settingsBtn = CreateNavItemButton(
-1,
"설정",
UTKMaterialIcons.Settings,
isTailItem: true
);
footer.Add(settingsBtn);
// 토글 버튼 (열기/닫기)
_toggleBtn = new Button { name = "toggle-btn" };
_toggleBtn.AddToClassList("ewlk-nav__toggle-btn");
_toggleIcon = new Label(UTKMaterialIcons.ChevronLeft);
_toggleIcon.AddToClassList("ewlk-nav__icon-label");
UTKMaterialIcons.ApplyIconStyle(_toggleIcon, 20);
_toggleBtn.Add(_toggleIcon);
footer.Add(_toggleBtn);
Add(footer);
}
/// <summary>
/// 내비게이션 항목 Button을 생성합니다.
/// </summary>
/// <param name="index">아이템 인덱스 (-1이면 Tail 항목)</param>
/// <param name="labelText">표시 이름</param>
/// <param name="iconChar">Material Icons 유니코드 문자</param>
/// <param name="isTailItem">Tail 항목 여부</param>
private NavItemButton CreateNavItemButton(
int index,
string labelText,
string iconChar,
bool isTailItem = false
)
{
int capturedIndex = index;
var btn = new NavItemButton(_ => OnNavItemClicked(capturedIndex));
btn.AddToClassList("ewlk-nav__item");
if (isTailItem)
btn.AddToClassList("ewlk-nav__item--tail");
// 아이콘 레이블
var iconLabel = new Label(iconChar);
iconLabel.AddToClassList("ewlk-nav__item-icon");
UTKMaterialIcons.ApplyIconStyle(iconLabel, 18);
// 텍스트 레이블
var textLabel = new Label(labelText);
textLabel.AddToClassList("ewlk-nav__item-label");
btn.Add(iconLabel);
btn.Add(textLabel);
// Tail 항목이 아닌 경우에만 리스트 추적
if (!isTailItem)
{
_navItemBtns.Add(btn);
_itemLabels.Add(textLabel);
}
return btn;
}
#endregion
#region (Public Methods)
/// <summary>
/// 메뉴 항목 목록을 설정합니다. 기존 항목을 모두 제거하고 새로 구성합니다.
/// </summary>
/// <param name="items">표시할 항목 데이터 목록</param>
public void SetMenuItems(IReadOnlyList<NavItemData> items)
{
// 기존 항목 정리
foreach (var btn in _navItemBtns)
{
_menuContainer?.Remove(btn);
btn.Dispose();
}
_navItemBtns.Clear();
_itemLabels.Clear();
ActiveIndex = -1;
// 새 항목 추가
for (int i = 0; i < items.Count; i++)
{
var btn = CreateNavItemButton(i, items[i].Label, items[i].IconChar);
_menuContainer?.Add(btn);
}
// 현재 확장 상태에 맞게 레이블 가시성 동기화
UpdateLabelsVisibility(_isExpanded);
}
/// <summary>
/// 지정 인덱스 항목을 활성 상태로 표시합니다.
/// </summary>
/// <param name="index">활성화할 아이템 인덱스</param>
public void SetActiveItem(int index)
{
// 이전 활성 항목 해제
if (ActiveIndex >= 0 && ActiveIndex < _navItemBtns.Count)
_navItemBtns[ActiveIndex].RemoveFromClassList("ewlk-nav__item--active");
ActiveIndex = index;
if (index >= 0 && index < _navItemBtns.Count)
_navItemBtns[index].AddToClassList("ewlk-nav__item--active");
}
/// <summary>
/// 메뉴 열기/닫기 상태를 설정합니다.
/// </summary>
/// <param name="expanded">true이면 열기, false이면 닫기</param>
public void SetExpanded(bool expanded)
{
if (_isExpanded == expanded) return;
_isExpanded = expanded;
AnimateWidth(expanded ? ExpandedWidth : CollapsedWidth);
UpdateLabelsVisibility(expanded);
UpdateToggleIcon();
}
/// <summary>현재 열기/닫기 상태를 토글합니다.</summary>
public void Toggle() => SetExpanded(!_isExpanded);
#endregion
#region (Event Handling)
private void OnAttachToPanel(AttachToPanelEvent evt)
{
// 닫기 버튼 이벤트 등록
_onCloseBtnClick = _ => SetExpanded(false);
_closeBtn?.RegisterCallback(_onCloseBtnClick);
// 토글 버튼 이벤트 등록
_onToggleBtnClick = _ => Toggle();
_toggleBtn?.RegisterCallback(_onToggleBtnClick);
}
private void OnDetachFromPanel(DetachFromPanelEvent evt)
{
// 닫기 버튼 이벤트 해제
if (_onCloseBtnClick != null)
{
_closeBtn?.UnregisterCallback(_onCloseBtnClick);
_onCloseBtnClick = null;
}
// 토글 버튼 이벤트 해제
if (_onToggleBtnClick != null)
{
_toggleBtn?.UnregisterCallback(_onToggleBtnClick);
_onToggleBtnClick = null;
}
}
private void OnNavItemClicked(int index)
{
if (index < 0) return; // 설정 버튼(index == -1) 처리는 외부에서
SetActiveItem(index);
OnNavItemSelected?.Invoke(index);
}
#endregion
#region (Animation)
private void AnimateWidth(float targetWidth)
{
_animTween?.Kill();
_animTween = DOTween
.To(
() => resolvedStyle.width,
x => style.width = x,
targetWidth,
AnimDuration
)
.SetEase(Ease.InOutQuad);
}
/// <summary>확장 상태에 따라 로고 텍스트와 항목 레이블 가시성을 설정합니다.</summary>
private void UpdateLabelsVisibility(bool visible)
{
var display = visible ? DisplayStyle.Flex : DisplayStyle.None;
if (_logoText != null)
_logoText.style.display = display;
if (_closeBtn != null)
_closeBtn.style.display = display;
foreach (var label in _itemLabels)
label.style.display = display;
}
/// <summary>확장 상태에 맞게 토글 버튼 아이콘을 갱신합니다.</summary>
private void UpdateToggleIcon()
{
if (_toggleIcon == null) return;
// 확장 → ChevronLeft(닫기), 축소 → Menu(열기)
_toggleIcon.text = _isExpanded
? UTKMaterialIcons.ChevronLeft
: UTKMaterialIcons.Menu;
}
#endregion
#region IDisposable
/// <summary>등록된 이벤트 및 DOTween 리소스를 해제합니다.</summary>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// DOTween 정리
_animTween?.Kill();
_animTween = null;
// 생명주기 콜백 해제
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanel);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanel);
// 버튼 이벤트 해제
if (_onCloseBtnClick != null)
{
_closeBtn?.UnregisterCallback(_onCloseBtnClick);
_onCloseBtnClick = null;
}
if (_onToggleBtnClick != null)
{
_toggleBtn?.UnregisterCallback(_onToggleBtnClick);
_onToggleBtnClick = null;
}
// 메뉴 항목 정리
foreach (var btn in _navItemBtns)
btn.Dispose();
_navItemBtns.Clear();
_itemLabels.Clear();
// 외부 이벤트 정리
OnNavItemSelected = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 23f33d4a307cd6546becdf8801da0748

View File

@@ -0,0 +1,167 @@
#nullable enable
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.EnglewoodLAB.Data;
namespace UVC.EnglewoodLAB.UIToolkit
{
/// <summary>
/// OverView 모달 내부 테이블 컨텐츠.
/// 3개 작업장의 월간/일간 생산 실적(목표수량·현시점 계획·실적수량·구분)을 표시합니다.
/// </summary>
/// <example>
/// <code>
/// var content = new EWLKOverViewModalContent();
/// content.UpdateData(mqttService.CurrentData);
/// 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>
[UxmlElement]
public partial class EWLKOverViewModalContent : VisualElement, IDisposable
{
private const string USS_PATH = "EWLK/UIToolkit/Main/EWLKOverViewModalContentUss";
// 데이터 셀 참조 캐시 [작업장, 기간, 컬럼]
// 작업장: 0=제조작업장, 1=충포장(3F), 2=충포장(4F)
// 기간: 0=월간, 1=일간
// 컬럼: 0=목표수량, 1=현시점계획, 2=실적수량, 3=구분
private readonly Label[,,] _cells = new Label[3, 2, 4];
public EWLKOverViewModalContent()
{
var uss = Resources.Load<StyleSheet>(USS_PATH);
if (uss != null) styleSheets.Add(uss);
AddToClassList("ewlk-overview");
BuildUI();
}
// ── UI 구성 ───────────────────────────────────────────────
private void BuildUI()
{
Add(BuildHeader());
string[] names = { "제조작업장", "충포장작업장(3F)", "충포장작업장(4F)" };
for (int wi = 0; wi < 3; wi++)
Add(BuildWorkshopGroup(wi, names[wi]));
}
/// <summary>헤더 행을 생성합니다.</summary>
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>
/// 작업장 행 그룹을 생성합니다.
/// 왼쪽에 작업장 이름이 2행(월간+일간)에 걸쳐 표시됩니다.
/// </summary>
private VisualElement BuildWorkshopGroup(int wi, string name)
{
var group = new VisualElement();
group.AddToClassList("ewlk-overview__group");
// 왼쪽: 작업장 이름 셀 (2행 height 걸쳐 세로 중앙 정렬)
var nameCell = new Label(name);
nameCell.AddToClassList("ewlk-overview__cell");
nameCell.AddToClassList("ewlk-overview__cell--name");
group.Add(nameCell);
// 오른쪽: 월간/일간 행 컨테이너
var rows = new VisualElement();
rows.AddToClassList("ewlk-overview__rows");
string[] periodLabels = { "월간", "일간" };
for (int pi = 0; pi < 2; pi++)
{
var row = new VisualElement();
row.AddToClassList("ewlk-overview__row");
// 기간 레이블 (월간/일간)
var periodLabel = new Label(periodLabels[pi]);
periodLabel.AddToClassList("ewlk-overview__cell");
periodLabel.AddToClassList("ewlk-overview__cell--period");
row.Add(periodLabel);
// 데이터 셀 4개: 목표수량·현시점계획·실적수량·구분
for (int ci = 0; ci < 4; ci++)
{
var cell = new Label();
cell.AddToClassList("ewlk-overview__cell");
cell.AddToClassList(ci < 3
? "ewlk-overview__cell--data"
: "ewlk-overview__cell--type");
_cells[wi, pi, ci] = cell;
row.Add(cell);
}
rows.Add(row);
}
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>
/// OverView 데이터로 테이블을 갱신합니다.
/// MQTT 데이터 수신 시 또는 모달 열릴 때 호출합니다.
/// </summary>
/// <param name="data">갱신할 OverView 데이터</param>
public void UpdateData(EWLKOverViewData data)
{
var workshops = new[]
{
data.Manufacture,
data.Packing3F,
data.Packing4F,
};
for (int wi = 0; wi < 3; wi++)
{
ApplyPeriod(wi, 0, workshops[wi].Monthly);
ApplyPeriod(wi, 1, workshops[wi].Daily);
}
}
private void ApplyPeriod(int wi, int pi, EWLKOverViewPeriodData period)
{
_cells[wi, pi, 0].text = period.Target;
_cells[wi, pi, 1].text = period.Plan;
_cells[wi, pi, 2].text = period.Actual;
_cells[wi, pi, 3].text = period.Type;
}
// ── IDisposable ───────────────────────────────────────────
public void Dispose() { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d04fa3104d5991c4a92464244bfd962b

View File

@@ -0,0 +1,9 @@
{
"targetFrameRate": 60,
"mqtt": {
"host": "localhost",
"port": 1883,
"dataKey": "data",
"messagePack": false
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 8b272a9c533455349b8266b72f02c5ab
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: