2025-11-12 16:48:34 +09:00
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
2025-11-17 19:30:05 +09:00
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
using System.IO;
|
2025-11-12 16:48:34 +09:00
|
|
|
using UnityEngine;
|
2025-11-17 19:30:05 +09:00
|
|
|
using UnityEngine.UIElements;
|
2025-11-12 16:48:34 +09:00
|
|
|
|
|
|
|
|
namespace SHI.modal
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
2025-11-17 19:30:05 +09:00
|
|
|
/// 간트 데이터 로드 및(임시) UI Toolkit 렌더 스텁.
|
2025-11-12 16:48:34 +09:00
|
|
|
/// </summary>
|
|
|
|
|
public class ModelDetailChartView : MonoBehaviour
|
|
|
|
|
{
|
2025-11-13 20:16:25 +09:00
|
|
|
public Action<string>? OnRowClickedByKey;
|
2025-11-17 19:30:05 +09:00
|
|
|
public Action<Guid>? OnRowClicked;
|
2025-11-13 20:16:25 +09:00
|
|
|
private GanttChartData? _data;
|
2025-11-17 19:30:05 +09:00
|
|
|
[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}"); }
|
|
|
|
|
}
|
2025-11-13 20:16:25 +09:00
|
|
|
|
|
|
|
|
public void LoadData(GanttChartData data)
|
|
|
|
|
{
|
|
|
|
|
_data = data;
|
2025-11-17 19:30:05 +09:00
|
|
|
Debug.Log($"ChartView.LoadData: rows={data.Rows.Count} seg={data.Segments.Count} range={data.MinDate}->{data.MaxDate}");
|
2025-11-13 20:16:25 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void SelectByItemKey(string key)
|
|
|
|
|
{
|
2025-11-17 19:30:05 +09:00
|
|
|
if (_data == null) { Debug.Log("SelectByItemKey: no data"); return; }
|
|
|
|
|
_selectedKey = key ?? string.Empty;
|
|
|
|
|
ApplySelectionVisuals();
|
2025-11-13 20:16:25 +09:00
|
|
|
}
|
|
|
|
|
public void SelectByItemId(Guid id)
|
2025-11-17 19:30:05 +09:00
|
|
|
{ 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 (!EnsureUIDocument()) return;
|
|
|
|
|
var root = uiDocument!.rootVisualElement?.Q(chartRootName);
|
|
|
|
|
root?.Clear();
|
|
|
|
|
_rowElements.Clear();
|
|
|
|
|
_segmentElements.Clear();
|
|
|
|
|
_monthRowRef = _weekRowRef = _dayRowRef = null;
|
|
|
|
|
_bodyScrollRef = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private GanttChartData? TryParseStructured(string json)
|
|
|
|
|
{
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 간단 렌더(헤더+행 막대) 스텁
|
|
|
|
|
private void BuildStubUI()
|
|
|
|
|
{
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void EnsureStyleSheet(VisualElement container)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
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()
|
2025-11-12 16:48:34 +09:00
|
|
|
{
|
2025-11-17 19:30:05 +09:00
|
|
|
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;
|
2025-11-12 16:48:34 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 19:30:05 +09:00
|
|
|
// Keep UI Toolkit container sized to this RectTransform
|
|
|
|
|
private void SyncContainerSize()
|
2025-11-12 16:48:34 +09:00
|
|
|
{
|
2025-11-17 19:30:05 +09:00
|
|
|
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;
|
|
|
|
|
}
|
2025-11-12 16:48:34 +09:00
|
|
|
}
|
2025-11-14 19:54:04 +09:00
|
|
|
|
2025-11-17 19:30:05 +09:00
|
|
|
private void SyncContainerPosition()
|
2025-11-13 20:16:25 +09:00
|
|
|
{
|
2025-11-17 19:30:05 +09:00
|
|
|
if (!EnsureUIDocument()) return;
|
|
|
|
|
var rt = GetComponent<RectTransform>(); if (rt == null) return;
|
|
|
|
|
var canvas = GetComponentInParent<Canvas>();
|
2025-11-18 18:14:53 +09:00
|
|
|
Camera? cam = null;
|
2025-11-17 19:30:05 +09:00
|
|
|
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})");
|
2025-11-13 20:16:25 +09:00
|
|
|
}
|
|
|
|
|
|
2025-11-17 19:30:05 +09:00
|
|
|
// React to RectTransform size changes
|
|
|
|
|
private void OnRectTransformDimensionsChange()
|
2025-11-18 18:14:53 +09:00
|
|
|
{
|
2025-11-17 19:30:05 +09:00
|
|
|
SyncContainerSize();
|
|
|
|
|
SyncContainerPosition();
|
|
|
|
|
}
|
2025-11-12 16:48:34 +09:00
|
|
|
}
|
|
|
|
|
}
|