Files
XRLib/Assets/Scripts/SHI/modal/ISOP/ISOPChart.cs
2025-11-27 13:41:37 +09:00

358 lines
14 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.TextCore.Text;
using UnityEngine.UIElements;
using UVC.Util;
namespace SHI.Modal.ISOP
{
[UxmlElement]
public partial class ISOPChart : VisualElement
{
// 리소스 경로 상수
private const string UXML_PATH = "SHI/Modal/ISOP/ISOPChart"; // Resources 폴더 기준 경로
private const string ROW_UXML_PATH = "SHI/Modal/ISOP/ISOPChartRow"; // Resources 폴더 기준 경로
/// <summary>
/// 모델 뷰가 확장 요청될 때 발생합니다.
/// </summary>
public Action? OnExpand;
private Button? _expandBtn;
private VisualTreeAsset taskRowTemplate; // row with 8 cells
private Color planColor = new Color(0.2f, 0.4f, 0.8f, 0.8f);
private Color actualColor = new Color(0.2f, 0.9f, 0.3f, 0.9f);
private float pixelsPerDay = 18f; // day cell width
private VisualElement root;
private VisualElement headerTitle;
private VisualElement headerTimeline;
private VisualElement monthsLayer;
private VisualElement weeksLayer;
private VisualElement daysLayer;
private VisualElement timelineContent;
private ScrollView? contentScroll;
private List<ISOPChartDataTask> tasks;
private int maxL;
private DateTime projectStartDate;
private DateTime projectEndDate;
private int totalDays;
public ISOPChart()
{
// UXML 로드
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (visualTree != null)
{
visualTree.CloneTree(this);
}
else
{
Debug.LogError($"Failed to load UXML at path: {UXML_PATH}");
}
taskRowTemplate = Resources.Load<VisualTreeAsset>(ROW_UXML_PATH);
if (taskRowTemplate == null)
{
Debug.LogError($"[TreeMenu] Item UXML not found at: {ROW_UXML_PATH}");
return;
}
_expandBtn = this.Q<Button>("expand-btn");
if (_expandBtn != null)
{
_expandBtn.clicked += () =>
{
OnExpand?.Invoke();
};
}
}
public void Load(string jsonFileName)
{
root = this;
LoadData(jsonFileName);
CalculateProjectRange();
InitializeUI();
SetupThreeTierHeader();
RenderTasks();
}
void LoadData(string jsonFileName)
{
var json = File.ReadAllText(jsonFileName);
var wrapper = JsonUtility.FromJson<ISOPChartDataWrapper>("{\"items\":" + json + "}");
tasks = wrapper.items.Where(t => !string.IsNullOrEmpty(t.STDT21) && t.STDT21 != "null").ToList();
foreach (var task in tasks)
task.CalculatedProgress = CalculateProgress(task);
maxL = CalculateMaxLevel();
}
int CalculateMaxLevel()
{
if (tasks.Any(t => !string.IsNullOrEmpty(t.L8))) return 8;
if (tasks.Any(t => !string.IsNullOrEmpty(t.L7))) return 7;
if (tasks.Any(t => !string.IsNullOrEmpty(t.L6))) return 6;
if (tasks.Any(t => !string.IsNullOrEmpty(t.L5))) return 5;
if (tasks.Any(t => !string.IsNullOrEmpty(t.L4))) return 4;
if (tasks.Any(t => !string.IsNullOrEmpty(t.L3))) return 3;
if (tasks.Any(t => !string.IsNullOrEmpty(t.L2))) return 2;
if (tasks.Any(t => !string.IsNullOrEmpty(t.L1))) return 1;
return 0;
}
void CalculateProjectRange()
{
var dates = new List<DateTime>();
foreach (var task in tasks)
{
if (task.GetPlanStart().HasValue) dates.Add(task.GetPlanStart().Value);
if (task.GetPlanEnd().HasValue) dates.Add(task.GetPlanEnd().Value);
if (task.GetActualStart().HasValue) dates.Add(task.GetActualStart().Value);
if (task.GetActualEnd().HasValue) dates.Add(task.GetActualEnd().Value);
}
projectStartDate = dates.Any() ? dates.Min().AddDays(-3) : new DateTime(2025, 1, 6);
projectEndDate = dates.Any() ? dates.Max().AddDays(3) : new DateTime(2025, 2, 28);
totalDays = (int)(projectEndDate - projectStartDate).TotalDays + 1;
}
void InitializeUI()
{
headerTitle = root.Q<VisualElement>("header-title");
headerTitle.style.width = maxL * 40;
timelineContent = root.Q<VisualElement>("content");
headerTimeline = root.Q<VisualElement>("header-timeline");
headerTimeline.style.width = totalDays * pixelsPerDay;
monthsLayer = headerTimeline.Q<VisualElement>("timeline-months");
weeksLayer = headerTimeline.Q<VisualElement>("timeline-weeks");
daysLayer = headerTimeline.Q<VisualElement>("timeline-days");
// ScrollView 가로 스크롤 시 header-timeline 동기화
contentScroll = root.Q<ScrollView>("content-scroll");
if (contentScroll != null)
{
contentScroll.horizontalScroller.valueChanged += OnHorizontalScroll;
}
}
void OnHorizontalScroll(float scrollValue)
{
// header 전체를 스크롤 (header-title + header-timeline)
var header = root.Q<VisualElement>("header");
if (header != null)
{
header.style.left = -scrollValue;
}
}
void SetupThreeTierHeader()
{
// Background base grid style color
Color lineColor = ColorUtil.FromHex("#CACACA"); // light blue lines
monthsLayer.Clear();
weeksLayer.Clear();
daysLayer.Clear();
foreach (var layer in new[] { monthsLayer, weeksLayer, daysLayer })
{
layer.style.width = totalDays * pixelsPerDay;
}
DateTime cursor = projectStartDate;
while (cursor <= projectEndDate)
{
DateTime monthStart = new DateTime(cursor.Year, cursor.Month, 1);
DateTime nextMonthStart = monthStart.AddMonths(1);
DateTime segmentStart = cursor; // first day we actually display within month
DateTime segmentEnd = nextMonthStart.AddDays(-1) < projectEndDate ? nextMonthStart.AddDays(-1) : projectEndDate;
int startIndex = (int)(segmentStart - projectStartDate).TotalDays;
int spanDays = (int)(segmentEnd - segmentStart).TotalDays + 1;
AddMonthLabel(monthsLayer, segmentStart.Year, segmentStart.Month, startIndex, spanDays, lineColor);
// Weeks inside month
DateTime weekCursor = segmentStart;
while (weekCursor <= segmentEnd)
{
DateTime weekStart = weekCursor;
// 1일이 속한 주를 1주차로, 일요일 기준으로 주차 계산
DateTime monthFirstDay = new DateTime(weekStart.Year, weekStart.Month, 1);
int firstDayOfWeek = (int)monthFirstDay.DayOfWeek; // 0=일요일, 1=월요일...
int weekOfMonth = ((weekStart.Day + firstDayOfWeek - 1) / 7) + 1;
// Keep the existing segmentation logic (end at Sunday or month end)
DateTime tentativeEnd = weekStart.AddDays(7 - (int)weekStart.DayOfWeek); // until Sunday
DateTime weekEnd = tentativeEnd > segmentEnd ? segmentEnd : tentativeEnd;
int weekStartIndex = (int)(weekStart - projectStartDate).TotalDays;
int weekSpanDays = (int)(weekEnd - weekStart).TotalDays + 1;
AddWeekLabel(weeksLayer, weekOfMonth, weekStartIndex, weekSpanDays, lineColor);
weekCursor = weekEnd.AddDays(1);
}
cursor = segmentEnd.AddDays(1);
}
// Days line
for (int i = 0; i < totalDays; i++)
{
DateTime date = projectStartDate.AddDays(i);
AddDayCell(daysLayer, date.Day, i, lineColor);
}
}
void AddMonthLabel(VisualElement parent, int year, int month, int startIndex, int spanDays, Color lineColor)
{
var ve = new VisualElement();
ve.style.width = spanDays * pixelsPerDay;
ve.style.height = pixelsPerDay;
ve.style.borderRightColor = lineColor;
ve.style.borderRightWidth = 1;
var lab = new Label($"{year}년 {month}월");
lab.style.unityTextAlign = TextAnchor.MiddleCenter;
lab.style.unityFontStyleAndWeight = FontStyle.Normal;
lab.style.width = Length.Percent(100);
lab.style.height = Length.Percent(100);
lab.style.fontSize = 10;
lab.style.marginBottom = 0;
lab.style.marginLeft = 0;
lab.style.marginRight = 0;
lab.style.marginTop = 0;
lab.style.paddingBottom = 0;
lab.style.paddingLeft = 0;
lab.style.paddingRight = 0;
lab.style.paddingTop = 0;
ve.Add(lab);
parent.Add(ve);
}
void AddWeekLabel(VisualElement parent, int weekOfMonth, int startIndex, int spanDays, Color lineColor)
{
var ve = new VisualElement();
ve.style.width = spanDays * pixelsPerDay;
ve.style.height = pixelsPerDay;
ve.style.borderRightColor = lineColor;
ve.style.borderRightWidth = 1;
var lab = new Label($"{weekOfMonth}주");
lab.style.unityTextAlign = TextAnchor.MiddleCenter;
lab.style.width = Length.Percent(100);
lab.style.height = Length.Percent(100);
lab.style.fontSize = 10;
lab.style.marginBottom = 0;
lab.style.marginLeft = 0;
lab.style.marginRight = 0;
lab.style.marginTop = 0;
lab.style.paddingBottom = 0;
lab.style.paddingLeft = 0;
lab.style.paddingRight = 0;
lab.style.paddingTop = 0;
ve.Add(lab);
parent.Add(ve);
}
void AddDayCell(VisualElement parent, int day, int dayIndex, Color lineColor)
{
var ve = new VisualElement();
ve.style.width = pixelsPerDay;
ve.style.height = pixelsPerDay;
ve.style.borderRightColor = lineColor;
ve.style.borderRightWidth = 1;
var lab = new Label(day.ToString("00"));
lab.style.unityTextAlign = TextAnchor.MiddleCenter;
lab.style.width = Length.Percent(100);
lab.style.height = Length.Percent(100);
lab.style.fontSize = 10;
lab.style.marginBottom = 0;
lab.style.marginLeft = 0;
lab.style.marginRight = 0;
lab.style.marginTop = 0;
lab.style.paddingBottom = 0;
lab.style.paddingLeft = 0;
lab.style.paddingRight = 0;
lab.style.paddingTop = 0;
ve.Add(lab);
parent.Add(ve);
}
void RenderTasks()
{
if (timelineContent == null) return;
timelineContent.Clear();
for (int i = 0; i < tasks.Count; i++)
{
var task = tasks[i];
var row = taskRowTemplate.CloneTree();
row.style.height = 40; // TemplateContainer 높이 설정
row.style.flexShrink = 0;
row.style.flexGrow = 0;
row.style.flexBasis = new StyleLength(StyleKeyword.Auto);
VisualElement lContainer = row.Q<VisualElement>("task-l");
Label txt = row.Q<Label>("task-txt");
txt.text = task.L1;
for (int j = 2; j <= maxL; j++)
{
string text = j == 2 ? task.L2 : j == 3 ? task.L3 : j == 4 ? task.L4 : j == 5 ? task.L5 : j == 6 ? task.L6 : j == 7 ? task.L7 : task.L8;
if (string.IsNullOrEmpty(text)) text = "";
var label = new Label($"{text}");
label.name = $"c{j}";
label.AddToClassList("task-txt");
lContainer.Add(label);
}
VisualElement taskCell = row.Q<VisualElement>("task-cell");
taskCell.style.width = totalDays * pixelsPerDay;
if (task.GetPlanStart().HasValue && task.GetPlanEnd().HasValue)
taskCell.Add(CreateBar(task, task.GetPlanStart().Value, task.GetPlanEnd().Value, planColor, 4));
if (task.GetActualStart().HasValue && task.GetActualEnd().HasValue)
taskCell.Add(CreateBar(task, task.GetActualStart().Value, task.GetActualEnd().Value, actualColor, 10));
timelineContent.Add(row);
}
}
VisualElement CreateBar(ISOPChartDataTask task, DateTime start, DateTime end, Color color, int top)
{
var bar = new VisualElement();
bar.style.position = Position.Absolute;
bar.style.backgroundColor = color;
bar.style.height = 6;
bar.style.top = top;
float startX = (float)(start - projectStartDate).TotalDays * pixelsPerDay;
float width = Mathf.Max((float)(end - start).TotalDays * pixelsPerDay, 6);
bar.style.left = startX;
bar.style.width = width;
bar.tooltip = $"{task.GetDisplayName()}\n{start:MM/dd} ~ {end:MM/dd}";
return bar;
}
float CalculateProgress(ISOPChartDataTask task)
{
var planStart = task.GetPlanStart(); var planEnd = task.GetPlanEnd(); var actualEnd = task.GetActualEnd();
if (!planStart.HasValue || !planEnd.HasValue) return 0;
float total = (float)(planEnd.Value - planStart.Value).TotalDays; if (total <= 0) return 0;
if (actualEnd.HasValue)
{ float actualDays = (float)(actualEnd.Value - planStart.Value).TotalDays; return Mathf.Clamp(actualDays / total * 100, 0, 100); }
return 0;
}
}
}