leftSideBar 구현

- 버튼 클릭 시 열고 닫히는 애니메이션 작동
- 열었을 때 메뉴 상세이름 보이도록
- 메뉴 눌렀을 때 동작은 아직 없는 상태
This commit is contained in:
SOOBEEN HAN
2026-03-12 13:53:08 +09:00
parent cfba7fdf53
commit 5712da17d7
10 changed files with 1160 additions and 7 deletions

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

@@ -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

@@ -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

@@ -1,6 +1,92 @@
#nullable enable
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Core;
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 세 개와 씬 전용 서비스를 등록합니다.
/// </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);
}
}
/// <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,109 @@
#nullable enable
using Cysharp.Threading.Tasks;
using System;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.Core;
using UVC.EnglewoodLAB.UIToolkit;
using UVC.UIToolkit;
namespace UVC.EnglewoodLAB
{
public class EWLKSceneMain
/// <summary>
/// 메인 씬의 초기화 컨트롤러.
/// NavSideBar 설정 및 콘텐츠 뷰 전환을 담당합니다.
/// </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;
// ── 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()
{
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;
_navSideBar.SetActiveItem(0); // 첫 번째 항목 기본 활성화
}
// ── 이벤트 처리 ──────────────────────────────────
/// <summary>
/// NavSideBar 항목 선택 시 호출됩니다.
/// 인덱스에 따라 DynamicUI의 콘텐츠 뷰를 전환합니다.
/// </summary>
/// <param name="index">선택된 항목 인덱스</param>
private void OnNavItemSelected(int index)
{
// TODO: 인덱스별 콘텐츠 뷰(DynamicUI) 전환 구현 예정
Debug.Log($"[EWLKSceneMain] 메뉴 {index} 선택됨");
}
}
}

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