chart 개발 중
This commit is contained in:
@@ -311,9 +311,9 @@ RectTransform:
|
|||||||
m_Father: {fileID: 7604279544518364067}
|
m_Father: {fileID: 7604279544518364067}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 1, y: 0}
|
m_AnchorMin: {x: 1, y: 0}
|
||||||
m_AnchorMax: {x: 1, y: 0}
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 6, y: 0}
|
m_SizeDelta: {x: 6, y: -3}
|
||||||
m_Pivot: {x: 1, y: 1}
|
m_Pivot: {x: 1, y: 1}
|
||||||
--- !u!222 &725070725676732581
|
--- !u!222 &725070725676732581
|
||||||
CanvasRenderer:
|
CanvasRenderer:
|
||||||
@@ -397,7 +397,7 @@ MonoBehaviour:
|
|||||||
m_HandleRect: {fileID: 2751071246484970067}
|
m_HandleRect: {fileID: 2751071246484970067}
|
||||||
m_Direction: 2
|
m_Direction: 2
|
||||||
m_Value: 0
|
m_Value: 0
|
||||||
m_Size: 1
|
m_Size: 0.9322155
|
||||||
m_NumberOfSteps: 0
|
m_NumberOfSteps: 0
|
||||||
m_OnValueChanged:
|
m_OnValueChanged:
|
||||||
m_PersistentCalls:
|
m_PersistentCalls:
|
||||||
@@ -511,7 +511,7 @@ RectTransform:
|
|||||||
m_Father: {fileID: 5368123886321672438}
|
m_Father: {fileID: 5368123886321672438}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
m_AnchorMax: {x: 0.203125, y: 1}
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 0, y: 0}
|
m_SizeDelta: {x: 0, y: 0}
|
||||||
m_Pivot: {x: 0, y: 1}
|
m_Pivot: {x: 0, y: 1}
|
||||||
@@ -725,6 +725,61 @@ RectTransform:
|
|||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 0, y: 0}
|
m_SizeDelta: {x: 0, y: 0}
|
||||||
m_Pivot: {x: 0, y: 1}
|
m_Pivot: {x: 0, y: 1}
|
||||||
|
--- !u!1 &2990183197483319577
|
||||||
|
GameObject:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
serializedVersion: 6
|
||||||
|
m_Component:
|
||||||
|
- component: {fileID: 4804535188045471244}
|
||||||
|
- component: {fileID: 1865200477437870815}
|
||||||
|
m_Layer: 5
|
||||||
|
m_Name: UIDocument
|
||||||
|
m_TagString: Untagged
|
||||||
|
m_Icon: {fileID: 0}
|
||||||
|
m_NavMeshLayer: 0
|
||||||
|
m_StaticEditorFlags: 0
|
||||||
|
m_IsActive: 1
|
||||||
|
--- !u!224 &4804535188045471244
|
||||||
|
RectTransform:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2990183197483319577}
|
||||||
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
|
m_ConstrainProportionsScale: 0
|
||||||
|
m_Children: []
|
||||||
|
m_Father: {fileID: 9025896673595153456}
|
||||||
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
|
m_SizeDelta: {x: 0, y: 0}
|
||||||
|
m_Pivot: {x: 0, y: 1}
|
||||||
|
--- !u!114 &1865200477437870815
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 2990183197483319577}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 19102, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
m_PanelSettings: {fileID: 11400000, guid: d0483162e9c37f142808df93bbff57df, type: 2}
|
||||||
|
m_ParentUI: {fileID: 0}
|
||||||
|
sourceAsset: {fileID: 0}
|
||||||
|
m_SortingOrder: 0
|
||||||
|
m_WorldSpaceSizeMode: 1
|
||||||
|
m_WorldSpaceWidth: 1920
|
||||||
|
m_WorldSpaceHeight: 1080
|
||||||
--- !u!1 &3961259076805554449
|
--- !u!1 &3961259076805554449
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -760,9 +815,9 @@ RectTransform:
|
|||||||
m_Father: {fileID: 7604279544518364067}
|
m_Father: {fileID: 7604279544518364067}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
m_AnchorMax: {x: 1, y: 1}
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 0, y: 0}
|
m_SizeDelta: {x: -3, y: -3}
|
||||||
m_Pivot: {x: 0, y: 1}
|
m_Pivot: {x: 0, y: 1}
|
||||||
--- !u!222 &1518543331700824407
|
--- !u!222 &1518543331700824407
|
||||||
CanvasRenderer:
|
CanvasRenderer:
|
||||||
@@ -1051,6 +1106,8 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 4cdfc0facccb5164e87bd49a9a9be1e3, type: 3}
|
m_Script: {fileID: 11500000, guid: 4cdfc0facccb5164e87bd49a9a9be1e3, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
|
minLeftWeight: 0.1
|
||||||
|
maxLeftWeight: 0.9
|
||||||
--- !u!222 &97109830855792030
|
--- !u!222 &97109830855792030
|
||||||
CanvasRenderer:
|
CanvasRenderer:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -1167,7 +1224,7 @@ RectTransform:
|
|||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 1}
|
m_AnchorMin: {x: 0, y: 1}
|
||||||
m_AnchorMax: {x: 0, y: 1}
|
m_AnchorMax: {x: 0, y: 1}
|
||||||
m_AnchoredPosition: {x: 0, y: 0.00021767142}
|
m_AnchoredPosition: {x: 0, y: 69.000015}
|
||||||
m_SizeDelta: {x: 1920, y: 1020}
|
m_SizeDelta: {x: 1920, y: 1020}
|
||||||
m_Pivot: {x: 0, y: 1}
|
m_Pivot: {x: 0, y: 1}
|
||||||
--- !u!222 &7045571067825403265
|
--- !u!222 &7045571067825403265
|
||||||
@@ -1243,9 +1300,9 @@ RectTransform:
|
|||||||
m_Father: {fileID: 7604279544518364067}
|
m_Father: {fileID: 7604279544518364067}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
m_AnchorMax: {x: 1, y: 0}
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 0, y: 6}
|
m_SizeDelta: {x: -3, y: 6}
|
||||||
m_Pivot: {x: 0, y: 0}
|
m_Pivot: {x: 0, y: 0}
|
||||||
--- !u!222 &5169180487801724270
|
--- !u!222 &5169180487801724270
|
||||||
CanvasRenderer:
|
CanvasRenderer:
|
||||||
@@ -1329,7 +1386,7 @@ MonoBehaviour:
|
|||||||
m_HandleRect: {fileID: 2403078154567262100}
|
m_HandleRect: {fileID: 2403078154567262100}
|
||||||
m_Direction: 0
|
m_Direction: 0
|
||||||
m_Value: 0
|
m_Value: 0
|
||||||
m_Size: 0.19557291
|
m_Size: 0.203125
|
||||||
m_NumberOfSteps: 0
|
m_NumberOfSteps: 0
|
||||||
m_OnValueChanged:
|
m_OnValueChanged:
|
||||||
m_PersistentCalls:
|
m_PersistentCalls:
|
||||||
@@ -1638,6 +1695,7 @@ RectTransform:
|
|||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
m_Children:
|
m_Children:
|
||||||
- {fileID: 7604279544518364067}
|
- {fileID: 7604279544518364067}
|
||||||
|
- {fileID: 4804535188045471244}
|
||||||
- {fileID: 4167651103787393564}
|
- {fileID: 4167651103787393564}
|
||||||
m_Father: {fileID: 906658657234234412}
|
m_Father: {fileID: 906658657234234412}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
@@ -1658,6 +1716,13 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 1af5017c83fc5cf4d8fbd1d2a801a095, type: 3}
|
m_Script: {fileID: 11500000, guid: 1af5017c83fc5cf4d8fbd1d2a801a095, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
|
uiDocument: {fileID: 1865200477437870815}
|
||||||
|
chartRootName: gantt-root
|
||||||
|
dayWidth: 16
|
||||||
|
rowHeight: 24
|
||||||
|
defaultStyleSheet: {fileID: 0}
|
||||||
|
showTodayLine: 0
|
||||||
|
logAlignmentDebug: 0
|
||||||
--- !u!114 &5206902728958016667
|
--- !u!114 &5206902728958016667
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -1696,7 +1761,7 @@ GameObject:
|
|||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 0
|
||||||
--- !u!224 &7604279544518364067
|
--- !u!224 &7604279544518364067
|
||||||
RectTransform:
|
RectTransform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -1881,7 +1946,7 @@ RectTransform:
|
|||||||
m_Father: {fileID: 8165087109326168183}
|
m_Father: {fileID: 8165087109326168183}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
m_AnchorMax: {x: 1, y: 0.9322155}
|
||||||
m_AnchoredPosition: {x: 0, y: 0}
|
m_AnchoredPosition: {x: 0, y: 0}
|
||||||
m_SizeDelta: {x: 0, y: 0}
|
m_SizeDelta: {x: 0, y: 0}
|
||||||
m_Pivot: {x: 0, y: 1}
|
m_Pivot: {x: 0, y: 1}
|
||||||
@@ -1931,10 +1996,18 @@ PrefabInstance:
|
|||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_TransformParent: {fileID: 906658657234234412}
|
m_TransformParent: {fileID: 906658657234234412}
|
||||||
m_Modifications:
|
m_Modifications:
|
||||||
|
- target: {fileID: 212658739084426523, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_IsActive
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 709763408010213073, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
- target: {fileID: 709763408010213073, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
propertyPath: m_AnchoredPosition.y
|
propertyPath: m_AnchoredPosition.y
|
||||||
value: 0
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 1085624549214928409, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_IsActive
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 1574318677252675885, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
- target: {fileID: 1574318677252675885, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
propertyPath: m_Pivot.x
|
propertyPath: m_Pivot.x
|
||||||
value: 0
|
value: 0
|
||||||
@@ -2015,6 +2088,22 @@ PrefabInstance:
|
|||||||
propertyPath: m_LocalEulerAnglesHint.z
|
propertyPath: m_LocalEulerAnglesHint.z
|
||||||
value: 0
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 1765160980165362397, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_AnchorMax.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 1765160980165362397, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_AnchorMin.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 1765160980165362397, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_AnchoredPosition.x
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 1765160980165362397, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_AnchoredPosition.y
|
||||||
|
value: 0
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 1883121240313448487, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
- target: {fileID: 1883121240313448487, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
propertyPath: m_SizeDelta.x
|
propertyPath: m_SizeDelta.x
|
||||||
value: 0
|
value: 0
|
||||||
@@ -2027,6 +2116,18 @@ PrefabInstance:
|
|||||||
propertyPath: m_Name
|
propertyPath: m_Name
|
||||||
value: ShiHierarchyWindow
|
value: ShiHierarchyWindow
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 2713569951654231804, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_IsActive
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 2903664282734591486, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_IsActive
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 3351413207641142582, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
|
propertyPath: m_SizeDelta.y
|
||||||
|
value: 2
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 3351413207641142582, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
- target: {fileID: 3351413207641142582, guid: c6c35cdcefd487f4b910ceed76b50a8f, type: 3}
|
||||||
propertyPath: m_AnchoredPosition.y
|
propertyPath: m_AnchoredPosition.y
|
||||||
value: 0
|
value: 0
|
||||||
|
|||||||
8
Assets/Resources/SHI/Styles.meta
Normal file
8
Assets/Resources/SHI/Styles.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 64fb2d86b4390f04287dd53425bd2316
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
16
Assets/Resources/SHI/Styles/GanttChart.uss
Normal file
16
Assets/Resources/SHI/Styles/GanttChart.uss
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* Basic USS for Gantt */
|
||||||
|
.header-container { }
|
||||||
|
.month-row { }
|
||||||
|
.week-row { }
|
||||||
|
.day-row { }
|
||||||
|
.rows-container { }
|
||||||
|
.chart-row { flex-direction: row; height:28px; position: relative; border-bottom:1px solid #3A3A3A; }
|
||||||
|
.hierarchy-cell { font-size:11px; width:240px; padding-left:4px; }
|
||||||
|
.segments-layer { flex-grow:1; position: relative; }
|
||||||
|
.segment { border-radius:3px; height:22px; background-color: rgba(64,128,220,0.6); }
|
||||||
|
.marker { color:#2AA3FF; font-size:12px; }
|
||||||
|
.row-selected { background-color: rgba(255,255,0,0.15); }
|
||||||
|
.segment-selected { outline:2px solid #FFD500; }
|
||||||
|
.span-cell { text-align:center; font-size:11px; border-right:1px solid #222; }
|
||||||
|
.day-cell { text-align:center; font-size:10px; border-right:1px solid #333; width:16px; }
|
||||||
|
.today-line { position:absolute; width:1px; background:#ff6b00; top:0; bottom:0; }
|
||||||
11
Assets/Resources/SHI/Styles/GanttChart.uss.meta
Normal file
11
Assets/Resources/SHI/Styles/GanttChart.uss.meta
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4557a013dc4822f499286c5a202a41a5
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
disableValidation: 0
|
||||||
8
Assets/Resources/Styles.meta
Normal file
8
Assets/Resources/Styles.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 2f462e493bcdb4149ad987a8e92e24dc
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -38,21 +38,7 @@ public class ShiPopupSample : MonoBehaviour
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class RawScheduleSegment
|
|
||||||
{
|
|
||||||
public string ItemId;
|
|
||||||
public string Start;
|
|
||||||
public string End;
|
|
||||||
public float Progress;
|
|
||||||
public string Type;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable]
|
|
||||||
public class RawGanttChartData
|
|
||||||
{
|
|
||||||
public RawScheduleSegment[] Segments;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// StreamingAssets에서 샘플 간트 JSON과 모델을 읽어 모달에 적용합니다.
|
/// StreamingAssets에서 샘플 간트 JSON과 모델을 읽어 모달에 적용합니다.
|
||||||
@@ -67,43 +53,10 @@ public class ShiPopupSample : MonoBehaviour
|
|||||||
|
|
||||||
string sa = Application.streamingAssetsPath;
|
string sa = Application.streamingAssetsPath;
|
||||||
string glbPath = Path.Combine(sa, "block.glb");
|
string glbPath = Path.Combine(sa, "block.glb");
|
||||||
string jsonPath = Path.Combine(sa, "sample_gantt_data.json");
|
string jsonPath = "isop_chart_short.json";
|
||||||
|
|
||||||
// 플랫폼에 따라 UnityWebRequest 사용이 필요할 수 있음(여기선 Editor/Standalone 가정)
|
|
||||||
RawGanttChartData raw = null;
|
Debug.Log($"Loaded blockDetailModal:{blockDetailModal}");
|
||||||
try
|
await blockDetailModal.LoadData(glbPath, jsonPath);
|
||||||
{
|
|
||||||
if (File.Exists(jsonPath))
|
|
||||||
raw = JsonUtility.FromJson<RawGanttChartData>(File.ReadAllText(jsonPath));
|
|
||||||
else
|
|
||||||
Debug.LogWarning($"Sample JSON not found: {jsonPath}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.LogError($"Failed reading JSON: {ex}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var gantt = new GanttChartData();
|
|
||||||
if (raw?.Segments != null)
|
|
||||||
{
|
|
||||||
foreach (var s in raw.Segments)
|
|
||||||
{
|
|
||||||
DateTime start, end;
|
|
||||||
if (!DateTime.TryParse(s.Start, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out start))
|
|
||||||
start = DateTime.UtcNow;
|
|
||||||
if (!DateTime.TryParse(s.End, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out end))
|
|
||||||
end = start.AddDays(1);
|
|
||||||
gantt.Segments.Add(new ScheduleSegment
|
|
||||||
{
|
|
||||||
ItemKey = s.ItemId,
|
|
||||||
Start = start,
|
|
||||||
End = end,
|
|
||||||
Progress = s.Progress,
|
|
||||||
Type = s.Type ?? string.Empty
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Debug.Log($"Loaded blockDetailModal:{blockDetailModal} {gantt.Segments.Count} gantt segments.");
|
|
||||||
await blockDetailModal.LoadData(glbPath, gantt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,7 +285,6 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: d549b7af14589244f84abc73c089c735, type: 3}
|
m_Script: {fileID: 11500000, guid: d549b7af14589244f84abc73c089c735, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
blockDetailModal: {fileID: 0}
|
|
||||||
blockDetailModalPrefab: {fileID: 8096719646118634716, guid: 468ba7fb3e976344eb340d05295e56e6, type: 3}
|
blockDetailModalPrefab: {fileID: 8096719646118634716, guid: 468ba7fb3e976344eb340d05295e56e6, type: 3}
|
||||||
openModalButton: {fileID: 1154598113}
|
openModalButton: {fileID: 1154598113}
|
||||||
--- !u!4 &717482005
|
--- !u!4 &717482005
|
||||||
@@ -504,7 +503,7 @@ Canvas:
|
|||||||
m_AdditionalShaderChannelsFlag: 25
|
m_AdditionalShaderChannelsFlag: 25
|
||||||
m_UpdateRectTransformForStandalone: 0
|
m_UpdateRectTransformForStandalone: 0
|
||||||
m_SortingLayerID: 0
|
m_SortingLayerID: 0
|
||||||
m_SortingOrder: 0
|
m_SortingOrder: 1
|
||||||
m_TargetDisplay: 0
|
m_TargetDisplay: 0
|
||||||
--- !u!224 &1304768793
|
--- !u!224 &1304768793
|
||||||
RectTransform:
|
RectTransform:
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
|
using Best.HTTP.JSON.LitJson;
|
||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UI;
|
using UnityEngine.UI;
|
||||||
|
using UVC.Json;
|
||||||
|
|
||||||
namespace SHI.modal
|
namespace SHI.modal
|
||||||
{
|
{
|
||||||
@@ -192,9 +196,9 @@ namespace SHI.modal
|
|||||||
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
|
/// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="gltfPath">glTF/glb 파일 경로.</param>
|
/// <param name="gltfPath">glTF/glb 파일 경로.</param>
|
||||||
/// <param name="gantt">간트 데이터셋.</param>
|
/// <param name="ganttPath">간트 데이터셋 경로.</param>
|
||||||
/// <param name="externalCt">외부 취소 토큰.</param>
|
/// <param name="externalCt">외부 취소 토큰.</param>
|
||||||
public async UniTask LoadData(string gltfPath, GanttChartData gantt, CancellationToken externalCt = default)
|
public async UniTask LoadData(string gltfPath, string ganttPath, CancellationToken externalCt = default)
|
||||||
{
|
{
|
||||||
Debug.Log($"BlockDetailModal: LoadData {gltfPath}");
|
Debug.Log($"BlockDetailModal: LoadData {gltfPath}");
|
||||||
|
|
||||||
@@ -224,7 +228,7 @@ namespace SHI.modal
|
|||||||
BuildKeyMaps(items);
|
BuildKeyMaps(items);
|
||||||
|
|
||||||
if (listView != null) listView.SetupData(items);
|
if (listView != null) listView.SetupData(items);
|
||||||
if (chartView != null) chartView.LoadData(gantt);
|
if (chartView != null) chartView.LoadFromStreamingAssets(ganttPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BuildKeyMaps(IEnumerable<ModelDetailListItemData> items)
|
private void BuildKeyMaps(IEnumerable<ModelDetailListItemData> items)
|
||||||
|
|||||||
@@ -23,12 +23,86 @@ namespace SHI.modal
|
|||||||
public string Type { get; set; } = string.Empty;
|
public string Type { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 기간/마커 정보(Structured 행 모드용).
|
||||||
|
/// </summary>
|
||||||
|
public class DateRange
|
||||||
|
{
|
||||||
|
public DateTime? Start { get; set; }
|
||||||
|
public DateTime? End { get; set; }
|
||||||
|
public int? Duration { get; set; }
|
||||||
|
public bool IsMarker => Start.HasValue && !End.HasValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 블록/행 단위 일정 레코드.
|
||||||
|
/// </summary>
|
||||||
|
public class BlockScheduleRow
|
||||||
|
{
|
||||||
|
public string? ProjNo { get; set; }
|
||||||
|
public string? BlockNo { get; set; }
|
||||||
|
public string? L1 { get; set; }
|
||||||
|
public string? L2 { get; set; }
|
||||||
|
public string? L3 { get; set; }
|
||||||
|
public string? L4 { get; set; }
|
||||||
|
public string? L5 { get; set; }
|
||||||
|
public string? L6 { get; set; }
|
||||||
|
public string? L7 { get; set; }
|
||||||
|
public string? L8 { get; set; }
|
||||||
|
/// <summary>코드(예:43,4B 등)별 기간/마커.</summary>
|
||||||
|
public Dictionary<string, DateRange> Periods { get; } = new Dictionary<string, DateRange>();
|
||||||
|
// 추가 메타 정보(JSON 컬럼 매핑)
|
||||||
|
public string? BlkDsc { get; set; } // BLK_DSC
|
||||||
|
public string? ShipType { get; set; } // SHIP_TYPE
|
||||||
|
public string? ShrGb { get; set; } // SHR_GB
|
||||||
|
public float? Lth { get; set; } // LTH
|
||||||
|
public float? Bth { get; set; } // BTH
|
||||||
|
public float? Hgt { get; set; } // HGT
|
||||||
|
public float? NetWgt { get; set; } // NET_WGT
|
||||||
|
public float? EqupWgt { get; set; } // EQUP_WGT
|
||||||
|
public float? LftWgt { get; set; } // LFT_WGT
|
||||||
|
public string? WkType { get; set; } // WK_TYPE
|
||||||
|
public string? Wc31 { get; set; } // WC31
|
||||||
|
|
||||||
|
// 편의 접근자: 자주 쓰는 코드 맵핑
|
||||||
|
/// <summary>STDT01: 파란색 @ 표시 일(yyyymmdd)</summary>
|
||||||
|
public DateTime? STDT01 { get => GetStart("01"); set => SetStart("01", value); }
|
||||||
|
/// <summary>STDT21: 파란색 막대 시작일(yyyymmdd)</summary>
|
||||||
|
public DateTime? STDT21 { get => GetStart("21"); set => SetStart("21", value); }
|
||||||
|
/// <summary>FNDT21: 파란색 막대 종료일(yyyymmdd)</summary>
|
||||||
|
public DateTime? FNDT21 { get => GetEnd("21"); set => SetEnd("21", value); }
|
||||||
|
/// <summary>STDT23: 연두색 막대 시작일(yyyymmdd)</summary>
|
||||||
|
public DateTime? STDT23 { get => GetStart("23"); set => SetStart("23", value); }
|
||||||
|
/// <summary>FNDT23: 연두색 막대 종료일(yyyymmdd)</summary>
|
||||||
|
public DateTime? FNDT23 { get => GetEnd("23"); set => SetEnd("23", value); }
|
||||||
|
|
||||||
|
private DateRange GetOrCreateRange(string code)
|
||||||
|
{
|
||||||
|
if (!Periods.TryGetValue(code, out var dr) || dr == null)
|
||||||
|
{
|
||||||
|
dr = new DateRange();
|
||||||
|
Periods[code] = dr;
|
||||||
|
}
|
||||||
|
return dr;
|
||||||
|
}
|
||||||
|
private DateTime? GetStart(string code) => Periods.TryGetValue(code, out var dr) ? dr.Start : null;
|
||||||
|
private DateTime? GetEnd(string code) => Periods.TryGetValue(code, out var dr) ? dr.End : null;
|
||||||
|
private void SetStart(string code, DateTime? v) { var dr = GetOrCreateRange(code); dr.Start = v; }
|
||||||
|
private void SetEnd(string code, DateTime? v) { var dr = GetOrCreateRange(code); dr.End = v; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 간단한 간트 차트 데이터셋입니다.
|
/// 간단한 간트 차트 데이터셋입니다.
|
||||||
|
/// 기존 Segments 기반(Flat) + Rows 기반(Structured) 동시 지원.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class GanttChartData
|
public class GanttChartData
|
||||||
{
|
{
|
||||||
/// <summary>표시 순서대로의 세그먼트 컬렉션.</summary>
|
/// <summary>표시 순서대로의 세그먼트 컬렉션 (Flat 모드).</summary>
|
||||||
public List<ScheduleSegment> Segments { get; set; } = new List<ScheduleSegment>();
|
public List<ScheduleSegment> Segments { get; set; } = new List<ScheduleSegment>();
|
||||||
|
/// <summary>Structured 행 모드 컬렉션.</summary>
|
||||||
|
public List<BlockScheduleRow> Rows { get; set; } = new List<BlockScheduleRow>();
|
||||||
|
public DateTime? MinDate { get; set; }
|
||||||
|
public DateTime? MaxDate { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
202
Assets/Scripts/SHI/modal/GanttJsonParser.cs
Normal file
202
Assets/Scripts/SHI/modal/GanttJsonParser.cs
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
// GanttJsonParser: 행 기반(STDTxx/FNDTxx/DURxx) 간트 데이터 파서
|
||||||
|
// 요구: 오류가 있어도 삭제하지 말 것.
|
||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace SHI.modal
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// isop_chart.json과 같은 행 배열(딕셔너리)의 STDTxx / FNDTxx / DURxx 패턴을 해석하여 <see cref="GanttChartData"/>를 생성합니다.
|
||||||
|
/// </summary>
|
||||||
|
public static class GanttJsonParser
|
||||||
|
{
|
||||||
|
private static readonly Regex StartRegex = new Regex("^STDT([0-9A-FM]{2})$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex EndRegex = new Regex("^FNDT([0-9A-FM]{2})$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex DurRegex = new Regex("^DUR([0-9A-FM]{2})$", RegexOptions.Compiled);
|
||||||
|
private const string DateFormat = "yyyyMMdd";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 배열 문자열을 파싱해 Rows/Segments/MinDate/MaxDate를 채웁니다.
|
||||||
|
/// Duration 누락 시 (End-Start)+1을 사용합니다.
|
||||||
|
/// </summary>
|
||||||
|
public static GanttChartData Parse(string json, bool computeDurationIfMissing = true)
|
||||||
|
{
|
||||||
|
var data = new GanttChartData();
|
||||||
|
if (string.IsNullOrWhiteSpace(json)) return data;
|
||||||
|
|
||||||
|
List<Dictionary<string, object>>? rows;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Newtonsoft.Json 사용 (Assembly 내 참조 필요). 실패 시 경고 후 빈 데이터 반환.
|
||||||
|
rows = Newtonsoft.Json.JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"GanttJsonParser.Parse: JSON deserialize failed: {ex.Message}");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (rows == null) return data;
|
||||||
|
|
||||||
|
DateTime? min = null; DateTime? max = null;
|
||||||
|
|
||||||
|
foreach (var raw in rows)
|
||||||
|
{
|
||||||
|
if (raw == null) continue;
|
||||||
|
var row = new BlockScheduleRow
|
||||||
|
{
|
||||||
|
ProjNo = GetStr(raw, "PROJ_NO"),
|
||||||
|
BlockNo = GetStr(raw, "BLK_NO"),
|
||||||
|
L1 = GetStr(raw, "L1"),
|
||||||
|
L2 = GetStr(raw, "L2"),
|
||||||
|
L3 = GetStr(raw, "L3"),
|
||||||
|
L4 = GetStr(raw, "L4"),
|
||||||
|
L5 = GetStr(raw, "L5"),
|
||||||
|
L6 = GetStr(raw, "L6"),
|
||||||
|
L7 = GetStr(raw, "L7"),
|
||||||
|
L8 = GetStr(raw, "L8"),
|
||||||
|
// 추가 메타
|
||||||
|
BlkDsc = GetStr(raw, "BLK_DSC"),
|
||||||
|
ShipType = GetStr(raw, "SHIP_TYPE"),
|
||||||
|
ShrGb = GetStr(raw, "SHR_GB"),
|
||||||
|
Lth = GetFloat(raw, "LTH"),
|
||||||
|
Bth = GetFloat(raw, "BTH"),
|
||||||
|
Hgt = GetFloat(raw, "HGT"),
|
||||||
|
NetWgt = GetFloat(raw, "NET_WGT"),
|
||||||
|
EqupWgt = GetFloat(raw, "EQUP_WGT"),
|
||||||
|
LftWgt = GetFloat(raw, "LFT_WGT"),
|
||||||
|
WkType = GetStr(raw, "WK_TYPE"),
|
||||||
|
Wc31 = GetStr(raw, "WC31"),
|
||||||
|
};
|
||||||
|
|
||||||
|
var startMap = new Dictionary<string, DateTime>();
|
||||||
|
var endMap = new Dictionary<string, DateTime>();
|
||||||
|
var durMap = new Dictionary<string, int>();
|
||||||
|
|
||||||
|
foreach (var kv in raw)
|
||||||
|
{
|
||||||
|
if (kv.Key == null || kv.Value == null) continue;
|
||||||
|
var val = kv.Value.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(val)) continue;
|
||||||
|
|
||||||
|
var sm = StartRegex.Match(kv.Key);
|
||||||
|
if (sm.Success)
|
||||||
|
{
|
||||||
|
if (TryParseDate(val, out var sdt))
|
||||||
|
{
|
||||||
|
var code = sm.Groups[1].Value;
|
||||||
|
if (startMap.ContainsKey(code)) Debug.Log($"GanttJsonParser: duplicate STDT for code={code}, last value wins.");
|
||||||
|
startMap[code] = sdt; UpdateMinMax(ref min, ref max, sdt); continue;
|
||||||
|
}
|
||||||
|
else { Debug.LogWarning($"GanttJsonParser: start date parse failed key={kv.Key} value={val}"); continue; }
|
||||||
|
}
|
||||||
|
var em = EndRegex.Match(kv.Key);
|
||||||
|
if (em.Success)
|
||||||
|
{
|
||||||
|
if (TryParseDate(val, out var edt))
|
||||||
|
{
|
||||||
|
var code = em.Groups[1].Value;
|
||||||
|
if (endMap.ContainsKey(code)) Debug.Log($"GanttJsonParser: duplicate FNDT for code={code}, last value wins.");
|
||||||
|
endMap[code] = edt; UpdateMinMax(ref min, ref max, edt); continue;
|
||||||
|
}
|
||||||
|
else { Debug.LogWarning($"GanttJsonParser: end date parse failed key={kv.Key} value={val}"); continue; }
|
||||||
|
}
|
||||||
|
var dm = DurRegex.Match(kv.Key);
|
||||||
|
if (dm.Success)
|
||||||
|
{
|
||||||
|
var code = dm.Groups[1].Value;
|
||||||
|
if (int.TryParse(val, out var dur))
|
||||||
|
{
|
||||||
|
if (durMap.ContainsKey(code)) Debug.Log($"GanttJsonParser: duplicate DUR for code={code}, last value wins.");
|
||||||
|
durMap[code] = dur; continue;
|
||||||
|
}
|
||||||
|
else { Debug.LogWarning($"GanttJsonParser: duration parse failed key={kv.Key} value={val}"); continue; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var codes = new HashSet<string>(startMap.Keys);
|
||||||
|
codes.UnionWith(endMap.Keys);
|
||||||
|
codes.UnionWith(durMap.Keys);
|
||||||
|
|
||||||
|
foreach (var code in codes)
|
||||||
|
{
|
||||||
|
var range = new DateRange();
|
||||||
|
if (startMap.TryGetValue(code, out var s)) range.Start = s;
|
||||||
|
if (endMap.TryGetValue(code, out var e)) range.End = e;
|
||||||
|
if (durMap.TryGetValue(code, out var d)) range.Duration = d;
|
||||||
|
else if (computeDurationIfMissing && range.Start.HasValue && range.End.HasValue)
|
||||||
|
range.Duration = (int)(range.End.Value.Date - range.Start.Value.Date).TotalDays + 1;
|
||||||
|
|
||||||
|
// Start > End 정책: Skip + Warning
|
||||||
|
if (range.Start.HasValue && range.End.HasValue && range.End.Value.Date < range.Start.Value.Date)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"GanttJsonParser: Start > End. Skip record block={row.BlockNo} code={code} start={range.Start:yyyyMMdd} end={range.End:yyyyMMdd}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
row.Periods[code] = range;
|
||||||
|
|
||||||
|
if (range.Start.HasValue && range.End.HasValue)
|
||||||
|
{
|
||||||
|
// Flat segment 동시 생성 (유효한 경우만)
|
||||||
|
data.Segments.Add(new ScheduleSegment
|
||||||
|
{
|
||||||
|
ItemKey = (row.BlockNo ?? string.Empty) + "|" + code,
|
||||||
|
ItemId = Guid.Empty,
|
||||||
|
Start = range.Start.Value,
|
||||||
|
End = range.End.Value,
|
||||||
|
Progress = 0f,
|
||||||
|
Type = code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.Rows.Add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.MinDate = min; data.MaxDate = max;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateMinMax(ref DateTime? min, ref DateTime? max, DateTime v)
|
||||||
|
{
|
||||||
|
if (!min.HasValue || v < min.Value) min = v;
|
||||||
|
if (!max.HasValue || v > max.Value) max = v;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseDate(string s, out DateTime dt)
|
||||||
|
=> DateTime.TryParseExact(s, DateFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out dt);
|
||||||
|
|
||||||
|
private static string? GetStr(Dictionary<string, object> dict, string key)
|
||||||
|
=> dict.TryGetValue(key, out var v) ? v?.ToString() : null;
|
||||||
|
|
||||||
|
private static float? GetFloat(Dictionary<string, object> dict, string key)
|
||||||
|
{
|
||||||
|
if (!dict.TryGetValue(key, out var v) || v == null) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
switch (v)
|
||||||
|
{
|
||||||
|
case float f: return f;
|
||||||
|
case double d: return (float)d;
|
||||||
|
case decimal m: return (float)m;
|
||||||
|
case int i: return i;
|
||||||
|
case long l: return l;
|
||||||
|
case short s: return s;
|
||||||
|
case string str:
|
||||||
|
if (string.IsNullOrWhiteSpace(str)) return null;
|
||||||
|
if (float.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out var pf)) return pf;
|
||||||
|
if (float.TryParse(str, NumberStyles.Any, CultureInfo.CurrentCulture, out pf)) return pf;
|
||||||
|
return null;
|
||||||
|
default:
|
||||||
|
// 마지막 시도: System.Convert
|
||||||
|
return (float)Convert.ToDouble(v, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/SHI/modal/GanttJsonParser.cs.meta
Normal file
2
Assets/Scripts/SHI/modal/GanttJsonParser.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0472849d1587ac84491dbf3f1b4c68ee
|
||||||
@@ -1,61 +1,513 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.UIElements;
|
||||||
|
|
||||||
namespace SHI.modal
|
namespace SHI.modal
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 차트 패널: 간트 데이터 바인딩/선택 동기화용 경량 래퍼.
|
/// 간트 데이터 로드 및(임시) UI Toolkit 렌더 스텁.
|
||||||
/// 실제 UI Toolkit 간트 구현 전까지 스텁 동작을 수행합니다.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ModelDetailChartView : MonoBehaviour
|
public class ModelDetailChartView : MonoBehaviour
|
||||||
{
|
{
|
||||||
public Action<string>? OnRowClickedByKey;
|
public Action<string>? OnRowClickedByKey;
|
||||||
public Action<Guid>? OnRowClicked; // backward compat
|
public Action<Guid>? OnRowClicked;
|
||||||
|
|
||||||
private GanttChartData? _data;
|
private GanttChartData? _data;
|
||||||
|
[SerializeField] private UIDocument? uiDocument; // UI Toolkit 루트 문서
|
||||||
|
[SerializeField] private string chartRootName = "gantt-root"; // 동적 삽입 컨테이너 이름
|
||||||
|
[SerializeField] private float dayWidth = 16f;
|
||||||
|
[SerializeField] private int rowHeight = 24;
|
||||||
|
[SerializeField] private StyleSheet? defaultStyleSheet; // USS 연결용 (Inspector 가능)
|
||||||
|
[SerializeField] private bool showTodayLine = false;
|
||||||
|
[SerializeField] private bool logAlignmentDebug = false; // 위치 동기화 디버그 로그
|
||||||
|
|
||||||
|
// 선택 상태 관리
|
||||||
|
private readonly Dictionary<string, VisualElement> _rowElements = new Dictionary<string, VisualElement>();
|
||||||
|
private readonly Dictionary<string, VisualElement> _segmentElements = new Dictionary<string, VisualElement>();
|
||||||
|
private string? _selectedKey;
|
||||||
|
private string? _selectedSegmentKey;
|
||||||
|
|
||||||
|
// 필터 상태
|
||||||
|
private DateTime? _filterFrom;
|
||||||
|
private DateTime? _filterTo;
|
||||||
|
private string? _filterCode;
|
||||||
|
private string? _filterBlockNo;
|
||||||
|
|
||||||
|
// 헤더 스크롤 동기화 참조
|
||||||
|
private VisualElement? _monthRowRef;
|
||||||
|
private VisualElement? _weekRowRef;
|
||||||
|
private VisualElement? _dayRowRef;
|
||||||
|
private ScrollView? _bodyScrollRef;
|
||||||
|
|
||||||
|
public void LoadFromStreamingAssets(string fileName = "isop_chart.json")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Ensure a UIDocument exists so we can render
|
||||||
|
EnsureUIDocument();
|
||||||
|
|
||||||
|
var path = Path.Combine(Application.streamingAssetsPath, fileName);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
Debug.LogError($"File not found: {path}");
|
||||||
|
// 더미 데이터 로드 정책
|
||||||
|
_data = CreateDummyData();
|
||||||
|
BuildStubUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//5MB 초과 경고
|
||||||
|
try
|
||||||
|
{ var fi = new FileInfo(path); if (fi.Exists && fi.Length > 5 * 1024 * 1024) Debug.LogWarning($"Chart JSON is large ({fi.Length / (1024 * 1024f):F1} MB). Consider streaming parser."); }
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
|
||||||
|
// 행 기반(STDT/FNDT/DUR) 시도
|
||||||
|
var structured = TryParseStructured(json);
|
||||||
|
if (structured != null)
|
||||||
|
{
|
||||||
|
LoadData(structured);
|
||||||
|
BuildStubUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.LogWarning("Unsupported chart JSON format.");
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Debug.LogError($"LoadFromStreamingAssets failed: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 간트 데이터를 바인딩합니다(스텁 구현).
|
|
||||||
/// </summary>
|
|
||||||
public void LoadData(GanttChartData data)
|
public void LoadData(GanttChartData data)
|
||||||
{
|
{
|
||||||
_data = data;
|
_data = data;
|
||||||
Debug.Log($"ModelDetailChartView.LoadData: segments={data?.Segments?.Count ??0}");
|
Debug.Log($"ChartView.LoadData: rows={data.Rows.Count} seg={data.Segments.Count} range={data.MinDate}->{data.MaxDate}");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 외부 키로 행을 하이라이트합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void SelectByItemKey(string key)
|
public void SelectByItemKey(string key)
|
||||||
{
|
{
|
||||||
if (_data == null) { Debug.Log("ChartView.SelectByItemKey: no data"); return; }
|
if (_data == null) { Debug.Log("SelectByItemKey: no data"); return; }
|
||||||
Debug.Log($"Chart highlight by key: {key}");
|
_selectedKey = key ?? string.Empty;
|
||||||
|
ApplySelectionVisuals();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Guid 식별자로 행을 하이라이트합니다.
|
|
||||||
/// </summary>
|
|
||||||
public void SelectByItemId(Guid id)
|
public void SelectByItemId(Guid id)
|
||||||
|
{ if (_data == null) { Debug.Log("SelectByItemId: no data"); return; } Debug.Log($"Highlight row id={id}"); }
|
||||||
|
public void SimulateRowClickKey(string key) => OnRowClickedByKey?.Invoke(key);
|
||||||
|
public void SimulateRowClick(string id) { if (Guid.TryParse(id, out var g)) OnRowClicked?.Invoke(g); }
|
||||||
|
public void Dispose() { _data = null; ClearUI(); _rowElements.Clear(); _segmentElements.Clear(); _selectedKey = null; _selectedSegmentKey = null; }
|
||||||
|
public void ToggleTodayLine(bool enabled) { showTodayLine = enabled; BuildStubUI(); }
|
||||||
|
public void SetZoom(float newDayWidth) { var clamped = Mathf.Clamp(newDayWidth, 8f, 32f); if (Mathf.Approximately(clamped, dayWidth)) return; dayWidth = clamped; BuildStubUI(); }
|
||||||
|
public void ApplyFilter(DateTime? from, DateTime? to, string? code = null, string? blockNo = null)
|
||||||
|
{ _filterFrom = from; _filterTo = to; _filterCode = string.IsNullOrEmpty(code) ? null : code; _filterBlockNo = string.IsNullOrEmpty(blockNo) ? null : blockNo; BuildStubUI(); }
|
||||||
|
|
||||||
|
private void ClearUI()
|
||||||
{
|
{
|
||||||
if (_data == null) { Debug.Log("ChartView.SelectByItemId: no data"); return; }
|
if (!EnsureUIDocument()) return;
|
||||||
Debug.Log($"Chart highlight by id: {id}");
|
var root = uiDocument!.rootVisualElement?.Q(chartRootName);
|
||||||
|
root?.Clear();
|
||||||
|
_rowElements.Clear();
|
||||||
|
_segmentElements.Clear();
|
||||||
|
_monthRowRef = _weekRowRef = _dayRowRef = null;
|
||||||
|
_bodyScrollRef = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI 시뮬레이션 콜백
|
private GanttChartData? TryParseStructured(string json)
|
||||||
public void SimulateRowClickKey(string key)
|
|
||||||
{
|
{
|
||||||
OnRowClickedByKey?.Invoke(key);
|
try
|
||||||
|
{
|
||||||
|
var data = GanttJsonParser.Parse(json, computeDurationIfMissing: true);
|
||||||
|
// 구조적 파싱 성공 조건: 최소 한 행 또는 최소 한 기간
|
||||||
|
if (data != null && (data.Rows.Count > 0 || data.Segments.Count > 0))
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"Structured parse failed: {ex.Message}");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SimulateRowClick(string id)
|
// 간단 렌더(헤더+행 막대) 스텁
|
||||||
|
private void BuildStubUI()
|
||||||
{
|
{
|
||||||
if (Guid.TryParse(id, out var guid)) OnRowClicked?.Invoke(guid);
|
if (!EnsureUIDocument()) { Debug.Log("BuildStubUI: UIDocument missing and could not be created"); return; }
|
||||||
|
if (_data == null) { Debug.Log("BuildStubUI: no data"); return; }
|
||||||
|
var root = uiDocument!.rootVisualElement;
|
||||||
|
root.style.backgroundColor = StyleKeyword.Null; // 전체 패널은 투명 유지
|
||||||
|
var container = root.Q(chartRootName) ?? new VisualElement { name = chartRootName };
|
||||||
|
if (container.parent == null) root.Add(container);
|
||||||
|
container.Clear();
|
||||||
|
container.style.flexDirection = FlexDirection.Column;
|
||||||
|
container.style.position = Position.Absolute;
|
||||||
|
container.style.backgroundColor = Color.white; // 뷰 영역만 흰색
|
||||||
|
container.style.overflow = Overflow.Hidden; // 뷰 밖으로 나가는 내용 클리핑
|
||||||
|
EnsureStyleSheet(container);
|
||||||
|
|
||||||
|
// Sync size and position to RectTransform
|
||||||
|
SyncContainerSize();
|
||||||
|
SyncContainerPosition();
|
||||||
|
|
||||||
|
var min = _data.MinDate ?? DateTime.Today;
|
||||||
|
var max = _data.MaxDate ?? min;
|
||||||
|
var totalDays = Math.Max(1, (int)(max.Date - min.Date).TotalDays + 1);
|
||||||
|
var timelineWidth = totalDays * dayWidth;
|
||||||
|
|
||||||
|
// Header container (Month/Week/Day)
|
||||||
|
var header = new VisualElement { name = "gantt-header" };
|
||||||
|
header.AddToClassList("header-container");
|
||||||
|
header.style.flexDirection = FlexDirection.Column;
|
||||||
|
header.style.backgroundColor = new Color(0.13f, 0.13f, 0.15f, 1f);
|
||||||
|
header.style.height = 60;
|
||||||
|
header.style.position = Position.Relative;
|
||||||
|
header.style.width = timelineWidth;
|
||||||
|
BuildHeaderRows(header, min, max, totalDays, showTodayLine);
|
||||||
|
container.Add(header);
|
||||||
|
|
||||||
|
_rowElements.Clear(); _segmentElements.Clear();
|
||||||
|
|
||||||
|
// Body scroll
|
||||||
|
var bodyScroll = new ScrollView(ScrollViewMode.VerticalAndHorizontal) { name = "body-scroll" };
|
||||||
|
bodyScroll.contentContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
container.Add(bodyScroll);
|
||||||
|
_bodyScrollRef = bodyScroll;
|
||||||
|
|
||||||
|
var rowsContainer = new VisualElement { name = "rows-container" };
|
||||||
|
rowsContainer.AddToClassList("rows-container");
|
||||||
|
rowsContainer.style.flexDirection = FlexDirection.Column;
|
||||||
|
rowsContainer.style.width = timelineWidth; // 수평 스크롤 기준 폭
|
||||||
|
bodyScroll.Add(rowsContainer);
|
||||||
|
|
||||||
|
// Rows만 렌더 (Segments Fallback 제거)
|
||||||
|
foreach (var row in _data.Rows)
|
||||||
|
{
|
||||||
|
if (_filterBlockNo != null && !string.Equals(row.BlockNo ?? string.Empty, _filterBlockNo, StringComparison.OrdinalIgnoreCase)) { bool hasAlt = false; if (!hasAlt) continue; }
|
||||||
|
BuildRow(rowsContainer, row.BlockNo ?? row.L1 ?? "", min, timelineWidth, row);
|
||||||
|
}
|
||||||
|
|
||||||
|
HookHeaderScrollSync();
|
||||||
|
// 선택 상태 반영
|
||||||
|
ApplySelectionVisuals();
|
||||||
|
ApplySegmentSelectionVisuals();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void EnsureStyleSheet(VisualElement container)
|
||||||
/// 바인딩된 데이터를 해제합니다.
|
{
|
||||||
/// </summary>
|
try
|
||||||
public void Dispose() { _data = null; }
|
{
|
||||||
|
if (!EnsureUIDocument()) return;
|
||||||
|
var root = uiDocument!.rootVisualElement;
|
||||||
|
if (defaultStyleSheet != null)
|
||||||
|
{
|
||||||
|
if (!root.styleSheets.Contains(defaultStyleSheet)) root.styleSheets.Add(defaultStyleSheet);
|
||||||
|
if (!container.styleSheets.Contains(defaultStyleSheet)) container.styleSheets.Add(defaultStyleSheet);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Resources/Styles/GanttChart.uss 를 시도
|
||||||
|
var ss = Resources.Load<StyleSheet>("SHI/Styles/GanttChart");
|
||||||
|
if (ss != null)
|
||||||
|
{
|
||||||
|
if (!root.styleSheets.Contains(ss)) root.styleSheets.Add(ss);
|
||||||
|
if (!container.styleSheets.Contains(ss)) container.styleSheets.Add(ss);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Debug.LogWarning($"EnsureStyleSheet failed: {ex.Message}"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildHeaderRows(VisualElement header, DateTime min, DateTime max, int totalDays, bool addToday)
|
||||||
|
{
|
||||||
|
var timelineWidth = totalDays * dayWidth;
|
||||||
|
|
||||||
|
// Month row
|
||||||
|
var monthRow = new VisualElement { name = "month-row" };
|
||||||
|
monthRow.AddToClassList("month-row");
|
||||||
|
monthRow.style.flexDirection = FlexDirection.Row;
|
||||||
|
monthRow.style.height = 20;
|
||||||
|
header.Add(monthRow);
|
||||||
|
|
||||||
|
// iterate months
|
||||||
|
var cur = new DateTime(min.Year, min.Month, 1);
|
||||||
|
var end = new DateTime(max.Year, max.Month, 1).AddMonths(1).AddDays(-1);
|
||||||
|
if (end < max) end = max;
|
||||||
|
while (cur <= max)
|
||||||
|
{
|
||||||
|
var monthStart = cur < min ? min : cur;
|
||||||
|
var lastDayOfMonth = new DateTime(cur.Year, cur.Month, DateTime.DaysInMonth(cur.Year, cur.Month));
|
||||||
|
var monthEnd = lastDayOfMonth > max ? max : lastDayOfMonth;
|
||||||
|
var days = Math.Max(1, (int)(monthEnd.Date - monthStart.Date).TotalDays + 1);
|
||||||
|
var cell = new Label(cur.ToString("yyyy MMM", CultureInfo.InvariantCulture));
|
||||||
|
cell.AddToClassList("span-cell");
|
||||||
|
cell.style.width = days * dayWidth;
|
||||||
|
cell.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
cell.style.color = new Color(0.9f, 0.9f, 0.9f, 1f); // 밝은 헤더 텍스트
|
||||||
|
monthRow.Add(cell);
|
||||||
|
cur = cur.AddMonths(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Week row (ISO Week, Mon-Sun)
|
||||||
|
var weekRow = new VisualElement { name = "week-row" };
|
||||||
|
weekRow.AddToClassList("week-row");
|
||||||
|
weekRow.style.flexDirection = FlexDirection.Row;
|
||||||
|
weekRow.style.height = 20;
|
||||||
|
header.Add(weekRow);
|
||||||
|
|
||||||
|
var culture = CultureInfo.InvariantCulture;
|
||||||
|
var cal = culture.Calendar;
|
||||||
|
var weekRule = CalendarWeekRule.FirstFourDayWeek;
|
||||||
|
var firstDay = DayOfWeek.Monday;
|
||||||
|
var d = min.Date;
|
||||||
|
while (d <= max.Date)
|
||||||
|
{
|
||||||
|
var isoWeek = cal.GetWeekOfYear(d, weekRule, firstDay);
|
||||||
|
// span until week end or max
|
||||||
|
var weekEnd = d.AddDays((7 + (int)DayOfWeek.Sunday - (int)d.DayOfWeek) % 7); // upcoming Sunday (inclusive)
|
||||||
|
if (weekEnd > max.Date) weekEnd = max.Date;
|
||||||
|
var days = Math.Max(1, (int)(weekEnd - d).TotalDays + 1);
|
||||||
|
var cell = new Label($"W{isoWeek}");
|
||||||
|
cell.AddToClassList("span-cell");
|
||||||
|
cell.style.width = days * dayWidth;
|
||||||
|
cell.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
cell.style.color = new Color(0.85f, 0.85f, 0.85f, 1f);
|
||||||
|
weekRow.Add(cell);
|
||||||
|
d = weekEnd.AddDays(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Day row
|
||||||
|
var dayRow = new VisualElement { name = "day-row" };
|
||||||
|
dayRow.AddToClassList("day-row");
|
||||||
|
dayRow.style.flexDirection = FlexDirection.Row;
|
||||||
|
dayRow.style.height = 20;
|
||||||
|
dayRow.style.width = timelineWidth;
|
||||||
|
var dc = min.Date;
|
||||||
|
while (dc <= max.Date)
|
||||||
|
{
|
||||||
|
var cell = new Label(dc.Day.ToString()) { name = $"day-{dc:yyyyMMdd}" };
|
||||||
|
cell.AddToClassList("day-cell");
|
||||||
|
cell.style.width = dayWidth;
|
||||||
|
cell.style.unityTextAlign = TextAnchor.MiddleCenter;
|
||||||
|
cell.style.color = new Color(0.8f, 0.8f, 0.8f, 1f);
|
||||||
|
dayRow.Add(cell);
|
||||||
|
dc = dc.AddDays(1);
|
||||||
|
}
|
||||||
|
header.Add(dayRow);
|
||||||
|
|
||||||
|
// Today line on header
|
||||||
|
if (addToday)
|
||||||
|
{
|
||||||
|
var today = DateTime.Today.Date;
|
||||||
|
if (today >= min.Date && today <= max.Date)
|
||||||
|
{
|
||||||
|
var leftDays = (int)(today - min.Date).TotalDays;
|
||||||
|
var line = new VisualElement { name = "today-line-header" };
|
||||||
|
line.AddToClassList("today-line");
|
||||||
|
line.style.position = Position.Absolute;
|
||||||
|
line.style.left = leftDays * dayWidth;
|
||||||
|
line.style.width = 1;
|
||||||
|
line.style.top = 0;
|
||||||
|
line.style.bottom = 0;
|
||||||
|
line.style.backgroundColor = new Color(1f, 0.42f, 0f, 1f);
|
||||||
|
header.Add(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_monthRowRef = monthRow; _weekRowRef = weekRow; _dayRowRef = dayRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HookHeaderScrollSync()
|
||||||
|
{
|
||||||
|
if (_bodyScrollRef == null) return;
|
||||||
|
void Sync()
|
||||||
|
{ try { var off = _bodyScrollRef.scrollOffset; float x = off.x; if (_monthRowRef != null) _monthRowRef.style.marginLeft = -x; if (_weekRowRef != null) _weekRowRef.style.marginLeft = -x; if (_dayRowRef != null) _dayRowRef.style.marginLeft = -x; } catch { } }
|
||||||
|
// 초기1회
|
||||||
|
Sync();
|
||||||
|
try { if (_bodyScrollRef.horizontalScroller != null) _bodyScrollRef.horizontalScroller.valueChanged += _ => Sync(); } catch { }
|
||||||
|
// fallback: schedule polling (lightweight)
|
||||||
|
_bodyScrollRef.schedule.Execute(() => Sync()).Every(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildRow(VisualElement container, string key, DateTime min, float timelineWidth, BlockScheduleRow row)
|
||||||
|
{
|
||||||
|
var rowVE = new VisualElement { name = "row-" + key };
|
||||||
|
rowVE.AddToClassList("chart-row"); rowVE.style.flexDirection = FlexDirection.Row; rowVE.style.height = rowHeight; rowVE.style.borderBottomWidth = 1; rowVE.style.borderBottomColor = new Color(0, 0, 0, 0.4f);
|
||||||
|
|
||||||
|
var label = new Label(string.IsNullOrEmpty(row.BlockNo) ? (row.L1 ?? key) : row.BlockNo) { name = "row-label" };
|
||||||
|
label.AddToClassList("hierarchy-cell"); label.style.width = 240; rowVE.Add(label);
|
||||||
|
|
||||||
|
// Segment layer
|
||||||
|
var segLayer = new VisualElement { name = "segments" };
|
||||||
|
segLayer.AddToClassList("segments-layer"); segLayer.style.flexGrow = 1; segLayer.style.position = Position.Relative; segLayer.style.width = timelineWidth; rowVE.Add(segLayer);
|
||||||
|
|
||||||
|
bool anyRendered = false;
|
||||||
|
foreach (var kv in row.Periods)
|
||||||
|
{
|
||||||
|
var code = kv.Key; var dr = kv.Value;
|
||||||
|
if (_filterCode != null && !string.Equals(code, _filterCode, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
if (_filterFrom.HasValue || _filterTo.HasValue)
|
||||||
|
{ var start = dr.Start; var end = dr.End ?? dr.Start; if (start.HasValue) { var s = start.Value.Date; var e = end.HasValue ? end.Value.Date : s; if (!IsOverlapping(_filterFrom, _filterTo, s, e)) continue; } }
|
||||||
|
if (!dr.Start.HasValue) continue;
|
||||||
|
if (dr.End.HasValue && dr.End.Value.Date < dr.Start.Value.Date) { Debug.LogWarning($"Start > End detected. Skip: key={key} code={code} start={dr.Start:yyyyMMdd} end={dr.End:yyyyMMdd}"); continue; }
|
||||||
|
var segKey = (row.BlockNo ?? key) + "|" + code;
|
||||||
|
if (dr.Start.HasValue && dr.End.HasValue)
|
||||||
|
{
|
||||||
|
var leftDays = (int)(dr.Start.Value.Date - min.Date).TotalDays; var durDays = Math.Max(1, (int)(dr.End.Value.Date - dr.Start.Value.Date).TotalDays + 1);
|
||||||
|
var seg = new VisualElement { name = $"seg-{code}" };
|
||||||
|
seg.AddToClassList("segment"); seg.AddToClassList($"seg-code-{code}");
|
||||||
|
seg.style.position = Position.Absolute; seg.style.left = leftDays * dayWidth; seg.style.width = durDays * dayWidth; seg.style.height = rowHeight - 4; seg.style.backgroundColor = new Color(0.25f, 0.55f, 0.85f, 0.6f);
|
||||||
|
seg.style.borderTopLeftRadius = 3; seg.style.borderBottomLeftRadius = 3; seg.style.borderTopRightRadius = 3; seg.style.borderBottomRightRadius = 3;
|
||||||
|
seg.tooltip = $"{row.BlockNo ?? key}|{code}: {dr.Start:yyyy-MM-dd} ~ {dr.End:yyyy-MM-dd}";
|
||||||
|
seg.RegisterCallback<ClickEvent>(_ => { _selectedKey = row.BlockNo ?? key; _selectedSegmentKey = segKey; ApplySelectionVisuals(); ApplySegmentSelectionVisuals(); OnRowClickedByKey?.Invoke(row.BlockNo ?? key); });
|
||||||
|
segLayer.Add(seg); if (!_segmentElements.ContainsKey(segKey)) _segmentElements[segKey] = seg; anyRendered = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var leftDays = (int)(dr.Start.Value.Date - min.Date).TotalDays;
|
||||||
|
var marker = new Label("@") { name = $"mark-{code}" };
|
||||||
|
marker.AddToClassList("marker"); marker.AddToClassList($"marker-code-{code}");
|
||||||
|
marker.style.position = Position.Absolute; marker.style.left = leftDays * dayWidth; marker.style.width = dayWidth; marker.style.height = rowHeight - 4; marker.style.unityTextAlign = TextAnchor.MiddleCenter; marker.style.color = new Color(0.2f, 0.8f, 1f, 1f);
|
||||||
|
marker.tooltip = $"{row.BlockNo ?? key}|{code}: {dr.Start:yyyy-MM-dd}";
|
||||||
|
marker.RegisterCallback<ClickEvent>(_ => { _selectedKey = row.BlockNo ?? key; _selectedSegmentKey = segKey; ApplySelectionVisuals(); ApplySegmentSelectionVisuals(); OnRowClickedByKey?.Invoke(row.BlockNo ?? key); });
|
||||||
|
segLayer.Add(marker); if (!_segmentElements.ContainsKey(segKey)) _segmentElements[segKey] = marker; anyRendered = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!anyRendered) return; // 필터에 의해 비어지면 행 숨김
|
||||||
|
container.Add(rowVE);
|
||||||
|
if (!string.IsNullOrEmpty(key) && !_rowElements.ContainsKey(key)) _rowElements[key] = rowVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsOverlapping(DateTime? from, DateTime? to, DateTime s, DateTime e)
|
||||||
|
{ if (!from.HasValue && !to.HasValue) return true; if (from.HasValue && e < from.Value.Date) return false; if (to.HasValue && s > to.Value.Date) return false; return true; }
|
||||||
|
|
||||||
|
private void ApplySelectionVisuals()
|
||||||
|
{
|
||||||
|
// 모든 하이라이트 제거
|
||||||
|
foreach (var kv in _rowElements)
|
||||||
|
{
|
||||||
|
kv.Value.RemoveFromClassList("row-selected");
|
||||||
|
// 백업 인라인 스타일 제거/초기화
|
||||||
|
kv.Value.style.backgroundColor = StyleKeyword.Null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(_selectedKey)) return;
|
||||||
|
if (_rowElements.TryGetValue(_selectedKey, out var ve))
|
||||||
|
{
|
||||||
|
ve.AddToClassList("row-selected");
|
||||||
|
// USS 없을 때 가시성 확보용 백업 스타일
|
||||||
|
ve.style.backgroundColor = new Color(1f, 1f, 0f, 0.15f);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.Log($"ApplySelectionVisuals: row not found for key={_selectedKey}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySegmentSelectionVisuals()
|
||||||
|
{
|
||||||
|
foreach (var kv in _segmentElements) kv.Value.RemoveFromClassList("segment-selected");
|
||||||
|
if (string.IsNullOrEmpty(_selectedSegmentKey)) return;
|
||||||
|
if (_segmentElements.TryGetValue(_selectedSegmentKey, out var ve)) ve.AddToClassList("segment-selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GanttChartData CreateDummyData()
|
||||||
|
{
|
||||||
|
var min = DateTime.Today.AddDays(-3); var max = DateTime.Today.AddDays(10);
|
||||||
|
var row1 = new BlockScheduleRow { BlockNo = "DUMMY-1" }; row1.Periods["21"] = new DateRange { Start = min.AddDays(2), End = min.AddDays(6) }; row1.Periods["23"] = new DateRange { Start = min.AddDays(8) };
|
||||||
|
var row2 = new BlockScheduleRow { BlockNo = "DUMMY-2" }; row2.Periods["21"] = new DateRange { Start = min.AddDays(1), End = min.AddDays(3) }; row2.Periods["23"] = new DateRange { Start = min.AddDays(5), End = max };
|
||||||
|
return new GanttChartData { Rows = new List<BlockScheduleRow> { row1, row2 }, MinDate = min, MaxDate = max };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to ensure a UIDocument exists so the view can render even if not wired in inspector
|
||||||
|
private bool EnsureUIDocument()
|
||||||
|
{
|
||||||
|
if (uiDocument != null) return true;
|
||||||
|
|
||||||
|
// Try get existing on this GameObject
|
||||||
|
var ud = GetComponent<UIDocument>();
|
||||||
|
if (ud == null)
|
||||||
|
{
|
||||||
|
// Create a new UIDocument component
|
||||||
|
try { ud = gameObject.AddComponent<UIDocument>(); }
|
||||||
|
catch (Exception ex) { Debug.LogWarning($"Failed to add UIDocument: {ex.Message}"); return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try find or create PanelSettings
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (ud.panelSettings == null)
|
||||||
|
{
|
||||||
|
// Try Resources first
|
||||||
|
var ps = Resources.Load<PanelSettings>("SHI/PanelSettings/DefaultPanelSettings");
|
||||||
|
if (ps == null)
|
||||||
|
{
|
||||||
|
// Create runtime instance as fallback (not saved as asset)
|
||||||
|
ps = ScriptableObject.CreateInstance<PanelSettings>();
|
||||||
|
}
|
||||||
|
ud.panelSettings = ps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
|
||||||
|
uiDocument = ud;
|
||||||
|
return uiDocument != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep UI Toolkit container sized to this RectTransform
|
||||||
|
private void SyncContainerSize()
|
||||||
|
{
|
||||||
|
if (!EnsureUIDocument()) return;
|
||||||
|
var rt = GetComponent<RectTransform>();
|
||||||
|
if (rt == null) return;
|
||||||
|
var size = rt.rect.size;
|
||||||
|
var root = uiDocument!.rootVisualElement;
|
||||||
|
var container = root.Q(chartRootName) ?? new VisualElement { name = chartRootName };
|
||||||
|
if (container.parent == null) root.Add(container);
|
||||||
|
container.style.position = Position.Absolute;
|
||||||
|
container.style.width = size.x;
|
||||||
|
container.style.height = size.y;
|
||||||
|
|
||||||
|
// Adjust body scroll height if available (header fixed height60)
|
||||||
|
var body = container.Q("body-scroll");
|
||||||
|
if (body != null)
|
||||||
|
{
|
||||||
|
var h = Mathf.Max(0, size.y - 60f);
|
||||||
|
body.style.height = h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncContainerPosition()
|
||||||
|
{
|
||||||
|
if (!EnsureUIDocument()) return;
|
||||||
|
var rt = GetComponent<RectTransform>(); if (rt == null) return;
|
||||||
|
var canvas = GetComponentInParent<Canvas>();
|
||||||
|
Camera cam = null;
|
||||||
|
if (canvas != null)
|
||||||
|
{
|
||||||
|
if (canvas.renderMode == RenderMode.ScreenSpaceCamera || canvas.renderMode == RenderMode.WorldSpace)
|
||||||
|
cam = canvas.worldCamera != null ? canvas.worldCamera : Camera.main;
|
||||||
|
}
|
||||||
|
var corners = new Vector3[4];
|
||||||
|
rt.GetWorldCorners(corners); //0:BL,1:TL,2:TR,3:BR
|
||||||
|
var tlWorld = corners[1];
|
||||||
|
var tlScreen = RectTransformUtility.WorldToScreenPoint(cam, tlWorld);
|
||||||
|
var left = tlScreen.x;
|
||||||
|
var top = Screen.height - tlScreen.y; // convert to top-left origin
|
||||||
|
var root = uiDocument!.rootVisualElement;
|
||||||
|
var container = root.Q(chartRootName) ?? new VisualElement { name = chartRootName };
|
||||||
|
if (container.parent == null) root.Add(container);
|
||||||
|
container.style.position = Position.Absolute;
|
||||||
|
container.style.left = left;
|
||||||
|
container.style.top = top;
|
||||||
|
|
||||||
|
if (logAlignmentDebug)
|
||||||
|
Debug.Log($"Align container to RectTransform TL screen=({left:F1},{top:F1}) size=({rt.rect.width:F1},{rt.rect.height:F1})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// React to RectTransform size changes
|
||||||
|
private void OnRectTransformDimensionsChange()
|
||||||
|
{
|
||||||
|
SyncContainerSize();
|
||||||
|
SyncContainerPosition();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
Assets/StreamingAssets/isop_chart.json
Normal file
1
Assets/StreamingAssets/isop_chart.json
Normal file
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 7d10ed05d091bee418cfec1b13cd1f35
|
guid: ff0f561565b35914fa1f73f069c13058
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
1
Assets/StreamingAssets/isop_chart_short.json
Normal file
1
Assets/StreamingAssets/isop_chart_short.json
Normal file
File diff suppressed because one or more lines are too long
7
Assets/StreamingAssets/isop_chart_short.json.meta
Normal file
7
Assets/StreamingAssets/isop_chart_short.json.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8323ff7ff1709114bb72dc8380c5dbf9
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
{
|
|
||||||
"project": {
|
|
||||||
"name": "B-Project Gantt Chart",
|
|
||||||
"startDate": "2024-10-31",
|
|
||||||
"endDate": "2024-11-21",
|
|
||||||
"timeUnit": "day",
|
|
||||||
"totalDays": 21
|
|
||||||
},
|
|
||||||
"timeAxis": {
|
|
||||||
"dates": [
|
|
||||||
"2024-10-31",
|
|
||||||
"2024-11-01",
|
|
||||||
"2024-11-02",
|
|
||||||
"2024-11-03",
|
|
||||||
"2024-11-04",
|
|
||||||
"2024-11-05",
|
|
||||||
"2024-11-06",
|
|
||||||
"2024-11-07",
|
|
||||||
"2024-11-08",
|
|
||||||
"2024-11-09",
|
|
||||||
"2024-11-10",
|
|
||||||
"2024-11-11",
|
|
||||||
"2024-11-12",
|
|
||||||
"2024-11-13",
|
|
||||||
"2024-11-14",
|
|
||||||
"2024-11-15",
|
|
||||||
"2024-11-16",
|
|
||||||
"2024-11-17",
|
|
||||||
"2024-11-18",
|
|
||||||
"2024-11-19",
|
|
||||||
"2024-11-20",
|
|
||||||
"2024-11-21"
|
|
||||||
],
|
|
||||||
"format": "MM/dd",
|
|
||||||
"label": "일자"
|
|
||||||
},
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"id": "B111P",
|
|
||||||
"name": "B111P",
|
|
||||||
"startDate": "2024-10-31",
|
|
||||||
"endDate": "2024-11-07",
|
|
||||||
"durationDays": 7,
|
|
||||||
"progress": 100,
|
|
||||||
"owner": "팀A",
|
|
||||||
"color": "#4CAF50",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "B112P",
|
|
||||||
"name": "B112P",
|
|
||||||
"startDate": "2024-11-01",
|
|
||||||
"endDate": "2024-11-07",
|
|
||||||
"durationDays": 6,
|
|
||||||
"progress": 36,
|
|
||||||
"owner": "팀B",
|
|
||||||
"color": "#2196F3",
|
|
||||||
"priority": "medium",
|
|
||||||
"dependencies": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "B121P",
|
|
||||||
"name": "B121P",
|
|
||||||
"startDate": "2024-11-08",
|
|
||||||
"endDate": "2024-11-21",
|
|
||||||
"durationDays": 13,
|
|
||||||
"progress": 30,
|
|
||||||
"owner": "팀C",
|
|
||||||
"color": "#FF9800",
|
|
||||||
"priority": "medium",
|
|
||||||
"dependencies": [ "B111P" ]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "B122P",
|
|
||||||
"name": "B122P",
|
|
||||||
"startDate": "2024-11-08",
|
|
||||||
"endDate": "2024-11-21",
|
|
||||||
"durationDays": 13,
|
|
||||||
"progress": 95,
|
|
||||||
"owner": "팀D",
|
|
||||||
"color": "#F44336",
|
|
||||||
"priority": "high",
|
|
||||||
"dependencies": [ "B112P" ]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "B123P",
|
|
||||||
"name": "B123P",
|
|
||||||
"startDate": "2024-11-08",
|
|
||||||
"endDate": "2024-11-21",
|
|
||||||
"durationDays": 13,
|
|
||||||
"progress": 65,
|
|
||||||
"owner": "팀E",
|
|
||||||
"color": "#FFEB3B",
|
|
||||||
"priority": "medium",
|
|
||||||
"dependencies": [ "B111P", "B112P" ]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "B124P",
|
|
||||||
"name": "B124P",
|
|
||||||
"startDate": "2024-11-08",
|
|
||||||
"endDate": "2024-11-21",
|
|
||||||
"durationDays": 13,
|
|
||||||
"progress": 42,
|
|
||||||
"owner": "팀F",
|
|
||||||
"color": "#9C27B0",
|
|
||||||
"priority": "low",
|
|
||||||
"dependencies": [ "B121P" ]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"uiSettings": {
|
|
||||||
"rowHeight": 40,
|
|
||||||
"headerHeight": 50,
|
|
||||||
"taskBarHeight": 24,
|
|
||||||
"showProgressText": true,
|
|
||||||
"showOwner": true,
|
|
||||||
"showDependencies": true,
|
|
||||||
"gridLineColor": "#E0E0E0",
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"headerBackgroundColor": "#F5F5F5",
|
|
||||||
"taskListWidth": 200,
|
|
||||||
"timelineWidth": 840,
|
|
||||||
"dependencyLineColor": "#666666",
|
|
||||||
"dependencyLineWidth": 1,
|
|
||||||
"progressTextColor": "#FFFFFF",
|
|
||||||
"progressFontSize": 10,
|
|
||||||
"ownerTextColor": "#777777",
|
|
||||||
"ownerFontSize": 11
|
|
||||||
}
|
|
||||||
}
|
|
||||||
8
Assets/UI Toolkit.meta
Normal file
8
Assets/UI Toolkit.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b5b7597830068404bb8080c502b41ec8
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
46
Assets/UI Toolkit/PanelSettings.asset
Normal file
46
Assets/UI Toolkit/PanelSettings.asset
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &11400000
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 19101, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
m_Name: PanelSettings
|
||||||
|
m_EditorClassIdentifier:
|
||||||
|
themeUss: {fileID: -4733365628477956816, guid: 7f6362c1cd0b3074ab0f0f88dd27536b, type: 3}
|
||||||
|
m_DisableNoThemeWarning: 0
|
||||||
|
m_TargetTexture: {fileID: 0}
|
||||||
|
m_RenderMode: 0
|
||||||
|
m_WorldSpaceLayer: 0
|
||||||
|
m_ScaleMode: 1
|
||||||
|
m_ReferenceSpritePixelsPerUnit: 100
|
||||||
|
m_PixelsPerUnit: 100
|
||||||
|
m_Scale: 1
|
||||||
|
m_ReferenceDpi: 96
|
||||||
|
m_FallbackDpi: 96
|
||||||
|
m_ReferenceResolution: {x: 1200, y: 800}
|
||||||
|
m_ScreenMatchMode: 0
|
||||||
|
m_Match: 0
|
||||||
|
m_SortingOrder: 0
|
||||||
|
m_TargetDisplay: 0
|
||||||
|
m_BindingLogLevel: 0
|
||||||
|
m_ClearDepthStencil: 1
|
||||||
|
m_ClearColor: 0
|
||||||
|
m_ColorClearValue: {r: 0, g: 0, b: 0, a: 0}
|
||||||
|
m_VertexBudget: 0
|
||||||
|
m_DynamicAtlasSettings:
|
||||||
|
m_MinAtlasSize: 64
|
||||||
|
m_MaxAtlasSize: 4096
|
||||||
|
m_MaxSubTextureSize: 64
|
||||||
|
m_ActiveFilters: -1
|
||||||
|
m_AtlasBlitShader: {fileID: 9101, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_RuntimeShader: {fileID: 9100, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_RuntimeWorldShader: {fileID: 9102, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
|
m_ICUDataAsset: {fileID: 0}
|
||||||
|
forceGammaRendering: 0
|
||||||
|
textSettings: {fileID: 0}
|
||||||
8
Assets/UI Toolkit/PanelSettings.asset.meta
Normal file
8
Assets/UI Toolkit/PanelSettings.asset.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: d0483162e9c37f142808df93bbff57df
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 11400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Assets/UI Toolkit/UnityThemes.meta
Normal file
8
Assets/UI Toolkit/UnityThemes.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 162b1e89af74c404380d45248512962f
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import url("unity-theme://default");
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7f6362c1cd0b3074ab0f0f88dd27536b
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0}
|
||||||
|
disableValidation: 0
|
||||||
248
Assets/work.md
Normal file
248
Assets/work.md
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
# 간트 차트 구현 작업 지시서 (개선판 v1.3)
|
||||||
|
|
||||||
|
##1. 목표 (Overview)
|
||||||
|
- `StreamingAssets/isop_chart.json` 로드 후 UI Toolkit 기반 간트 차트 생성.
|
||||||
|
- 좌측 계층 컬럼(L1~L8) 표시, 우측 월/주/일3단 헤더 + 막대(기간) / 마커(단일일) 렌더.
|
||||||
|
- 막대/마커 클릭 시 `OnRowClickedByKey` 호출.
|
||||||
|
- 외부 선택 API(`SelectByItemKey`)로 행 하이라이트.
|
||||||
|
- 수평/수직 스크롤 및 향후 줌(dayWidth) 지원.
|
||||||
|
|
||||||
|
##1.1 프로젝트 개요
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|------|------|
|
||||||
|
| 프로젝트명 | SN2662 블록 B11GP 생산 계획 시각화 시스템 |
|
||||||
|
| 목적 | 블록 생산 일정 실시간 모니터링 / 지연 즉시 파악 / 현장 의사결정 지원 |
|
||||||
|
| 주요 사용자 | 생산 관리자, 현장 반장, 설계/자재 담당자 |
|
||||||
|
| 사용 환경 | 태블릿(터치) + PC(마우스) |
|
||||||
|
|
||||||
|
##2. 용어 & 기본 파라미터
|
||||||
|
| 항목 | 의미 | 기본값 | 조정 범위 |
|
||||||
|
|------|------|--------|-----------|
|
||||||
|
| dayWidth |1일당 픽셀 폭 |16px |8~32px (줌) |
|
||||||
|
| rowHeight |1행 높이 |28px |24~40px |
|
||||||
|
| MinDate/MaxDate | 전체 데이터 기간 경계 | 파싱 계산 | - |
|
||||||
|
| Periods | 코드별 기간/마커 딕셔너리 | 각 Row별 | - |
|
||||||
|
|
||||||
|
##3. 데이터 구조 (Model)
|
||||||
|
```csharp
|
||||||
|
class GanttChartData { List<BlockScheduleRow> Rows; DateTime? MinDate, MaxDate; }
|
||||||
|
class BlockScheduleRow { string? ProjNo, BlockNo, L1,L2,L3,L4,L5,L6,L7,L8; Dictionary<string,DateRange> Periods; }
|
||||||
|
class DateRange { DateTime? Start, End; int? Duration; bool IsMarker => Start.HasValue && !End.HasValue; }
|
||||||
|
```
|
||||||
|
- JSON 필드 패턴: `STDTxx` = 시작일(Start), `FNDTxx` = 종료일(End), `DURxx` = 기간(일수).
|
||||||
|
- 날짜 문자열 형식: `yyyyMMdd`.
|
||||||
|
|
||||||
|
##3.1 계획/실적 분리 확대 (미래 확장)
|
||||||
|
현재 JSON은 계획(Plan)만 제공. 실적(Actual) 데이터 추가 시 권장 구조:
|
||||||
|
```csharp
|
||||||
|
class ExtendedScheduleItem {
|
||||||
|
public string Code;
|
||||||
|
public DateTime? PlannedStart, PlannedEnd;
|
||||||
|
public DateTime? ActualStart, ActualEnd;
|
||||||
|
public int? PlannedDuration, ActualDuration;
|
||||||
|
public float Progress; //0..1
|
||||||
|
public bool IsCritical; // 임계경로
|
||||||
|
public int DelayDays => (ActualEnd.HasValue && PlannedEnd.HasValue) ? (int)(ActualEnd.Value - PlannedEnd.Value).TotalDays :0; // 음수면 조기 완료
|
||||||
|
public int EarlyFinishDays => (DelayDays <0) ? -DelayDays :0;
|
||||||
|
public bool IsDelayed => DelayDays >0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- DelayDays 음수일 경우 조기 완료(EarlyFinish) 표시 가능.
|
||||||
|
|
||||||
|
##4. 파싱 규칙
|
||||||
|
```regex
|
||||||
|
^STDT([0-9A-FM]{2})$ // 시작일
|
||||||
|
^FNDT([0-9A-FM]{2})$ // 종료일
|
||||||
|
^DUR([0-9A-FM]{2})$ // 일수
|
||||||
|
```
|
||||||
|
- 시작만 있고 종료 없음 → 마커(단일 이벤트).
|
||||||
|
- 시작/종료 모두 존재 → 기간 막대.
|
||||||
|
- Duration 없으면 캘린더 일수로 자동 계산.
|
||||||
|
|
||||||
|
##5. Duration 계산 정책 (확정)
|
||||||
|
- 기본: 캘린더 일수 `(End - Start).Days +1`.
|
||||||
|
- 옵션: 근무일(월~금) 필요 시 별도 플래그 `useBusinessDays` → 계산 시 주말 제외.
|
||||||
|
|
||||||
|
##6. 레이아웃 & 타임라인 렌더링
|
||||||
|
- `left(px) = (Start - MinDate).TotalDays * dayWidth`
|
||||||
|
- `width(px) = ((End - Start).TotalDays +1) * dayWidth`
|
||||||
|
- 헤더3단: Month / ISO Week / Day.
|
||||||
|
- Span cell width = 구간 일수 * dayWidth.
|
||||||
|
|
||||||
|
##7. UI 계층 구조
|
||||||
|
```
|
||||||
|
Root
|
||||||
|
├─ HeaderContainer (MonthRow, WeekRow, DayRow)
|
||||||
|
└─ BodyScroll
|
||||||
|
└─ RowsContainer
|
||||||
|
└─ Row
|
||||||
|
├─ HierarchyContainer (L1~L8)
|
||||||
|
└─ SegmentsLayer (절대 위치 막대/마커)
|
||||||
|
```
|
||||||
|
|
||||||
|
##8. 스타일 전략
|
||||||
|
- 코드별 USS 클래스: `seg-code-XX`, `marker-code-XX`.
|
||||||
|
- 선택 상태: `row-selected`, `segment-selected`.
|
||||||
|
- 외부 스타일 매핑 가능: `GanttStyleConfig.json` or ScriptableObject.
|
||||||
|
|
||||||
|
##9. 상호작용 & 이벤트
|
||||||
|
- Segment/Marker 클릭 → `OnRowClickedByKey(BlockNo)`.
|
||||||
|
- 세그먼트 식별 확장: `BlockNo|Code` 문자열 전달 고려.
|
||||||
|
- 터치 vs 마우스:
|
||||||
|
```csharp
|
||||||
|
private void OnSegmentPointerDown(PointerDownEvent evt, BlockScheduleRow row, string code){
|
||||||
|
if(evt.pointerType=="touch") ShowTouchPopup(row, code, evt.position);
|
||||||
|
else ShowTooltip(row, code, evt.position);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##10. 아키텍처 & 모듈 분리
|
||||||
|
| 모듈 | 책임 |
|
||||||
|
|------|------|
|
||||||
|
| GanttJsonParser | JSON → 모델 변환 |
|
||||||
|
| GanttBuilder | UI 생성/레이아웃 |
|
||||||
|
| GanttInteraction | 이벤트/툴팁/선택 |
|
||||||
|
| GanttVirtualizer | 대량 행 가상화 |
|
||||||
|
| GanttZoomController | 줌/재계산/캐시 무효화 |
|
||||||
|
| CriticalPathAnalyzer | 선후관계 기반 임계경로 계산 |
|
||||||
|
|
||||||
|
##11. 에러 처리 & 로깅 (정책 확정)
|
||||||
|
| 케이스 | 처리 | 로그 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 파일 없음 | 더미 데이터 로드 후 경고 | Error |
|
||||||
|
| 날짜 파싱 실패 | 해당 코드 Skip | Warning |
|
||||||
|
| Start > End | 무효 처리 (Skip) | Warning |
|
||||||
|
| 중복 Start/End | 마지막 값 채택 | Info |
|
||||||
|
| 파일 >5MB | 경고 + 스트리밍 파서 권고 | Info |
|
||||||
|
|
||||||
|
##12. 성능 & 가상화
|
||||||
|
- 행 >300 → 가상화 고려.
|
||||||
|
- 목표:1000행 로드 <500ms / 메모리 증가 <50MB.
|
||||||
|
- `CalculateBarRect` 캐시: Key=(Start,End,dayWidth,MinDate) → 줌(dayWidth)·범위 변경 시 캐시 Flush.
|
||||||
|
|
||||||
|
##13. 줌 & 스크롤 동기화
|
||||||
|
- 헤더/바디 스크롤 동기화: 바디 Scroll 이벤트에서 헤더 translateX 적용.
|
||||||
|
- 줌 변경 순서: dayWidth 변경 → 좌표 재계산 → 캐시 Flush → Layout pass.
|
||||||
|
|
||||||
|
##14. 확장 로드맵
|
||||||
|
- Tooltip / Today 라인 / 필터 API / PNG Export.
|
||||||
|
- 임계경로 강조 / D-3 알림 아이콘 / 조기완료(EarlyFinish) 표시.
|
||||||
|
|
||||||
|
##15. 체크리스트
|
||||||
|
### Mandatory
|
||||||
|
- [ ] JSON 로드
|
||||||
|
- [ ] 파싱 & Periods 구성
|
||||||
|
- [ ] Min/Max 날짜 계산
|
||||||
|
- [ ] 헤더 렌더
|
||||||
|
- [ ] 행 + 막대/마커 렌더
|
||||||
|
- [ ] 클릭 이벤트 연결
|
||||||
|
- [ ] 선택 하이라이트
|
||||||
|
- [ ] USS 기본 스타일
|
||||||
|
|
||||||
|
### Optional
|
||||||
|
- [ ] 근무일 Duration 옵션
|
||||||
|
- [ ] 가상화
|
||||||
|
- [ ] 줌 컨트롤
|
||||||
|
- [ ] Tooltip
|
||||||
|
- [ ] Today 라인
|
||||||
|
- [ ] 외부 색상 매핑
|
||||||
|
- [ ] 계획/실적 분리 모델
|
||||||
|
- [ ] 임계경로 분석
|
||||||
|
- [ ] D-3 알림 / 지연 깜빡임 / 조기완료 표시
|
||||||
|
|
||||||
|
##16. 구현 스켈레톤
|
||||||
|
```csharp
|
||||||
|
public void LoadFromStreamingAssets() {
|
||||||
|
var path = Path.Combine(Application.streamingAssetsPath, "isop_chart.json");
|
||||||
|
if (!File.Exists(path)) {
|
||||||
|
Debug.LogError($"File not found: {path}");
|
||||||
|
_data = CreateDummyData(); // 더미 로드 정책
|
||||||
|
BuildUI();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var json = File.ReadAllText(path);
|
||||||
|
var raw = JsonHelper.FromJsonDictArray(json);
|
||||||
|
var data = GanttJsonParser.Parse(raw);
|
||||||
|
LoadData(data);
|
||||||
|
BuildUI();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##17. 테스트 계획
|
||||||
|
| 분류 | 항목 |
|
||||||
|
|------|------|
|
||||||
|
| 단위 | 날짜 파싱 / Start>End Skip 검증 |
|
||||||
|
| 단위 | Duration 자동 계산(캘린더) & 근무일 옵션 |
|
||||||
|
| 단위 | 마커 vs 기간 구분(IsMarker) |
|
||||||
|
| 단위 | 캐시 Flush(zoom/minDate 변경) |
|
||||||
|
| 통합 | 클릭 이벤트 전달(BlockNo|Code) |
|
||||||
|
| 통합 | 선택 하이라이트 토글 |
|
||||||
|
| 성능 |1000 /5000 행 로드 시간 & 메모리 측정 |
|
||||||
|
| UX | 터치 팝업 ≤300ms 반응 |
|
||||||
|
| 시각 | 줌 변경 후 헤더/세그먼트 정렬 유지 |
|
||||||
|
|
||||||
|
##18. 리스크 & 대응
|
||||||
|
| 리스크 | 대응 |
|
||||||
|
|--------|------|
|
||||||
|
| 대용량 JSON | 스트리밍/청크 파서 |
|
||||||
|
| 잦은 줌 변경 | 최소/최대 줌 제한 + 캐시 재사용 |
|
||||||
|
| 색상 유지보수 | 외부 매핑 파일화 |
|
||||||
|
| 계획/실적 혼동 | 확장 시 모델 전환 마이그레이션 문서 |
|
||||||
|
| 태블릿 성능 저하 | 가상화 + 스타일 최소화 |
|
||||||
|
|
||||||
|
##19. Pending 결정
|
||||||
|
- 근무일 옵션 실제 사용 여부.
|
||||||
|
- 색상/코드 매핑 저장 위치.
|
||||||
|
- 임계경로 입력 포맷(선후관계) 정의.
|
||||||
|
|
||||||
|
##20. 사용 흐름
|
||||||
|
```csharp
|
||||||
|
var chart = FindObjectOfType<ModelDetailChartView>();
|
||||||
|
chart.LoadFromStreamingAssets();
|
||||||
|
chart.SelectByItemKey("B11GP");
|
||||||
|
```
|
||||||
|
|
||||||
|
##21. 향후 API 예시
|
||||||
|
```csharp
|
||||||
|
public void ApplyFilter(DateTime? from, DateTime? to, string? code = null, string? blockNo = null);
|
||||||
|
public void SetZoom(float newDayWidth); // Flush + 재계산
|
||||||
|
public void ToggleTodayLine(bool enabled);
|
||||||
|
public void SetActualDates(string blockNo, string code, DateTime? actualStart, DateTime? actualEnd);
|
||||||
|
public IReadOnlyList<string> GetCriticalPathCodes();
|
||||||
|
```
|
||||||
|
|
||||||
|
##22. USS 샘플
|
||||||
|
```css
|
||||||
|
.chart-row { flex-direction: row; height:28px; position: relative; border-bottom:1px solid #3A3A3A; }
|
||||||
|
.hierarchy-container { width:240px; flex-wrap: wrap; }
|
||||||
|
.hierarchy-cell { font-size:11px; width:60px; padding-left:4px; }
|
||||||
|
.segments-layer { flex-grow:1; position: relative; }
|
||||||
|
.segment { border-radius:3px; height:22px; }
|
||||||
|
.seg-code-21 { background:#3A7BFF; }
|
||||||
|
.seg-code-23 { background:#7BD95A; }
|
||||||
|
.marker { color:#2AA3FF; font-size:12px; }
|
||||||
|
.row-selected { background:rgba(255,255,0,0.15); }
|
||||||
|
.span-cell { text-align:center; font-size:11px; border-right:1px solid #222; }
|
||||||
|
.day-cell { text-align:center; font-size:10px; border-right:1px solid #333; width:16px; }
|
||||||
|
.segment.delayed { background:#d0021b; animation: blink1s infinite; }
|
||||||
|
.today-line { position:absolute; width:1px; background:#ff6b00; top:0; bottom:0; }
|
||||||
|
@keyframes blink {0%,100% { opacity:1; }50% { opacity:.3; } }
|
||||||
|
```
|
||||||
|
|
||||||
|
##23. 데이터 누락 대응
|
||||||
|
- 렌더 필터:4x 계열 우선 →3x/5x 존재 시 추가 →0x/Mx 전부 null이면 제외.
|
||||||
|
- Null 날짜: 렌더 생략, Progress=0.
|
||||||
|
- 파싱 실패: Skip + Warning.
|
||||||
|
|
||||||
|
##24. 최종 검수 체크리스트
|
||||||
|
| 항목 | 기준 |
|
||||||
|
|------|------|
|
||||||
|
| 날짜 정확성 | STDT/FNDT UI 값 = JSON 원본 |
|
||||||
|
| 가독성 | 태블릿 폰트 ≥14px, 대비 확보 |
|
||||||
|
| 반응성 | 터치 팝업 ≤300ms |
|
||||||
|
| 안정성 | Null90% 상황 문제 없음 |
|
||||||
|
| 임계경로 | 지연/조기완료 시 색상/아이콘 반영 |
|
||||||
|
| 줌 | dayWidth 변경 후 좌표/헤더 싱크 정확 |
|
||||||
|
|
||||||
|
##25. 종료
|
||||||
|
위 내용으로 구현 진행. Pending 결정 완료 시 문서 재갱신. (문서 버전: v1.3)
|
||||||
7
Assets/work.md.meta
Normal file
7
Assets/work.md.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 45336961e01ddb24f96ffe8794244ae2
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Reference in New Issue
Block a user