diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 6a06fb10..599e83dc 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -29,7 +29,11 @@
"Bash(del:*)",
"Bash(cmd /c \"del /q \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Resources\\\\UIToolkit\\\\Property\\\\Views\\\\UTKIntPropertyItemViewSliderUss.uss\"\" \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Resources\\\\UIToolkit\\\\Property\\\\Views\\\\UTKFloatPropertyItemViewSliderUss.uss\"\"\")",
"mcp__UnityMCP__refresh_unity",
- "Bash(powershell \"Get-Process Unity -ErrorAction SilentlyContinue | Select-Object -First 1 | Format-List\")"
+ "Bash(powershell \"Get-Process Unity -ErrorAction SilentlyContinue | Select-Object -First 1 | Format-List\")",
+ "Bash(cmd /c \"mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Scripts\\\\UVC\\\\UIToolkit\\\\ToolBar\\\\Data\"\" && mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Scripts\\\\UVC\\\\UIToolkit\\\\ToolBar\\\\Items\"\" && mkdir \"\"d:\\\\works\\\\2025\\\\02.Studio\\\\dev\\\\base\\\\XRBase\\\\Assets\\\\Resources\\\\UIToolkit\\\\ToolBar\"\"\")",
+ "Bash(/bin/ls:*)",
+ "Bash(/bin/mkdir -p:*)",
+ "Bash(/bin/rm:*)"
],
"deny": [],
"ask": []
diff --git a/Assets/Resources/UIToolkit/Sample/Menu.meta b/Assets/Resources/UIToolkit/Sample/Menu.meta
new file mode 100644
index 00000000..757f49fb
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Menu.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 91c1ca31501570048823ba41e18c9a7b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uss b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uss
new file mode 100644
index 00000000..b1fe8ff1
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uss
@@ -0,0 +1,30 @@
+/*
+ * ===================================
+ * UTKTopMenuSample.uss
+ * Styles for UTKTopMenu sample
+ * ===================================
+ */
+
+.utk-menu-sample-container {
+ min-height: 44px;
+ background-color: var(--color-bg-secondary);
+ border-radius: 6px;
+ padding: 0;
+ overflow: visible;
+}
+
+.utk-menu-sample-container--vertical {
+ min-height: 180px;
+ max-width: 160px;
+}
+
+.utk-menu-sample-result {
+ font-size: 12px;
+ color: var(--color-text-secondary);
+ margin-top: 8px;
+ padding: 8px 12px;
+ background-color: var(--color-bg-secondary);
+ border-radius: 4px;
+ min-height: 20px;
+ white-space: normal;
+}
diff --git a/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uss.meta b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uss.meta
new file mode 100644
index 00000000..6f33e16c
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: c11c996b7b7032549a2d8039bce83f85
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uxml b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uxml
new file mode 100644
index 00000000..6e75ed06
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uxml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uxml.meta b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uxml.meta
new file mode 100644
index 00000000..9ef55704
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/Menu/UTKTopMenuSample.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 50dc639c380919a4884c8f844748f8f1
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/Sample/ToolBar.meta b/Assets/Resources/UIToolkit/Sample/ToolBar.meta
new file mode 100644
index 00000000..30b08424
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/ToolBar.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 13901de85d1af3e448f1317374f2e7da
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uss b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uss
new file mode 100644
index 00000000..57a151bf
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uss
@@ -0,0 +1,44 @@
+/*
+ * ===================================
+ * UTKToolBarSample.uss
+ * Styles for UTKToolBar sample
+ * ===================================
+ */
+
+.utk-toolbar-sample-container {
+ min-height: 48px;
+ background-color: var(--color-bg-secondary);
+ border-radius: 6px;
+ padding: 4px;
+ overflow: visible;
+ align-items: center;
+ align-self: flex-start;
+}
+
+.utk-toolbar-sample-container--vertical {
+ min-height: 240px;
+ max-width: 52px;
+}
+
+.utk-toolbar-sample-result {
+ font-size: 12px;
+ color: var(--color-text-secondary);
+ margin-top: 8px;
+ padding: 8px 12px;
+ background-color: var(--color-bg-secondary);
+ border-radius: 4px;
+ min-height: 20px;
+ white-space: normal;
+}
+
+.utk-toolbar-sample-log {
+ font-size: 11px;
+ color: var(--color-text-secondary);
+ padding: 8px 12px;
+ background-color: var(--color-bg-secondary);
+ border-radius: 4px;
+ min-height: 60px;
+ max-height: 120px;
+ white-space: normal;
+ overflow: hidden;
+}
diff --git a/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uss.meta b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uss.meta
new file mode 100644
index 00000000..64ffa941
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 3c7e537449067fd41b2caeec2ea6eedb
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uxml b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uxml
new file mode 100644
index 00000000..5d159e4b
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uxml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uxml.meta b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uxml.meta
new file mode 100644
index 00000000..6e705fba
--- /dev/null
+++ b/Assets/Resources/UIToolkit/Sample/ToolBar/UTKToolBarSample.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: a3d648b54b5194f4d9577a6de6dd703a
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss b/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss
index 436e0a72..63d19c49 100644
--- a/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss
+++ b/Assets/Resources/UIToolkit/Style/UTKThemeDark.uss
@@ -136,6 +136,7 @@
--color-bg-panel: var(--color-base-20);
--color-bg-input: var(--color-base-19);
--color-bg-hover: var(--color-base-16);
+ --color-bg-active: var(--color-base-14);
--color-bg-tooltip: rgba(38, 38, 38, 0.95);
--color-bg-overlay: rgba(0, 0, 0, 0.5);
@@ -169,6 +170,13 @@
--color-calendar-range: var(--color-blue-06);
--color-calendar-range-hover: var(--color-blue-07);
+ /* ===================================
+ Semantic Colors - Primary (color-primary-*)
+ =================================== */
+ --color-primary: var(--color-blue-05);
+ --color-primary-hover: var(--color-blue-06);
+ --color-primary-active: var(--color-blue-08);
+
/* ===================================
Semantic Colors - State (color-state-*)
=================================== */
diff --git a/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss b/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss
index cf5b6f0f..c3aafa0a 100644
--- a/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss
+++ b/Assets/Resources/UIToolkit/Style/UTKThemeLight.uss
@@ -147,6 +147,7 @@
--color-bg-panel: var(--color-base-01);
--color-bg-input: var(--color-base-01);
--color-bg-hover: var(--color-base-04);
+ --color-bg-active: var(--color-base-05);
--color-bg-tooltip: rgba(50, 50, 50, 0.95);
--color-bg-overlay: rgba(0, 0, 0, 0.3);
@@ -180,6 +181,13 @@
--color-calendar-range: var(--color-blue-02);
--color-calendar-range-hover: var(--color-blue-03);
+ /* ===================================
+ Semantic Colors - Primary (color-primary-*)
+ =================================== */
+ --color-primary: var(--color-blue-05);
+ --color-primary-hover: var(--color-blue-06);
+ --color-primary-active: var(--color-blue-07);
+
/* ===================================
Semantic Colors - State (color-state-*)
=================================== */
diff --git a/Assets/Resources/UIToolkit/ToolBar.meta b/Assets/Resources/UIToolkit/ToolBar.meta
new file mode 100644
index 00000000..e60c7736
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 16e7b0cea2218874dae5634bc4835c27
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBar.uxml b/Assets/Resources/UIToolkit/ToolBar/UTKToolBar.uxml
new file mode 100644
index 00000000..572913bb
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBar.uxml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBar.uxml.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBar.uxml.meta
new file mode 100644
index 00000000..19c4405d
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBar.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 67f7dc49efdc14b4f8263c9acb0630a4
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButton.uxml b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButton.uxml
new file mode 100644
index 00000000..144628be
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButton.uxml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButton.uxml.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButton.uxml.meta
new file mode 100644
index 00000000..f641235c
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButton.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: aa3e7c26d0d75ca4b8784ee75c0a6348
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButtonUss.uss b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButtonUss.uss
new file mode 100644
index 00000000..bcf381b0
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButtonUss.uss
@@ -0,0 +1,57 @@
+/* ===================================
+ * UTKToolBarButtonUss.uss
+ * 툴바 버튼 스타일
+ * =================================== */
+
+.utk-toolbar-btn {
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-width: 32px;
+ min-height: 32px;
+ padding: var(--space-xs);
+ border-radius: var(--radius-s);
+ border-width: 0;
+ background-color: transparent;
+ cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
+ transition: background-color var(--anim-fast);
+}
+
+.utk-toolbar-btn:hover {
+ background-color: var(--color-bg-hover);
+}
+
+.utk-toolbar-btn:active {
+ background-color: var(--color-bg-active);
+}
+
+.utk-toolbar-btn--disabled {
+ opacity: 0.4;
+}
+
+.utk-toolbar-btn__icon {
+ width: var(--size-icon-btn);
+ height: var(--size-icon-btn);
+ font-size: 18px;
+ -unity-text-align: middle-center;
+ color: var(--color-text-primary);
+ padding: 0;
+ margin: 4px;
+ -unity-font-definition: resource('Fonts/Icons/MaterialSymbolsOutlined');
+ cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
+}
+
+.utk-toolbar-btn__icon--image {
+ -unity-background-scale-mode: scale-to-fit;
+ -unity-font-definition: none;
+ font-size: 0;
+}
+
+.utk-toolbar-btn__label {
+ font-size: var(--font-size-label4);
+ color: var(--color-text-secondary);
+ -unity-text-align: upper-center;
+ margin-top: 1px;
+ display: none;
+ cursor: resource('UIToolkit/Images/cursor_point_white_32') 14 5;
+}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButtonUss.uss.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButtonUss.uss.meta
new file mode 100644
index 00000000..c86ef507
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarButtonUss.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: db912499a72f8b94ab2d6d9ae94847a8
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButton.uxml b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButton.uxml
new file mode 100644
index 00000000..d51f58a9
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButton.uxml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButton.uxml.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButton.uxml.meta
new file mode 100644
index 00000000..d212a26d
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButton.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: f26343b39f7a20b4e99fd1bc9fd5a1fe
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButtonUss.uss b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButtonUss.uss
new file mode 100644
index 00000000..1e53e7ad
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButtonUss.uss
@@ -0,0 +1,38 @@
+/* ===================================
+ * UTKToolBarExpandableButtonUss.uss
+ * 확장 버튼 스타일 (서브 메뉴 화살표)
+ * =================================== */
+
+.utk-toolbar-expandable {
+ position: relative;
+}
+
+.utk-toolbar-expandable__arrow {
+ width: 6px;
+ height: 6px;
+ position: absolute;
+ bottom: 2px;
+ right: 2px;
+}
+
+/* 가로 배치: 아래 화살표 ▼ */
+.utk-toolbar--horizontal .utk-toolbar-expandable__arrow {
+ border-left-width: 3px;
+ border-right-width: 3px;
+ border-top-width: 4px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ border-top-color: var(--color-text-secondary);
+ border-bottom-width: 0;
+}
+
+/* 세로 배치: 오른쪽 화살표 ▶ */
+.utk-toolbar--vertical .utk-toolbar-expandable__arrow {
+ border-top-width: 3px;
+ border-bottom-width: 3px;
+ border-left-width: 4px;
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+ border-left-color: var(--color-text-secondary);
+ border-right-width: 0;
+}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButtonUss.uss.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButtonUss.uss.meta
new file mode 100644
index 00000000..a0d8c93d
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButtonUss.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5951a6797681fed438dc5939f3a38266
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparator.uxml b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparator.uxml
new file mode 100644
index 00000000..fffc8185
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparator.uxml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparator.uxml.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparator.uxml.meta
new file mode 100644
index 00000000..9e0d2b7d
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparator.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: 982b4ee5d7a6a0b40ab4ffe78d71fb94
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparatorUss.uss b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparatorUss.uss
new file mode 100644
index 00000000..edbb1758
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparatorUss.uss
@@ -0,0 +1,20 @@
+/* ===================================
+ * UTKToolBarSeparatorUss.uss
+ * 구분선 스타일
+ * =================================== */
+
+/* 가로 배치: 세로 구분선 */
+.utk-toolbar--horizontal .utk-toolbar-separator {
+ width: var(--border-width);
+ height: 20px;
+ margin: 0 var(--space-s);
+ background-color: var(--color-border);
+}
+
+/* 세로 배치: 가로 구분선 */
+.utk-toolbar--vertical .utk-toolbar-separator {
+ height: var(--border-width);
+ width: 20px;
+ margin: var(--space-s) 0;
+ background-color: var(--color-border);
+}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparatorUss.uss.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparatorUss.uss.meta
new file mode 100644
index 00000000..7d9366c5
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparatorUss.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 808c6701fa4d7834e9f7c58273631725
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenu.uxml b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenu.uxml
new file mode 100644
index 00000000..fcdc03dc
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenu.uxml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenu.uxml.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenu.uxml.meta
new file mode 100644
index 00000000..86788b08
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenu.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: a5dc4901fda3a4047a167867faaa66a4
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenuUss.uss b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenuUss.uss
new file mode 100644
index 00000000..1207fce5
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenuUss.uss
@@ -0,0 +1,39 @@
+/* ===================================
+ * UTKToolBarSubMenuUss.uss
+ * 확장 버튼 서브 메뉴 스타일
+ * =================================== */
+
+.utk-toolbar-submenu {
+ position: absolute;
+ background-color: var(--color-bg-primary);
+ border-width: var(--border-width);
+ border-color: var(--color-border);
+ border-radius: var(--radius-m);
+ padding: var(--space-xs);
+ min-width: 120px;
+}
+
+.utk-toolbar-submenu__container {
+ flex-direction: column;
+}
+
+/* 서브 메뉴 내 버튼은 가로로 펼침 */
+.utk-toolbar-submenu .utk-toolbar-btn {
+ flex-direction: row;
+ min-width: 100px;
+ min-height: 28px;
+ justify-content: flex-start;
+ padding: var(--space-xs) var(--space-m);
+ margin: 1px 0;
+}
+
+.utk-toolbar-submenu .utk-toolbar-btn__icon {
+ margin-right: var(--space-s);
+}
+
+.utk-toolbar-submenu .utk-toolbar-btn__label {
+ display: flex;
+ font-size: var(--font-size-body2);
+ color: var(--color-text-primary);
+ -unity-text-align: middle-left;
+}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenuUss.uss.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenuUss.uss.meta
new file mode 100644
index 00000000..4958c8b3
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenuUss.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b692a3d78f8602e478a07d72398388ff
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButton.uxml b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButton.uxml
new file mode 100644
index 00000000..f287471b
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButton.uxml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButton.uxml.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButton.uxml.meta
new file mode 100644
index 00000000..3347f301
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButton.uxml.meta
@@ -0,0 +1,10 @@
+fileFormatVersion: 2
+guid: b1c910e06cdb3b442813e22530596643
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButtonUss.uss b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButtonUss.uss
new file mode 100644
index 00000000..f48966f0
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButtonUss.uss
@@ -0,0 +1,22 @@
+/* ===================================
+ * UTKToolBarToggleButtonUss.uss
+ * 토글/라디오 버튼 선택 상태 스타일
+ * =================================== */
+
+/* 선택 상태: 특수성을 높여 기본 스타일 오버라이드 */
+.utk-toolbar-btn.utk-toolbar-toggle--selected {
+ background-color: var(--color-primary);
+ border-radius: var(--radius-s);
+}
+
+.utk-toolbar-btn.utk-toolbar-toggle--selected:hover {
+ background-color: var(--color-primary-hover);
+}
+
+.utk-toolbar-btn.utk-toolbar-toggle--selected:active {
+ background-color: var(--color-primary-active);
+}
+
+.utk-toolbar-toggle--selected .utk-toolbar-btn__icon {
+ color: var(--color-text-on-primary);
+}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButtonUss.uss.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButtonUss.uss.meta
new file mode 100644
index 00000000..de104ac9
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButtonUss.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 10bacd7792291454184c14add3bb2896
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarUss.uss b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarUss.uss
new file mode 100644
index 00000000..f3d58006
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarUss.uss
@@ -0,0 +1,49 @@
+/* ===================================
+ * UTKToolBarUss.uss
+ * 툴바 메인 스타일
+ * =================================== */
+
+/* === 공통 === */
+.utk-toolbar {
+ overflow: visible;
+ flex-grow: 1;
+}
+
+.utk-toolbar__container {
+ overflow: visible;
+}
+
+/* === 가로 배치 === */
+.utk-toolbar--horizontal {
+ flex-direction: row;
+ align-items: center;
+ align-self: flex-start;
+ height: 40px;
+ background-color: var(--color-bg-secondary);
+ /* border-bottom-width: var(--border-width); */
+ /* border-bottom-color: var(--color-border); */
+ padding: 0 var(--space-s);
+}
+
+.utk-toolbar--horizontal > .utk-toolbar__container {
+ flex-direction: row;
+ align-items: center;
+}
+
+/* === 세로 배치 === */
+.utk-toolbar--vertical {
+ flex-direction: column;
+ align-items: center;
+ width: 40px;
+ height: auto;
+ background-color: var(--color-bg-secondary);
+ /* border-right-width: var(--border-width);
+ border-right-color: var(--color-border); */
+ padding: var(--space-s) 0;
+}
+
+.utk-toolbar--vertical > .utk-toolbar__container {
+ flex-direction: column;
+ align-items: center;
+ flex-grow: 1;
+}
diff --git a/Assets/Resources/UIToolkit/ToolBar/UTKToolBarUss.uss.meta b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarUss.uss.meta
new file mode 100644
index 00000000..c1aeaeac
--- /dev/null
+++ b/Assets/Resources/UIToolkit/ToolBar/UTKToolBarUss.uss.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: dab8ffb85c679be44a624b68a5364734
+ScriptedImporter:
+ internalIDToNameTable: []
+ externalObjects: {}
+ serializedVersion: 2
+ userData:
+ assetBundleName:
+ assetBundleVariant:
+ script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
+ disableValidation: 0
diff --git a/Assets/Sample/Injector/InjectorSampleAudioManager.cs b/Assets/Sample/Injector/InjectorSampleAudioManager.cs
index 02626796..c70e9dba 100644
--- a/Assets/Sample/Injector/InjectorSampleAudioManager.cs
+++ b/Assets/Sample/Injector/InjectorSampleAudioManager.cs
@@ -13,13 +13,13 @@ namespace Sample
/// [ 특징 ]
///
/// - 프리팹 없이 런타임에 새 GameObject가 자동 생성됨
- /// - Injector가 new GameObject().AddComponent<T>()로 생성
+ /// - Injector가 new GameObject().AddComponent()로 생성
/// - App 라이프사이클이면 DontDestroyOnLoad 자동 적용
/// - 다른 서비스([Inject] ILogService)에 대한 의존성 주입 지원
///
///
/// [ 등록 방법 ]
- /// Injector.Register<IAudioManager, InjectorSampleAudioManager>(ServiceLifetime.App);
+ /// Injector.Register(ServiceLifetime.App);
///
/// [ 사용 방법 ]
///
@@ -41,8 +41,8 @@ namespace Sample
///
/// private void Awake()
/// {
- /// bgmSource = gameObject.AddComponent<AudioSource>();
- /// sfxSource = gameObject.AddComponent<AudioSource>();
+ /// bgmSource = gameObject.AddComponent();
+ /// sfxSource = gameObject.AddComponent();
/// bgmSource.loop = true;
/// }
///
diff --git a/Assets/Sample/Injector/InjectorSampleNetworkManager.cs b/Assets/Sample/Injector/InjectorSampleNetworkManager.cs
index 2e61a759..38398aac 100644
--- a/Assets/Sample/Injector/InjectorSampleNetworkManager.cs
+++ b/Assets/Sample/Injector/InjectorSampleNetworkManager.cs
@@ -7,7 +7,7 @@ namespace Sample
/// Type D 예시: MonoBehaviour SingletonApp - 네트워크 관리자
///
///
- /// [ 타입 ] Type D - MonoBehaviour SingletonApp (SingletonApp<T> 상속)
+ /// [ 타입 ] Type D - MonoBehaviour SingletonApp (SingletonApp 상속)
/// [ 라이프사이클 ] App - 씬 전환 시에도 유지 (DontDestroyOnLoad)
///
/// [ 특징 ]
@@ -19,7 +19,7 @@ namespace Sample
///
///
/// [ 등록 방법 ]
- /// Injector.RegisterSingleton<InjectorSampleNetworkManager>();
+ /// Injector.RegisterSingleton();
///
/// [ 씬 설정 (선택) ]
///
@@ -40,8 +40,8 @@ namespace Sample
///
/// [ InjectorSampleSettingsManager와의 차이점 ]
///
- /// - SettingsManagerSingleton<T> - 순수 C#, GameObject 없음
- /// - NetworkManagerSingletonApp<T> - MonoBehaviour, GameObject 필요
+ /// - SettingsManagerSingleton - 순수 C#, GameObject 없음
+ /// - NetworkManagerSingletonApp - MonoBehaviour, GameObject 필요
///
///
public class InjectorSampleNetworkManager : SingletonApp
diff --git a/Assets/Sample/Injector/InjectorSampleSettingsManager.cs b/Assets/Sample/Injector/InjectorSampleSettingsManager.cs
index d51282c1..17509dac 100644
--- a/Assets/Sample/Injector/InjectorSampleSettingsManager.cs
+++ b/Assets/Sample/Injector/InjectorSampleSettingsManager.cs
@@ -7,7 +7,7 @@ namespace Sample
/// Type D 예시: 순수 C# Singleton - 게임 설정 관리자
///
///
- /// [ 타입 ] Type D - 순수 C# Singleton (Singleton<T> 상속)
+ /// [ 타입 ] Type D - 순수 C# Singleton (Singleton 상속)
/// [ 라이프사이클 ] App - 애플리케이션 전체 유지
///
/// [ 특징 ]
@@ -19,7 +19,7 @@ namespace Sample
///
///
/// [ 등록 방법 ]
- /// Injector.RegisterSingleton<InjectorSampleSettingsManager>();
+ /// Injector.RegisterSingleton();
///
/// [ 사용 방법 ]
///
diff --git a/Assets/Sample/Injector/InjectorSampleUIManager.cs b/Assets/Sample/Injector/InjectorSampleUIManager.cs
index 3bc9156c..88ca11d6 100644
--- a/Assets/Sample/Injector/InjectorSampleUIManager.cs
+++ b/Assets/Sample/Injector/InjectorSampleUIManager.cs
@@ -34,7 +34,7 @@ namespace Sample
///
/// protected override void RegisterServices()
/// {
- /// Injector.RegisterPrefab<IUIManager>(uiManagerPrefab.gameObject, ServiceLifetime.App);
+ /// Injector.RegisterPrefab(uiManagerPrefab.gameObject, ServiceLifetime.App);
/// }
///
///
diff --git a/Assets/Sample/Injector/InjectorSamples.cs b/Assets/Sample/Injector/InjectorSamples.cs
index 2638b1c5..f9485388 100644
--- a/Assets/Sample/Injector/InjectorSamples.cs
+++ b/Assets/Sample/Injector/InjectorSamples.cs
@@ -33,7 +33,7 @@ namespace Sample
///
///
/// 구현체: ConsoleLogger
- /// 등록: Injector.Register<ILogService, ConsoleLogger>(ServiceLifetime.App)
+ /// 등록: Injector.Register(ServiceLifetime.App)
/// 사용: [Inject] private ILogService _logger;
///
public interface ILogService
@@ -73,8 +73,8 @@ namespace Sample
///
///
/// 구현체: SceneConfig
- /// 등록: Injector.Register<ISceneConfig, SceneConfig>(ServiceLifetime.Scene)
- /// 또는 Factory: Injector.RegisterFactory<ISceneConfig>(injector => new SceneConfig { ... })
+ /// 등록: Injector.Register(ServiceLifetime.Scene)
+ /// 또는 Factory: Injector.RegisterFactory(injector => new SceneConfig { ... })
///
public interface ISceneConfig
{
@@ -129,7 +129,7 @@ namespace Sample
///
///
/// // 등록
- /// Injector.Register<IGameService, GameService>(ServiceLifetime.App);
+ /// Injector.Register(ServiceLifetime.App);
///
/// // 사용
/// [Inject] private IGameService _gameService;
@@ -154,7 +154,7 @@ namespace Sample
///
/// 구현체: InjectorSampleAudioManager (별도 파일)
/// 타입: Type B - MonoBehaviour 동적 생성
- /// 등록: Injector.Register<IAudioManager, InjectorSampleAudioManager>(ServiceLifetime.App)
+ /// 등록: Injector.Register(ServiceLifetime.App)
/// 특징: 런타임에 새 GameObject가 생성되고 AudioSource 등 Unity 컴포넌트 활용 가능
///
public interface IAudioManager
@@ -173,7 +173,7 @@ namespace Sample
///
/// 구현체: InjectorSampleEnemySpawner (별도 파일)
/// 타입: Type B - MonoBehaviour 동적 생성
- /// 등록: Injector.Register<IEnemySpawner, InjectorSampleEnemySpawner>(ServiceLifetime.Scene)
+ /// 등록: Injector.Register(ServiceLifetime.Scene)
/// 라이프사이클: Scene - 씬 전환 시 적 스포너와 생성된 적들이 함께 정리됨
///
public interface IEnemySpawner
@@ -194,7 +194,7 @@ namespace Sample
///
/// 구현체: InjectorSampleUIManager (별도 파일)
/// 타입: Type C - Prefab 기반 MonoBehaviour
- /// 등록: Injector.RegisterPrefab<IUIManager>(uiManagerPrefab, ServiceLifetime.App)
+ /// 등록: Injector.RegisterPrefab(uiManagerPrefab, ServiceLifetime.App)
/// 특징:
///
/// - 프리팹에 미리 설정된 UI 요소들이 유지됨
@@ -220,7 +220,7 @@ namespace Sample
///
/// 구현체: InjectorSampleSceneUI (별도 파일)
/// 타입: Type C - Prefab 기반 MonoBehaviour
- /// 등록: Injector.RegisterPrefab<ISceneUI>(sceneUIPrefab, ServiceLifetime.Scene)
+ /// 등록: Injector.RegisterPrefab(sceneUIPrefab, ServiceLifetime.Scene)
/// 라이프사이클: Scene - 씬 전환 시 UI가 함께 정리됨
///
public interface ISceneUI
@@ -268,11 +268,11 @@ namespace Sample
///
///
/// // 등록
- /// Injector.Register<IRequestHandler, RequestHandler>(ServiceLifetime.Transient);
+ /// Injector.Register(ServiceLifetime.Transient);
///
/// // 사용 - 매번 다른 인스턴스
- /// var handler1 = Injector.Resolve<IRequestHandler>(); // RequestId: "a1b2c3d4"
- /// var handler2 = Injector.Resolve<IRequestHandler>(); // RequestId: "e5f6g7h8"
+ /// var handler1 = Injector.Resolve(); // RequestId: "a1b2c3d4"
+ /// var handler2 = Injector.Resolve(); // RequestId: "e5f6g7h8"
///
///
public class RequestHandler : IRequestHandler
diff --git a/Assets/Sample/UIToolkit/UTKStyleGuideSample.Menu.cs b/Assets/Sample/UIToolkit/UTKStyleGuideSample.Menu.cs
new file mode 100644
index 00000000..4bc2a667
--- /dev/null
+++ b/Assets/Sample/UIToolkit/UTKStyleGuideSample.Menu.cs
@@ -0,0 +1,344 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UIElements;
+using UVC.UIToolkit;
+using UVC.UI.Commands;
+
+///
+/// UTKStyleGuideSample의 Menu 카테고리 Initialize 메서드들
+///
+public partial class UTKStyleGuideSample
+{
+ #region Menu Fields
+
+ private UTKTopMenu? _sampleMenuHorizontal;
+ private UTKTopMenuModel? _sampleModelHorizontal;
+ private UTKTopMenu? _sampleMenuIcon;
+ private UTKTopMenuModel? _sampleModelIcon;
+ private UTKTopMenu? _sampleMenuVertical;
+ private UTKTopMenuModel? _sampleModelVertical;
+
+ #endregion
+
+ #region Menu Initializers
+
+ private void InitializeTopMenuSample(VisualElement root)
+ {
+ InitializeHorizontalMenu(root);
+ InitializeIconMenu(root);
+ InitializeVerticalMenu(root);
+ InitializeApiTest(root);
+
+ SetCodeSamples(root,
+ csharpCode: @"// === 기본 가로 메뉴 ===
+var menuView = new UTKTopMenu();
+var menuModel = new UTKTopMenuModel();
+
+// 메뉴 아이템 추가
+var fileMenu = new UTKMenuItemData(""file"", ""파일"");
+fileMenu.AddSubMenuItem(new UTKMenuItemData(
+ ""file_new"", ""새 파일"",
+ new DebugLogCommand(""새 파일 생성""),
+ shortcut: ""Ctrl+N""
+));
+fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
+fileMenu.AddSubMenuItem(new UTKMenuItemData(
+ ""file_save"", ""저장"",
+ new DebugLogCommand(""저장""),
+ shortcut: ""Ctrl+S""
+));
+menuModel.AddMenuItem(fileMenu);
+
+// View에 메뉴 생성
+menuView.CreateMenuItems(
+ menuModel.MenuItems, menuView.MenuContainer);
+
+// 클릭 이벤트
+menuView.OnMenuItemClicked += (data) =>
+ Debug.Log($""클릭: {data.ItemId}"");
+
+// === 세로 메뉴 ===
+var vertMenu = new UTKTopMenu();
+vertMenu.Orientation = UTKMenuOrientation.Vertical;
+vertMenu.ItemSpacing = 4f;
+
+// === 아이콘 메뉴 ===
+var iconMenu = new UTKMenuImageItemData(
+ ""settings"",
+ UTKMaterialIcons.Settings,
+ useMaterialIcon: true,
+ imageSize: 24f
+);
+
+// === API ===
+// ItemId로 Command 실행
+bool ok = menuView.ExecuteCommand(""file_new"");
+
+// ItemId로 데이터 조회
+if (menuView.TryGetMenuItemData(""file_save"", out var d))
+ Debug.Log($""Enabled: {d?.IsEnabled}"");",
+ uxmlCode: @"
+
+
+
+
+
+
+
+
+");
+ }
+
+ ///
+ /// 가로 메뉴 초기화
+ ///
+ private void InitializeHorizontalMenu(VisualElement root)
+ {
+ var container = root.Q("horizontal-menu-container");
+ if (container == null) return;
+
+ _sampleMenuHorizontal = new UTKTopMenu();
+ _sampleModelHorizontal = new UTKTopMenuModel();
+
+ // 파일 메뉴
+ var fileMenu = new UTKMenuItemData("s_file", "파일");
+ fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_new", "새 파일", new DebugLogCommand("새 파일 생성"), shortcut: "Ctrl+N"));
+ fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_open", "열기", new DebugLogCommand("파일 열기"), shortcut: "Ctrl+O"));
+ fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
+ fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_save", "저장", new DebugLogCommand("파일 저장"), shortcut: "Ctrl+S"));
+ fileMenu.AddSubMenuItem(new UTKMenuItemData("s_file_exit", "종료", new DebugLogCommand("종료"), shortcut: "Alt+F4"));
+ _sampleModelHorizontal.AddMenuItem(fileMenu);
+
+ // 편집 메뉴
+ var editMenu = new UTKMenuItemData("s_edit", "편집");
+ editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_undo", "실행 취소", new DebugLogCommand("실행 취소"), shortcut: "Ctrl+Z"));
+ editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_redo", "다시 실행", new DebugLogCommand("다시 실행"), shortcut: "Ctrl+Y"));
+ editMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
+ editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_copy", "복사", new DebugLogCommand("복사"), shortcut: "Ctrl+C"));
+ editMenu.AddSubMenuItem(new UTKMenuItemData("s_edit_paste", "붙여넣기", new DebugLogCommand("붙여넣기"), shortcut: "Ctrl+V"));
+ _sampleModelHorizontal.AddMenuItem(editMenu);
+
+ // 보기 메뉴 (2 depth)
+ var viewMenu = new UTKMenuItemData("s_view", "보기");
+ var layoutMenu = new UTKMenuItemData("s_view_layout", "레이아웃");
+ layoutMenu.AddSubMenuItem(new UTKMenuItemData("s_view_layout_default", "기본", new DebugLogCommand("기본 레이아웃")));
+ layoutMenu.AddSubMenuItem(new UTKMenuItemData("s_view_layout_wide", "와이드", new DebugLogCommand("와이드 레이아웃")));
+ viewMenu.AddSubMenuItem(layoutMenu);
+ viewMenu.AddSubMenuItem(new UTKMenuItemData("s_view_fullscreen", "전체 화면", new DebugLogCommand("전체 화면"), shortcut: "F11"));
+ _sampleModelHorizontal.AddMenuItem(viewMenu);
+
+ // 도움말 메뉴
+ var helpMenu = new UTKMenuItemData("s_help", "도움말");
+ helpMenu.AddSubMenuItem(new UTKMenuItemData("s_help_doc", "문서", new DebugLogCommand("문서 열기"), shortcut: "F1"));
+ helpMenu.AddSubMenuItem(new UTKMenuItemData("s_help_about", "정보", new DebugLogCommand("정보")));
+ _sampleModelHorizontal.AddMenuItem(helpMenu);
+
+ // View에 메뉴 생성
+ if (_sampleMenuHorizontal.MenuContainer != null)
+ {
+ _sampleMenuHorizontal.CreateMenuItems(_sampleModelHorizontal.MenuItems, _sampleMenuHorizontal.MenuContainer);
+ }
+
+ _sampleMenuHorizontal.OnMenuItemClicked += OnSampleMenuClicked;
+ container.Add(_sampleMenuHorizontal);
+ }
+
+ ///
+ /// 아이콘 메뉴 초기화 (ItemSpacing 적용)
+ ///
+ private void InitializeIconMenu(VisualElement root)
+ {
+ var container = root.Q("icon-menu-container");
+ if (container == null) return;
+
+ _sampleMenuIcon = new UTKTopMenu();
+ _sampleMenuIcon.ItemSpacing = 4f;
+ _sampleModelIcon = new UTKTopMenuModel();
+
+ // 홈 아이콘
+ var homeMenu = new UTKMenuImageItemData("s_icon_home", UTKMaterialIcons.Home, useMaterialIcon: true, imageSize: 24f);
+ homeMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_home_dashboard", "대시보드", new DebugLogCommand("대시보드")));
+ homeMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_home_recent", "최근 항목", new DebugLogCommand("최근 항목")));
+ _sampleModelIcon.AddMenuItem(homeMenu);
+
+ // 설정 아이콘
+ var settingsMenu = new UTKMenuImageItemData("s_icon_settings", UTKMaterialIcons.Settings, useMaterialIcon: true, imageSize: 24f);
+ settingsMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_settings_general", "일반", new DebugLogCommand("일반 설정")));
+ settingsMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_settings_display", "화면", new DebugLogCommand("화면 설정")));
+ _sampleModelIcon.AddMenuItem(settingsMenu);
+
+ // 알림 아이콘
+ var notifMenu = new UTKMenuImageItemData("s_icon_notif", UTKMaterialIcons.Notifications, useMaterialIcon: true, imageSize: 24f);
+ notifMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_notif_all", "전체 알림", new DebugLogCommand("전체 알림")));
+ notifMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_notif_clear", "알림 지우기", new DebugLogCommand("알림 지우기")));
+ _sampleModelIcon.AddMenuItem(notifMenu);
+
+ // 계정 아이콘
+ var accountMenu = new UTKMenuImageItemData("s_icon_account", UTKMaterialIcons.AccountCircle, useMaterialIcon: true, imageSize: 24f);
+ accountMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_account_profile", "프로필", new DebugLogCommand("프로필")));
+ accountMenu.AddSubMenuItem(new UTKMenuItemData("s_icon_account_logout", "로그아웃", new DebugLogCommand("로그아웃")));
+ _sampleModelIcon.AddMenuItem(accountMenu);
+
+ if (_sampleMenuIcon.MenuContainer != null)
+ {
+ _sampleMenuIcon.CreateMenuItems(_sampleModelIcon.MenuItems, _sampleMenuIcon.MenuContainer);
+ }
+
+ _sampleMenuIcon.OnMenuItemClicked += OnSampleMenuClicked;
+ container.Add(_sampleMenuIcon);
+ }
+
+ ///
+ /// 세로 메뉴 초기화
+ ///
+ private void InitializeVerticalMenu(VisualElement root)
+ {
+ var container = root.Q("vertical-menu-container");
+ if (container == null) return;
+
+ _sampleMenuVertical = new UTKTopMenu();
+ _sampleMenuVertical.Orientation = UTKMenuOrientation.Vertical;
+ _sampleMenuVertical.ItemSpacing = 2f;
+ _sampleMenuVertical.SubMenuOffsetX = -10f;
+ _sampleMenuVertical.SubMenuOffsetY = 4f;
+ _sampleModelVertical = new UTKTopMenuModel();
+
+ // 파일
+ var fileMenu = new UTKMenuItemData("sv_file", "파일");
+ fileMenu.AddSubMenuItem(new UTKMenuItemData("sv_file_new", "새 파일", new DebugLogCommand("세로: 새 파일"), shortcut: "Ctrl+N"));
+ fileMenu.AddSubMenuItem(new UTKMenuItemData("sv_file_open", "열기", new DebugLogCommand("세로: 열기"), shortcut: "Ctrl+O"));
+ fileMenu.AddSubMenuItem(UTKMenuItemData.CreateSeparator());
+ fileMenu.AddSubMenuItem(new UTKMenuItemData("sv_file_save", "저장", new DebugLogCommand("세로: 저장"), shortcut: "Ctrl+S"));
+ _sampleModelVertical.AddMenuItem(fileMenu);
+
+ // 편집
+ var editMenu = new UTKMenuItemData("sv_edit", "편집");
+ editMenu.AddSubMenuItem(new UTKMenuItemData("sv_edit_undo", "실행 취소", new DebugLogCommand("세로: 실행 취소"), shortcut: "Ctrl+Z"));
+ editMenu.AddSubMenuItem(new UTKMenuItemData("sv_edit_redo", "다시 실행", new DebugLogCommand("세로: 다시 실행"), shortcut: "Ctrl+Y"));
+ _sampleModelVertical.AddMenuItem(editMenu);
+
+ // 보기
+ var viewMenu = new UTKMenuItemData("sv_view", "보기");
+ viewMenu.AddSubMenuItem(new UTKMenuItemData("sv_view_fullscreen", "전체 화면", new DebugLogCommand("세로: 전체 화면"), shortcut: "F11"));
+ _sampleModelVertical.AddMenuItem(viewMenu);
+
+ // 도움말
+ var helpMenu = new UTKMenuItemData("sv_help", "도움말");
+ helpMenu.AddSubMenuItem(new UTKMenuItemData("sv_help_about", "정보", new DebugLogCommand("세로: 정보")));
+ _sampleModelVertical.AddMenuItem(helpMenu);
+
+ if (_sampleMenuVertical.MenuContainer != null)
+ {
+ _sampleMenuVertical.CreateMenuItems(_sampleModelVertical.MenuItems, _sampleMenuVertical.MenuContainer);
+ }
+
+ _sampleMenuVertical.OnMenuItemClicked += OnSampleMenuClicked;
+ container.Add(_sampleMenuVertical);
+ }
+
+ ///
+ /// API 테스트 버튼 초기화
+ ///
+ private void InitializeApiTest(VisualElement root)
+ {
+ var container = root.Q("api-test-container");
+ var resultLabel = root.Q
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var boundsField = new UTKBoundsField("경계");
/// boundsField.ErrorMessage = "크기는 양수여야 합니다.";
/// boundsField.Validation = () => boundsField.Value.size.x > 0 && boundsField.Value.size.y > 0 && boundsField.Value.size.z > 0;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs
index cd806af8..28332553 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKDoubleField.cs
@@ -48,7 +48,7 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var precisionField = new UTKDoubleField("정밀 값", 0);
/// precisionField.ErrorMessage = "값은 0보다 커야 합니다.";
/// precisionField.Validation = () => precisionField.Value > 0;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs
index d575692d..240c5b5e 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatField.cs
@@ -48,7 +48,7 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var speedField = new UTKFloatField("속도", 0f);
/// speedField.ErrorMessage = "속도는 0보다 커야 합니다.";
/// speedField.Validation = () => speedField.Value > 0f;
@@ -58,7 +58,7 @@ namespace UVC.UIToolkit
/// // 범위 검증
/// var temperatureField = new UTKFloatField("온도", 20f);
/// temperatureField.ErrorMessage = "온도는 -40 ~ 60 사이여야 합니다.";
- /// temperatureField.Validation = () => temperatureField.Value >= -40f && temperatureField.Value <= 60f;
+ /// temperatureField.Validation = () => temperatureField.Value >= -40f && temperatureField.Value <= 60f;
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = speedField.Validate();
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs
index 07a775c9..62a0c280 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKFloatStepper.cs
@@ -66,7 +66,7 @@ namespace UVC.UIToolkit
///
/// var volumeStepper = new UTKFloatStepper(0f, 1f, 0.5f, 0.1f);
/// volumeStepper.ErrorMessage = "볼륨은 0~1 사이여야 합니다.";
- /// volumeStepper.Validation = () => volumeStepper.Value >= 0f && volumeStepper.Value <= 1f;
+ /// volumeStepper.Validation = () => volumeStepper.Value >= 0f && volumeStepper.Value <= 1f;
///
/// UXML에서 사용:
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var emailInput = new UTKInputField("이메일", "example@email.com");
/// emailInput.ErrorMessage = "올바른 이메일 형식이 아닙니다.";
/// emailInput.Validation = () => emailInput.Value.Contains("@");
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs
index 0fe218d0..b9198e09 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntStepper.cs
@@ -85,7 +85,7 @@ namespace UVC.UIToolkit
///
/// var monthStepper = new UTKIntStepper(1, 12, 1, 1);
/// monthStepper.ErrorMessage = "유효하지 않은 월입니다.";
- /// monthStepper.Validation = () => monthStepper.Value >= 1 && monthStepper.Value <= 12;
+ /// monthStepper.Validation = () => monthStepper.Value >= 1 && monthStepper.Value <= 12;
///
/// bool isValid = monthStepper.Validate();
/// monthStepper.ClearError();
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs
index d2623051..293ca029 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKIntegerField.cs
@@ -46,10 +46,10 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var ageField = new UTKIntegerField("나이", 0);
/// ageField.ErrorMessage = "나이는 1~150 사이여야 합니다.";
- /// ageField.Validation = () => ageField.Value >= 1 && ageField.Value <= 150;
+ /// ageField.Validation = () => ageField.Value >= 1 && ageField.Value <= 150;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs
index e3194ca4..7b1ef6b5 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKLongField.cs
@@ -43,7 +43,7 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var fileSizeField = new UTKLongField("파일 크기", 0);
/// fileSizeField.ErrorMessage = "파일 크기는 0보다 커야 합니다.";
/// fileSizeField.Validation = () => fileSizeField.Value > 0;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs
index 2871b3f2..3478a120 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKRectField.cs
@@ -51,7 +51,7 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var areaField = new UTKRectField("영역");
/// areaField.ErrorMessage = "너비와 높이는 0보다 커야 합니다.";
/// areaField.Validation = () => areaField.Value.width > 0 && areaField.Value.height > 0;
@@ -91,7 +91,7 @@ namespace UVC.UIToolkit
/// var uvField = new UTKRectField("UV 영역");
/// uvField.Value = sprite.rect;
/// uvField.ErrorMessage = "UV 영역은 텍스처 범위를 초과할 수 없습니다.";
- /// uvField.Validation = () => uvField.Value.xMax <= texture.width && uvField.Value.yMax <= texture.height;
+ /// uvField.Validation = () => uvField.Value.xMax <= texture.width && uvField.Value.yMax <= texture.height;
/// uvField.OnValueChanged += (newRect) => {
/// // 스프라이트 영역 업데이트
/// UpdateSpriteRect(sprite, newRect);
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs
index e66d5b75..258a4fc8 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector2Field.cs
@@ -53,7 +53,7 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var sizeField = new UTKVector2Field("크기");
/// sizeField.ErrorMessage = "크기는 양수여야 합니다.";
/// sizeField.Validation = () => sizeField.Value.x > 0 && sizeField.Value.y > 0;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs
index 1e80220e..29fa67d3 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector3Field.cs
@@ -54,7 +54,7 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var posField = new UTKVector3Field("위치");
/// posField.ErrorMessage = "Y값은 0 이상이어야 합니다.";
/// posField.Validation = () => posField.Value.y >= 0;
diff --git a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs
index 6eddb697..ca709db2 100644
--- a/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Input/UTKVector4Field.cs
@@ -54,10 +54,10 @@ namespace UVC.UIToolkit
///
/// Validation (입력 검증):
///
- /// // 검증 함수 설정 (Func<bool>)
+ /// // 검증 함수 설정 (Func)
/// var colorField = new UTKVector4Field("색상");
/// colorField.ErrorMessage = "알파 값은 0~1 사이여야 합니다.";
- /// colorField.Validation = () => colorField.Value.w >= 0 && colorField.Value.w <= 1;
+ /// colorField.Validation = () => colorField.Value.w >= 0 && colorField.Value.w <= 1;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
diff --git a/Assets/Scripts/UVC/UIToolkit/Menu/README.md b/Assets/Scripts/UVC/UIToolkit/Menu/CLAUDE.md
similarity index 96%
rename from Assets/Scripts/UVC/UIToolkit/Menu/README.md
rename to Assets/Scripts/UVC/UIToolkit/Menu/CLAUDE.md
index 8ffbfe90..ac9331a3 100644
--- a/Assets/Scripts/UVC/UIToolkit/Menu/README.md
+++ b/Assets/Scripts/UVC/UIToolkit/Menu/CLAUDE.md
@@ -62,7 +62,7 @@ Assets/Scripts/UVC/UIToolkit/Menu/
├── UTKMenuItemBase.cs # 메뉴 아이템 베이스 클래스
├── UTKTopMenuItem.cs # 텍스트 메뉴 아이템 컴포넌트
├── UTKTopMenuImageItem.cs # 이미지 메뉴 아이템 컴포넌트
-└── README.md
+└── CLAUDE.md
Assets/Resources/UIToolkit/Menu/
├── UTKTopMenu.uxml # 메인 메뉴 구조
@@ -693,6 +693,21 @@ subMenuContainer.style.display = DisplayStyle.Flex; // 표시
---
+## 유지보수 가이드
+
+> **소스 코드를 수정할 때 반드시 CLAUDE.md와 코드 주석(XML 문서)도 함께 업데이트해야 합니다.**
+
+| 변경 사항 | 업데이트 대상 |
+|-----------|--------------|
+| 클래스/메서드 추가·삭제·이름 변경 | CLAUDE API 문서 + 파일 구조 + 해당 클래스 XML 주석 |
+| 생성자 파라미터 변경 | CLAUDE API 문서 + 사용 예제 + XML `` 태그 |
+| 공개 속성(Property) 추가·삭제 | CLAUDE API 테이블 + XML `` 태그 |
+| 이벤트 추가·삭제 | CLAUDE 이벤트 테이블 + 메모리 관리 섹션 |
+| UXML/USS 파일 추가·삭제 | CLAUDE 파일 구조 섹션 |
+| 새로운 기능 추가 | CLAUDE 주요 기능 + 사용 예제 섹션 |
+
+---
+
## 라이선스
이 프로젝트는 UVC 프레임워크의 일부입니다.
diff --git a/Assets/Scripts/UVC/UIToolkit/Menu/README.md.meta b/Assets/Scripts/UVC/UIToolkit/Menu/CLAUDE.md.meta
similarity index 100%
rename from Assets/Scripts/UVC/UIToolkit/Menu/README.md.meta
rename to Assets/Scripts/UVC/UIToolkit/Menu/CLAUDE.md.meta
diff --git a/Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md b/Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md
new file mode 100644
index 00000000..9b0bd4b2
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md
@@ -0,0 +1,713 @@
+# UIToolkit Property System
+
+UIToolkit 기반의 Property 편집 시스템입니다. 데이터(Item)와 뷰(View)를 분리하는 MVVM 아키텍처를 적용하여, 20가지 속성 타입을 지원합니다.
+
+## 📋 목차
+- [주요 기능](#주요-기능)
+- [아키텍처](#아키텍처)
+- [파일 구조](#파일-구조)
+- [빠른 시작](#빠른-시작)
+- [사용 예제](#사용-예제)
+- [지원 타입 목록](#지원-타입-목록)
+- [API 문서](#api-문서)
+- [메모리 관리](#메모리-관리)
+- [성능 최적화](#성능-최적화)
+
+---
+
+## 주요 기능
+
+✅ **데이터/뷰 완전 분리 (MVVM)**
+- Item: 순수 데이터 레이어 (UI 코드 없음)
+- View: 순수 UI 레이어 (비즈니스 로직 없음)
+- Bind/Unbind 패턴으로 양방향 데이터 동기화
+
+✅ **20가지 속성 타입 지원**
+- 기본: String, Int, Float, Bool, Vector2, Vector3, Color
+- 날짜: Date, DateTime, DateRange, DateTimeRange
+- 선택: Dropdown, Enum, Radio, MultiSelectDropdown
+- 범위: IntRange, FloatRange
+- 특수: ColorState, FloatDropdown, Button
+
+✅ **ReadOnly 모드**
+- 모든 View가 ReadOnly 상태 전환 지원
+- ReadOnly 시 편집 컨트롤이 읽기 전용 InputField로 전환
+- 그룹 단위 일괄 ReadOnly 설정 가능
+
+✅ **그룹(Group) 관리**
+- 속성 아이템을 그룹으로 묶어 관리
+- 그룹 접기/펼치기 지원
+- 아이템 추가/제거 이벤트
+
+✅ **Factory 패턴**
+- `UTKPropertyItemViewFactory`로 데이터 타입에 맞는 View 자동 생성
+- 커스텀 View 등록/교체 가능
+
+✅ **완벽한 메모리 관리**
+- 모든 클래스 IDisposable 구현
+- Bind/Unbind 대칭 이벤트 관리
+- 정적 UXML/USS 캐싱으로 반복 로드 방지
+
+---
+
+## 아키텍처
+
+```
+Data Layer (Items) View Layer (Views)
+───────────────── ──────────────────
+IUTKPropertyEntry IUTKPropertyItemView
+ ├─ IUTKPropertyGroup IUTKPropertyItemView
+ └─ IUTKPropertyItem UTKPropertyItemViewBase (abstract)
+ └─ UTKPropertyItemBase └─ UTK{Type}PropertyItemView
+ └─ UTK{Type}PropertyItem
+
+ Factory Bridge
+ ──────────────
+ UTKPropertyItemViewFactory
+ CreateView(IUTKPropertyItem)
+ → new View
+ → View.Bind(item)
+ → return VisualElement
+```
+
+### 데이터 흐름
+
+```
+1. Item 생성 (데이터)
+ var item = new UTKFloatPropertyItem("speed", "이동 속도", 1.0f);
+
+2. Factory가 View 생성 + 바인딩
+ var view = UTKPropertyItemViewFactory.CreateView(item);
+
+3. 양방향 동기화
+ Item.Value 변경 → OnTypedValueChanged → View.RefreshUI()
+ View 사용자 입력 → OnValueChanged → Item.Value 업데이트
+```
+
+---
+
+## 파일 구조
+
+```
+Assets/Scripts/UVC/UIToolkit/Property/
+├── Core/ # 인터페이스 & 핵심 타입
+│ ├── IUTKPropertyEntry.cs # 트리 엔트리 베이스 인터페이스
+│ ├── IUTKPropertyGroup.cs # 그룹 인터페이스
+│ ├── IUTKPropertyItem.cs # 속성 아이템 인터페이스 (비제네릭/제네릭)
+│ ├── UTKPropertyGroup.cs # 그룹 구현체
+│ ├── UTKPropertyType.cs # 속성 타입 열거형 (20종)
+│ └── UTKPropertyValueChangedEventArgs.cs # 값 변경 이벤트 인자
+├── Data/ # 복합 데이터 구조체
+│ ├── UTKColorState.cs # 상태-색상 쌍
+│ ├── UTKRangeValue.cs # 범위 값 (Int/Float/Date/DateTime)
+│ └── UTKFloatDropdownValue.cs # Float + Dropdown 복합 값
+├── Items/ # 데이터 레이어 (Item 클래스)
+│ ├── Base/
+│ │ └── UTKPropertyItemBase.cs # Item 추상 베이스 클래스
+│ ├── UTKStringPropertyItem.cs
+│ ├── UTKIntPropertyItem.cs
+│ ├── UTKFloatPropertyItem.cs
+│ ├── UTKBoolPropertyItem.cs
+│ ├── UTKVector2PropertyItem.cs
+│ ├── UTKVector3PropertyItem.cs
+│ ├── UTKColorPropertyItem.cs
+│ ├── UTKColorStatePropertyItem.cs
+│ ├── UTKDatePropertyItem.cs
+│ ├── UTKDateRangePropertyItem.cs
+│ ├── UTKDateTimePropertyItem.cs
+│ ├── UTKDateTimeRangePropertyItem.cs
+│ ├── UTKDropdownPropertyItem.cs
+│ ├── UTKEnumPropertyItem.cs
+│ ├── UTKRadioPropertyItem.cs
+│ ├── UTKIntRangePropertyItem.cs
+│ ├── UTKFloatRangePropertyItem.cs
+│ ├── UTKMultiSelectDropdownPropertyItem.cs
+│ ├── UTKFloatDropdownPropertyItem.cs
+│ └── UTKButtonItem.cs
+└── Views/ # 뷰 레이어 (View 클래스)
+ ├── IUTKPropertyItemView.cs # View 인터페이스 (비제네릭/제네릭)
+ ├── UTKPropertyItemViewBase.cs # View 추상 베이스 클래스
+ ├── UTKPropertyItemViewFactory.cs # View 팩토리 (타입별 자동 생성)
+ ├── UTKStringPropertyItemView.cs
+ ├── UTKIntPropertyItemView.cs
+ ├── UTKFloatPropertyItemView.cs
+ ├── UTKBoolPropertyItemView.cs
+ ├── UTKVector2PropertyItemView.cs
+ ├── UTKVector3PropertyItemView.cs
+ ├── UTKColorPropertyItemView.cs
+ ├── UTKColorStatePropertyItemView.cs
+ ├── UTKDatePropertyItemView.cs
+ ├── UTKDateRangePropertyItemView.cs
+ ├── UTKDateTimePropertyItemView.cs
+ ├── UTKDateTimeRangePropertyItemView.cs
+ ├── UTKDropdownPropertyItemView.cs
+ ├── UTKEnumPropertyItemView.cs
+ ├── UTKRadioPropertyItemView.cs
+ ├── UTKIntRangePropertyItemView.cs
+ ├── UTKFloatRangePropertyItemView.cs
+ ├── UTKMultiSelectDropdownPropertyItemView.cs
+ ├── UTKFloatDropdownPropertyItemView.cs
+ ├── UTKButtonItemView.cs
+ └── CLAUDE.md
+
+Assets/Resources/UIToolkit/Property/Views/
+├── UTKPropertyItemViewCommonUss.uss # 공통 스타일 (모든 View 공유)
+├── UTKStringPropertyItemView.uxml
+├── UTKStringPropertyItemViewUss.uss
+├── UTKIntPropertyItemView.uxml
+├── UTKIntPropertyItemViewUss.uss
+├── UTKFloatPropertyItemView.uxml
+├── UTKFloatPropertyItemViewUss.uss
+├── UTKBoolPropertyItemView.uxml
+├── UTKBoolPropertyItemViewUss.uss
+├── UTKVector2PropertyItemView.uxml
+├── UTKVector2PropertyItemViewUss.uss
+├── UTKVector3PropertyItemView.uxml
+├── UTKVector3PropertyItemViewUss.uss
+├── UTKColorPropertyItemView.uxml
+├── UTKColorPropertyItemViewUss.uss
+├── UTKColorStatePropertyItemView.uxml
+├── UTKColorStatePropertyItemViewUss.uss
+├── UTKDatePropertyItemView.uxml
+├── UTKDatePropertyItemViewUss.uss
+├── UTKDateRangePropertyItemView.uxml
+├── UTKDateRangePropertyItemViewUss.uss
+├── UTKDateTimePropertyItemView.uxml
+├── UTKDateTimePropertyItemViewUss.uss
+├── UTKDateTimeRangePropertyItemView.uxml
+├── UTKDateTimeRangePropertyItemViewUss.uss
+├── UTKDropdownPropertyItemView.uxml
+├── UTKDropdownPropertyItemViewUss.uss
+├── UTKEnumPropertyItemView.uxml
+├── UTKEnumPropertyItemViewUss.uss
+├── UTKRadioPropertyItemView.uxml
+├── UTKRadioPropertyItemViewUss.uss
+├── UTKIntRangePropertyItemView.uxml
+├── UTKIntRangePropertyItemViewUss.uss
+├── UTKFloatRangePropertyItemView.uxml
+├── UTKFloatRangePropertyItemViewUss.uss
+├── UTKMultiSelectDropdownPropertyItemView.uxml
+├── UTKMultiSelectDropdownPropertyItemViewUss.uss
+├── UTKFloatDropdownPropertyItemView.uxml
+├── UTKFloatDropdownPropertyItemViewUss.uss
+├── UTKButtonItemView.uxml
+└── UTKButtonItemViewUss.uss
+```
+
+---
+
+## 빠른 시작
+
+### 1. 단일 속성 아이템 생성
+
+```csharp
+// 1. 데이터(Item) 생성
+var nameItem = new UTKStringPropertyItem("name", "이름", "홍길동");
+
+// 2. Factory로 View 생성 + 바인딩
+var nameView = UTKPropertyItemViewFactory.CreateView(nameItem);
+
+// 3. UI에 추가
+rootVisualElement.Add(nameView);
+
+// 4. 값 변경 감지
+nameItem.OnTypedValueChanged += (item, oldVal, newVal) =>
+{
+ Debug.Log($"이름 변경: {oldVal} → {newVal}");
+};
+```
+
+### 2. 그룹으로 묶어서 관리
+
+```csharp
+// 그룹 생성
+var transformGroup = new UTKPropertyGroup("transform", "Transform");
+
+// 아이템 추가
+transformGroup.AddItem(new UTKVector3PropertyItem("position", "Position", Vector3.zero));
+transformGroup.AddItem(new UTKVector3PropertyItem("rotation", "Rotation", Vector3.zero));
+transformGroup.AddItem(new UTKVector3PropertyItem("scale", "Scale", Vector3.one));
+
+// 그룹 전체 ReadOnly 설정
+transformGroup.SetAllItemsReadOnly(true);
+```
+
+---
+
+## 사용 예제
+
+### 다양한 속성 타입 생성
+
+```csharp
+// 기본 타입
+var stringItem = new UTKStringPropertyItem("name", "이름", "홍길동");
+var intItem = new UTKIntPropertyItem("count", "개수", 10);
+var floatItem = new UTKFloatPropertyItem("speed", "속도", 1.5f);
+var boolItem = new UTKBoolPropertyItem("active", "활성화", true);
+
+// 벡터 타입
+var posItem = new UTKVector2PropertyItem("pos", "위치", Vector2.zero);
+var rotItem = new UTKVector3PropertyItem("rot", "회전", Vector3.zero);
+
+// 색상 타입
+var colorItem = new UTKColorPropertyItem("color", "색상", Color.white);
+colorItem.UseAlpha = true; // 알파 채널 사용
+
+// 날짜 타입
+var dateItem = new UTKDatePropertyItem("start", "시작일", DateTime.Today);
+dateItem.DateFormat = "yyyy-MM-dd";
+
+// 드롭다운 타입
+var dropdownItem = new UTKDropdownPropertyItem("type", "유형", "TypeA");
+dropdownItem.Choices = new List { "TypeA", "TypeB", "TypeC" };
+
+// Enum 타입
+var enumItem = new UTKEnumPropertyItem("align", "정렬", TextAlignment.Center);
+```
+
+### 슬라이더/스테퍼 활용
+
+```csharp
+// Int 슬라이더
+var intItem = new UTKIntPropertyItem("volume", "볼륨", 50);
+intItem.UseSlider = true;
+intItem.MinValue = 0;
+intItem.MaxValue = 100;
+
+// Float 스테퍼
+var floatItem = new UTKFloatPropertyItem("opacity", "투명도", 1.0f);
+floatItem.UseStepper = true;
+floatItem.Step = 0.1f;
+floatItem.MinValue = 0f;
+floatItem.MaxValue = 1f;
+```
+
+### 범위(Range) 타입 사용
+
+```csharp
+// Int 범위
+var intRange = new UTKIntRangePropertyItem("level", "레벨 범위",
+ new UTKIntRange { Min = 1, Max = 10 });
+intRange.UseStepper = true;
+intRange.StepperStep = 1;
+
+// Float 범위
+var floatRange = new UTKFloatRangePropertyItem("temp", "온도 범위",
+ new UTKFloatRange { Min = -10f, Max = 40f });
+
+// 날짜 범위
+var dateRange = new UTKDateRangePropertyItem("period", "기간",
+ new UTKDateRange { Start = DateTime.Today, End = DateTime.Today.AddDays(30) });
+```
+
+### 멀티셀렉트 드롭다운
+
+```csharp
+var multiSelect = new UTKMultiSelectDropdownPropertyItem(
+ "tags", "태그", new List { "UI", "3D" });
+multiSelect.Choices = new List { "UI", "3D", "Network", "Audio", "Physics" };
+
+// 프로그래밍 방식 선택
+multiSelect.SelectAll();
+multiSelect.ClearSelection();
+multiSelect.SetSelectedValues(new List { "UI", "Audio" });
+```
+
+### ColorState 사용
+
+```csharp
+var colorState = new UTKColorStatePropertyItem("status", "상태",
+ new UTKColorState { State = "Normal", Color = Color.green });
+
+// 상태/색상 개별 변경
+colorState.SetState("Warning");
+colorState.SetColor(Color.yellow);
+```
+
+### FloatDropdown 복합 타입
+
+```csharp
+var floatDropdown = new UTKFloatDropdownPropertyItem("size", "크기",
+ new UTKFloatDropdownValue { FloatValue = 100f, DropdownValue = "px" });
+floatDropdown.Choices = new List { "px", "%", "em", "rem" };
+floatDropdown.UseStepper = true;
+```
+
+### Button 아이템
+
+```csharp
+var buttonItem = new UTKButtonItem("save", "저장");
+buttonItem.Text = "저장하기";
+buttonItem.Icon = UTKMaterialIcons.Save;
+buttonItem.Variant = UTKButton.ButtonVariant.Primary;
+buttonItem.ActionName = "save_action";
+```
+
+### String + ActionButton 조합
+
+```csharp
+var pathItem = new UTKStringPropertyItem("path", "파일 경로", "");
+
+// ActionButton 설정 (찾아보기 버튼)
+var browseButton = new UTKButtonItem("browse", "찾아보기");
+browseButton.Icon = UTKMaterialIcons.FolderOpen;
+browseButton.IconOnly = true;
+pathItem.ActionButton = browseButton;
+```
+
+### 커스텀 View 등록
+
+```csharp
+// 기본 View를 커스텀 View로 교체
+UTKPropertyItemViewFactory.RegisterCustomView(
+ UTKPropertyType.String,
+ () => new MyCustomStringView()
+);
+
+// 해제
+UTKPropertyItemViewFactory.UnregisterCustomView(UTKPropertyType.String);
+```
+
+### 값 변경 이벤트 처리
+
+```csharp
+// 제네릭 이벤트 (타입 안전)
+floatItem.OnTypedValueChanged += (item, oldVal, newVal) =>
+{
+ Debug.Log($"{item.Name}: {oldVal} → {newVal}");
+};
+
+// 비제네릭 이벤트 (범용)
+floatItem.OnValueChanged += (item, oldVal, newVal, notify) =>
+{
+ Debug.Log($"{item.Id} changed");
+};
+
+// 상태 변경 이벤트 (ReadOnly, ShowLabel 등)
+floatItem.OnStateChanged += (item) =>
+{
+ Debug.Log($"{item.Id} state changed, ReadOnly={item.IsReadOnly}");
+};
+```
+
+---
+
+## 지원 타입 목록
+
+| UTKPropertyType | Item 클래스 | View 클래스 | 데이터 타입 | 주요 컨트롤 |
+|-----------------|------------|------------|------------|------------|
+| `String` | UTKStringPropertyItem | UTKStringPropertyItemView | string | UTKInputField + ActionButton |
+| `Int` | UTKIntPropertyItem | UTKIntPropertyItemView | int | UTKIntegerField + Slider/Stepper |
+| `Float` | UTKFloatPropertyItem | UTKFloatPropertyItemView | float | UTKFloatField + Slider/Stepper |
+| `Bool` | UTKBoolPropertyItem | UTKBoolPropertyItemView | bool | UTKToggle |
+| `Vector2` | UTKVector2PropertyItem | UTKVector2PropertyItemView | Vector2 | UTKVector2Field |
+| `Vector3` | UTKVector3PropertyItem | UTKVector3PropertyItemView | Vector3 | UTKVector3Field |
+| `Color` | UTKColorPropertyItem | UTKColorPropertyItemView | Color | ColorPreview + UTKColorPicker |
+| `ColorState` | UTKColorStatePropertyItem | UTKColorStatePropertyItemView | UTKColorState | Label + ColorPreview + Picker |
+| `Date` | UTKDatePropertyItem | UTKDatePropertyItemView | DateTime | UTKInputField + UTKDatePicker |
+| `DateTime` | UTKDateTimePropertyItem | UTKDateTimePropertyItemView | DateTime | UTKInputField + UTKDatePicker |
+| `DateRange` | UTKDateRangePropertyItem | UTKDateRangePropertyItemView | UTKDateRange | InputField×2 + DatePicker×2 |
+| `DateTimeRange` | UTKDateTimeRangePropertyItem | UTKDateTimeRangePropertyItemView | UTKDateTimeRange | InputField×2 + DatePicker×2 |
+| `Enum` | UTKEnumPropertyItem | UTKEnumPropertyItemView | Enum | UTKEnumDropDown |
+| `DropdownList` | UTKDropdownPropertyItem | UTKDropdownPropertyItemView | string | UTKDropdown |
+| `MultiSelectDropdownList` | UTKMultiSelectDropdownPropertyItem | UTKMultiSelectDropdownPropertyItemView | List\ | UTKMultiSelectDropdown |
+| `RadioGroup` | UTKRadioPropertyItem | UTKRadioPropertyItemView | int (index) | UTKRadioButton × N |
+| `IntRange` | UTKIntRangePropertyItem | UTKIntRangePropertyItemView | UTKIntRange | IntegerField×2 + Stepper×2 |
+| `FloatRange` | UTKFloatRangePropertyItem | UTKFloatRangePropertyItemView | UTKFloatRange | FloatField×2 + Stepper×2 |
+| `FloatDropdown` | UTKFloatDropdownPropertyItem | UTKFloatDropdownPropertyItemView | UTKFloatDropdownValue | FloatField/Stepper + Dropdown |
+| `Button` | UTKButtonItem | UTKButtonItemView | string (actionName) | UTKButton |
+
+---
+
+## API 문서
+
+### IUTKPropertyItem (데이터 인터페이스)
+
+#### Properties
+
+| 속성 | 타입 | 설명 |
+|------|------|------|
+| `Id` | string | 고유 식별자 |
+| `Name` | string | 표시 이름 |
+| `DisplayName` | string | 표시 이름 (Name과 동일) |
+| `Description` | string | 설명 |
+| `Tooltip` | string | 툴팁 텍스트 |
+| `IsReadOnly` | bool | 읽기 전용 여부 |
+| `IsVisible` | bool | 표시 여부 |
+| `ShowLabel` | bool | 라벨 표시 여부 |
+| `PropertyType` | UTKPropertyType | 속성 타입 열거형 |
+| `GroupId` | string | 소속 그룹 ID |
+
+#### Methods
+
+| 메서드 | 설명 | 반환 |
+|--------|------|------|
+| `GetValue()` | 현재 값 가져오기 (object?) | object? |
+| `SetValue(object?, bool)` | 값 설정 (notify로 이벤트 발생 제어) | void |
+
+#### Events
+
+| 이벤트 | 타입 | 설명 |
+|--------|------|------|
+| `OnValueChanged` | Action\ | 값 변경 (비제네릭) |
+| `OnTypedValueChanged` | Action\, T, T\> | 값 변경 (제네릭, 타입 안전) |
+| `OnStateChanged` | Action\ | 상태 변경 (ReadOnly, ShowLabel 등) |
+
+### IUTKPropertyGroup (그룹 인터페이스)
+
+#### Properties
+
+| 속성 | 타입 | 설명 |
+|------|------|------|
+| `GroupId` | string | 그룹 고유 ID |
+| `GroupName` | string | 그룹 표시 이름 |
+| `IsExpanded` | bool | 접기/펼치기 상태 |
+| `Items` | IReadOnlyList\ | 하위 아이템 목록 |
+| `ItemCount` | int | 아이템 수 |
+
+#### Methods
+
+| 메서드 | 설명 | 파라미터 |
+|--------|------|----------|
+| `AddItem(item)` | 아이템 추가 | IUTKPropertyItem |
+| `RemoveItem(itemId)` | 아이템 제거 | string → bool |
+| `GetItem(itemId)` | 아이템 검색 | string → IUTKPropertyItem? |
+| `Clear()` | 전체 아이템 제거 | - |
+| `SetAllItemsReadOnly(isReadOnly)` | 일괄 ReadOnly 설정 | bool |
+
+#### Events
+
+| 이벤트 | 타입 | 설명 |
+|--------|------|------|
+| `OnItemAdded` | Action\ | 아이템 추가 |
+| `OnItemRemoved` | Action\ | 아이템 제거 |
+| `OnExpandedChanged` | Action\ | 접기/펼치기 변경 |
+
+### UTKPropertyItemViewFactory (팩토리)
+
+#### Public Methods
+
+| 메서드 | 설명 | 반환 |
+|--------|------|------|
+| `CreateView(data)` | View 생성 + 바인딩 후 VisualElement 반환 | VisualElement? |
+| `CreateViewInstance(data)` | View 인스턴스만 생성 (데이터 기반 초기화) | IUTKPropertyItemView? |
+| `CreateViewInstance(type)` | View 인스턴스만 생성 (타입 기반) | IUTKPropertyItemView? |
+| `GetViewType(data)` | 데이터에 맞는 View Type 조회 | Type? |
+| `RegisterCustomView(type, factory)` | 커스텀 View 등록 | void |
+| `UnregisterCustomView(type)` | 커스텀 View 해제 | void |
+| `ClearCustomViews()` | 모든 커스텀 View 해제 | void |
+
+### UTKPropertyItemViewBase (View 베이스)
+
+#### UxmlAttributes
+
+| 속성 | UXML 어트리뷰트 | 타입 | 설명 |
+|------|----------------|------|------|
+| `Label` | `label` | string | 라벨 텍스트 |
+| `IsReadOnly` | `is-read-only` | bool | 읽기 전용 |
+| `IsVisible` | `is-visible` | bool | 표시 여부 |
+| `TooltipText` | `tooltip-text` | string | 툴팁 |
+| `ShowLabel` | `show-label` | bool | 라벨 표시 여부 |
+
+#### Public Methods
+
+| 메서드 | 설명 |
+|--------|------|
+| `Bind(data)` | 데이터 바인딩 (양방향 동기화 시작) |
+| `Unbind()` | 바인딩 해제 (이벤트 구독 해제) |
+| `RefreshUI()` | UI 갱신 |
+| `Dispose()` | 리소스 정리 |
+| `ClearAllCache()` | 정적 UXML/USS 캐시 전체 정리 (static) |
+
+---
+
+## 메모리 관리
+
+### Bind/Unbind 패턴
+
+모든 View는 `Bind()`에서 이벤트를 구독하고 `Unbind()`에서 해제합니다:
+
+```csharp
+// View 내부 구현 패턴
+public void Bind(IUTKPropertyItem data)
+{
+ _boundData = data;
+ data.OnTypedValueChanged += OnDataValueChanged;
+ data.OnStateChanged += OnDataStateChanged;
+ RefreshUI();
+}
+
+public void Unbind()
+{
+ if (_boundData != null)
+ {
+ _boundData.OnTypedValueChanged -= OnDataValueChanged;
+ _boundData.OnStateChanged -= OnDataStateChanged;
+ _boundData = null;
+ }
+}
+```
+
+### IDisposable 구현
+
+모든 Item과 View 클래스가 IDisposable을 구현합니다:
+
+```csharp
+// Item Dispose
+var item = new UTKFloatPropertyItem("speed", "속도", 1.0f);
+// ... 사용 ...
+item.Dispose(); // 모든 이벤트 구독 해제
+
+// Group Dispose
+var group = new UTKPropertyGroup("transform", "Transform");
+group.AddItem(item);
+// ... 사용 ...
+group.Dispose(); // 하위 아이템 전부 Dispose + 이벤트 해제
+
+// View Dispose
+var view = UTKPropertyItemViewFactory.CreateView(item);
+// ... 사용 ...
+if (view is IDisposable disposable)
+ disposable.Dispose(); // Unbind + 테마 구독 해제 + 콜백 해제
+```
+
+### 테마 이벤트 관리
+
+View 베이스 클래스에서 `AttachToPanelEvent`/`DetachFromPanelEvent`로 테마 구독을 관리합니다:
+
+```csharp
+// UTKPropertyItemViewBase 내부
+private void OnAttachToPanel(AttachToPanelEvent evt)
+{
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+}
+
+private void OnDetachFromPanel(DetachFromPanelEvent evt)
+{
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+}
+```
+
+---
+
+## 성능 최적화
+
+### 정적 UXML/USS 캐싱
+
+View 베이스 클래스에서 UXML/USS를 정적 Dictionary로 캐싱하여 `Resources.Load` 호출을 최소화합니다:
+
+```csharp
+// UTKPropertyItemViewBase 내부
+private static readonly Dictionary _uxmlCache = new();
+private static readonly Dictionary _ussCache = new();
+private static StyleSheet? _commonUssCache;
+
+// 최초 1회만 Resources.Load 호출, 이후 캐시에서 반환
+// 결과: 20개 View 타입 × 다수 인스턴스 → 각 타입당 Resources.Load 1회
+```
+
+### Fallback UI 패턴
+
+UXML 로드 실패 시 코드로 UI를 생성합니다:
+
+```csharp
+// UXML 존재 → UXML 기반 UI 생성
+if (CreateUIFromUxml())
+{
+ // _labelElement, _valueContainer 자동 쿼리
+}
+else
+{
+ // UXML 없음 → 코드로 Fallback UI 생성
+ CreateUIFallback();
+}
+```
+
+### ReadOnly 전환 최적화
+
+컨트롤을 생성/파괴하지 않고 DisplayStyle 토글로 전환합니다:
+
+```csharp
+// ❌ 나쁜 예: 매번 생성/파괴
+if (isReadOnly)
+{
+ Remove(_editControl);
+ Add(new UTKInputField(...));
+}
+
+// ✅ 좋은 예: 미리 생성 후 DisplayStyle 전환
+_editControl.style.display = isReadOnly ? DisplayStyle.None : DisplayStyle.Flex;
+_readOnlyField.style.display = isReadOnly ? DisplayStyle.Flex : DisplayStyle.None;
+```
+
+### 성능 요약
+
+| 항목 | 설명 |
+|------|------|
+| **UXML/USS 로드** | 정적 Dictionary 캐싱, 타입당 1회만 로드 |
+| **공통 USS** | `UTKPropertyItemViewCommonUss.uss` 1회 로드 후 전체 View 공유 |
+| **쿼리 캐싱** | `Q()` 결과를 필드에 캐싱 (초기화 시 1회) |
+| **ReadOnly 전환** | DisplayStyle 토글 (생성/파괴 없음) |
+| **이벤트 관리** | Bind/Unbind로 정확한 구독/해제 대칭 |
+| **GC Alloc** | LINQ 미사용, 클로저 캡처 최소화 |
+
+---
+
+## 문제 해결
+
+### View가 표시되지 않는 경우
+
+1. **Factory 사용 확인**
+ ```csharp
+ var view = UTKPropertyItemViewFactory.CreateView(item);
+ if (view == null)
+ {
+ Debug.LogError($"지원하지 않는 PropertyType: {item.PropertyType}");
+ }
+ ```
+
+2. **Bind 호출 확인**
+ - `CreateView()`는 내부적으로 Bind까지 수행
+ - `CreateViewInstance()`는 Bind를 별도로 호출해야 함
+
+3. **UXML/USS 경로 확인**
+ - 리소스 경로: `UIToolkit/Property/Views/{ViewTypeName}`
+ - USS 접미사: `{ViewTypeName}Uss`
+
+### 값이 동기화되지 않는 경우
+
+1. **Bind 상태 확인**: `Unbind()` 후 다시 `Bind()` 필요
+2. **이벤트 확인**: `OnTypedValueChanged` 구독이 정상적으로 등록되었는지 확인
+3. **SetValue notify 파라미터**: `SetValue(value, false)`는 이벤트를 발생시키지 않음
+
+---
+
+## 유지보수 가이드
+
+> **소스 코드를 수정할 때 반드시 CLAUDE.md와 코드 주석(XML 문서)도 함께 업데이트해야 합니다.**
+
+| 변경 사항 | 업데이트 대상 |
+|-----------|--------------|
+| 클래스/메서드 추가·삭제·이름 변경 | CLAUDE API 문서 + 파일 구조 + 해당 클래스 XML 주석 |
+| 생성자 파라미터 변경 | CLAUDE API 문서 + 사용 예제 + XML `` 태그 |
+| 공개 속성(Property) 추가·삭제 | CLAUDE API 테이블 + 지원 타입 목록 + XML `` 태그 |
+| 이벤트 추가·삭제 | CLAUDE 이벤트 테이블 + 메모리 관리 섹션 |
+| 새로운 PropertyType 추가 | CLAUDE 지원 타입 목록 + Factory 문서 + 파일 구조 |
+| UXML/USS 파일 추가·삭제 | CLAUDE 파일 구조 섹션 |
+| 새로운 기능 추가 | CLAUDE 주요 기능 + 사용 예제 섹션 |
+
+---
+
+## 라이선스
+
+이 프로젝트는 UVC 프레임워크의 일부입니다.
+
+---
+
+## 작성자
+
+- **작성일**: 2026-02-19
+- **작성자**: Claude Code Assistant
+- **버전**: 1.0.0
diff --git a/Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md.meta b/Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md.meta
new file mode 100644
index 00000000..1653f399
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/Property/CLAUDE.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: ec07f57efe3a4eb44a2f9af0e4ba1c43
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar.meta
new file mode 100644
index 00000000..b7462f04
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5eff1e00a9c969746bc445a6e3d2c3f8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md b/Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md
new file mode 100644
index 00000000..37ea5344
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md
@@ -0,0 +1,122 @@
+# UTKToolBar
+
+UIToolkit 기반 툴바 컴포넌트입니다. 가로/세로 배치 전환을 지원하며, 4가지 버튼 타입과 구분선을 제공합니다.
+
+## 구조
+
+```
+ToolBar/
+├── Data/ # 데이터 레이어
+│ ├── IUTKToolBarItem.cs # 아이템 인터페이스
+│ ├── UTKToolBarButtonData.cs # 버튼 데이터 추상 클래스
+│ ├── UTKToolBarStandardButtonData.cs
+│ ├── UTKToolBarToggleButtonData.cs
+│ ├── UTKToolBarRadioButtonData.cs
+│ ├── UTKToolBarRadioButtonGroup.cs
+│ ├── UTKToolBarExpandableButtonData.cs
+│ └── UTKToolBarSeparatorData.cs
+├── Items/ # View 아이템
+│ ├── UTKToolBarButtonBase.cs # 버튼 View 추상 클래스
+│ ├── UTKToolBarStandardButton.cs
+│ ├── UTKToolBarToggleButton.cs
+│ ├── UTKToolBarRadioButton.cs
+│ ├── UTKToolBarExpandableButton.cs
+│ └── UTKToolBarSeparator.cs
+├── UTKToolBar.cs # 메인 View
+├── UTKToolBarModel.cs # 데이터 모델 (팩토리)
+└── UTKToolBarEnums.cs # 열거형, 이벤트 인자
+```
+
+## 버튼 타입
+
+| 타입 | 설명 |
+|------|------|
+| **Standard** | 단순 클릭 버튼 |
+| **Toggle** | On/Off 상태 전환 |
+| **Radio** | 그룹 내 상호 배타적 선택 |
+| **Expandable** | 서브 메뉴 확장 (Lazy Loading) |
+| **Separator** | 시각적 구분선 |
+
+## 사용법
+
+### 기본 (View 직접 사용)
+
+```csharp
+// 1. 모델 생성
+var model = new UTKToolBarModel();
+model.AddStandardButton("저장", UTKMaterialIcons.Save);
+model.AddSeparator();
+model.AddToggleButton("그리드", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff);
+model.AddRadioButton("tool", "선택", true, UTKMaterialIcons.NearMe);
+model.AddRadioButton("tool", "이동", false, UTKMaterialIcons.OpenWith);
+
+// 2. View 생성 및 빌드
+var toolbar = new UTKToolBar();
+toolbar.Orientation = UTKToolBarOrientation.Horizontal;
+parent.Add(toolbar);
+toolbar.BuildToolBar(model);
+
+// 3. 이벤트 구독
+toolbar.OnAction += args => Debug.Log($"{args.Text}: {args.ActionType}");
+
+// 4. 배치 방향 전환
+toolbar.SetOrientation(UTKToolBarOrientation.Vertical);
+```
+
+### Controller 사용 (MonoBehaviour)
+
+```csharp
+var controller = gameObject.AddComponent();
+
+var model = new UTKToolBarModel();
+// ... 모델 설정 ...
+
+controller.SetData(model);
+controller.Initialize();
+controller.OnAction += args => Debug.Log($"{args.Text}: {args.ActionType}");
+```
+
+### 확장 버튼 (서브 메뉴)
+
+```csharp
+var shapes = model.AddExpandableButton("도형", UTKMaterialIcons.Category, updateIconOnSelection: true);
+shapes.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "사각형", IconPath = UTKMaterialIcons.CropSquare, UseMaterialIcon = true });
+shapes.SubButtons.Add(new UTKToolBarStandardButtonData { Text = "원형", IconPath = UTKMaterialIcons.Circle, UseMaterialIcon = true });
+```
+
+## API
+
+### UTKToolBarModel
+
+| 메서드 | 설명 |
+|--------|------|
+| `AddStandardButton()` | 일반 버튼 추가 |
+| `AddToggleButton()` | 토글 버튼 추가 |
+| `AddRadioButton()` | 라디오 버튼 추가 (자동 그룹 등록) |
+| `AddExpandableButton()` | 확장 버튼 추가 |
+| `AddSeparator()` | 구분선 추가 |
+| `SetRadioButtonSelectionByText()` | 텍스트로 라디오 선택 |
+| `SetToggleButtonStateByText()` | 텍스트로 토글 상태 변경 |
+| `GetToggleButtonState()` | 토글 상태 조회 |
+
+### UTKToolBar (View)
+
+| 멤버 | 설명 |
+|------|------|
+| `Orientation` | 배치 방향 (Horizontal/Vertical) |
+| `BuildToolBar(model)` | 모델로 툴바 빌드 |
+| `ClearToolBar()` | 모든 아이템 제거 |
+| `SetOrientation()` | 배치 방향 변경 |
+| `OnAction` | 버튼 액션 이벤트 |
+
+### 리소스 경로
+
+```
+Resources/UIToolkit/ToolBar/
+├── UTKToolBar.uxml / UTKToolBarUss.uss
+├── UTKToolBarButton.uxml / UTKToolBarButtonUss.uss
+├── UTKToolBarToggleButton.uxml / UTKToolBarToggleButtonUss.uss
+├── UTKToolBarExpandableButton.uxml / UTKToolBarExpandableButtonUss.uss
+├── UTKToolBarSeparator.uxml / UTKToolBarSeparatorUss.uss
+└── UTKToolBarSubMenu.uxml / UTKToolBarSubMenuUss.uss
+```
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md.meta
new file mode 100644
index 00000000..cd3a7e0d
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/CLAUDE.md.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b3ef3a53fad49ce44b17e6bc0364dee7
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data.meta
new file mode 100644
index 00000000..49bb1e1b
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c31476da5ce38e74b98dc0915ac84dca
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs
new file mode 100644
index 00000000..673b5d74
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs
@@ -0,0 +1,13 @@
+#nullable enable
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 툴바에 추가될 수 있는 모든 항목(버튼, 구분선 등)의 기본 인터페이스입니다.
+ ///
+ public interface IUTKToolBarItem
+ {
+ /// 아이템 고유 식별자
+ string ItemId { get; }
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs.meta
new file mode 100644
index 00000000..dc9a08ab
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: aba55b5f2a940314f9c7057358cbbaab
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarButtonData.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarButtonData.cs
new file mode 100644
index 00000000..49850f72
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarButtonData.cs
@@ -0,0 +1,198 @@
+#nullable enable
+
+using System;
+using UVC.UI.Commands;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 모든 툴바 버튼의 공통 데이터를 정의하는 추상 클래스입니다.
+ /// Text, Icon, Enabled, Tooltip, Command 등의 공통 속성과 상태 변경 이벤트를 제공합니다.
+ ///
+ public abstract class UTKToolBarButtonData : IUTKToolBarItem, IDisposable
+ {
+ #region Fields
+
+ private string _text = "";
+ private string? _iconPath;
+ private bool _isEnabled = true;
+ private bool _disposed;
+
+ #endregion
+
+ #region Properties
+
+ /// 아이템 고유 식별자
+ public string ItemId { get; private set; }
+
+ ///
+ /// 버튼 텍스트 (다국어 키). 변경 시 OnStateChanged 이벤트 발생.
+ ///
+ public string Text
+ {
+ get => _text;
+ set
+ {
+ if (_text != value)
+ {
+ _text = value;
+ OnStateChanged?.Invoke();
+ }
+ }
+ }
+
+ ///
+ /// 아이콘 경로. Material Icon 유니코드 또는 Resources 경로.
+ /// 변경 시 OnStateChanged 이벤트 발생.
+ ///
+ public string? IconPath
+ {
+ get => _iconPath;
+ set
+ {
+ if (_iconPath != value)
+ {
+ _iconPath = value;
+ OnStateChanged?.Invoke();
+ }
+ }
+ }
+
+ /// Material Icon 사용 여부 (true: 폰트 아이콘, false: 이미지)
+ public bool UseMaterialIcon { get; set; } = true;
+
+ ///
+ /// 활성화 상태. 변경 시 OnStateChanged 이벤트 발생.
+ ///
+ public bool IsEnabled
+ {
+ get => _isEnabled;
+ set
+ {
+ if (_isEnabled != value)
+ {
+ _isEnabled = value;
+ OnStateChanged?.Invoke();
+ }
+ }
+ }
+
+ /// 툴팁 텍스트 (다국어 키)
+ public string? Tooltip { get; set; }
+
+ /// 실행할 명령
+ public ICommand? ClickCommand { get; set; }
+
+ #endregion
+
+ #region Events
+
+ /// Text, Icon, Enabled 등 시각적 상태 변경 시 발생
+ public event Action? OnStateChanged;
+
+ /// 버튼 클릭 시 발생
+ public event Action? OnClicked;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarButtonData의 새 인스턴스를 초기화합니다.
+ ///
+ /// 아이템 고유 식별자. null이면 GUID 자동 생성.
+ protected UTKToolBarButtonData(string? itemId = null)
+ {
+ ItemId = itemId ?? Guid.NewGuid().ToString();
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// 클릭 실행. Command 실행 + OnClicked 이벤트 발생.
+ /// IUndoableCommand인 경우 UndoRedoManager를 통해 실행합니다.
+ ///
+ /// ClickCommand에 전달할 파라미터
+ public virtual void ExecuteClick(object? parameter = null)
+ {
+ if (!IsEnabled) return;
+
+ if (ClickCommand != null)
+ {
+ if (ClickCommand is IUndoableCommand undoableCommand)
+ {
+ var undoRedoManager = UnityEngine.Object.FindAnyObjectByType();
+ if (undoRedoManager != null)
+ {
+ undoRedoManager.ExecuteCommand(undoableCommand, parameter);
+ }
+ else
+ {
+ ClickCommand.Execute(parameter);
+ }
+ }
+ else
+ {
+ ClickCommand.Execute(parameter);
+ }
+ }
+
+ OnClicked?.Invoke();
+ }
+
+ ///
+ /// OnStateChanged 이벤트를 수동으로 발생시킵니다.
+ /// 여러 속성을 변경 후 한 번에 UI 업데이트를 트리거할 때 사용합니다.
+ ///
+ public void NotifyStateChanged()
+ {
+ OnStateChanged?.Invoke();
+ }
+
+ ///
+ /// 모든 이벤트 핸들러를 해제합니다.
+ ///
+ public virtual void ClearEventHandlers()
+ {
+ OnStateChanged = null;
+ OnClicked = null;
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ /// 리소스를 정리합니다. Command가 IDisposable이면 함께 정리합니다.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// 리소스 정리 구현.
+ ///
+ /// 관리 리소스 정리 여부
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ if (disposing)
+ {
+ ClearEventHandlers();
+ if (ClickCommand is IDisposable disposableCommand)
+ {
+ disposableCommand.Dispose();
+ }
+ ClickCommand = null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarButtonData.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarButtonData.cs.meta
new file mode 100644
index 00000000..791f9ccc
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarButtonData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cacd491a50884cc44a93efc10241adc0
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarExpandableButtonData.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarExpandableButtonData.cs
new file mode 100644
index 00000000..2cb24a95
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarExpandableButtonData.cs
@@ -0,0 +1,144 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 서브 버튼 목록을 가진 확장 가능한 버튼 데이터입니다.
+ /// 클릭 시 서브 메뉴를 표시하고, 서브 버튼 선택 시 메인 아이콘을 업데이트할 수 있습니다.
+ ///
+ public class UTKToolBarExpandableButtonData : UTKToolBarButtonData
+ {
+ #region Fields
+
+ private UTKToolBarButtonData? _selectedSubButton;
+ private string _originalText = "";
+
+ #endregion
+
+ #region Properties
+
+ /// 서브 버튼 목록
+ public List SubButtons { get; private set; } = new();
+
+ /// 서브 버튼 선택 시 메인 아이콘 업데이트 여부
+ public bool UpdateIconOnSelection { get; set; }
+
+ /// 현재 선택된 서브 버튼
+ public UTKToolBarButtonData? SelectedSubButton => _selectedSubButton;
+
+ /// 원본 텍스트 (서브 버튼 선택 시 변경 전 저장용)
+ public string OriginalText => _originalText;
+
+ /// 서브 버튼 선택 콜백
+ public Action? OnSubButtonSelected { get; set; }
+
+ #endregion
+
+ #region Events
+
+ /// 서브 버튼 선택 변경 이벤트 (mainText, selectedSubText)
+ public event Action? OnSubButtonSelectionChanged;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarExpandableButtonData의 새 인스턴스를 초기화합니다.
+ ///
+ /// 아이템 고유 식별자. null이면 GUID 자동 생성.
+ public UTKToolBarExpandableButtonData(string? itemId = null) : base(itemId)
+ {
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// 원본 텍스트를 설정합니다. AddExpandableButton에서 호출됩니다.
+ ///
+ /// 원본 텍스트
+ public void SetOriginalText(string text)
+ {
+ _originalText = text;
+ }
+
+ ///
+ /// 서브 버튼을 선택합니다. UpdateIconOnSelection이 true이면 메인 아이콘도 업데이트합니다.
+ ///
+ /// 선택할 서브 버튼
+ public void SelectSubButton(UTKToolBarButtonData selectedSubButton)
+ {
+ if (selectedSubButton == null || !selectedSubButton.IsEnabled) return;
+
+ // 동일 버튼 재선택 시 무시
+ if (_selectedSubButton == selectedSubButton) return;
+
+ _selectedSubButton = selectedSubButton;
+
+ if (UpdateIconOnSelection)
+ {
+ if (Text != selectedSubButton.Text)
+ {
+ Text = selectedSubButton.Text;
+ }
+ if (IconPath != selectedSubButton.IconPath)
+ {
+ IconPath = selectedSubButton.IconPath;
+ }
+ }
+
+ OnSubButtonSelected?.Invoke(selectedSubButton);
+ OnSubButtonSelectionChanged?.Invoke(_originalText, selectedSubButton.Text);
+ }
+
+ ///
+ /// 클릭 실행. 기본 Command를 실행합니다.
+ /// 서브 메뉴 표시/숨김은 View에서 처리합니다.
+ ///
+ /// ClickCommand에 전달할 파라미터
+ public override void ExecuteClick(object? parameter = null)
+ {
+ if (!IsEnabled) return;
+ base.ExecuteClick(parameter);
+ }
+
+ ///
+ /// 모든 이벤트 핸들러를 해제합니다. 서브 버튼의 핸들러도 정리합니다.
+ ///
+ public override void ClearEventHandlers()
+ {
+ base.ClearEventHandlers();
+ OnSubButtonSelected = null;
+ OnSubButtonSelectionChanged = null;
+
+ foreach (var subButton in SubButtons)
+ {
+ subButton.ClearEventHandlers();
+ }
+ }
+
+ ///
+ /// 리소스 정리. 서브 버튼도 재귀적으로 정리합니다.
+ ///
+ /// 관리 리소스 정리 여부
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ foreach (var subButton in SubButtons)
+ {
+ subButton.Dispose();
+ }
+ SubButtons.Clear();
+ }
+ base.Dispose(disposing);
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarExpandableButtonData.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarExpandableButtonData.cs.meta
new file mode 100644
index 00000000..a967cd07
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarExpandableButtonData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: aa64cd1b0827c724b965c2fba2d37ce1
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonData.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonData.cs
new file mode 100644
index 00000000..ed1bec7c
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonData.cs
@@ -0,0 +1,79 @@
+#nullable enable
+
+using System;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 그룹 내 상호 배타적 선택을 지원하는 라디오 버튼 데이터입니다.
+ /// 동일한 GroupName을 가진 라디오 버튼들 중 하나만 선택됩니다.
+ ///
+ public class UTKToolBarRadioButtonData : UTKToolBarToggleButtonData
+ {
+ #region Properties
+
+ /// 소속 라디오 그룹 이름
+ public string GroupName { get; private set; }
+
+ /// 라디오 그룹 참조 (모델에서 설정)
+ internal UTKToolBarRadioButtonGroup? RadioGroup { get; set; }
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarRadioButtonData의 새 인스턴스를 초기화합니다.
+ ///
+ /// 소속 라디오 그룹 이름. null이거나 비어있을 수 없습니다.
+ /// 아이템 고유 식별자. null이면 GUID 자동 생성.
+ /// groupName이 null이거나 빈 문자열일 경우
+ public UTKToolBarRadioButtonData(string groupName, string? itemId = null) : base(itemId)
+ {
+ if (string.IsNullOrEmpty(groupName))
+ {
+ throw new ArgumentNullException(nameof(groupName), "라디오 버튼은 반드시 GroupName을 가져야 합니다.");
+ }
+ GroupName = groupName;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// 클릭 시 그룹 내 다른 버튼은 해제하고 이 버튼만 선택합니다.
+ /// 선택된 상태에서만 ClickCommand가 실행됩니다.
+ ///
+ /// ClickCommand에 전달할 파라미터
+ public override void ExecuteClick(object? parameter = null)
+ {
+ if (!IsEnabled) return;
+
+ if (RadioGroup != null)
+ {
+ RadioGroup.SetSelected(this);
+ }
+ else
+ {
+ UnityEngine.Debug.LogWarning($"UTKToolBarRadioButtonData '{Text}' (그룹: {GroupName})에 RadioGroup이 할당되지 않았습니다.");
+ }
+
+ // 선택된 상태에서만 Command 실행
+ if (IsSelected && ClickCommand != null)
+ {
+ ClickCommand.Execute(parameter ?? this);
+ }
+ }
+
+ ///
+ /// 모든 이벤트 핸들러를 해제합니다.
+ ///
+ public override void ClearEventHandlers()
+ {
+ base.ClearEventHandlers();
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonData.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonData.cs.meta
new file mode 100644
index 00000000..91ab7b48
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 18507fdc9b7c65649b9f5604b45532d8
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonGroup.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonGroup.cs
new file mode 100644
index 00000000..98b518cb
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonGroup.cs
@@ -0,0 +1,165 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 라디오 버튼 그룹을 관리합니다. 하나의 버튼만 선택 상태를 유지합니다.
+ ///
+ public class UTKToolBarRadioButtonGroup : IDisposable
+ {
+ #region Fields
+
+ private readonly List _buttons = new();
+ private bool _disposed;
+
+ #endregion
+
+ #region Properties
+
+ /// 그룹 이름
+ public string GroupName { get; private set; }
+
+ /// 현재 선택된 버튼
+ public UTKToolBarRadioButtonData? SelectedButton { get; private set; }
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarRadioButtonGroup의 새 인스턴스를 초기화합니다.
+ ///
+ /// 그룹 이름
+ public UTKToolBarRadioButtonGroup(string groupName)
+ {
+ GroupName = groupName;
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// 버튼을 그룹에 등록합니다.
+ ///
+ /// 등록할 라디오 버튼
+ public void RegisterButton(UTKToolBarRadioButtonData button)
+ {
+ if (!_buttons.Contains(button))
+ {
+ _buttons.Add(button);
+ }
+ }
+
+ ///
+ /// 특정 버튼을 선택합니다. 나머지 버튼은 해제됩니다.
+ ///
+ /// 선택할 버튼
+ /// 이벤트 발생 여부
+ public void SetSelected(UTKToolBarRadioButtonData? buttonToSelect, bool raiseEvent = true)
+ {
+ if (buttonToSelect != null && !_buttons.Contains(buttonToSelect))
+ {
+ UnityEngine.Debug.LogWarning($"SetSelected: 버튼 '{buttonToSelect.Text}'은 그룹 '{GroupName}'에 등록되어 있지 않습니다.");
+ return;
+ }
+
+ // 이미 선택된 버튼을 다시 클릭한 경우 무시
+ if (SelectedButton == buttonToSelect && buttonToSelect != null && buttonToSelect.IsSelected)
+ {
+ return;
+ }
+
+ SelectedButton = buttonToSelect;
+ foreach (var button in _buttons)
+ {
+ bool shouldBeSelected = (button == buttonToSelect);
+ button.SetSelected(shouldBeSelected, raiseEvent);
+ }
+ }
+
+ ///
+ /// 모든 선택을 해제합니다.
+ ///
+ /// 이벤트 발생 여부
+ public void ClearSelection(bool raiseEvent = true)
+ {
+ SelectedButton = null;
+ foreach (var button in _buttons)
+ {
+ button.SetSelected(false, raiseEvent);
+ }
+ }
+
+ ///
+ /// 그룹 내 버튼 목록을 반환합니다.
+ ///
+ /// 버튼 목록 (읽기 전용)
+ public IReadOnlyList GetButtons()
+ {
+ return _buttons.AsReadOnly();
+ }
+
+ ///
+ /// 텍스트로 버튼을 검색합니다.
+ ///
+ /// 검색할 텍스트
+ /// 일치하는 버튼 또는 null
+ public UTKToolBarRadioButtonData? FindButtonByText(string text)
+ {
+ foreach (var button in _buttons)
+ {
+ if (string.Equals(button.Text, text, StringComparison.Ordinal))
+ {
+ return button;
+ }
+ }
+ return null;
+ }
+
+ ///
+ /// 초기 선택 상태를 적용합니다.
+ /// IsSelected가 true인 버튼이 있으면 해당 버튼을 선택합니다.
+ ///
+ internal void InitializeSelection()
+ {
+ if (_buttons.Count == 0) return;
+
+ UTKToolBarRadioButtonData? initialButton = null;
+ foreach (var button in _buttons)
+ {
+ if (button.IsSelected)
+ {
+ initialButton = button;
+ break;
+ }
+ }
+
+ if (initialButton != null)
+ {
+ SetSelected(initialButton);
+ }
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ /// 리소스를 정리합니다.
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ _buttons.Clear();
+ SelectedButton = null;
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonGroup.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonGroup.cs.meta
new file mode 100644
index 00000000..41ecb13c
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonGroup.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d601a9d6935c50a40b474d4587c15deb
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarSeparatorData.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarSeparatorData.cs
new file mode 100644
index 00000000..99b58305
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarSeparatorData.cs
@@ -0,0 +1,24 @@
+#nullable enable
+
+using System;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 툴바 내 시각적 구분선 데이터입니다.
+ ///
+ public class UTKToolBarSeparatorData : IUTKToolBarItem
+ {
+ /// 아이템 고유 식별자
+ public string ItemId { get; private set; }
+
+ ///
+ /// UTKToolBarSeparatorData의 새 인스턴스를 초기화합니다.
+ ///
+ /// 아이템 고유 식별자. null이면 GUID 자동 생성.
+ public UTKToolBarSeparatorData(string? itemId = null)
+ {
+ ItemId = itemId ?? Guid.NewGuid().ToString();
+ }
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarSeparatorData.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarSeparatorData.cs.meta
new file mode 100644
index 00000000..cb5b51b6
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarSeparatorData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 96caf0e0ca9659b4eb7f8c1b56139813
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarStandardButtonData.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarStandardButtonData.cs
new file mode 100644
index 00000000..a844961c
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarStandardButtonData.cs
@@ -0,0 +1,18 @@
+#nullable enable
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 단순 클릭 동작의 일반 버튼 데이터입니다.
+ ///
+ public class UTKToolBarStandardButtonData : UTKToolBarButtonData
+ {
+ ///
+ /// UTKToolBarStandardButtonData의 새 인스턴스를 초기화합니다.
+ ///
+ /// 아이템 고유 식별자. null이면 GUID 자동 생성.
+ public UTKToolBarStandardButtonData(string? itemId = null) : base(itemId)
+ {
+ }
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarStandardButtonData.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarStandardButtonData.cs.meta
new file mode 100644
index 00000000..97317262
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarStandardButtonData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: baa2093a77f0b6c42bda1eab25c03ce2
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarToggleButtonData.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarToggleButtonData.cs
new file mode 100644
index 00000000..83ce5611
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarToggleButtonData.cs
@@ -0,0 +1,135 @@
+#nullable enable
+
+using System;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// On/Off 상태를 가지는 토글 버튼 데이터입니다.
+ /// 클릭 시 IsSelected 상태가 반전되고, OnToggleStateChanged 이벤트가 발생합니다.
+ ///
+ public class UTKToolBarToggleButtonData : UTKToolBarButtonData
+ {
+ #region Fields
+
+ private bool _isSelected;
+ private string? _offIconPath;
+
+ #endregion
+
+ #region Properties
+
+ ///
+ /// 현재 선택(On) 상태. 변경 시 OnToggleStateChanged, OnStateChanged 이벤트 발생.
+ ///
+ public bool IsSelected
+ {
+ get => _isSelected;
+ set
+ {
+ if (_isSelected != value)
+ {
+ _isSelected = value;
+ OnToggle?.Invoke(_isSelected);
+ OnToggleStateChanged?.Invoke(_isSelected);
+ NotifyStateChanged();
+ }
+ }
+ }
+
+ /// Off 상태 아이콘 경로
+ public string? OffIconPath
+ {
+ get => _offIconPath;
+ set
+ {
+ if (_offIconPath != value)
+ {
+ _offIconPath = value;
+ NotifyStateChanged();
+ }
+ }
+ }
+
+ /// 토글 상태 변경 시 콜백
+ public Action? OnToggle { get; set; }
+
+ #endregion
+
+ #region Events
+
+ /// IsSelected 상태 변경 시 발생
+ public event Action? OnToggleStateChanged;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarToggleButtonData의 새 인스턴스를 초기화합니다.
+ ///
+ /// 아이템 고유 식별자. null이면 GUID 자동 생성.
+ public UTKToolBarToggleButtonData(string? itemId = null) : base(itemId)
+ {
+ }
+
+ #endregion
+
+ #region Methods
+
+ ///
+ /// 이벤트 발생 여부를 선택하여 선택 상태를 설정합니다.
+ ///
+ /// 새로운 선택 상태
+ /// true이면 OnToggle 콜백을 호출, false이면 UI 이벤트만 발생
+ public void SetSelected(bool isSelected, bool raiseEvent = true)
+ {
+ if (_isSelected != isSelected)
+ {
+ _isSelected = isSelected;
+ if (raiseEvent)
+ {
+ OnToggle?.Invoke(_isSelected);
+ }
+ OnToggleStateChanged?.Invoke(_isSelected);
+ NotifyStateChanged();
+ }
+ }
+
+ ///
+ /// 클릭 시 상태를 반전시키고 Command를 실행합니다.
+ ///
+ /// 전달된 파라미터. bool이면 직접 상태 설정, 아니면 토글.
+ public override void ExecuteClick(object? parameter = null)
+ {
+ if (!IsEnabled) return;
+
+ if (parameter is bool newState)
+ {
+ IsSelected = newState;
+ }
+ else
+ {
+ IsSelected = !IsSelected;
+ }
+
+ // Command 실행 (현재 IsSelected 상태를 파라미터로 전달)
+ if (ClickCommand != null)
+ {
+ ClickCommand.Execute(IsSelected);
+ }
+ }
+
+ ///
+ /// 모든 이벤트 핸들러를 해제합니다.
+ ///
+ public override void ClearEventHandlers()
+ {
+ base.ClearEventHandlers();
+ OnToggleStateChanged = null;
+ OnToggle = null;
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarToggleButtonData.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarToggleButtonData.cs.meta
new file mode 100644
index 00000000..5e109339
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarToggleButtonData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 368ce741cc208274989e0f6e37be2d87
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items.meta
new file mode 100644
index 00000000..7001414b
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 49f69438f86bb8040ab9173e5233be8b
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarButtonBase.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarButtonBase.cs
new file mode 100644
index 00000000..6d4f75cd
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarButtonBase.cs
@@ -0,0 +1,378 @@
+#nullable enable
+
+using System;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 툴바 버튼 VisualElement의 추상 베이스 클래스입니다.
+ /// 아이콘(Material Icon / Image), 텍스트, 활성화 상태의 공통 UI를 제공합니다.
+ ///
+ public abstract partial class UTKToolBarButtonBase : VisualElement, IDisposable
+ {
+ #region Fields
+
+ /// 아이콘 요소 (Material Icon Label 또는 Image)
+ protected Label? _iconLabel;
+
+ /// 텍스트 라벨
+ protected Label? _textLabel;
+
+ /// 루트 버튼 요소
+ protected VisualElement? _rootButton;
+
+ /// 바인딩된 데이터
+ protected UTKToolBarButtonData? _data;
+
+ /// 클릭 콜백
+ protected EventCallback? _onClickCallback;
+
+ /// 정리 여부
+ protected bool _disposed;
+
+ /// UXML 리소스 경로
+ protected string _uxmlPath = "";
+
+ /// USS 리소스 경로
+ protected string _ussPath = "";
+
+ #endregion
+
+ #region Events
+
+ /// 버튼 클릭 이벤트 (데이터 전달)
+ public event Action? OnButtonClicked;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarButtonBase의 새 인스턴스를 초기화합니다.
+ ///
+ protected UTKToolBarButtonBase()
+ {
+ // 1. 테마 적용
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+
+ // 2. 테마 구독
+ SubscribeToThemeChanges();
+ }
+
+ #endregion
+
+ #region Setup
+
+ ///
+ /// UI를 생성합니다. UXML 로드 또는 코드 Fallback.
+ ///
+ protected virtual void CreateUI()
+ {
+ // USS 로드
+ if (!string.IsNullOrEmpty(_ussPath))
+ {
+ var uss = Resources.Load(_ussPath);
+ if (uss != null)
+ {
+ styleSheets.Add(uss);
+ }
+ }
+
+ // UXML 로드
+ if (!string.IsNullOrEmpty(_uxmlPath))
+ {
+ var asset = Resources.Load(_uxmlPath);
+ if (asset != null)
+ {
+ CreateUIFromUxml(asset);
+ return;
+ }
+ }
+
+ // Fallback
+ CreateUIFallback();
+ }
+
+ ///
+ /// UXML에서 UI를 생성합니다.
+ ///
+ /// UXML 에셋
+ protected virtual void CreateUIFromUxml(VisualTreeAsset asset)
+ {
+ var root = asset.Instantiate();
+ _rootButton = root.Q("button-root");
+ _iconLabel = root.Q("icon");
+ _textLabel = root.Q("label");
+
+ // TemplateContainer가 아이콘 정렬을 방해하지 않도록 설정
+ root.style.flexGrow = 1;
+ root.style.alignItems = Align.Center;
+ root.style.justifyContent = Justify.Center;
+
+ Add(root);
+
+ // 클릭 이벤트 등록
+ if (_rootButton != null)
+ {
+ _onClickCallback = OnClick;
+ _rootButton.RegisterCallback(_onClickCallback);
+ }
+ }
+
+ ///
+ /// 코드 Fallback으로 UI를 생성합니다.
+ ///
+ protected virtual void CreateUIFallback()
+ {
+ _rootButton = new VisualElement();
+ _rootButton.AddToClassList("utk-toolbar-btn");
+
+ _iconLabel = new Label();
+ _iconLabel.AddToClassList("utk-toolbar-btn__icon");
+ _rootButton.Add(_iconLabel);
+
+ _textLabel = new Label();
+ _textLabel.AddToClassList("utk-toolbar-btn__label");
+ _rootButton.Add(_textLabel);
+
+ Add(_rootButton);
+
+ // 클릭 이벤트 등록
+ _onClickCallback = OnClick;
+ _rootButton.RegisterCallback(_onClickCallback);
+ }
+
+ #endregion
+
+ #region Data Binding
+
+ ///
+ /// 데이터를 바인딩합니다. OnStateChanged 이벤트를 구독합니다.
+ ///
+ /// 바인딩할 데이터
+ public virtual void BindData(UTKToolBarButtonData data)
+ {
+ // 기존 바인딩 해제
+ UnbindData();
+
+ _data = data;
+ _data.OnStateChanged += OnDataStateChanged;
+
+ // 초기 UI 업데이트
+ UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
+ UpdateText(_data.Text);
+ UpdateEnabled(_data.IsEnabled);
+ }
+
+ ///
+ /// 데이터 바인딩을 해제합니다.
+ ///
+ public virtual void UnbindData()
+ {
+ if (_data != null)
+ {
+ _data.OnStateChanged -= OnDataStateChanged;
+ _data = null;
+ }
+ }
+
+ /// 바인딩된 데이터 (읽기 전용)
+ public UTKToolBarButtonData? BoundData => _data;
+
+ ///
+ /// 바인딩된 데이터의 활성화 상태를 변경합니다.
+ ///
+ /// 활성화 여부
+ public void SetDataEnabled(bool isEnabled)
+ {
+ if (_data != null)
+ {
+ _data.IsEnabled = isEnabled;
+ }
+ }
+
+ #endregion
+
+ #region UI Update
+
+ ///
+ /// 아이콘을 업데이트합니다.
+ ///
+ /// 아이콘 경로 (Material Icon 유니코드 또는 Resources 경로)
+ /// Material Icon 사용 여부
+ protected void UpdateIcon(string? iconPath, bool useMaterialIcon)
+ {
+ if (_iconLabel == null) return;
+
+ if (string.IsNullOrEmpty(iconPath))
+ {
+ _iconLabel.style.display = DisplayStyle.None;
+ return;
+ }
+
+ _iconLabel.style.display = DisplayStyle.Flex;
+
+ if (useMaterialIcon)
+ {
+ // Material Icon (폰트 기반)
+ _iconLabel.RemoveFromClassList("utk-toolbar-btn__icon--image");
+ _iconLabel.text = iconPath;
+ _iconLabel.style.backgroundImage = StyleKeyword.None;
+ }
+ else
+ {
+ // 이미지 아이콘
+ _iconLabel.AddToClassList("utk-toolbar-btn__icon--image");
+ _iconLabel.text = "";
+ var sprite = Resources.Load(iconPath);
+ if (sprite != null)
+ {
+ _iconLabel.style.backgroundImage = new StyleBackground(sprite);
+ }
+ else
+ {
+ var texture = Resources.Load(iconPath);
+ if (texture != null)
+ {
+ _iconLabel.style.backgroundImage = new StyleBackground(texture);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 텍스트를 업데이트합니다.
+ ///
+ /// 버튼 텍스트
+ protected void UpdateText(string text)
+ {
+ if (_textLabel != null)
+ {
+ _textLabel.text = text;
+ }
+ }
+
+ ///
+ /// 활성화 상태를 업데이트합니다.
+ ///
+ /// 활성화 여부
+ protected void UpdateEnabled(bool isEnabled)
+ {
+ if (_rootButton != null)
+ {
+ if (isEnabled)
+ {
+ _rootButton.RemoveFromClassList("utk-toolbar-btn--disabled");
+ }
+ else
+ {
+ _rootButton.AddToClassList("utk-toolbar-btn--disabled");
+ }
+ }
+ }
+
+ ///
+ /// 모델 상태 변경 핸들러. 바인딩된 데이터의 시각 상태를 UI에 반영합니다.
+ ///
+ protected virtual void OnDataStateChanged()
+ {
+ if (_data == null) return;
+ UpdateIcon(_data.IconPath, _data.UseMaterialIcon);
+ UpdateText(_data.Text);
+ UpdateEnabled(_data.IsEnabled);
+ }
+
+ #endregion
+
+ #region Click Handler
+
+ ///
+ /// 클릭 이벤트 핸들러.
+ ///
+ /// 클릭 이벤트
+ protected virtual void OnClick(ClickEvent evt)
+ {
+ if (_data == null || !_data.IsEnabled) return;
+ _data.ExecuteClick();
+ RaiseOnButtonClicked(_data);
+ }
+
+ ///
+ /// OnButtonClicked 이벤트를 발생시킵니다. 파생 클래스에서 사용합니다.
+ ///
+ /// 버튼 데이터
+ protected void RaiseOnButtonClicked(UTKToolBarButtonData data)
+ {
+ OnButtonClicked?.Invoke(data);
+ }
+
+ #endregion
+
+ #region Theme
+
+ ///
+ /// 테마 변경 이벤트를 구독합니다.
+ ///
+ private void SubscribeToThemeChanges()
+ {
+ UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
+ RegisterCallback(OnAttachToPanelForTheme);
+ RegisterCallback(OnDetachFromPanelForTheme);
+ }
+
+ private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
+ {
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+ }
+
+ private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
+ {
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ }
+
+ private void OnThemeChanged(UTKTheme theme)
+ {
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ /// 리소스를 정리합니다.
+ ///
+ public virtual void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ // 데이터 바인딩 해제
+ UnbindData();
+
+ // 테마 구독 해제
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ UnregisterCallback(OnAttachToPanelForTheme);
+ UnregisterCallback(OnDetachFromPanelForTheme);
+
+ // 클릭 이벤트 해제
+ if (_rootButton != null && _onClickCallback != null)
+ {
+ _rootButton.UnregisterCallback(_onClickCallback);
+ }
+
+ // 참조 정리
+ OnButtonClicked = null;
+ _rootButton = null;
+ _iconLabel = null;
+ _textLabel = null;
+ _onClickCallback = null;
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarButtonBase.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarButtonBase.cs.meta
new file mode 100644
index 00000000..ed933aa7
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarButtonBase.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9b48321f7efc72c43a616200f604416b
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs
new file mode 100644
index 00000000..989b925f
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs
@@ -0,0 +1,360 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 클릭 시 서브 메뉴를 펼치는 확장 버튼 컴포넌트입니다.
+ /// 서브 버튼 목록을 지연 생성(Lazy Loading)합니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarExpandableButton : UTKToolBarButtonBase
+ {
+ #region Fields
+
+ private VisualElement? _arrowIcon;
+ private VisualElement? _subMenuContainer;
+ private List? _subMenuItems;
+ private VisualTreeAsset? _cachedSubMenuAsset;
+ private StyleSheet? _cachedSubMenuUss;
+ private bool _subMenuCreated;
+
+ #endregion
+
+ #region Properties
+
+ /// 서브 메뉴 열림 상태
+ public bool IsSubMenuOpen { get; private set; }
+
+ /// 현재 툴바 배치 방향 (서브 메뉴 위치 계산용)
+ public UTKToolBarOrientation CurrentOrientation { get; set; } = UTKToolBarOrientation.Horizontal;
+
+ #endregion
+
+ #region Events
+
+ /// 서브 메뉴 열림/닫힘 이벤트
+ public event Action? OnSubMenuToggled;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarExpandableButton의 새 인스턴스를 초기화합니다.
+ ///
+ public UTKToolBarExpandableButton() : base()
+ {
+ _uxmlPath = "UIToolkit/ToolBar/UTKToolBarExpandableButton";
+ _ussPath = "UIToolkit/ToolBar/UTKToolBarExpandableButtonUss";
+
+ // 버튼 기본 USS도 로드
+ var buttonUss = Resources.Load("UIToolkit/ToolBar/UTKToolBarButtonUss");
+ if (buttonUss != null)
+ {
+ styleSheets.Add(buttonUss);
+ }
+
+ CreateUI();
+ }
+
+ #endregion
+
+ #region Setup
+
+ ///
+ /// UXML에서 UI 생성 후 화살표 아이콘 참조를 가져옵니다.
+ ///
+ /// UXML 에셋
+ protected override void CreateUIFromUxml(VisualTreeAsset asset)
+ {
+ base.CreateUIFromUxml(asset);
+ _arrowIcon = this.Q("arrow");
+ }
+
+ ///
+ /// 코드 Fallback으로 UI 생성 시 화살표 아이콘을 추가합니다.
+ ///
+ protected override void CreateUIFallback()
+ {
+ base.CreateUIFallback();
+
+ _arrowIcon = new VisualElement();
+ _arrowIcon.AddToClassList("utk-toolbar-expandable__arrow");
+ _rootButton?.Add(_arrowIcon);
+
+ _rootButton?.AddToClassList("utk-toolbar-expandable");
+ }
+
+ #endregion
+
+ #region Click Handler
+
+ ///
+ /// 클릭 시 서브 메뉴를 토글합니다.
+ /// 서브 메뉴 영역 내 클릭은 무시합니다 (버블링 방지).
+ ///
+ /// 클릭 이벤트
+ protected override void OnClick(ClickEvent evt)
+ {
+ if (_data == null || !_data.IsEnabled) return;
+
+ // 서브 메뉴 내부 클릭이면 무시 (서브 버튼이 자체 처리)
+ if (_subMenuContainer != null && evt.target is VisualElement target)
+ {
+ var ancestor = target;
+ while (ancestor != null)
+ {
+ if (ancestor == _subMenuContainer) return;
+ ancestor = ancestor.parent;
+ }
+ }
+
+ ToggleSubMenu();
+ RaiseOnButtonClicked(_data);
+ }
+
+ #endregion
+
+ #region Sub Menu
+
+ ///
+ /// 서브 메뉴를 토글합니다.
+ ///
+ public void ToggleSubMenu()
+ {
+ if (IsSubMenuOpen)
+ {
+ CloseSubMenu();
+ }
+ else
+ {
+ OpenSubMenu();
+ }
+ }
+
+ ///
+ /// 서브 메뉴를 엽니다. 처음 열 때 지연 생성합니다.
+ /// panel.visualTree에 서브 메뉴를 추가하여 overflow 제약 없이 표시합니다.
+ ///
+ private void OpenSubMenu()
+ {
+ if (_data is not UTKToolBarExpandableButtonData expandableData) return;
+ if (panel == null) return;
+
+ if (!_subMenuCreated)
+ {
+ CreateSubMenu(expandableData);
+ }
+
+ if (_subMenuContainer != null)
+ {
+ // panel.visualTree로 이동 (UTKDropdown 패턴)
+ if (_subMenuContainer.parent != panel.visualTree)
+ {
+ _subMenuContainer.RemoveFromHierarchy();
+ panel.visualTree.Add(_subMenuContainer);
+ UTKThemeManager.Instance.ApplyThemeToElement(_subMenuContainer);
+
+ if (_cachedSubMenuUss != null)
+ {
+ _subMenuContainer.styleSheets.Add(_cachedSubMenuUss);
+ }
+ }
+
+ _subMenuContainer.style.display = DisplayStyle.Flex;
+ PositionSubMenu();
+ }
+
+ IsSubMenuOpen = true;
+ OnSubMenuToggled?.Invoke(true);
+ }
+
+ ///
+ /// 서브 메뉴를 닫습니다.
+ /// 서브 메뉴를 원래 위치(this)로 되돌립니다.
+ ///
+ public void CloseSubMenu()
+ {
+ if (_subMenuContainer != null)
+ {
+ _subMenuContainer.style.display = DisplayStyle.None;
+
+ // panel.visualTree에서 제거하여 원래 위치로 되돌림
+ if (_subMenuContainer.parent != this)
+ {
+ _subMenuContainer.RemoveFromHierarchy();
+ Add(_subMenuContainer);
+ }
+ }
+
+ IsSubMenuOpen = false;
+ OnSubMenuToggled?.Invoke(false);
+ }
+
+ ///
+ /// 서브 메뉴를 생성합니다 (지연 로드).
+ /// 서브 메뉴는 초기에는 this에 추가되며, Open 시 panel.visualTree로 이동합니다.
+ ///
+ /// 확장 버튼 데이터
+ private void CreateSubMenu(UTKToolBarExpandableButtonData expandableData)
+ {
+ _subMenuCreated = true;
+
+ // 서브 메뉴 리소스 캐싱
+ if (_cachedSubMenuAsset == null)
+ {
+ _cachedSubMenuAsset = Resources.Load("UIToolkit/ToolBar/UTKToolBarSubMenu");
+ _cachedSubMenuUss = Resources.Load("UIToolkit/ToolBar/UTKToolBarSubMenuUss");
+ }
+
+ VisualElement? container;
+
+ if (_cachedSubMenuAsset != null)
+ {
+ var subMenuRoot = _cachedSubMenuAsset.Instantiate();
+ _subMenuContainer = subMenuRoot.Q("submenu-root");
+ container = subMenuRoot.Q("submenu-container");
+
+ if (_subMenuContainer != null)
+ {
+ // TemplateContainer에서 분리하여 직접 관리
+ _subMenuContainer.RemoveFromHierarchy();
+ }
+ else
+ {
+ _subMenuContainer = subMenuRoot;
+ }
+ }
+ else
+ {
+ // Fallback: 코드로 서브 메뉴 컨테이너 생성
+ _subMenuContainer = new VisualElement();
+ _subMenuContainer.AddToClassList("utk-toolbar-submenu");
+
+ container = new VisualElement();
+ container.AddToClassList("utk-toolbar-submenu__container");
+ _subMenuContainer.Add(container);
+ }
+
+ // 서브 메뉴를 닫힌 상태로 this에 추가 (Open 시 panel.visualTree로 이동)
+ _subMenuContainer.style.display = DisplayStyle.None;
+ Add(_subMenuContainer);
+
+ // 서브 버튼 생성
+ container ??= _subMenuContainer;
+ _subMenuItems = new List();
+ foreach (var subButtonData in expandableData.SubButtons)
+ {
+ var subButton = new UTKToolBarStandardButton();
+ subButton.BindData(subButtonData);
+ subButton.OnButtonClicked += OnSubButtonClicked;
+ container.Add(subButton);
+ _subMenuItems.Add(subButton);
+ }
+ }
+
+ ///
+ /// 서브 버튼 클릭 핸들러. 서브 메뉴를 닫고 선택을 반영합니다.
+ ///
+ /// 클릭된 서브 버튼 데이터
+ private void OnSubButtonClicked(UTKToolBarButtonData clickedData)
+ {
+ if (_data is UTKToolBarExpandableButtonData expandableData)
+ {
+ expandableData.SelectSubButton(clickedData);
+ }
+ CloseSubMenu();
+ }
+
+ ///
+ /// 지정된 요소가 서브 메뉴 내부에 있는지 확인합니다.
+ /// panel.visualTree에 추가된 서브 메뉴의 외부 클릭 감지에 사용됩니다.
+ ///
+ /// 확인할 요소
+ /// 서브 메뉴 내부이면 true
+ public bool IsInsideSubMenu(VisualElement target)
+ {
+ if (_subMenuContainer == null) return false;
+
+ var ancestor = target;
+ while (ancestor != null)
+ {
+ if (ancestor == _subMenuContainer) return true;
+ ancestor = ancestor.parent;
+ }
+ return false;
+ }
+
+ ///
+ /// 서브 메뉴 위치를 계산합니다 (가로/세로 배치 대응).
+ /// panel.visualTree에 추가된 상태이므로 worldBound 기준으로 절대 위치를 설정합니다.
+ ///
+ private void PositionSubMenu()
+ {
+ if (_subMenuContainer == null || _rootButton == null) return;
+
+ // schedule로 다음 프레임에 위치 계산 (레이아웃 완료 후)
+ schedule.Execute(() =>
+ {
+ if (_rootButton == null || _subMenuContainer == null) return;
+
+ var buttonBounds = _rootButton.worldBound;
+
+ _subMenuContainer.style.position = Position.Absolute;
+
+ if (CurrentOrientation == UTKToolBarOrientation.Horizontal)
+ {
+ // 가로 배치: 버튼 아래로 펼침
+ _subMenuContainer.style.left = buttonBounds.x;
+ _subMenuContainer.style.top = buttonBounds.yMax + 2;
+ }
+ else
+ {
+ // 세로 배치: 버튼 오른쪽으로 펼침
+ _subMenuContainer.style.left = buttonBounds.xMax + 2;
+ _subMenuContainer.style.top = buttonBounds.y;
+ }
+ });
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ /// 리소스를 정리합니다.
+ ///
+ public override void Dispose()
+ {
+ if (_disposed) return;
+
+ // 서브 메뉴 아이템 정리
+ if (_subMenuItems != null)
+ {
+ foreach (var item in _subMenuItems)
+ {
+ item.OnButtonClicked -= OnSubButtonClicked;
+ item.Dispose();
+ }
+ _subMenuItems.Clear();
+ _subMenuItems = null;
+ }
+
+ // panel.visualTree에 남아 있는 서브 메뉴 제거
+ _subMenuContainer?.RemoveFromHierarchy();
+
+ OnSubMenuToggled = null;
+ _subMenuContainer = null;
+ _arrowIcon = null;
+
+ base.Dispose();
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs.meta
new file mode 100644
index 00000000..14e0a175
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 2b9f4074756caae49a1b07fe73fac86d
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarRadioButton.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarRadioButton.cs
new file mode 100644
index 00000000..ad1c094a
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarRadioButton.cs
@@ -0,0 +1,22 @@
+#nullable enable
+
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 라디오 그룹 내에서 상호 배타적으로 선택되는 버튼 컴포넌트입니다.
+ /// UTKToolBarToggleButton을 상속하여 토글 시각 효과를 재사용합니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarRadioButton : UTKToolBarToggleButton
+ {
+ ///
+ /// UTKToolBarRadioButton의 새 인스턴스를 초기화합니다.
+ /// Toggle과 동일한 UXML/USS를 사용합니다.
+ ///
+ public UTKToolBarRadioButton() : base()
+ {
+ }
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarRadioButton.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarRadioButton.cs.meta
new file mode 100644
index 00000000..629be98e
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarRadioButton.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4c7739980bc18db40b0d276d4cb9875b
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarSeparator.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarSeparator.cs
new file mode 100644
index 00000000..d5718d52
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarSeparator.cs
@@ -0,0 +1,117 @@
+#nullable enable
+
+using System;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 툴바 내 시각적 구분선 컴포넌트입니다.
+ /// 가로 배치 시 세로선, 세로 배치 시 가로선으로 표시됩니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarSeparator : VisualElement, IDisposable
+ {
+ #region Fields
+
+ private bool _disposed;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBarSeparator의 새 인스턴스를 초기화합니다.
+ ///
+ public UTKToolBarSeparator()
+ {
+ // 1. 테마 적용
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+
+ // 2. USS 로드
+ var uss = Resources.Load("UIToolkit/ToolBar/UTKToolBarSeparatorUss");
+ if (uss != null)
+ {
+ styleSheets.Add(uss);
+ }
+
+ // 3. UI 생성
+ CreateUI();
+
+ // 4. 테마 구독
+ SubscribeToThemeChanges();
+ }
+
+ #endregion
+
+ #region Setup
+
+ ///
+ /// UI를 생성합니다.
+ ///
+ private void CreateUI()
+ {
+ var asset = Resources.Load("UIToolkit/ToolBar/UTKToolBarSeparator");
+ if (asset != null)
+ {
+ var root = asset.Instantiate();
+ Add(root);
+ }
+ else
+ {
+ // Fallback
+ var separator = new VisualElement();
+ separator.AddToClassList("utk-toolbar-separator");
+ Add(separator);
+ }
+ }
+
+ #endregion
+
+ #region Theme
+
+ private void SubscribeToThemeChanges()
+ {
+ UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
+ RegisterCallback(OnAttachToPanelForTheme);
+ RegisterCallback(OnDetachFromPanelForTheme);
+ }
+
+ private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
+ {
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+ }
+
+ private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
+ {
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ }
+
+ private void OnThemeChanged(UTKTheme theme)
+ {
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ /// 리소스를 정리합니다.
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ UnregisterCallback(OnAttachToPanelForTheme);
+ UnregisterCallback(OnDetachFromPanelForTheme);
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarSeparator.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarSeparator.cs.meta
new file mode 100644
index 00000000..9811716c
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarSeparator.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 7348b7369e146ca43aed516ed25f874e
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarStandardButton.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarStandardButton.cs
new file mode 100644
index 00000000..eb91d7f0
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarStandardButton.cs
@@ -0,0 +1,23 @@
+#nullable enable
+
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 단순 클릭 동작의 툴바 버튼 컴포넌트입니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarStandardButton : UTKToolBarButtonBase
+ {
+ ///
+ /// UTKToolBarStandardButton의 새 인스턴스를 초기화합니다.
+ ///
+ public UTKToolBarStandardButton() : base()
+ {
+ _uxmlPath = "UIToolkit/ToolBar/UTKToolBarButton";
+ _ussPath = "UIToolkit/ToolBar/UTKToolBarButtonUss";
+ CreateUI();
+ }
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarStandardButton.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarStandardButton.cs.meta
new file mode 100644
index 00000000..4e9c45f2
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarStandardButton.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9379d9d45829a8b49a7e4b9d8be18305
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarToggleButton.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarToggleButton.cs
new file mode 100644
index 00000000..2fd7a8d9
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarToggleButton.cs
@@ -0,0 +1,129 @@
+#nullable enable
+
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// On/Off 상태 전환이 가능한 토글 버튼 컴포넌트입니다.
+ /// 선택 상태에 따라 아이콘과 스타일이 변경됩니다.
+ /// 라디오 버튼의 베이스로도 사용됩니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarToggleButton : UTKToolBarButtonBase
+ {
+ #region Constructor
+
+ ///
+ /// UTKToolBarToggleButton의 새 인스턴스를 초기화합니다.
+ ///
+ public UTKToolBarToggleButton() : base()
+ {
+ _uxmlPath = "UIToolkit/ToolBar/UTKToolBarToggleButton";
+ _ussPath = "UIToolkit/ToolBar/UTKToolBarToggleButtonUss";
+
+ // 버튼 기본 USS도 로드
+ var buttonUss = Resources.Load("UIToolkit/ToolBar/UTKToolBarButtonUss");
+ if (buttonUss != null)
+ {
+ styleSheets.Add(buttonUss);
+ }
+
+ CreateUI();
+ }
+
+ #endregion
+
+ #region Data Binding
+
+ ///
+ /// 데이터 바인딩. Toggle 전용 이벤트를 추가로 구독합니다.
+ ///
+ /// 바인딩할 데이터
+ public override void BindData(UTKToolBarButtonData data)
+ {
+ base.BindData(data);
+
+ if (data is UTKToolBarToggleButtonData toggleData)
+ {
+ toggleData.OnToggleStateChanged += OnToggleStateChanged;
+ UpdateToggleVisuals(toggleData.IsSelected);
+ }
+ }
+
+ ///
+ /// 데이터 바인딩 해제. Toggle 전용 이벤트도 해제합니다.
+ ///
+ public override void UnbindData()
+ {
+ if (_data is UTKToolBarToggleButtonData toggleData)
+ {
+ toggleData.OnToggleStateChanged -= OnToggleStateChanged;
+ }
+ base.UnbindData();
+ }
+
+ #endregion
+
+ #region Toggle Visuals
+
+ ///
+ /// 토글 상태에 따른 시각적 업데이트.
+ /// 선택 시 선택 스타일 추가, 아이콘 전환.
+ ///
+ /// 선택 상태
+ protected virtual void UpdateToggleVisuals(bool isSelected)
+ {
+ if (_rootButton == null) return;
+
+ if (isSelected)
+ {
+ _rootButton.AddToClassList("utk-toolbar-toggle--selected");
+ }
+ else
+ {
+ _rootButton.RemoveFromClassList("utk-toolbar-toggle--selected");
+ }
+
+ // 아이콘 전환 (On/Off)
+ if (_data is UTKToolBarToggleButtonData toggleData)
+ {
+ string? iconPath = isSelected ? toggleData.IconPath : toggleData.OffIconPath;
+ // OffIconPath가 없으면 기본 아이콘 사용
+ if (string.IsNullOrEmpty(iconPath))
+ {
+ iconPath = toggleData.IconPath;
+ }
+ UpdateIcon(iconPath, toggleData.UseMaterialIcon);
+ }
+ }
+
+ ///
+ /// 토글 상태 변경 핸들러.
+ ///
+ /// 새로운 선택 상태
+ private void OnToggleStateChanged(bool isSelected)
+ {
+ UpdateToggleVisuals(isSelected);
+ }
+
+ #endregion
+
+ #region Override
+
+ ///
+ /// 모델 상태 변경 시 토글 시각 효과도 업데이트합니다.
+ ///
+ protected override void OnDataStateChanged()
+ {
+ base.OnDataStateChanged();
+ if (_data is UTKToolBarToggleButtonData toggleData)
+ {
+ UpdateToggleVisuals(toggleData.IsSelected);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarToggleButton.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarToggleButton.cs.meta
new file mode 100644
index 00000000..c61bba51
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarToggleButton.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 0d04bf339742d7a4d92b2afebca10e49
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs
new file mode 100644
index 00000000..120de7ff
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs
@@ -0,0 +1,570 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// UIToolkit 기반 툴바 메인 컴포넌트입니다.
+ /// 가로/세로 배치를 지원하며, 4가지 버튼 타입과 구분선을 렌더링합니다.
+ ///
+ /// 주요 기능:
+ ///
+ /// - 가로/세로 배치 전환 (Orientation)
+ /// - Standard, Toggle, Radio, Expandable 버튼 지원
+ /// - 구분선 지원
+ /// - 서브 메뉴 외부 클릭 감지 자동 닫기
+ /// - 테마 변경 지원 (UTKThemeManager 연동)
+ /// - 성능 최적화 (리소스 캐싱, Dictionary 추적)
+ ///
+ ///
+ ///
+ ///
+ /// // 1. 모델 생성
+ /// var model = new UTKToolBarModel();
+ /// model.AddStandardButton("Save", UTKMaterialIcons.Save);
+ /// model.AddSeparator();
+ /// model.AddToggleButton("Grid", false, UTKMaterialIcons.GridOn, UTKMaterialIcons.GridOff);
+ ///
+ /// // 2. View 생성 및 추가
+ /// var toolbar = new UTKToolBar();
+ /// toolbar.Orientation = UTKToolBarOrientation.Horizontal;
+ /// uiDocument.rootVisualElement.Add(toolbar);
+ ///
+ /// // 3. 툴바 빌드
+ /// toolbar.BuildToolBar(model);
+ ///
+ /// // 4. 이벤트 구독
+ /// toolbar.OnAction += args => Debug.Log($"{args.Text}: {args.ActionType}");
+ ///
+ /// // 5. 배치 방향 변경
+ /// toolbar.SetOrientation(UTKToolBarOrientation.Vertical);
+ ///
+ ///
+ [UxmlElement]
+ public partial class UTKToolBar : VisualElement, IDisposable
+ {
+ #region Constants
+
+ private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBar";
+ private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarUss";
+
+ #endregion
+
+ #region Fields
+
+ private UTKToolBarOrientation _orientation = UTKToolBarOrientation.Horizontal;
+ private VisualElement? _itemContainer;
+ private readonly List _itemElements = new();
+ private readonly Dictionary _buttonMap = new();
+ private readonly List _expandableButtons = new();
+ private bool _disposed;
+
+ #endregion
+
+ #region UXML Attributes
+
+ ///
+ /// 툴바 배치 방향. 변경 시 즉시 레이아웃이 전환됩니다.
+ ///
+ [UxmlAttribute("orientation")]
+ public UTKToolBarOrientation Orientation
+ {
+ get => _orientation;
+ set
+ {
+ if (_orientation != value)
+ {
+ _orientation = value;
+ ApplyOrientation();
+ }
+ }
+ }
+
+ /// 아이템 간 간격 (px)
+ [UxmlAttribute("item-spacing")]
+ public float ItemSpacing { get; set; } = 2f;
+
+ /// 툴바 크기 (가로 시 높이, 세로 시 너비)
+ [UxmlAttribute("toolbar-size")]
+ public float ToolBarSize { get; set; } = 40f;
+
+ #endregion
+
+ #region Events
+
+ /// 버튼 액션 이벤트
+ public event Action? OnAction;
+
+ #endregion
+
+ #region Constructor
+
+ ///
+ /// UTKToolBar의 새 인스턴스를 초기화합니다.
+ ///
+ public UTKToolBar()
+ {
+ // 1. 테마 적용
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+
+ // 2. USS 로드
+ var uss = Resources.Load(USS_PATH);
+ if (uss != null)
+ {
+ styleSheets.Add(uss);
+ }
+
+ // 3. UI 생성
+ CreateUI();
+
+ // 4. 배치 방향 적용
+ ApplyOrientation();
+
+ // 5. 테마 구독
+ SubscribeToThemeChanges();
+ }
+
+ #endregion
+
+ #region Setup
+
+ ///
+ /// UXML/USS 로드 및 UI 구성.
+ ///
+ private void CreateUI()
+ {
+ var asset = Resources.Load(UXML_PATH);
+ if (asset != null)
+ {
+ var root = asset.Instantiate();
+ root.style.flexGrow = 1;
+ _itemContainer = root.Q("toolbar-container");
+ Add(root);
+ }
+ else
+ {
+ // Fallback
+ AddToClassList("utk-toolbar");
+ _itemContainer = new VisualElement();
+ _itemContainer.AddToClassList("utk-toolbar__container");
+ Add(_itemContainer);
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// 모델 데이터로 툴바를 생성합니다.
+ ///
+ /// 툴바 데이터 모델
+ public void BuildToolBar(UTKToolBarModel model)
+ {
+ ClearToolBar();
+
+ if (_itemContainer == null || model == null) return;
+
+ foreach (var item in model.Items)
+ {
+ var element = CreateItemElement(item);
+ if (element != null)
+ {
+ _itemContainer.Add(element);
+ _itemElements.Add(element);
+ }
+ }
+
+ // 아이템 간격 적용
+ ApplyItemSpacing();
+ }
+
+ ///
+ /// 툴바의 모든 아이템을 제거합니다.
+ ///
+ public void ClearToolBar()
+ {
+ // 서브 메뉴 닫기
+ CloseAllSubMenus();
+
+ // 버튼 정리
+ foreach (var kvp in _buttonMap)
+ {
+ kvp.Value.OnButtonClicked -= OnItemClicked;
+ kvp.Value.Dispose();
+ }
+ _buttonMap.Clear();
+
+ // 확장 버튼 리스트 정리
+ _expandableButtons.Clear();
+
+ // 구분선 정리
+ foreach (var element in _itemElements)
+ {
+ if (element is UTKToolBarSeparator separator)
+ {
+ separator.Dispose();
+ }
+ }
+ _itemElements.Clear();
+
+ _itemContainer?.Clear();
+ }
+
+ ///
+ /// 특정 버튼의 활성화 상태를 변경합니다.
+ ///
+ /// 아이템 ID
+ /// 활성화 여부
+ public void SetButtonEnabled(string itemId, bool isEnabled)
+ {
+ if (_buttonMap.TryGetValue(itemId, out var button))
+ {
+ button.SetDataEnabled(isEnabled);
+ }
+ }
+
+ ///
+ /// 배치 방향을 변경합니다.
+ ///
+ /// 새 배치 방향
+ public void SetOrientation(UTKToolBarOrientation orientation)
+ {
+ Orientation = orientation;
+ }
+
+ ///
+ /// 특정 버튼 요소를 가져옵니다.
+ ///
+ /// 아이템 ID
+ /// 찾은 버튼 (out)
+ /// 존재 여부
+ public bool TryGetButtonElement(string itemId, out UTKToolBarButtonBase? button)
+ {
+ return _buttonMap.TryGetValue(itemId, out button);
+ }
+
+ #endregion
+
+ #region Private Methods
+
+ ///
+ /// 배치 방향을 적용합니다. CSS 클래스를 전환합니다.
+ ///
+ private void ApplyOrientation()
+ {
+ RemoveFromClassList("utk-toolbar--horizontal");
+ RemoveFromClassList("utk-toolbar--vertical");
+
+ // 루트 요소에도 클래스 적용 (자손 선택자 동작을 위해)
+ var root = this.Q("toolbar-root");
+ root?.RemoveFromClassList("utk-toolbar--horizontal");
+ root?.RemoveFromClassList("utk-toolbar--vertical");
+
+ if (_orientation == UTKToolBarOrientation.Horizontal)
+ {
+ AddToClassList("utk-toolbar--horizontal");
+ root?.AddToClassList("utk-toolbar--horizontal");
+ }
+ else
+ {
+ AddToClassList("utk-toolbar--vertical");
+ root?.AddToClassList("utk-toolbar--vertical");
+ }
+
+ // 확장 버튼의 방향 업데이트
+ foreach (var expandable in _expandableButtons)
+ {
+ expandable.CurrentOrientation = _orientation;
+ }
+
+ // 아이템 간격 재적용 (방향에 따라 margin 축이 변경됨)
+ ApplyItemSpacing();
+
+ // 열린 서브 메뉴 닫기 (위치 재계산 필요)
+ CloseAllSubMenus();
+ }
+
+ ///
+ /// 아이템 간격을 적용합니다. 첫 번째 아이템을 제외하고 방향에 맞는 margin을 설정합니다.
+ ///
+ private void ApplyItemSpacing()
+ {
+ bool isHorizontal = _orientation == UTKToolBarOrientation.Horizontal;
+
+ for (int i = 0; i < _itemElements.Count; i++)
+ {
+ var element = _itemElements[i];
+ if (i == 0)
+ {
+ element.style.marginLeft = 0;
+ element.style.marginTop = 0;
+ }
+ else
+ {
+ if (isHorizontal)
+ {
+ element.style.marginLeft = ItemSpacing;
+ element.style.marginTop = 0;
+ }
+ else
+ {
+ element.style.marginTop = ItemSpacing;
+ element.style.marginLeft = 0;
+ }
+ }
+ }
+ }
+
+ ///
+ /// 모델 아이템으로 개별 버튼/구분선을 생성합니다.
+ ///
+ /// 아이템 데이터
+ /// 생성된 VisualElement
+ private VisualElement? CreateItemElement(IUTKToolBarItem item)
+ {
+ switch (item)
+ {
+ case UTKToolBarRadioButtonData radioData:
+ {
+ var button = new UTKToolBarRadioButton();
+ button.BindData(radioData);
+ button.OnButtonClicked += OnItemClicked;
+ _buttonMap[radioData.ItemId] = button;
+
+ // 라디오 버튼 선택 변경 → OnAction 이벤트
+ radioData.OnToggleStateChanged += (isSelected) =>
+ {
+ if (isSelected)
+ {
+ RaiseOnAction(radioData.GroupName, UTKToolBarActionType.Radio, radioData.Text);
+ }
+ };
+ return button;
+ }
+
+ case UTKToolBarToggleButtonData toggleData:
+ {
+ var button = new UTKToolBarToggleButton();
+ button.BindData(toggleData);
+ button.OnButtonClicked += OnItemClicked;
+ _buttonMap[toggleData.ItemId] = button;
+
+ // 토글 상태 변경 → OnAction 이벤트
+ toggleData.OnToggleStateChanged += (isSelected) =>
+ {
+ RaiseOnAction(toggleData.Text, UTKToolBarActionType.Toggle, isSelected);
+ };
+ return button;
+ }
+
+ case UTKToolBarExpandableButtonData expandableData:
+ {
+ var button = new UTKToolBarExpandableButton();
+ button.CurrentOrientation = _orientation;
+ button.BindData(expandableData);
+ button.OnButtonClicked += OnItemClicked;
+ _buttonMap[expandableData.ItemId] = button;
+ _expandableButtons.Add(button);
+
+ // 서브 버튼 선택 변경 → OnAction 이벤트
+ expandableData.OnSubButtonSelectionChanged += (mainText, subText) =>
+ {
+ RaiseOnAction(mainText, UTKToolBarActionType.Expandable, subText);
+ };
+ return button;
+ }
+
+ case UTKToolBarStandardButtonData standardData:
+ {
+ var button = new UTKToolBarStandardButton();
+ button.BindData(standardData);
+ button.OnButtonClicked += OnItemClicked;
+ _buttonMap[standardData.ItemId] = button;
+
+ // 클릭 → OnAction 이벤트
+ standardData.OnClicked += () =>
+ {
+ RaiseOnAction(standardData.Text, UTKToolBarActionType.Standard, null);
+ };
+ return button;
+ }
+
+ case UTKToolBarSeparatorData:
+ {
+ var separator = new UTKToolBarSeparator();
+ return separator;
+ }
+
+ default:
+ return null;
+ }
+ }
+
+ ///
+ /// 아이템 클릭 핸들러 (공통).
+ ///
+ /// 클릭된 버튼 데이터
+ private void OnItemClicked(UTKToolBarButtonData data)
+ {
+ if (data is UTKToolBarExpandableButtonData)
+ {
+ // 다른 Expandable 버튼의 서브 메뉴 닫기 (클릭한 것만 남김)
+ foreach (var expandable in _expandableButtons)
+ {
+ if (expandable.IsSubMenuOpen && expandable.BoundData != data)
+ {
+ expandable.CloseSubMenu();
+ }
+ }
+ }
+ else
+ {
+ // 일반 버튼 클릭 시, 열린 서브 메뉴 모두 닫기
+ CloseAllSubMenus();
+ }
+ }
+
+ ///
+ /// 모든 열린 서브 메뉴를 닫습니다.
+ ///
+ private void CloseAllSubMenus()
+ {
+ foreach (var expandable in _expandableButtons)
+ {
+ if (expandable.IsSubMenuOpen)
+ {
+ expandable.CloseSubMenu();
+ }
+ }
+ }
+
+ ///
+ /// OnAction 이벤트를 발생시킵니다.
+ ///
+ /// 버튼 텍스트
+ /// 액션 타입
+ /// 액션 값
+ private void RaiseOnAction(string text, UTKToolBarActionType actionType, object? value = null)
+ {
+ OnAction?.Invoke(new UTKToolBarActionEventArgs
+ {
+ Text = text,
+ ActionType = actionType,
+ Value = value
+ });
+ }
+
+ ///
+ /// 외부 클릭 감지 (서브 메뉴 닫기용).
+ /// Panel의 PointerDownEvent를 캡처 단계에서 감지합니다.
+ ///
+ /// 포인터 다운 이벤트
+ private void OnPanelPointerDown(PointerDownEvent evt)
+ {
+ // 열린 서브 메뉴가 없으면 무시
+ bool hasOpenSubMenu = false;
+ foreach (var expandable in _expandableButtons)
+ {
+ if (expandable.IsSubMenuOpen)
+ {
+ hasOpenSubMenu = true;
+ break;
+ }
+ }
+ if (!hasOpenSubMenu) return;
+
+ var target = evt.target as VisualElement;
+ if (target != null)
+ {
+ // 이 UTKToolBar 내부 클릭이면 무시 (개별 버튼이 처리)
+ var ancestor = target;
+ while (ancestor != null)
+ {
+ if (ancestor == this) return;
+ ancestor = ancestor.parent;
+ }
+
+ // panel.visualTree에 추가된 서브 메뉴 내부 클릭이면 무시
+ foreach (var expandable in _expandableButtons)
+ {
+ if (expandable.IsSubMenuOpen && expandable.IsInsideSubMenu(target))
+ {
+ return;
+ }
+ }
+ }
+
+ // 외부 클릭 → 모든 서브 메뉴 닫기
+ CloseAllSubMenus();
+ }
+
+ #endregion
+
+ #region Theme
+
+ private void SubscribeToThemeChanges()
+ {
+ UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
+ RegisterCallback(OnAttachToPanelForTheme);
+ RegisterCallback(OnDetachFromPanelForTheme);
+ }
+
+ private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
+ {
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+
+ // 패널 레벨 외부 클릭 감지 등록
+ if (panel != null)
+ {
+ panel.visualTree.RegisterCallback(OnPanelPointerDown, TrickleDown.TrickleDown);
+ }
+ }
+
+ private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt)
+ {
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+
+ // 패널 레벨 외부 클릭 감지 해제
+ if (evt.originPanel?.visualTree != null)
+ {
+ evt.originPanel.visualTree.UnregisterCallback(OnPanelPointerDown, TrickleDown.TrickleDown);
+ }
+ }
+
+ private void OnThemeChanged(UTKTheme theme)
+ {
+ UTKThemeManager.Instance.ApplyThemeToElement(this);
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ /// 리소스를 정리합니다.
+ ///
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ ClearToolBar();
+
+ // 테마 구독 해제
+ UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
+ UnregisterCallback(OnAttachToPanelForTheme);
+ UnregisterCallback(OnDetachFromPanelForTheme);
+
+ OnAction = null;
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs.meta
new file mode 100644
index 00000000..b990868c
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 92ab9bd5842453e4eb2a6679da362627
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs
new file mode 100644
index 00000000..21a099f4
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs
@@ -0,0 +1,48 @@
+#nullable enable
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// 툴바 배치 방향을 정의합니다.
+ ///
+ public enum UTKToolBarOrientation
+ {
+ /// 가로 배치 (좌→우)
+ Horizontal,
+ /// 세로 배치 (위→아래)
+ Vertical
+ }
+
+ ///
+ /// 툴바 버튼 액션 타입을 정의합니다.
+ ///
+ public enum UTKToolBarActionType
+ {
+ /// 일반 클릭
+ Standard,
+ /// 라디오 그룹 선택
+ Radio,
+ /// 토글 상태 변경
+ Toggle,
+ /// 확장 버튼 서브 선택
+ Expandable
+ }
+
+ ///
+ /// 툴바 버튼 액션 이벤트 인자입니다.
+ ///
+ public class UTKToolBarActionEventArgs
+ {
+ /// 버튼 텍스트 (또는 라디오 그룹 이름)
+ public string Text { get; set; } = "";
+
+ ///
+ /// 컨텍스트별 값.
+ /// Standard: null, Toggle: bool, Radio: 선택 텍스트, Expandable: 서브 텍스트
+ ///
+ public object? Value { get; set; }
+
+ /// 액션 타입
+ public UTKToolBarActionType ActionType { get; set; }
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs.meta
new file mode 100644
index 00000000..53704df1
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarEnums.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 056960f2aef4c834898d8fe6abe6dc5a
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs
new file mode 100644
index 00000000..4cc746e4
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs
@@ -0,0 +1,382 @@
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using UVC.UI.Commands;
+
+namespace UVC.UIToolkit
+{
+ ///
+ /// UTKToolBar의 데이터 모델입니다.
+ /// 아이템 컬렉션을 관리하고, 라디오 그룹 등록을 자동화합니다.
+ /// 기존 ToolbarModel과 동일한 팩토리 API를 제공합니다.
+ ///
+ public class UTKToolBarModel : IDisposable
+ {
+ #region Fields
+
+ private readonly Dictionary _radioGroups = new();
+ private bool _disposed;
+
+ #endregion
+
+ #region Properties
+
+ /// 전체 아이템 목록 (순서 유지)
+ public List Items { get; private set; } = new();
+
+ #endregion
+
+ #region Add Methods
+
+ ///
+ /// 아이템을 모델에 추가합니다.
+ /// ToolbarRadioButton이면 자동으로 그룹에 등록합니다.
+ ///
+ /// 추가할 아이템
+ public void AddItem(IUTKToolBarItem item)
+ {
+ Items.Add(item);
+
+ if (item is UTKToolBarRadioButtonData radioButton)
+ {
+ if (!_radioGroups.TryGetValue(radioButton.GroupName, out var group))
+ {
+ group = new UTKToolBarRadioButtonGroup(radioButton.GroupName);
+ _radioGroups.Add(radioButton.GroupName, group);
+ }
+ group.RegisterButton(radioButton);
+ radioButton.RadioGroup = group;
+ }
+ }
+
+ ///
+ /// 일반 버튼을 추가합니다.
+ ///
+ /// 버튼 텍스트 (다국어 키)
+ /// 아이콘 경로
+ /// 실행할 명령
+ /// 툴팁 (다국어 키)
+ /// Material Icon 사용 여부 (기본값: true)
+ /// 생성된 버튼 데이터
+ public UTKToolBarStandardButtonData AddStandardButton(
+ string text,
+ string? iconPath = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool useMaterialIcon = true)
+ {
+ var button = new UTKToolBarStandardButtonData
+ {
+ Text = text,
+ IconPath = iconPath,
+ ClickCommand = command,
+ Tooltip = tooltip,
+ UseMaterialIcon = useMaterialIcon
+ };
+ AddItem(button);
+ return button;
+ }
+
+ ///
+ /// 토글 버튼을 추가합니다.
+ ///
+ /// 버튼 텍스트
+ /// 초기 On/Off 상태
+ /// On 상태 아이콘
+ /// Off 상태 아이콘
+ /// 토글 콜백
+ /// 실행할 명령
+ /// 툴팁
+ /// Material Icon 사용 여부
+ /// 생성된 토글 버튼 데이터
+ public UTKToolBarToggleButtonData AddToggleButton(
+ string text,
+ bool initialState = false,
+ string? onIconPath = null,
+ string? offIconPath = null,
+ Action? onToggle = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool useMaterialIcon = true)
+ {
+ var button = new UTKToolBarToggleButtonData
+ {
+ Text = text,
+ IconPath = onIconPath,
+ OffIconPath = offIconPath,
+ OnToggle = onToggle,
+ ClickCommand = command,
+ Tooltip = tooltip,
+ UseMaterialIcon = useMaterialIcon
+ };
+ // IsSelected를 직접 필드에 설정 (이벤트 발생 방지)
+ button.SetSelected(initialState, false);
+ AddItem(button);
+ return button;
+ }
+
+ ///
+ /// 라디오 버튼을 추가합니다.
+ ///
+ /// 라디오 그룹 이름
+ /// 버튼 텍스트
+ /// 초기 선택 상태
+ /// 선택 상태 아이콘
+ /// 비선택 상태 아이콘
+ /// 토글 콜백
+ /// 실행할 명령
+ /// 툴팁
+ /// Material Icon 사용 여부
+ /// 생성된 라디오 버튼 데이터
+ public UTKToolBarRadioButtonData AddRadioButton(
+ string groupName,
+ string text,
+ bool initialState = false,
+ string? onIconPath = null,
+ string? offIconPath = null,
+ Action? onToggle = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool useMaterialIcon = true)
+ {
+ var button = new UTKToolBarRadioButtonData(groupName)
+ {
+ Text = text,
+ IconPath = onIconPath,
+ OffIconPath = offIconPath,
+ OnToggle = onToggle,
+ ClickCommand = command,
+ Tooltip = tooltip,
+ UseMaterialIcon = useMaterialIcon
+ };
+ button.SetSelected(initialState, false);
+ AddItem(button);
+
+ // initialState가 true이면 그룹에서 명시적으로 선택
+ if (initialState && _radioGroups.TryGetValue(groupName, out var group))
+ {
+ group.SetSelected(button, false);
+ }
+ return button;
+ }
+
+ ///
+ /// 확장 버튼을 추가합니다.
+ ///
+ /// 버튼 텍스트
+ /// 아이콘 경로
+ /// 실행할 명령
+ /// 툴팁
+ /// 서브 선택 시 아이콘 업데이트 여부
+ /// Material Icon 사용 여부
+ /// 생성된 확장 버튼 데이터
+ public UTKToolBarExpandableButtonData AddExpandableButton(
+ string text,
+ string? iconPath = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool updateIconOnSelection = false,
+ bool useMaterialIcon = true)
+ {
+ var button = new UTKToolBarExpandableButtonData
+ {
+ Text = text,
+ IconPath = iconPath,
+ ClickCommand = command,
+ Tooltip = tooltip,
+ UpdateIconOnSelection = updateIconOnSelection,
+ UseMaterialIcon = useMaterialIcon
+ };
+ button.SetOriginalText(text);
+ AddItem(button);
+ return button;
+ }
+
+ ///
+ /// 구분선을 추가합니다.
+ ///
+ /// 생성된 구분선 데이터
+ public UTKToolBarSeparatorData AddSeparator()
+ {
+ var separator = new UTKToolBarSeparatorData();
+ AddItem(separator);
+ return separator;
+ }
+
+ #endregion
+
+ #region State Management
+
+ ///
+ /// 라디오 그룹의 특정 버튼을 선택합니다.
+ ///
+ /// 그룹 이름
+ /// 선택할 버튼. null이면 모두 해제.
+ /// 이벤트 발생 여부
+ /// 성공 여부
+ public bool SetRadioButtonSelection(string groupName, UTKToolBarRadioButtonData? buttonToSelect, bool raiseEvent = true)
+ {
+ if (!_radioGroups.TryGetValue(groupName, out var group))
+ {
+ UnityEngine.Debug.LogWarning($"SetRadioButtonSelection: 그룹 '{groupName}'을 찾을 수 없습니다.");
+ return false;
+ }
+
+ if (buttonToSelect == null)
+ {
+ group.ClearSelection(raiseEvent);
+ }
+ else
+ {
+ group.SetSelected(buttonToSelect, raiseEvent);
+ }
+ return true;
+ }
+
+ ///
+ /// 텍스트로 라디오 버튼을 찾아 선택합니다.
+ ///
+ /// 그룹 이름
+ /// 버튼 텍스트. null이면 모두 해제.
+ /// 이벤트 발생 여부
+ /// 선택된 버튼 또는 null
+ public UTKToolBarRadioButtonData? SetRadioButtonSelectionByText(string groupName, string? buttonText, bool raiseEvent = true)
+ {
+ if (!_radioGroups.TryGetValue(groupName, out var group))
+ {
+ UnityEngine.Debug.LogWarning($"SetRadioButtonSelectionByText: 그룹 '{groupName}'을 찾을 수 없습니다.");
+ return null;
+ }
+
+ if (string.IsNullOrEmpty(buttonText))
+ {
+ group.ClearSelection(raiseEvent);
+ return null;
+ }
+
+ var button = group.FindButtonByText(buttonText);
+ if (button == null)
+ {
+ UnityEngine.Debug.LogWarning($"SetRadioButtonSelectionByText: 그룹 '{groupName}'에서 '{buttonText}' 버튼을 찾을 수 없습니다.");
+ return null;
+ }
+
+ group.SetSelected(button, raiseEvent);
+ return button;
+ }
+
+ ///
+ /// 라디오 그룹 선택 해제.
+ ///
+ /// 그룹 이름
+ /// 이벤트 발생 여부
+ /// 성공 여부
+ public bool ClearRadioButtonSelection(string groupName, bool raiseEvent = true)
+ {
+ return SetRadioButtonSelection(groupName, null, raiseEvent);
+ }
+
+ ///
+ /// 토글 버튼 상태 변경.
+ ///
+ /// 토글 버튼
+ /// 설정할 상태
+ /// 이벤트 발생 여부
+ public void SetToggleButtonState(UTKToolBarToggleButtonData toggleButton, bool isSelected, bool raiseEvent = true)
+ {
+ toggleButton.SetSelected(isSelected, raiseEvent);
+ }
+
+ ///
+ /// 텍스트로 토글 버튼을 찾아 상태를 변경합니다.
+ ///
+ /// 버튼 텍스트
+ /// 설정할 상태
+ /// 이벤트 발생 여부
+ /// 찾은 토글 버튼 또는 null
+ public UTKToolBarToggleButtonData? SetToggleButtonStateByText(string buttonText, bool isSelected, bool raiseEvent = true)
+ {
+ foreach (var item in Items)
+ {
+ if (item is UTKToolBarToggleButtonData toggleButton && string.Equals(toggleButton.Text, buttonText, StringComparison.Ordinal))
+ {
+ toggleButton.SetSelected(isSelected, raiseEvent);
+ return toggleButton;
+ }
+ }
+ UnityEngine.Debug.LogWarning($"SetToggleButtonStateByText: '{buttonText}' 토글 버튼을 찾을 수 없습니다.");
+ return null;
+ }
+
+ ///
+ /// 텍스트로 토글 버튼 상태를 조회합니다.
+ ///
+ /// 버튼 텍스트
+ /// 선택 상태. 버튼을 찾지 못하면 false.
+ public bool GetToggleButtonState(string buttonText)
+ {
+ foreach (var item in Items)
+ {
+ if (item is UTKToolBarToggleButtonData toggleButton && string.Equals(toggleButton.Text, buttonText, StringComparison.Ordinal))
+ {
+ return toggleButton.IsSelected;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// 라디오 그룹을 가져옵니다.
+ ///
+ /// 그룹 이름
+ /// 라디오 그룹 또는 null
+ public UTKToolBarRadioButtonGroup? GetRadioButtonGroup(string groupName)
+ {
+ return _radioGroups.TryGetValue(groupName, out var group) ? group : null;
+ }
+
+ #endregion
+
+ #region IDisposable
+
+ ///
+ /// 모든 아이템과 라디오 그룹을 정리합니다.
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// 리소스 정리 구현.
+ ///
+ /// 관리 리소스 정리 여부
+ protected virtual void Dispose(bool disposing)
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ if (disposing)
+ {
+ foreach (var item in Items)
+ {
+ if (item is IDisposable disposable)
+ {
+ disposable.Dispose();
+ }
+ }
+ Items.Clear();
+
+ foreach (var group in _radioGroups.Values)
+ {
+ group.Dispose();
+ }
+ _radioGroups.Clear();
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs.meta b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs.meta
new file mode 100644
index 00000000..cc6a1f18
--- /dev/null
+++ b/Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 8c621f3a44b612e47bd1b04140477c88
\ No newline at end of file
diff --git a/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs
index 062e536b..0d1267b3 100644
--- a/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs
+++ b/Assets/Scripts/UVC/UIToolkit/Window/UTKPropertyListWindow.cs
@@ -70,10 +70,10 @@ namespace UVC.UIToolkit
///
/// 사용 예 (UXML):
///
- /// <?xml version="1.0" encoding="utf-8"?>
- /// <UXML xmlns="UnityEngine.UIElements" xmlns:utk="UVC.UIToolkit">
- /// <utk:UTKPropertyListWindow name="property-window" />
- /// </UXML>
+ ///
+ ///
+ ///
+ ///
///
///
[UxmlElement]
diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json
index 9eed9dfd..945fcf6e 100644
--- a/Packages/packages-lock.json
+++ b/Packages/packages-lock.json
@@ -5,9 +5,10 @@
"depth": 0,
"source": "git",
"dependencies": {
- "com.unity.nuget.newtonsoft-json": "3.0.2"
+ "com.unity.nuget.newtonsoft-json": "3.0.2",
+ "com.unity.test-framework": "1.1.31"
},
- "hash": "ad2f6376d4ce744e9e200d84307a479cd7b7db3c"
+ "hash": "12dd9bd516aba822a9206209880a0b650f60ccfa"
},
"com.cysharp.unitask": {
"version": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
diff --git a/작업지시서_ToolBar_UIToolkit_마이그레이션.md b/작업지시서_ToolBar_UIToolkit_마이그레이션.md
new file mode 100644
index 00000000..9163644d
--- /dev/null
+++ b/작업지시서_ToolBar_UIToolkit_마이그레이션.md
@@ -0,0 +1,1662 @@
+# ToolBar UIToolkit 마이그레이션 작업지시서
+
+## 📋 개요
+uGUI 기반의 ToolBar 시스템(`Assets/Scripts/UVC/UI/ToolBar/`)을 UI Toolkit 기반으로 마이그레이션하는 작업입니다.
+기존 ToolbarModel의 데이터 구조와 Toolbar/Toolbox의 public API를 동일하게 유지하면서,
+**가로/세로 배치 전환 기능**을 추가하고, 관심사 분리 원칙을 준수합니다.
+
+---
+
+## 🎯 작업 목표
+1. **독립적인 데이터 구조**: UIToolkit 전용 `UTKToolBarModel`, `UTKToolBarItemData` 별도 구현
+2. **Controller 호환성**: `Toolbar`/`Toolbox`의 public API 동일하게 구현
+3. **UIToolkit 전환**: View를 UIToolkit 기반으로 재구현
+4. **가로/세로 배치**: `Orientation` 속성으로 Horizontal/Vertical 전환 지원
+5. **4가지 버튼 타입**: Standard, Toggle, Radio, Expandable 모두 지원
+6. **관심사 분리**: Model, View, Controller 명확히 분리
+7. **메모리 관리**: IDisposable 구현, 이벤트 정리, 메모리 누수 방지
+8. **성능 최적화**: 쿼리 캐싱, GC 최소화, 불필요한 리빌드 방지
+9. **완전한 문서화**: 모든 public/protected 멤버에 XML 주석 필수
+
+---
+
+## 📁 파일 구조
+
+### 기존 파일 (참조용)
+```
+Assets/Scripts/UVC/UI/ToolBar/
+├── Model/
+│ ├── IToolbarItem.cs # 툴바 아이템 인터페이스
+│ ├── ToolbarButtonBase.cs # 버튼 추상 베이스
+│ ├── ToolbarStandardButton.cs # 일반 버튼
+│ ├── ToolbarToggleButton.cs # 토글 버튼
+│ ├── ToolbarRadioButton.cs # 라디오 버튼
+│ ├── ToolbarRadioButtonGroup.cs # 라디오 그룹 관리
+│ ├── ToolbarExpandableButton.cs # 확장 버튼 (서브메뉴)
+│ ├── ToolbarSeparator.cs # 구분선
+│ └── ToolbarModel.cs # 모델 (아이템 컬렉션)
+├── View/
+│ ├── IButtonViewProcessor.cs # 버튼별 UI 처리 인터페이스
+│ ├── ToolbarStandardButtonViewProcessor.cs
+│ ├── ToolbarToggleButtonViewProcessor.cs
+│ ├── ToolbarRadioButtonViewProcessor.cs
+│ ├── ToolbarExpandableButtonViewProcessor.cs
+│ ├── ToggleGroupManager.cs # 라디오 그룹 UI 관리
+│ ├── SubMenuHandler.cs # 확장 버튼 서브메뉴 관리
+│ └── ToolbarView.cs # 메인 뷰
+├── Toolbar.cs # MonoBehaviour 컨트롤러
+└── Toolbox.cs # 대안 컨트롤러
+```
+
+### 신규 파일 (생성)
+```
+Assets/Scripts/UVC/UIToolkit/ToolBar/
+├── UTKToolBar.cs # ⭐ 메인 View (VisualElement)
+├── UTKToolBarModel.cs # ⭐ 데이터 모델
+├── UTKToolBarController.cs # ⭐ MonoBehaviour 컨트롤러
+├── Data/
+│ ├── IUTKToolBarItem.cs # 아이템 인터페이스
+│ ├── UTKToolBarButtonData.cs # 버튼 데이터 베이스
+│ ├── UTKToolBarStandardButtonData.cs # 일반 버튼 데이터
+│ ├── UTKToolBarToggleButtonData.cs # 토글 버튼 데이터
+│ ├── UTKToolBarRadioButtonData.cs # 라디오 버튼 데이터
+│ ├── UTKToolBarRadioButtonGroup.cs # 라디오 그룹 관리
+│ ├── UTKToolBarExpandableButtonData.cs # 확장 버튼 데이터
+│ └── UTKToolBarSeparatorData.cs # 구분선 데이터
+├── Items/
+│ ├── UTKToolBarButtonBase.cs # ⭐ 버튼 VisualElement 베이스
+│ ├── UTKToolBarStandardButton.cs # 일반 버튼 컴포넌트
+│ ├── UTKToolBarToggleButton.cs # 토글 버튼 컴포넌트
+│ ├── UTKToolBarRadioButton.cs # 라디오 버튼 컴포넌트
+│ ├── UTKToolBarExpandableButton.cs # 확장 버튼 컴포넌트
+│ └── UTKToolBarSeparator.cs # 구분선 컴포넌트
+└── README.md
+
+Assets/Resources/UIToolkit/ToolBar/
+├── UTKToolBar.uxml # 메인 툴바 구조
+├── UTKToolBarUss.uss # 메인 툴바 스타일
+├── UTKToolBarButton.uxml # 버튼 아이템 템플릿
+├── UTKToolBarButtonUss.uss # 버튼 아이템 스타일
+├── UTKToolBarToggleButton.uxml # 토글 버튼 템플릿
+├── UTKToolBarToggleButtonUss.uss # 토글 버튼 스타일
+├── UTKToolBarExpandableButton.uxml # 확장 버튼 템플릿
+├── UTKToolBarExpandableButtonUss.uss # 확장 버튼 스타일
+├── UTKToolBarSeparator.uxml # 구분선 템플릿
+├── UTKToolBarSeparatorUss.uss # 구분선 스타일
+├── UTKToolBarSubMenu.uxml # 서브메뉴 컨테이너
+└── UTKToolBarSubMenuUss.uss # 서브메뉴 스타일
+```
+
+---
+
+## 🔧 구현 상세
+
+### 1. 열거형 및 인터페이스
+
+#### 1.1 UTKToolBarOrientation
+```csharp
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 툴바 배치 방향을 정의합니다.
+ ///
+ public enum UTKToolBarOrientation
+ {
+ /// 가로 배치 (좌→우)
+ Horizontal,
+ /// 세로 배치 (위→아래)
+ Vertical
+ }
+}
+```
+
+#### 1.2 UTKToolBarActionType
+```csharp
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 툴바 버튼 액션 타입을 정의합니다.
+ ///
+ public enum UTKToolBarActionType
+ {
+ /// 일반 클릭
+ Standard,
+ /// 라디오 그룹 선택
+ Radio,
+ /// 토글 상태 변경
+ Toggle,
+ /// 확장 버튼 서브 선택
+ Expandable
+ }
+}
+```
+
+#### 1.3 IUTKToolBarItem
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/IUTKToolBarItem.cs`
+
+```csharp
+#nullable enable
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 툴바 아이템의 기본 인터페이스입니다.
+ /// 모든 툴바 아이템(버튼, 구분선 등)은 이 인터페이스를 구현합니다.
+ ///
+ public interface IUTKToolBarItem
+ {
+ /// 아이템 고유 식별자
+ string ItemId { get; }
+ }
+}
+```
+
+---
+
+### 2. 데이터 레이어
+
+#### 2.1 UTKToolBarButtonData (추상 베이스)
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarButtonData.cs`
+
+**책임**:
+- 모든 툴바 버튼의 공통 데이터 (Text, Icon, Enabled, Tooltip, Command)
+- 상태 변경 이벤트 (OnStateChanged, OnClicked)
+
+```csharp
+#nullable enable
+using System;
+using UVC.UI.Commands;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 모든 툴바 버튼의 공통 데이터를 정의하는 추상 클래스입니다.
+ ///
+ public abstract class UTKToolBarButtonData : IUTKToolBarItem, IDisposable
+ {
+ // --- 속성 ---
+ /// 아이템 고유 식별자
+ public string ItemId { get; private set; }
+
+ /// 버튼 텍스트 (다국어 키)
+ public string Text { get; set; }
+
+ /// 아이콘 리소스 경로 또는 Material Icon 유니코드
+ public string? IconPath { get; set; }
+
+ /// Material Icon 사용 여부 (true: 폰트 아이콘, false: 이미지)
+ public bool UseMaterialIcon { get; set; }
+
+ /// 활성화 상태
+ public bool IsEnabled { get; set; }
+
+ /// 툴팁 텍스트 (다국어 키)
+ public string? Tooltip { get; set; }
+
+ /// 실행할 명령
+ public ICommand? ClickCommand { get; set; }
+
+ // --- 이벤트 ---
+ /// Text, Icon, Enabled 등 시각적 상태 변경 시 발생
+ public event Action? OnStateChanged;
+
+ /// 버튼 클릭 시 발생
+ public event Action? OnClicked;
+
+ // --- 메서드 ---
+ /// 클릭 실행 (Command 실행 + 이벤트 발생)
+ public virtual void ExecuteClick(object? parameter = null);
+
+ /// 상태 변경 알림
+ public void NotifyStateChanged();
+
+ /// 모든 이벤트 핸들러 해제
+ public void ClearEventHandlers();
+
+ // --- IDisposable ---
+ private bool _disposed;
+ public void Dispose();
+ protected virtual void Dispose(bool disposing);
+ }
+}
+```
+
+**구현 지침**:
+1. `ExecuteClick()`에서 `IUndoableCommand` 지원 (Execute 후 Undo 스택 관리)
+2. `Dispose()`에서 이벤트 핸들러 해제 + Command가 IDisposable이면 정리
+
+---
+
+#### 2.2 UTKToolBarStandardButtonData
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarStandardButtonData.cs`
+
+```csharp
+#nullable enable
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 단순 클릭 동작의 일반 버튼 데이터입니다.
+ ///
+ public class UTKToolBarStandardButtonData : UTKToolBarButtonData
+ {
+ // ToolbarStandardButton과 동일한 구조
+ // 추가 속성 없음 (베이스 클래스 기능만 사용)
+ }
+}
+```
+
+---
+
+#### 2.3 UTKToolBarToggleButtonData
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarToggleButtonData.cs`
+
+```csharp
+#nullable enable
+using System;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// On/Off 상태를 가지는 토글 버튼 데이터입니다.
+ ///
+ public class UTKToolBarToggleButtonData : UTKToolBarButtonData
+ {
+ /// 현재 선택(On) 상태
+ public bool IsSelected { get; private set; }
+
+ /// Off 상태 아이콘 경로
+ public string? OffIconPath { get; set; }
+
+ /// 토글 시 콜백
+ public Action? OnToggle { get; set; }
+
+ /// 토글 상태 변경 이벤트
+ public event Action? OnToggleStateChanged;
+
+ ///
+ /// 선택 상태를 변경합니다.
+ ///
+ /// 새로운 선택 상태
+ /// 이벤트 발생 여부
+ public void SetSelected(bool isSelected, bool raiseEvent = true);
+
+ ///
+ /// 클릭 시 상태를 반전시키고 Command를 실행합니다.
+ ///
+ public override void ExecuteClick(object? parameter = null);
+ }
+}
+```
+
+---
+
+#### 2.4 UTKToolBarRadioButtonData
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonData.cs`
+
+```csharp
+#nullable enable
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 그룹 내 상호 배타적 선택을 지원하는 라디오 버튼 데이터입니다.
+ ///
+ public class UTKToolBarRadioButtonData : UTKToolBarToggleButtonData
+ {
+ /// 소속 라디오 그룹 이름
+ public string GroupName { get; private set; }
+
+ /// 라디오 그룹 참조 (모델에서 설정)
+ internal UTKToolBarRadioButtonGroup? RadioGroup { get; set; }
+
+ ///
+ /// 클릭 시 그룹 내 다른 버튼은 해제하고 이 버튼만 선택합니다.
+ ///
+ public override void ExecuteClick(object? parameter = null);
+ }
+}
+```
+
+---
+
+#### 2.5 UTKToolBarRadioButtonGroup
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarRadioButtonGroup.cs`
+
+```csharp
+#nullable enable
+using System;
+using System.Collections.Generic;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 라디오 버튼 그룹을 관리합니다. 하나의 버튼만 선택 상태를 유지합니다.
+ ///
+ public class UTKToolBarRadioButtonGroup : IDisposable
+ {
+ /// 그룹 이름
+ public string GroupName { get; private set; }
+
+ /// 버튼 등록
+ public void RegisterButton(UTKToolBarRadioButtonData button);
+
+ /// 특정 버튼 선택 (나머지 해제)
+ public void SetSelected(UTKToolBarRadioButtonData buttonToSelect, bool raiseEvent = true);
+
+ /// 모든 선택 해제
+ public void ClearSelection(bool raiseEvent = true);
+
+ /// 그룹 내 버튼 목록 반환
+ public IReadOnlyList GetButtons();
+
+ /// 텍스트로 버튼 검색
+ public UTKToolBarRadioButtonData? FindButtonByText(string text);
+
+ /// 초기 선택 상태 적용
+ public void InitializeSelection();
+
+ // --- IDisposable ---
+ public void Dispose();
+ }
+}
+```
+
+---
+
+#### 2.6 UTKToolBarExpandableButtonData
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarExpandableButtonData.cs`
+
+```csharp
+#nullable enable
+using System;
+using System.Collections.Generic;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 서브 버튼 목록을 가진 확장 가능한 버튼 데이터입니다.
+ ///
+ public class UTKToolBarExpandableButtonData : UTKToolBarButtonData
+ {
+ /// 서브 버튼 목록
+ public List SubButtons { get; private set; }
+
+ /// 서브 버튼 선택 시 메인 아이콘 업데이트 여부
+ public bool UpdateIconOnSelection { get; set; }
+
+ /// 현재 선택된 서브 버튼
+ public UTKToolBarButtonData? SelectedSubButton { get; private set; }
+
+ /// 원본 텍스트 (서브 버튼 선택 시 변경 전 저장용)
+ public string OriginalText { get; private set; }
+
+ /// 서브 버튼 선택 콜백
+ public Action? OnSubButtonSelected { get; set; }
+
+ /// 서브 버튼 선택 변경 이벤트 (mainText, selectedSubText)
+ public event Action? OnSubButtonSelectionChanged;
+
+ /// 서브 버튼을 선택합니다.
+ public void SelectSubButton(UTKToolBarButtonData selectedSubButton);
+
+ /// 원본 텍스트를 설정합니다.
+ public void SetOriginalText(string text);
+ }
+}
+```
+
+---
+
+#### 2.7 UTKToolBarSeparatorData
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Data/UTKToolBarSeparatorData.cs`
+
+```csharp
+#nullable enable
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 툴바 내 시각적 구분선 데이터입니다.
+ ///
+ public class UTKToolBarSeparatorData : IUTKToolBarItem
+ {
+ /// 아이템 고유 식별자
+ public string ItemId { get; private set; }
+
+ public UTKToolBarSeparatorData(string? itemId = null);
+ }
+}
+```
+
+---
+
+### 3. 모델 레이어
+
+#### 3.1 UTKToolBarModel ⭐
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarModel.cs`
+
+**책임**:
+- 툴바 아이템 컬렉션 관리
+- 라디오 그룹 자동 관리
+- 팩토리 메서드로 아이템 생성
+
+```csharp
+#nullable enable
+using System;
+using System.Collections.Generic;
+using UVC.UI.Commands;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// UTKToolBar의 데이터 모델입니다.
+ /// 아이템 컬렉션을 관리하고, 라디오 그룹 등록을 자동화합니다.
+ ///
+ public class UTKToolBarModel : IDisposable
+ {
+ /// 전체 아이템 목록 (순서 유지)
+ public List Items { get; private set; }
+
+ // --- 팩토리 메서드 (ToolbarModel과 동일 API) ---
+
+ /// 일반 버튼 추가
+ /// 버튼 텍스트 (다국어 키)
+ /// 아이콘 경로 (Material Icon 또는 Resources 경로)
+ /// 실행할 명령
+ /// 툴팁 (다국어 키)
+ /// Material Icon 사용 여부
+ /// 생성된 버튼 데이터
+ public UTKToolBarStandardButtonData AddStandardButton(
+ string text,
+ string? iconPath = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool useMaterialIcon = true);
+
+ /// 토글 버튼 추가
+ /// 버튼 텍스트
+ /// 초기 On/Off 상태
+ /// On 상태 아이콘
+ /// Off 상태 아이콘
+ /// 토글 콜백
+ /// 실행할 명령
+ /// 툴팁
+ /// Material Icon 사용 여부
+ /// 생성된 토글 버튼 데이터
+ public UTKToolBarToggleButtonData AddToggleButton(
+ string text,
+ bool initialState = false,
+ string? onIconPath = null,
+ string? offIconPath = null,
+ Action? onToggle = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool useMaterialIcon = true);
+
+ /// 라디오 버튼 추가
+ /// 라디오 그룹 이름
+ /// 버튼 텍스트
+ /// 초기 선택 상태
+ /// 선택 상태 아이콘
+ /// 비선택 상태 아이콘
+ /// 토글 콜백
+ /// 실행할 명령
+ /// 툴팁
+ /// Material Icon 사용 여부
+ /// 생성된 라디오 버튼 데이터
+ public UTKToolBarRadioButtonData AddRadioButton(
+ string groupName,
+ string text,
+ bool initialState = false,
+ string? onIconPath = null,
+ string? offIconPath = null,
+ Action? onToggle = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool useMaterialIcon = true);
+
+ /// 확장 버튼 추가
+ /// 버튼 텍스트
+ /// 아이콘 경로
+ /// 실행할 명령
+ /// 툴팁
+ /// 서브 선택 시 아이콘 업데이트 여부
+ /// Material Icon 사용 여부
+ /// 생성된 확장 버튼 데이터
+ public UTKToolBarExpandableButtonData AddExpandableButton(
+ string text,
+ string? iconPath = null,
+ ICommand? command = null,
+ string? tooltip = null,
+ bool updateIconOnSelection = false,
+ bool useMaterialIcon = true);
+
+ /// 구분선 추가
+ public UTKToolBarSeparatorData AddSeparator();
+
+ // --- 상태 관리 메서드 ---
+
+ /// 라디오 그룹의 특정 버튼 선택
+ public void SetRadioButtonSelection(string groupName, string buttonText, bool raiseEvent = true);
+
+ /// 라디오 그룹의 특정 버튼 선택 (UTKToolBarRadioButtonData로)
+ public void SetRadioButtonSelection(string groupName, UTKToolBarRadioButtonData button, bool raiseEvent = true);
+
+ /// 라디오 그룹 선택 해제
+ public void ClearRadioButtonSelection(string groupName, bool raiseEvent = true);
+
+ /// 토글 버튼 상태 변경
+ public void SetToggleButtonState(string buttonText, bool isSelected, bool raiseEvent = true);
+
+ /// 토글 버튼 상태 조회
+ public bool GetToggleButtonState(string buttonText);
+
+ /// 라디오 그룹 가져오기
+ public UTKToolBarRadioButtonGroup? GetRadioButtonGroup(string groupName);
+
+ // --- IDisposable ---
+ private bool _disposed;
+ public void Dispose();
+ protected virtual void Dispose(bool disposing);
+ }
+}
+```
+
+**구현 지침**:
+1. `_radioGroups`를 `Dictionary`으로 관리
+2. `AddRadioButton()` 호출 시 자동으로 그룹 생성/등록
+3. `Dispose()`에서 모든 아이템과 라디오 그룹 정리
+
+---
+
+### 4. View 레이어
+
+#### 4.1 UTKToolBarButtonBase (추상 VisualElement 베이스) ⭐
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarButtonBase.cs`
+
+**책임**:
+- 모든 툴바 버튼 UI의 공통 기반
+- 아이콘(Material Icon / Image) + 텍스트 표시
+- 테마 연동
+- 데이터 바인딩 및 상태 동기화
+
+```csharp
+#nullable enable
+using System;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 툴바 버튼 VisualElement의 추상 베이스 클래스입니다.
+ /// 아이콘, 텍스트, 활성화 상태의 공통 UI를 제공합니다.
+ ///
+ public abstract partial class UTKToolBarButtonBase : VisualElement, IDisposable
+ {
+ // --- 캐시된 UI 요소 ---
+ protected VisualElement? _iconElement;
+ protected Label? _textLabel;
+ protected VisualElement? _rootButton;
+
+ // --- 데이터 참조 ---
+ protected UTKToolBarButtonData? _data;
+
+ // --- 이벤트 ---
+ /// 버튼 클릭 이벤트
+ public event Action? OnButtonClicked;
+
+ // --- 공통 메서드 ---
+ /// 데이터를 바인딩합니다.
+ public virtual void BindData(UTKToolBarButtonData data);
+
+ /// 데이터 바인딩을 해제합니다.
+ public virtual void UnbindData();
+
+ /// 아이콘을 업데이트합니다.
+ protected void UpdateIcon(string? iconPath, bool useMaterialIcon);
+
+ /// 텍스트를 업데이트합니다.
+ protected void UpdateText(string text);
+
+ /// 활성화 상태를 업데이트합니다.
+ protected void UpdateEnabled(bool isEnabled);
+
+ /// 모델 상태 변경 핸들러
+ protected virtual void OnDataStateChanged();
+
+ // --- 테마 ---
+ protected void SubscribeToThemeChanges();
+ private void OnAttachToPanelForTheme(AttachToPanelEvent evt);
+ private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt);
+ private void OnThemeChanged(UTKTheme theme);
+
+ // --- IDisposable ---
+ protected bool _disposed;
+ public virtual void Dispose();
+ }
+}
+```
+
+**구현 지침**:
+1. **아이콘 우선순위**: `UseMaterialIcon == true`이면 `UTKMaterialIcons` 폰트 사용, 아니면 `Resources.Load()` 사용
+2. **테마**: `UTKThemeManager` 패턴 (`AttachToPanelEvent`/`DetachFromPanelEvent`)
+3. **데이터 바인딩**: `BindData()`에서 `OnStateChanged` 구독, `UnbindData()`에서 해제
+
+---
+
+#### 4.2 UTKToolBarStandardButton
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarStandardButton.cs`
+
+```csharp
+#nullable enable
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 단순 클릭 동작의 툴바 버튼 컴포넌트입니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarStandardButton : UTKToolBarButtonBase
+ {
+ private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBarButton";
+ private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarButtonUss";
+
+ public UTKToolBarStandardButton();
+
+ /// 클릭 이벤트 처리
+ private void OnClick(ClickEvent evt);
+ }
+}
+```
+
+---
+
+#### 4.3 UTKToolBarToggleButton
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarToggleButton.cs`
+
+```csharp
+#nullable enable
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// On/Off 상태 전환이 가능한 토글 버튼 컴포넌트입니다.
+ /// 선택 상태에 따라 아이콘과 스타일이 변경됩니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarToggleButton : UTKToolBarButtonBase
+ {
+ private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBarToggleButton";
+ private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarToggleButtonUss";
+
+ public UTKToolBarToggleButton();
+
+ /// 데이터 바인딩 (Toggle 전용 이벤트 추가 구독)
+ public override void BindData(UTKToolBarButtonData data);
+
+ /// 토글 상태에 따른 시각적 업데이트
+ private void UpdateToggleVisuals(bool isSelected);
+
+ /// 토글 상태 변경 핸들러
+ private void OnToggleStateChanged(bool isSelected);
+
+ /// 클릭 이벤트 처리
+ private void OnClick(ClickEvent evt);
+ }
+}
+```
+
+---
+
+#### 4.4 UTKToolBarRadioButton
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarRadioButton.cs`
+
+```csharp
+#nullable enable
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 라디오 그룹 내에서 상호 배타적으로 선택되는 버튼 컴포넌트입니다.
+ /// UTKToolBarToggleButton을 상속하여 토글 시각 효과를 재사용합니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarRadioButton : UTKToolBarToggleButton
+ {
+ public UTKToolBarRadioButton();
+
+ /// 클릭 시 라디오 그룹 선택 처리
+ private void OnClick(ClickEvent evt);
+ }
+}
+```
+
+---
+
+#### 4.5 UTKToolBarExpandableButton
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarExpandableButton.cs`
+
+```csharp
+#nullable enable
+using System.Collections.Generic;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 클릭 시 서브 메뉴를 펼치는 확장 버튼 컴포넌트입니다.
+ /// 서브 버튼 목록을 지연 생성(Lazy Loading)합니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarExpandableButton : UTKToolBarButtonBase
+ {
+ private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBarExpandableButton";
+ private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarExpandableButtonUss";
+ private const string SUBMENU_UXML = "UIToolkit/ToolBar/UTKToolBarSubMenu";
+ private const string SUBMENU_USS = "UIToolkit/ToolBar/UTKToolBarSubMenuUss";
+
+ /// 서브 메뉴 열림 상태
+ public bool IsSubMenuOpen { get; private set; }
+
+ /// 서브 메뉴 열림/닫힘 이벤트
+ public event Action? OnSubMenuToggled;
+
+ public UTKToolBarExpandableButton();
+
+ /// 서브 메뉴 토글
+ public void ToggleSubMenu();
+
+ /// 서브 메뉴 닫기
+ public void CloseSubMenu();
+
+ /// 서브 메뉴 생성 (지연 로드)
+ private void CreateSubMenu();
+
+ /// 서브 메뉴 위치 계산 (가로/세로 배치 대응)
+ private void PositionSubMenu();
+
+ /// 확장 화살표 아이콘 (▼ 또는 ▶)
+ private VisualElement? _arrowIcon;
+
+ /// 서브 메뉴 컨테이너
+ private VisualElement? _subMenuContainer;
+
+ /// 서브 메뉴 아이템 요소 목록
+ private List? _subMenuItems;
+ }
+}
+```
+
+**서브 메뉴 위치 로직** (가로/세로 대응):
+```
+가로(Horizontal) 배치 시:
+┌──────────────────────────┐
+│ [Btn1] [Btn2] [▼Exp] ... │ ← 툴바
+└──────────────────────────┘
+ ┌──────────┐
+ │ SubBtn1 │ ← 서브 메뉴 (아래로 펼침)
+ │ SubBtn2 │
+ │ SubBtn3 │
+ └──────────┘
+
+세로(Vertical) 배치 시:
+┌──────┐
+│ Btn1 │
+│ Btn2 │
+│▶Exp │──┐
+│ Btn3 │ │ ┌──────────┐
+└──────┘ └→│ SubBtn1 │ ← 서브 메뉴 (오른쪽으로 펼침)
+ │ SubBtn2 │
+ │ SubBtn3 │
+ └──────────┘
+```
+
+---
+
+#### 4.6 UTKToolBarSeparator
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/Items/UTKToolBarSeparator.cs`
+
+```csharp
+#nullable enable
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 툴바 내 시각적 구분선 컴포넌트입니다.
+ /// 가로 배치 시 세로선, 세로 배치 시 가로선으로 표시됩니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBarSeparator : VisualElement, IDisposable
+ {
+ private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBarSeparator";
+ private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarSeparatorUss";
+
+ public UTKToolBarSeparator();
+
+ /// 배치 방향에 따라 구분선 방향을 업데이트합니다.
+ public void UpdateOrientation(UTKToolBarOrientation orientation);
+
+ public void Dispose();
+ }
+}
+```
+
+---
+
+#### 4.7 UTKToolBar (메인 View) ⭐⭐
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBar.cs`
+
+**책임**:
+- 전체 툴바 UI 생성/관리
+- 가로/세로 배치 전환
+- 아이템 렌더링 및 이벤트 중재
+- 서브 메뉴 외부 클릭 감지
+
+```csharp
+#nullable enable
+using System;
+using System.Collections.Generic;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// UIToolkit 기반 툴바 메인 컴포넌트입니다.
+ /// 가로/세로 배치를 지원하며, 4가지 버튼 타입과 구분선을 렌더링합니다.
+ ///
+ [UxmlElement]
+ public partial class UTKToolBar : VisualElement, IDisposable
+ {
+ #region Constants
+ private const string UXML_PATH = "UIToolkit/ToolBar/UTKToolBar";
+ private const string USS_PATH = "UIToolkit/ToolBar/UTKToolBarUss";
+ #endregion
+
+ #region UXML Attributes
+ /// 툴바 배치 방향
+ [UxmlAttribute("orientation")]
+ public UTKToolBarOrientation Orientation
+ {
+ get => _orientation;
+ set
+ {
+ _orientation = value;
+ ApplyOrientation();
+ }
+ }
+
+ /// 아이템 간 간격 (px)
+ [UxmlAttribute("item-spacing")]
+ public float ItemSpacing { get; set; } = 2f;
+
+ /// 툴바 크기 (가로 시 높이, 세로 시 너비)
+ [UxmlAttribute("toolbar-size")]
+ public float ToolBarSize { get; set; } = 40f;
+ #endregion
+
+ #region Fields
+ private UTKToolBarOrientation _orientation = UTKToolBarOrientation.Horizontal;
+ private VisualElement? _itemContainer;
+ private List _itemElements;
+ private Dictionary _buttonMap;
+ private bool _disposed;
+ #endregion
+
+ #region Events
+ /// 버튼 액션 이벤트
+ public event Action? OnAction;
+ #endregion
+
+ #region Constructor
+ ///
+ /// 기본 생성자. UXML에서 인스턴스화 시 사용됩니다.
+ ///
+ public UTKToolBar();
+ #endregion
+
+ #region Public Methods
+ /// 모델 데이터로 툴바를 생성합니다.
+ public void BuildToolBar(UTKToolBarModel model);
+
+ /// 툴바의 모든 아이템을 제거합니다.
+ public void ClearToolBar();
+
+ /// 특정 버튼의 활성화 상태를 변경합니다.
+ public void SetButtonEnabled(string itemId, bool isEnabled);
+
+ /// 배치 방향을 변경합니다.
+ public void SetOrientation(UTKToolBarOrientation orientation);
+
+ /// 특정 버튼 요소를 가져옵니다.
+ public bool TryGetButtonElement(string itemId, out UTKToolBarButtonBase? button);
+ #endregion
+
+ #region Private Methods
+ /// UXML/USS 로드 및 UI 구성
+ private void CreateUI();
+
+ /// 배치 방향을 적용합니다.
+ private void ApplyOrientation();
+
+ /// 모델 아이템으로 개별 버튼 생성
+ private VisualElement CreateItemElement(IUTKToolBarItem item);
+
+ /// 외부 클릭 감지 (서브 메뉴 닫기용)
+ private void OnPanelPointerDown(PointerDownEvent evt);
+
+ /// 모든 열린 서브 메뉴 닫기
+ private void CloseAllSubMenus();
+ #endregion
+
+ #region Theme
+ private void SubscribeToThemeChanges();
+ private void OnAttachToPanelForTheme(AttachToPanelEvent evt);
+ private void OnDetachFromPanelForTheme(DetachFromPanelEvent evt);
+ private void OnThemeChanged(UTKTheme theme);
+ #endregion
+
+ #region IDisposable
+ public void Dispose();
+ #endregion
+ }
+}
+```
+
+---
+
+#### 4.8 UTKToolBarActionEventArgs
+```csharp
+#nullable enable
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// 툴바 버튼 액션 이벤트 인자입니다.
+ ///
+ public class UTKToolBarActionEventArgs
+ {
+ /// 버튼 텍스트 (또는 그룹 이름)
+ public string Text { get; set; } = "";
+
+ /// 컨텍스트별 값 (Toggle: bool, Radio: 선택 텍스트, Expandable: 서브 텍스트)
+ public object? Value { get; set; }
+
+ /// 액션 타입
+ public UTKToolBarActionType ActionType { get; set; }
+ }
+}
+```
+
+---
+
+### 5. Controller 레이어
+
+#### 5.1 UTKToolBarController ⭐
+**파일 경로**: `Assets/Scripts/UVC/UIToolkit/ToolBar/UTKToolBarController.cs`
+
+**책임**:
+- MonoBehaviour로 UIDocument와 연동
+- UTKToolBarModel과 UTKToolBar(View) 중재
+- `Toolbar`/`Toolbox`의 public API 호환
+
+```csharp
+#nullable enable
+using System;
+using UnityEngine;
+using UnityEngine.UIElements;
+
+namespace UVC.UIToolkit.ToolBar
+{
+ ///
+ /// UTKToolBar의 MonoBehaviour 컨트롤러입니다.
+ /// 기존 Toolbar/Toolbox의 public API와 호환됩니다.
+ ///
+ public class UTKToolBarController : MonoBehaviour
+ {
+ // --- 인스펙터 ---
+ [SerializeField] private UIDocument? _uiDocument;
+
+ [SerializeField]
+ [Tooltip("툴바 배치 방향")]
+ private UTKToolBarOrientation _orientation = UTKToolBarOrientation.Horizontal;
+
+ // --- 필드 ---
+ private UTKToolBarModel? _model;
+ private UTKToolBar? _toolBarView;
+
+ // --- 이벤트 (Toolbar/Toolbox와 동일) ---
+ /// 버튼 액션 이벤트
+ public event Action? OnAction;
+
+ // --- Public 메서드 (Toolbar/Toolbox 호환 API) ---
+
+ /// 데이터 모델 설정
+ public void SetData(UTKToolBarModel model);
+
+ /// 초기화 (툴바 렌더링)
+ public virtual void Initialize();
+
+ /// 라디오 버튼 선택 (텍스트로)
+ public void SetRadioButtonSelection(string groupName, string buttonText, bool raiseEvent = true);
+
+ /// 라디오 버튼 선택 (데이터 참조로)
+ public void SetRadioButtonSelection(string groupName, UTKToolBarRadioButtonData button, bool raiseEvent = true);
+
+ /// 라디오 버튼 선택 해제
+ public void ClearRadioButtonSelection(string groupName, bool raiseEvent = true);
+
+ /// 토글 버튼 상태 설정
+ public void SetToggleButtonState(string buttonText, bool isSelected, bool raiseEvent = true);
+
+ /// 토글 버튼 상태 조회
+ public bool GetToggleButtonState(string buttonText);
+
+ /// 배치 방향 변경
+ public void SetOrientation(UTKToolBarOrientation orientation);
+
+ // --- Lifecycle ---
+ protected virtual void Awake();
+ protected virtual void OnDestroy();
+ }
+}
+```
+
+---
+
+### 6. UXML/USS 리소스
+
+#### 6.1 UTKToolBar.uxml
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBar.uxml`
+
+```xml
+
+
+
+
+
+```
+
+#### 6.2 UTKToolBarUss.uss
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarUss.uss`
+
+```css
+/* === 공통 === */
+.utk-toolbar {
+ overflow: visible;
+}
+
+.utk-toolbar__container {
+ overflow: visible;
+}
+
+/* === 가로 배치 === */
+.utk-toolbar--horizontal {
+ flex-direction: row;
+ align-items: center;
+ height: 40px;
+ width: auto;
+ background-color: var(--color-bg-secondary);
+ border-bottom-width: var(--border-width);
+ border-bottom-color: var(--color-border);
+ padding: 0 var(--space-s);
+}
+
+.utk-toolbar--horizontal > .utk-toolbar__container {
+ flex-direction: row;
+ align-items: center;
+ flex-grow: 1;
+}
+
+/* === 세로 배치 === */
+.utk-toolbar--vertical {
+ flex-direction: column;
+ align-items: center;
+ width: 40px;
+ height: auto;
+ background-color: var(--color-bg-secondary);
+ border-right-width: var(--border-width);
+ border-right-color: var(--color-border);
+ padding: var(--space-s) 0;
+}
+
+.utk-toolbar--vertical > .utk-toolbar__container {
+ flex-direction: column;
+ align-items: center;
+ flex-grow: 1;
+}
+```
+
+#### 6.3 UTKToolBarButton.uxml
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarButton.uxml`
+
+```xml
+
+
+
+
+
+
+```
+
+#### 6.4 UTKToolBarButtonUss.uss
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarButtonUss.uss`
+
+```css
+.utk-toolbar-btn {
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-width: 32px;
+ min-height: 32px;
+ padding: var(--space-xs);
+ margin: 0 1px;
+ border-radius: var(--radius-s);
+ border-width: 0;
+ background-color: transparent;
+ cursor: link;
+ transition: background-color var(--anim-fast);
+}
+
+.utk-toolbar-btn:hover {
+ background-color: var(--color-bg-hover);
+}
+
+.utk-toolbar-btn:active {
+ background-color: var(--color-bg-active);
+}
+
+.utk-toolbar-btn--disabled {
+ opacity: 0.4;
+ cursor: default;
+}
+
+.utk-toolbar-btn__icon {
+ width: var(--size-icon-btn);
+ height: var(--size-icon-btn);
+ -unity-background-scale-mode: scale-to-fit;
+}
+
+.utk-toolbar-btn__icon--material {
+ font-size: 18px;
+ -unity-text-align: middle-center;
+ color: var(--color-text-primary);
+}
+
+.utk-toolbar-btn__label {
+ font-size: var(--font-size-label4);
+ color: var(--color-text-secondary);
+ -unity-text-align: upper-center;
+ margin-top: 1px;
+ display: none; /* 기본: 아이콘만 표시. 텍스트 필요 시 display: flex; */
+}
+```
+
+#### 6.5 UTKToolBarToggleButton.uxml
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButton.uxml`
+
+```xml
+
+
+
+
+
+
+```
+
+#### 6.6 UTKToolBarToggleButtonUss.uss
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarToggleButtonUss.uss`
+
+```css
+.utk-toolbar-toggle--selected {
+ background-color: var(--color-primary);
+ border-radius: var(--radius-s);
+}
+
+.utk-toolbar-toggle--selected:hover {
+ background-color: var(--color-primary-hover);
+}
+
+.utk-toolbar-toggle--selected .utk-toolbar-btn__icon {
+ color: var(--color-text-on-primary);
+}
+
+.utk-toolbar-toggle--selected .utk-toolbar-btn__icon--material {
+ color: var(--color-text-on-primary);
+}
+```
+
+#### 6.7 UTKToolBarExpandableButton.uxml
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButton.uxml`
+
+```xml
+
+
+
+
+
+
+
+```
+
+#### 6.8 UTKToolBarExpandableButtonUss.uss
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarExpandableButtonUss.uss`
+
+```css
+.utk-toolbar-expandable {
+ position: relative;
+}
+
+.utk-toolbar-expandable__arrow {
+ width: 6px;
+ height: 6px;
+ position: absolute;
+ bottom: 2px;
+ right: 2px;
+}
+
+/* 가로 배치: 아래 화살표 ▼ */
+.utk-toolbar--horizontal .utk-toolbar-expandable__arrow {
+ border-left-width: 3px;
+ border-right-width: 3px;
+ border-top-width: 4px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ border-top-color: var(--color-text-secondary);
+ border-bottom-width: 0;
+}
+
+/* 세로 배치: 오른쪽 화살표 ▶ */
+.utk-toolbar--vertical .utk-toolbar-expandable__arrow {
+ border-top-width: 3px;
+ border-bottom-width: 3px;
+ border-left-width: 4px;
+ border-top-color: transparent;
+ border-bottom-color: transparent;
+ border-left-color: var(--color-text-secondary);
+ border-right-width: 0;
+}
+```
+
+#### 6.9 UTKToolBarSeparator.uxml
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparator.uxml`
+
+```xml
+
+
+
+```
+
+#### 6.10 UTKToolBarSeparatorUss.uss
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarSeparatorUss.uss`
+
+```css
+/* 가로 배치: 세로 구분선 */
+.utk-toolbar--horizontal .utk-toolbar-separator {
+ width: var(--border-width);
+ height: 20px;
+ margin: 0 var(--space-s);
+ background-color: var(--color-border);
+}
+
+/* 세로 배치: 가로 구분선 */
+.utk-toolbar--vertical .utk-toolbar-separator {
+ height: var(--border-width);
+ width: 20px;
+ margin: var(--space-s) 0;
+ background-color: var(--color-border);
+}
+```
+
+#### 6.11 UTKToolBarSubMenu.uxml
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenu.uxml`
+
+```xml
+
+
+
+```
+
+#### 6.12 UTKToolBarSubMenuUss.uss
+**경로**: `Assets/Resources/UIToolkit/ToolBar/UTKToolBarSubMenuUss.uss`
+
+```css
+.utk-toolbar-submenu {
+ position: absolute;
+ background-color: var(--color-bg-primary);
+ border-width: var(--border-width);
+ border-color: var(--color-border);
+ border-radius: var(--radius-m);
+ padding: var(--space-xs);
+ min-width: 120px;
+ overflow: visible;
+}
+
+.utk-toolbar-submenu__container {
+ flex-direction: column;
+}
+
+/* 서브 메뉴 내 버튼은 가로로 펼침 */
+.utk-toolbar-submenu .utk-toolbar-btn {
+ flex-direction: row;
+ min-width: 100px;
+ min-height: 28px;
+ justify-content: flex-start;
+ padding: var(--space-xs) var(--space-m);
+ margin: 1px 0;
+}
+
+.utk-toolbar-submenu .utk-toolbar-btn__icon {
+ margin-right: var(--space-s);
+}
+
+.utk-toolbar-submenu .utk-toolbar-btn__label {
+ display: flex;
+ font-size: var(--font-size-body2);
+ color: var(--color-text-primary);
+ -unity-text-align: middle-left;
+}
+```
+
+---
+
+## 🔑 핵심 기능: 가로/세로 배치
+
+### 배치 전환 메커니즘
+
+```csharp
+/// 배치 방향을 적용합니다.
+private void ApplyOrientation()
+{
+ // 1. 기존 클래스 제거
+ RemoveFromClassList("utk-toolbar--horizontal");
+ RemoveFromClassList("utk-toolbar--vertical");
+
+ // 2. 새 클래스 추가
+ if (_orientation == UTKToolBarOrientation.Horizontal)
+ {
+ AddToClassList("utk-toolbar--horizontal");
+ }
+ else
+ {
+ AddToClassList("utk-toolbar--vertical");
+ }
+
+ // 3. 구분선 방향 업데이트
+ foreach (var element in _itemElements)
+ {
+ if (element is UTKToolBarSeparator separator)
+ {
+ separator.UpdateOrientation(_orientation);
+ }
+ }
+
+ // 4. 확장 버튼 서브 메뉴 닫기 (위치 재계산 필요)
+ CloseAllSubMenus();
+}
+```
+
+### USS 활용 (가로/세로 자동 전환)
+
+구분선, 확장 버튼 화살표 등은 **부모 클래스(`.utk-toolbar--horizontal`/`.utk-toolbar--vertical`)에 의한 자손 선택자**로 자동 전환됩니다:
+
+```css
+/* 부모가 가로면 → 세로 구분선 */
+.utk-toolbar--horizontal .utk-toolbar-separator { width: 1px; height: 20px; }
+
+/* 부모가 세로면 → 가로 구분선 */
+.utk-toolbar--vertical .utk-toolbar-separator { height: 1px; width: 20px; }
+```
+
+### 레이아웃 예시
+
+**가로 배치 (Horizontal)**:
+```
+┌─────────────────────────────────────────────────┐
+│ [🏠] [💾] [📋] │ [🔲Toggle] │ ◉R1 ◉R2 ◉R3 │ [▼Exp] │
+└─────────────────────────────────────────────────┘
+```
+
+**세로 배치 (Vertical)**:
+```
+┌──────┐
+│ 🏠 │
+│ 💾 │
+│ 📋 │
+│──────│ ← 구분선 (가로)
+│🔲Tog │
+│──────│
+│ ◉ R1 │
+│ ◉ R2 │
+│ ◉ R3 │
+│──────│
+│ ▶Exp │
+└──────┘
+```
+
+---
+
+## 🔄 마이그레이션 순서
+
+### Phase 1: 기본 구조 구축
+1. 폴더 생성: `Assets/Scripts/UVC/UIToolkit/ToolBar/`, `Data/`, `Items/`
+2. 폴더 생성: `Assets/Resources/UIToolkit/ToolBar/`
+3. 열거형 및 인터페이스 구현:
+ - `UTKToolBarOrientation`
+ - `UTKToolBarActionType`
+ - `UTKToolBarActionEventArgs`
+ - `IUTKToolBarItem`
+4. UXML/USS 파일 생성 (12개)
+
+### Phase 2: 데이터 레이어 구현
+5. `UTKToolBarButtonData.cs` (추상 베이스) 구현
+ - IDisposable, 이벤트, Command 실행
+6. `UTKToolBarStandardButtonData.cs` 구현
+7. `UTKToolBarToggleButtonData.cs` 구현
+ - IsSelected, SetSelected(), OnToggleStateChanged
+8. `UTKToolBarRadioButtonData.cs` 구현
+ - GroupName, RadioGroup 연동
+9. `UTKToolBarRadioButtonGroup.cs` 구현
+ - 상호 배타 선택 로직
+10. `UTKToolBarExpandableButtonData.cs` 구현
+ - SubButtons, SelectSubButton()
+11. `UTKToolBarSeparatorData.cs` 구현
+12. `UTKToolBarModel.cs` 구현 ⭐
+ - 팩토리 메서드, 라디오 그룹 자동 등록, IDisposable
+
+### Phase 3: View 아이템 구현
+13. `UTKToolBarButtonBase.cs` (추상 VisualElement) 구현
+ - 아이콘(Material/Image), 텍스트, 테마, 데이터 바인딩
+14. `UTKToolBarStandardButton.cs` 구현
+15. `UTKToolBarToggleButton.cs` 구현
+ - 토글 시각 효과 (선택/비선택 스타일)
+16. `UTKToolBarRadioButton.cs` 구현
+17. `UTKToolBarExpandableButton.cs` 구현
+ - 서브 메뉴 지연 생성, 위치 계산, 외부 클릭 닫기
+18. `UTKToolBarSeparator.cs` 구현
+ - 가로/세로 방향 전환
+
+### Phase 4: 메인 View + Controller
+19. `UTKToolBar.cs` 구현 ⭐⭐
+ - Orientation 전환, BuildToolBar(), CloseAllSubMenus()
+ - 외부 클릭 감지 (panel.visualTree PointerDownEvent)
+20. `UTKToolBarController.cs` 구현
+ - Toolbar/Toolbox 호환 API
+
+### Phase 5: 테마 및 스타일
+21. UTKThemeManager 연동
+22. StyleGuide 스타일 적용 (Buttons, Sidebar 참조)
+23. 라이트/다크 테마 지원
+
+### Phase 6: 테스트, 최적화, 문서화 ⭐
+24. 샘플 씬 업데이트 (`UTKToolBarSample.cs`)
+ - 가로/세로 전환 버튼 포함
+ - 4가지 버튼 타입 데모
+25. **메모리 누수 점검** (필수):
+ - Unity Profiler로 메모리 확인
+ - 씬 전환 시 메모리 증가 없는지 확인
+ - IDisposable 정상 호출 확인
+ - 이벤트 구독 해제 확인
+26. **성능 최적화** (필수):
+ - Unity Profiler로 CPU/GC 확인
+ - 버튼 50개 생성 테스트
+ - 쿼리 캐싱 적용 확인
+27. **완전한 문서화** (필수):
+ - 모든 public/protected 멤버 XML 주석
+ - README.md 작성
+
+---
+
+## ⚠️ 주의사항
+
+### 필수 준수 사항 (위반 시 재작업)
+1. **`#nullable enable`** 파일 선두에 필수
+2. **Unity 6 방식**: `[UxmlElement]`, `partial class`, `[UxmlAttribute]` 사용
+3. **이벤트 등록**: `RegisterCallback()` 사용 (`RegisterValueChangedCallback` 금지)
+4. **UXML/USS 네이밍**:
+ - UXML: `UTKToolBar.uxml`
+ - USS: `UTKToolBarUss.uss` (접미사 `Uss` 필수)
+5. **케밥 케이스**: `[UxmlAttribute("orientation")]` 소문자 + 하이픈
+6. **IDisposable 구현** ⭐:
+ - 모든 데이터/View 클래스에 필수
+ - `_disposed` 플래그로 중복 호출 방지
+ - `RegisterCallback` ↔ `UnregisterCallback` 대칭
+ - `DetachFromPanelEvent`에서 정리
+ - 하위 객체도 재귀적으로 Dispose
+7. **아이콘 우선순위**: Material Icons → Image Icons 순서
+
+### 메모리 관리 체크리스트 ⭐
+- [ ] 모든 `RegisterCallback()` 대칭적으로 `UnregisterCallback()`
+- [ ] `UTKThemeManager.OnThemeChanged` 구독 해제
+- [ ] `Dictionary<>` 전체 Clear() 호출
+- [ ] 확장 버튼 서브 메뉴 재귀적 Dispose
+- [ ] 데이터 바인딩 해제 (`UnbindData()`)
+- [ ] Unity Profiler로 메모리 누수 확인
+
+### 성능 최적화 체크리스트 ⭐
+- [ ] `Q()` 결과 필드 캐싱 (생성 시 1회만)
+- [ ] 서브 메뉴 지연 생성 (Lazy Loading)
+- [ ] UXML/USS 리소스 캐싱 (반복 로드 방지)
+- [ ] LINQ 사용 금지 (foreach 사용)
+- [ ] 불필요한 `MarkDirtyRepaint()` 제거
+- [ ] DisplayStyle 토글 (Add/Remove 대신)
+- [ ] Unity Profiler로 CPU/GC 확인
+
+### 문서화 체크리스트 ⭐
+- [ ] 모든 public 멤버: `` 태그
+- [ ] 모든 protected 멤버: `` 태그
+- [ ] 모든 매개변수: `` 태그
+- [ ] 모든 반환값: `` 태그
+- [ ] 예외 발생 가능: `` 태그
+- [ ] 복잡한 로직: `` 태그 또는 인라인 주석
+- [ ] 사용 예제: `` 태그 (Controller)
+- [ ] README.md 작성
+
+### 코드 스타일
+- ✅ 한국어 주석 (XML 문서 포함)
+- ✅ BEM 네이밍 (CSS 클래스)
+- ✅ 파일 경로 참조: `file_path:line_number` 형식
+- ✅ `namespace UVC.UIToolkit.ToolBar`
+
+---
+
+## 📝 테스트 체크리스트
+
+### 기능 테스트
+- [ ] 일반 버튼 클릭 시 Command 실행
+- [ ] 토글 버튼 On/Off 전환
+- [ ] 라디오 버튼 그룹 내 상호 배타적 선택
+- [ ] 확장 버튼 서브 메뉴 열기/닫기
+- [ ] 확장 버튼 서브 아이템 선택 시 메인 아이콘 업데이트
+- [ ] 구분선 표시 (가로/세로)
+- [ ] 활성화/비활성화 상태 변경
+- [ ] 외부 클릭 시 서브 메뉴 닫기
+- [ ] `SetRadioButtonSelection()` API 동작
+- [ ] `SetToggleButtonState()` API 동작
+- [ ] `GetToggleButtonState()` API 동작
+- [ ] `ClearRadioButtonSelection()` API 동작
+- [ ] OnAction 이벤트 정상 발생
+
+### 가로/세로 전환 테스트
+- [ ] Horizontal → Vertical 전환 시 레이아웃 정상
+- [ ] Vertical → Horizontal 전환 시 레이아웃 정상
+- [ ] 전환 시 구분선 방향 자동 변경
+- [ ] 전환 시 확장 버튼 화살표 방향 변경 (▼ ↔ ▶)
+- [ ] 전환 시 서브 메뉴 위치 재계산 (아래 ↔ 오른쪽)
+- [ ] UXML `orientation` 속성으로 초기 방향 설정
+- [ ] 런타임 `SetOrientation()` 호출 시 즉시 전환
+
+### 성능 테스트
+- [ ] 버튼 50개 생성 시 렉 없음
+- [ ] 메모리 누수 없음 (Profiler 확인)
+- [ ] 서브 메뉴 지연 생성 동작 확인
+
+### 호환성 테스트
+- [ ] Toolbar/Toolbox public API 호환성
+- [ ] OnAction 이벤트 데이터 호환성 (Text, Value, ActionType)
+- [ ] 기존 프로젝트와 동시 사용 가능 (uGUI/UIToolkit)
+
+---
+
+## 📚 참고 자료
+
+### CLAUDE.md 관련 섹션
+- [3) 성능 최적화](#3-성능-최적화)
+- [4) 메모리 관리](#4-메모리-관리)
+- [6) 리소스 로드](#6-리소스-로드-addressablesresources)
+- [7) USS 스타일 가이드](#7-uss-스타일-가이드)
+- [8) 아이콘 사용 가이드](#8-아이콘-사용-가이드-icons)
+- [11) UTK 컴포넌트 기본 패턴](#11-utk-컴포넌트-기본-패턴)
+
+### StyleGuide 이미지
+- `StyleGuide/style_guide_Buttons.png` (버튼, Toggle, Icon 스타일)
+- `StyleGuide/style_guide_Sidebar.png` (세로 배치 참고)
+- `StyleGuide/style_guide_Menu.png` (서브 메뉴 스타일 참고)
+
+### 기존 구현 참조
+- `Assets/Scripts/UVC/UI/ToolBar/` (전체 uGUI 구현)
+- `Assets/Scripts/UVC/UIToolkit/Menu/` (UIToolkit 마이그레이션 패턴 참고)
+
+### UTKTopMenu 마이그레이션 작업지시서
+- `작업지시서_TopMenu_UIToolkit_마이그레이션.md` (동일 패턴 참조)
+
+---
+
+## ✅ 완료 조건 (모두 충족 필수)
+
+### 기능 완료
+- [ ] 모든 파일 생성 완료 (Data, Items, View, Controller)
+- [ ] 4가지 버튼 타입 정상 동작 (Standard, Toggle, Radio, Expandable)
+- [ ] 구분선 정상 표시
+- [ ] 가로/세로 배치 전환 정상 동작
+- [ ] Toolbar/Toolbox public API 호환
+- [ ] 샘플 씬에서 정상 동작 확인
+
+### 메모리 관리 완료 ⭐
+- [ ] 모든 클래스 IDisposable 구현
+- [ ] Unity Profiler로 메모리 누수 0 확인
+- [ ] 이벤트 구독/해제 대칭 확인
+- [ ] 데이터 바인딩/해제 대칭 확인
+
+### 성능 완료 ⭐
+- [ ] 서브 메뉴 지연 생성 적용
+- [ ] UXML/USS 리소스 캐싱 적용
+- [ ] 쿼리 캐싱 적용 확인
+- [ ] Unity Profiler로 CPU/GC 확인
+
+### 문서화 완료 ⭐
+- [ ] 모든 public/protected 멤버 XML 주석 완료
+- [ ] README.md 작성 완료
+- [ ] 사용 예제 코드 작성 완료
+
+### 스타일 완료
+- [ ] StyleGuide 스타일 적용
+- [ ] BEM 네이밍 준수
+- [ ] CLAUDE.md 가이드 100% 준수
+
+---
+
+**작성일**: 2026-02-19
+**작성자**: Claude Code Assistant
+**버전**: 1.0