2026-01-08 20:15:57 +09:00
|
|
|
#nullable enable
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2026-01-12 20:16:17 +09:00
|
|
|
using Cysharp.Threading.Tasks;
|
2026-01-08 20:15:57 +09:00
|
|
|
using UnityEngine;
|
|
|
|
|
using UnityEngine.UIElements;
|
|
|
|
|
using UVC.Locale;
|
|
|
|
|
using UVC.UIToolkit.Input;
|
|
|
|
|
|
|
|
|
|
namespace UVC.UIToolkit.Modal
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// UIToolkit 기반 날짜/시간 피커 모달
|
|
|
|
|
/// 캘린더 그리드 + 시간 선택 지원
|
|
|
|
|
/// </summary>
|
2026-01-12 20:16:17 +09:00
|
|
|
/// <example>
|
|
|
|
|
/// <code>
|
|
|
|
|
/// // 기본 사용법 (날짜만)
|
|
|
|
|
/// var picker = UTKDatePicker.Show(rootVisualElement, DateTime.Today, UTKDatePicker.PickerMode.DateOnly, "Select Date");
|
|
|
|
|
/// picker.OnDateSelected += (date) =>
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log($"Date selected: {date:yyyy-MM-dd}");
|
|
|
|
|
/// };
|
|
|
|
|
///
|
|
|
|
|
/// // 날짜 + 시간 선택
|
|
|
|
|
/// var dateTimePicker = UTKDatePicker.Show(rootVisualElement, DateTime.Now, UTKDatePicker.PickerMode.DateAndTime, "Select Date & Time");
|
|
|
|
|
/// dateTimePicker.OnDateSelected += (date) =>
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log($"DateTime selected: {date:yyyy-MM-dd HH:mm}");
|
|
|
|
|
/// };
|
|
|
|
|
///
|
|
|
|
|
/// // async/await 사용법 (UniTask)
|
|
|
|
|
/// DateTime? selectedDate = await UTKDatePicker.ShowAsync(rootVisualElement, DateTime.Today);
|
|
|
|
|
/// if (selectedDate.HasValue)
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log($"Selected: {selectedDate.Value:yyyy-MM-dd}");
|
|
|
|
|
/// }
|
|
|
|
|
/// else
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log("Cancelled");
|
|
|
|
|
/// }
|
|
|
|
|
///
|
|
|
|
|
/// // 날짜 범위 선택
|
|
|
|
|
/// var rangePicker = UTKDatePicker.ShowRange(rootVisualElement, DateTime.Today, DateTime.Today.AddDays(7));
|
|
|
|
|
/// rangePicker.OnDateRangeSelected += (start, end) =>
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log($"Range selected: {start:yyyy-MM-dd} ~ {end:yyyy-MM-dd}");
|
|
|
|
|
/// };
|
|
|
|
|
///
|
|
|
|
|
/// // 날짜 범위 async/await 사용법
|
|
|
|
|
/// var result = await UTKDatePicker.ShowRangeAsync(rootVisualElement, DateTime.Today, DateTime.Today.AddDays(7));
|
|
|
|
|
/// if (result.HasValue)
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log($"Range: {result.Value.Start:yyyy-MM-dd} ~ {result.Value.End:yyyy-MM-dd}");
|
|
|
|
|
/// }
|
|
|
|
|
/// else
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log("Cancelled");
|
|
|
|
|
/// }
|
|
|
|
|
///
|
|
|
|
|
/// // 인스턴스 직접 생성
|
|
|
|
|
/// var datePicker = new UTKDatePicker();
|
|
|
|
|
/// datePicker.SetDate(DateTime.Today);
|
|
|
|
|
/// container.Add(datePicker);
|
|
|
|
|
/// </code>
|
|
|
|
|
/// </example>
|
2026-01-08 20:15:57 +09:00
|
|
|
[UxmlElement]
|
|
|
|
|
public partial class UTKDatePicker : VisualElement, IDisposable
|
|
|
|
|
{
|
|
|
|
|
#region Enums
|
|
|
|
|
public enum PickerMode
|
|
|
|
|
{
|
|
|
|
|
DateOnly,
|
2026-01-12 20:16:17 +09:00
|
|
|
DateAndTime,
|
|
|
|
|
DateRange,
|
|
|
|
|
DateTimeRange
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 범위 선택 시 현재 선택 중인 날짜 타입
|
|
|
|
|
/// </summary>
|
|
|
|
|
private enum RangeSelectionState
|
|
|
|
|
{
|
|
|
|
|
SelectingStart,
|
|
|
|
|
SelectingEnd
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constants
|
|
|
|
|
private const string UXML_PATH = "UIToolkit/Modal/UTKDatePicker";
|
|
|
|
|
private const string USS_PATH = "UIToolkit/Modal/UTKDatePicker";
|
|
|
|
|
private const int DAYS_IN_GRID = 42; // 6 rows x 7 columns
|
|
|
|
|
private static readonly string[] DAY_NAME_KEYS = { "day_sun", "day_mon", "day_tue", "day_wed", "day_thu", "day_fri", "day_sat" };
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Fields
|
|
|
|
|
private bool _disposed;
|
|
|
|
|
private PickerMode _mode;
|
|
|
|
|
private DateTime _selectedDate = DateTime.Today;
|
|
|
|
|
private DateTime _displayMonth = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
// 범위 선택 관련 필드
|
|
|
|
|
private DateTime? _rangeStartDate;
|
|
|
|
|
private DateTime? _rangeEndDate;
|
|
|
|
|
private RangeSelectionState _rangeState = RangeSelectionState.SelectingStart;
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
private UTKModalBlocker? _blocker;
|
|
|
|
|
private readonly List<Button> _dayButtons = new();
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
// 범위 선택 UI 요소
|
|
|
|
|
private Label? _rangeInfoLabel;
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
// UI 요소
|
|
|
|
|
private Label? _titleLabel;
|
|
|
|
|
private Button? _closeButton;
|
|
|
|
|
private Label? _yearMonthLabel;
|
|
|
|
|
private Button? _prevYearButton;
|
|
|
|
|
private Button? _prevMonthButton;
|
|
|
|
|
private Button? _nextMonthButton;
|
|
|
|
|
private Button? _nextYearButton;
|
|
|
|
|
private VisualElement? _dayNamesRow;
|
|
|
|
|
private VisualElement? _timeRow;
|
|
|
|
|
private UTKNumberStepper? _hourStepper;
|
|
|
|
|
private UTKNumberStepper? _minuteStepper;
|
2026-01-12 20:16:17 +09:00
|
|
|
private UTKButton? _cancelButton;
|
|
|
|
|
private UTKButton? _confirmButton;
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Events
|
|
|
|
|
/// <summary>날짜가 선택되었을 때 발생</summary>
|
|
|
|
|
public event Action<DateTime>? OnDateSelected;
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
/// <summary>날짜 범위가 선택되었을 때 발생 (시작일, 종료일)</summary>
|
|
|
|
|
public event Action<DateTime, DateTime>? OnDateRangeSelected;
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
/// <summary>피커가 닫힐 때 발생</summary>
|
|
|
|
|
public event Action? OnClosed;
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Constructor
|
|
|
|
|
public UTKDatePicker()
|
|
|
|
|
{
|
|
|
|
|
// 테마 스타일시트 적용
|
|
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
|
|
|
|
|
|
// 컴포넌트 스타일시트 로드
|
|
|
|
|
var styleSheet = Resources.Load<StyleSheet>(USS_PATH);
|
|
|
|
|
if (styleSheet != null)
|
|
|
|
|
{
|
|
|
|
|
styleSheets.Add(styleSheet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
|
|
|
|
|
if (visualTree != null)
|
|
|
|
|
{
|
|
|
|
|
visualTree.CloneTree(this);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
CreateUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QueryElements();
|
|
|
|
|
SetupEvents();
|
|
|
|
|
SubscribeToThemeChanges();
|
|
|
|
|
|
|
|
|
|
// 캘린더 초기화
|
|
|
|
|
UpdateCalendar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SubscribeToThemeChanges()
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
|
|
|
|
|
|
|
|
|
|
// 패널에서 분리될 때 이벤트 구독 해제
|
|
|
|
|
RegisterCallback<DetachFromPanelEvent>(_ =>
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnThemeChanged(UTKTheme theme)
|
|
|
|
|
{
|
|
|
|
|
UTKThemeManager.Instance.ApplyThemeToElement(this);
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Static Factory
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 날짜 피커를 표시합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public static UTKDatePicker Show(
|
|
|
|
|
VisualElement parent,
|
|
|
|
|
DateTime initialDate,
|
|
|
|
|
PickerMode mode = PickerMode.DateOnly,
|
|
|
|
|
string title = "Select Date")
|
|
|
|
|
{
|
|
|
|
|
var picker = new UTKDatePicker();
|
|
|
|
|
picker._mode = mode;
|
|
|
|
|
picker._selectedDate = initialDate;
|
|
|
|
|
picker._displayMonth = new DateTime(initialDate.Year, initialDate.Month, 1);
|
|
|
|
|
|
|
|
|
|
// 블로커 추가
|
|
|
|
|
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
|
|
|
|
|
picker._blocker.OnBlockerClicked += picker.Close;
|
|
|
|
|
|
|
|
|
|
// 피커 추가
|
|
|
|
|
parent.Add(picker);
|
|
|
|
|
|
|
|
|
|
// UI 초기화
|
|
|
|
|
picker.SetTitle(title);
|
|
|
|
|
picker.SetTimeVisible(mode == PickerMode.DateAndTime);
|
|
|
|
|
picker.UpdateTimeFields();
|
|
|
|
|
picker.UpdateCalendar();
|
|
|
|
|
picker.CenterOnScreen();
|
|
|
|
|
|
|
|
|
|
return picker;
|
|
|
|
|
}
|
2026-01-12 20:16:17 +09:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 날짜 피커를 표시하고 날짜 선택을 기다립니다.
|
|
|
|
|
/// OK 버튼 클릭 시 선택된 날짜를 반환하고, 취소/닫기 시 null을 반환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="parent">부모 VisualElement</param>
|
|
|
|
|
/// <param name="initialDate">초기 날짜</param>
|
|
|
|
|
/// <param name="mode">피커 모드 (날짜만 또는 날짜+시간)</param>
|
|
|
|
|
/// <param name="title">피커 제목</param>
|
|
|
|
|
/// <returns>선택된 날짜 또는 null (취소 시)</returns>
|
|
|
|
|
/// <example>
|
|
|
|
|
/// <code>
|
|
|
|
|
/// // async/await 사용법
|
|
|
|
|
/// DateTime? selectedDate = await UTKDatePicker.ShowAsync(rootVisualElement, DateTime.Today);
|
|
|
|
|
/// if (selectedDate.HasValue)
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log($"Selected: {selectedDate.Value:yyyy-MM-dd}");
|
|
|
|
|
/// }
|
|
|
|
|
/// else
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log("Cancelled");
|
|
|
|
|
/// }
|
|
|
|
|
/// </code>
|
|
|
|
|
/// </example>
|
|
|
|
|
public static async UniTask<DateTime?> ShowAsync(
|
|
|
|
|
VisualElement parent,
|
|
|
|
|
DateTime initialDate,
|
|
|
|
|
PickerMode mode = PickerMode.DateOnly,
|
|
|
|
|
string title = "Select Date")
|
|
|
|
|
{
|
|
|
|
|
var tcs = new UniTaskCompletionSource<DateTime?>();
|
|
|
|
|
DateTime? resultDate = null;
|
|
|
|
|
|
|
|
|
|
var picker = Show(parent, initialDate, mode, title);
|
|
|
|
|
|
|
|
|
|
picker.OnDateSelected += (date) =>
|
|
|
|
|
{
|
|
|
|
|
resultDate = date;
|
|
|
|
|
tcs.TrySetResult(resultDate);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
picker.OnClosed += () =>
|
|
|
|
|
{
|
|
|
|
|
// OnDateSelected가 먼저 호출된 경우 이미 완료됨
|
|
|
|
|
tcs.TrySetResult(resultDate);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return await tcs.Task;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 날짜 범위 피커를 표시합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="parent">부모 VisualElement</param>
|
|
|
|
|
/// <param name="initialStartDate">초기 시작 날짜 (null이면 오늘)</param>
|
|
|
|
|
/// <param name="initialEndDate">초기 종료 날짜 (null이면 시작 날짜와 동일)</param>
|
|
|
|
|
/// <param name="includeTime">시간 선택 포함 여부</param>
|
|
|
|
|
/// <param name="title">피커 제목</param>
|
|
|
|
|
public static UTKDatePicker ShowRange(
|
|
|
|
|
VisualElement parent,
|
|
|
|
|
DateTime? initialStartDate = null,
|
|
|
|
|
DateTime? initialEndDate = null,
|
|
|
|
|
bool includeTime = false,
|
|
|
|
|
string title = "Select Date Range")
|
|
|
|
|
{
|
|
|
|
|
var picker = new UTKDatePicker();
|
|
|
|
|
picker._mode = includeTime ? PickerMode.DateTimeRange : PickerMode.DateRange;
|
|
|
|
|
|
|
|
|
|
var startDate = initialStartDate ?? DateTime.Today;
|
|
|
|
|
var endDate = initialEndDate ?? startDate;
|
|
|
|
|
|
|
|
|
|
// 시작일이 종료일보다 늦으면 스왑
|
|
|
|
|
if (startDate > endDate)
|
|
|
|
|
{
|
|
|
|
|
(startDate, endDate) = (endDate, startDate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
picker._rangeStartDate = startDate;
|
|
|
|
|
picker._rangeEndDate = endDate;
|
|
|
|
|
picker._rangeState = RangeSelectionState.SelectingStart;
|
|
|
|
|
picker._displayMonth = new DateTime(startDate.Year, startDate.Month, 1);
|
|
|
|
|
|
|
|
|
|
// 블로커 추가
|
|
|
|
|
picker._blocker = UTKModalBlocker.Show(parent, 0.5f, false);
|
|
|
|
|
picker._blocker.OnBlockerClicked += picker.Close;
|
|
|
|
|
|
|
|
|
|
// 피커 추가
|
|
|
|
|
parent.Add(picker);
|
|
|
|
|
|
|
|
|
|
// UI 초기화
|
|
|
|
|
picker.SetTitle(title);
|
|
|
|
|
picker.SetTimeVisible(includeTime);
|
|
|
|
|
picker.SetRangeInfoVisible(true);
|
|
|
|
|
picker.UpdateRangeInfo();
|
|
|
|
|
picker.UpdateCalendar();
|
|
|
|
|
picker.CenterOnScreen();
|
|
|
|
|
|
|
|
|
|
return picker;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 날짜 범위 피커를 표시하고 범위 선택을 기다립니다.
|
|
|
|
|
/// OK 버튼 클릭 시 선택된 범위를 반환하고, 취소/닫기 시 null을 반환합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="parent">부모 VisualElement</param>
|
|
|
|
|
/// <param name="initialStartDate">초기 시작 날짜</param>
|
|
|
|
|
/// <param name="initialEndDate">초기 종료 날짜</param>
|
|
|
|
|
/// <param name="includeTime">시간 선택 포함 여부</param>
|
|
|
|
|
/// <param name="title">피커 제목</param>
|
|
|
|
|
/// <returns>선택된 범위 (시작일, 종료일) 또는 null (취소 시)</returns>
|
|
|
|
|
/// <example>
|
|
|
|
|
/// <code>
|
|
|
|
|
/// // async/await 사용법
|
|
|
|
|
/// var result = await UTKDatePicker.ShowRangeAsync(rootVisualElement);
|
|
|
|
|
/// if (result.HasValue)
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log($"Range: {result.Value.Start:yyyy-MM-dd} ~ {result.Value.End:yyyy-MM-dd}");
|
|
|
|
|
/// }
|
|
|
|
|
/// else
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log("Cancelled");
|
|
|
|
|
/// }
|
|
|
|
|
/// </code>
|
|
|
|
|
/// </example>
|
|
|
|
|
public static async UniTask<(DateTime Start, DateTime End)?> ShowRangeAsync(
|
|
|
|
|
VisualElement parent,
|
|
|
|
|
DateTime? initialStartDate = null,
|
|
|
|
|
DateTime? initialEndDate = null,
|
|
|
|
|
bool includeTime = false,
|
|
|
|
|
string title = "Select Date Range")
|
|
|
|
|
{
|
|
|
|
|
var tcs = new UniTaskCompletionSource<(DateTime, DateTime)?>();
|
|
|
|
|
(DateTime, DateTime)? result = null;
|
|
|
|
|
|
|
|
|
|
var picker = ShowRange(parent, initialStartDate, initialEndDate, includeTime, title);
|
|
|
|
|
|
|
|
|
|
picker.OnDateRangeSelected += (start, end) =>
|
|
|
|
|
{
|
|
|
|
|
result = (start, end);
|
|
|
|
|
tcs.TrySetResult(result);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
picker.OnClosed += () =>
|
|
|
|
|
{
|
|
|
|
|
tcs.TrySetResult(result);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return await tcs.Task;
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Public Methods
|
|
|
|
|
public void SetDate(DateTime date)
|
|
|
|
|
{
|
|
|
|
|
_selectedDate = date;
|
|
|
|
|
_displayMonth = new DateTime(date.Year, date.Month, 1);
|
|
|
|
|
UpdateTimeFields();
|
|
|
|
|
UpdateCalendar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public DateTime GetDate()
|
|
|
|
|
{
|
|
|
|
|
if (_mode == PickerMode.DateAndTime)
|
|
|
|
|
{
|
|
|
|
|
int hour = _hourStepper?.Value ?? 0;
|
|
|
|
|
int minute = _minuteStepper?.Value ?? 0;
|
|
|
|
|
return new DateTime(
|
|
|
|
|
_selectedDate.Year,
|
|
|
|
|
_selectedDate.Month,
|
|
|
|
|
_selectedDate.Day,
|
|
|
|
|
hour,
|
|
|
|
|
minute,
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return _selectedDate.Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void PreviousYear() => NavigateMonth(-12);
|
|
|
|
|
public void NextYear() => NavigateMonth(12);
|
|
|
|
|
public void PreviousMonth() => NavigateMonth(-1);
|
|
|
|
|
public void NextMonth() => NavigateMonth(1);
|
|
|
|
|
|
|
|
|
|
public void Close()
|
|
|
|
|
{
|
|
|
|
|
OnClosed?.Invoke();
|
|
|
|
|
_blocker?.Hide();
|
|
|
|
|
RemoveFromHierarchy();
|
|
|
|
|
Dispose();
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Private Methods - UI Creation
|
|
|
|
|
private void CreateUI()
|
|
|
|
|
{
|
|
|
|
|
AddToClassList("utk-date-picker");
|
|
|
|
|
|
|
|
|
|
// Header
|
|
|
|
|
var header = new VisualElement { name = "header" };
|
|
|
|
|
header.AddToClassList("utk-date-picker__header");
|
|
|
|
|
|
|
|
|
|
_titleLabel = new Label("Select Date") { name = "title" };
|
|
|
|
|
_titleLabel.AddToClassList("utk-date-picker__title");
|
|
|
|
|
|
|
|
|
|
_closeButton = new Button { name = "close-btn", text = "\u2715" }; // X
|
|
|
|
|
_closeButton.AddToClassList("utk-date-picker__close-btn");
|
|
|
|
|
|
|
|
|
|
header.Add(_titleLabel);
|
|
|
|
|
header.Add(_closeButton);
|
|
|
|
|
Add(header);
|
|
|
|
|
|
|
|
|
|
// Navigation
|
|
|
|
|
var navRow = new VisualElement { name = "nav-row" };
|
|
|
|
|
navRow.AddToClassList("utk-date-picker__nav-row");
|
|
|
|
|
|
|
|
|
|
var leftNav = new VisualElement();
|
|
|
|
|
leftNav.AddToClassList("utk-date-picker__nav-group");
|
|
|
|
|
|
|
|
|
|
_prevYearButton = CreateNavButton("prev-year", "\u00AB"); // <<
|
|
|
|
|
_prevYearButton.AddToClassList("utk-date-picker__nav-btn--prev-year");
|
|
|
|
|
|
|
|
|
|
_prevMonthButton = CreateNavButton("prev-month", "\u2039"); // <
|
|
|
|
|
|
|
|
|
|
leftNav.Add(_prevYearButton);
|
|
|
|
|
leftNav.Add(_prevMonthButton);
|
|
|
|
|
|
|
|
|
|
_yearMonthLabel = new Label { name = "year-month" };
|
|
|
|
|
_yearMonthLabel.AddToClassList("utk-date-picker__year-month-label");
|
|
|
|
|
|
|
|
|
|
var rightNav = new VisualElement();
|
|
|
|
|
rightNav.AddToClassList("utk-date-picker__nav-group");
|
|
|
|
|
|
|
|
|
|
_nextMonthButton = CreateNavButton("next-month", "\u203A"); // >
|
|
|
|
|
|
|
|
|
|
_nextYearButton = CreateNavButton("next-year", "\u00BB"); // >>
|
|
|
|
|
_nextYearButton.AddToClassList("utk-date-picker__nav-btn--next-year");
|
|
|
|
|
|
|
|
|
|
rightNav.Add(_nextMonthButton);
|
|
|
|
|
rightNav.Add(_nextYearButton);
|
|
|
|
|
|
|
|
|
|
navRow.Add(leftNav);
|
|
|
|
|
navRow.Add(_yearMonthLabel);
|
|
|
|
|
navRow.Add(rightNav);
|
|
|
|
|
Add(navRow);
|
|
|
|
|
|
|
|
|
|
// Day names row
|
|
|
|
|
_dayNamesRow = new VisualElement { name = "day-names" };
|
|
|
|
|
_dayNamesRow.AddToClassList("utk-date-picker__day-names");
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var dayText = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
|
|
|
|
|
var label = new Label(dayText) { name = $"day-name-{i}" };
|
|
|
|
|
label.AddToClassList("utk-date-picker__day-name");
|
|
|
|
|
|
|
|
|
|
// 요일별 색상 클래스 (0=일요일, 6=토요일)
|
|
|
|
|
if (i == 0) label.AddToClassList("utk-date-picker__day-name--sunday");
|
|
|
|
|
else if (i == 6) label.AddToClassList("utk-date-picker__day-name--saturday");
|
|
|
|
|
|
|
|
|
|
_dayNamesRow.Add(label);
|
|
|
|
|
}
|
|
|
|
|
Add(_dayNamesRow);
|
|
|
|
|
|
|
|
|
|
// Calendar grid - 6 rows
|
|
|
|
|
for (int row = 0; row < 6; row++)
|
|
|
|
|
{
|
|
|
|
|
var rowContainer = new VisualElement { name = $"calendar-row-{row}" };
|
|
|
|
|
rowContainer.AddToClassList("utk-date-picker__calendar-row");
|
|
|
|
|
|
|
|
|
|
for (int col = 0; col < 7; col++)
|
|
|
|
|
{
|
|
|
|
|
var dayButton = new Button { name = $"day-{row * 7 + col}" };
|
|
|
|
|
dayButton.AddToClassList("utk-date-picker__day-btn");
|
|
|
|
|
|
|
|
|
|
int index = row * 7 + col;
|
|
|
|
|
dayButton.clicked += () => OnDayClicked(index);
|
|
|
|
|
|
|
|
|
|
_dayButtons.Add(dayButton);
|
|
|
|
|
rowContainer.Add(dayButton);
|
|
|
|
|
}
|
|
|
|
|
Add(rowContainer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Time row (optional)
|
|
|
|
|
_timeRow = new VisualElement { name = "time-row" };
|
|
|
|
|
_timeRow.AddToClassList("utk-date-picker__time-row");
|
|
|
|
|
|
|
|
|
|
var timeLabel = new Label("시간:");
|
|
|
|
|
timeLabel.AddToClassList("utk-date-picker__time-label");
|
|
|
|
|
|
|
|
|
|
_hourStepper = new UTKNumberStepper(0, 23, 0, 1) { name = "hour-stepper" };
|
|
|
|
|
_hourStepper.AddToClassList("utk-date-picker__time-stepper");
|
|
|
|
|
_hourStepper.WrapAround = true;
|
|
|
|
|
|
|
|
|
|
var colonLabel = new Label(":");
|
|
|
|
|
colonLabel.AddToClassList("utk-date-picker__time-separator");
|
|
|
|
|
|
|
|
|
|
_minuteStepper = new UTKNumberStepper(0, 59, 0, 1) { name = "minute-stepper" };
|
|
|
|
|
_minuteStepper.AddToClassList("utk-date-picker__time-stepper");
|
|
|
|
|
_minuteStepper.WrapAround = true;
|
|
|
|
|
|
|
|
|
|
// Tab 키로 시간 <-> 분 이동
|
|
|
|
|
_hourStepper.OnTabPressed += () => _minuteStepper?.Focus();
|
|
|
|
|
_minuteStepper.OnShiftTabPressed += () => _hourStepper?.Focus();
|
|
|
|
|
|
|
|
|
|
_timeRow.Add(timeLabel);
|
|
|
|
|
_timeRow.Add(_hourStepper);
|
|
|
|
|
_timeRow.Add(colonLabel);
|
|
|
|
|
_timeRow.Add(_minuteStepper);
|
|
|
|
|
Add(_timeRow);
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
// Range Info Label (범위 선택 시 시작일/종료일 표시)
|
|
|
|
|
_rangeInfoLabel = new Label { name = "range-info" };
|
|
|
|
|
_rangeInfoLabel.AddToClassList("utk-date-picker__range-info");
|
|
|
|
|
_rangeInfoLabel.style.display = DisplayStyle.None;
|
|
|
|
|
Add(_rangeInfoLabel);
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
// Buttons
|
|
|
|
|
var buttonRow = new VisualElement { name = "button-row" };
|
|
|
|
|
buttonRow.AddToClassList("utk-date-picker__button-row");
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
_cancelButton = new UTKButton("Cancel", "", UTKButton.ButtonVariant.Normal) { name = "cancel-btn" };
|
2026-01-08 20:15:57 +09:00
|
|
|
_cancelButton.AddToClassList("utk-date-picker__cancel-btn");
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
_confirmButton = new UTKButton("OK", "", UTKButton.ButtonVariant.Primary) { name = "confirm-btn" };
|
2026-01-08 20:15:57 +09:00
|
|
|
_confirmButton.AddToClassList("utk-date-picker__confirm-btn");
|
|
|
|
|
|
|
|
|
|
buttonRow.Add(_cancelButton);
|
|
|
|
|
buttonRow.Add(_confirmButton);
|
|
|
|
|
Add(buttonRow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Button CreateNavButton(string name, string text)
|
|
|
|
|
{
|
|
|
|
|
var btn = new Button { name = name, text = text };
|
|
|
|
|
btn.AddToClassList("utk-date-picker__nav-btn");
|
|
|
|
|
return btn;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void QueryElements()
|
|
|
|
|
{
|
|
|
|
|
_titleLabel ??= this.Q<Label>("title");
|
|
|
|
|
_closeButton ??= this.Q<Button>("close-btn");
|
|
|
|
|
_yearMonthLabel ??= this.Q<Label>("year-month");
|
|
|
|
|
_prevYearButton ??= this.Q<Button>("prev-year");
|
|
|
|
|
_prevMonthButton ??= this.Q<Button>("prev-month");
|
|
|
|
|
_nextMonthButton ??= this.Q<Button>("next-month");
|
|
|
|
|
_nextYearButton ??= this.Q<Button>("next-year");
|
|
|
|
|
_dayNamesRow ??= this.Q<VisualElement>("day-names");
|
|
|
|
|
_timeRow ??= this.Q<VisualElement>("time-row");
|
|
|
|
|
_hourStepper ??= this.Q<UTKNumberStepper>("hour-stepper");
|
|
|
|
|
_minuteStepper ??= this.Q<UTKNumberStepper>("minute-stepper");
|
2026-01-12 20:16:17 +09:00
|
|
|
_cancelButton ??= this.Q<UTKButton>("cancel-btn");
|
|
|
|
|
_confirmButton ??= this.Q<UTKButton>("confirm-btn");
|
|
|
|
|
_rangeInfoLabel ??= this.Q<Label>("range-info");
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
// Day buttons 쿼리
|
|
|
|
|
if (_dayButtons.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < DAYS_IN_GRID; i++)
|
|
|
|
|
{
|
|
|
|
|
var btn = this.Q<Button>($"day-{i}");
|
|
|
|
|
if (btn != null)
|
|
|
|
|
{
|
|
|
|
|
_dayButtons.Add(btn);
|
|
|
|
|
int index = i;
|
|
|
|
|
btn.clicked += () => OnDayClicked(index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetupEvents()
|
|
|
|
|
{
|
|
|
|
|
_closeButton?.RegisterCallback<ClickEvent>(_ => Close());
|
2026-01-12 20:16:17 +09:00
|
|
|
if (_cancelButton != null) _cancelButton.OnClicked += Close;
|
|
|
|
|
if (_confirmButton != null) _confirmButton.OnClicked += Confirm;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
_prevYearButton?.RegisterCallback<ClickEvent>(_ => PreviousYear());
|
|
|
|
|
_prevMonthButton?.RegisterCallback<ClickEvent>(_ => PreviousMonth());
|
|
|
|
|
_nextMonthButton?.RegisterCallback<ClickEvent>(_ => NextMonth());
|
|
|
|
|
_nextYearButton?.RegisterCallback<ClickEvent>(_ => NextYear());
|
|
|
|
|
|
|
|
|
|
// 언어 변경 이벤트 구독
|
|
|
|
|
LocalizationManager.Instance.OnLanguageChanged += OnLanguageChanged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnLanguageChanged(string newLanguage)
|
|
|
|
|
{
|
|
|
|
|
UpdateDayNameLabels();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateDayNameLabels()
|
|
|
|
|
{
|
|
|
|
|
if (_dayNamesRow == null) return;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < DAY_NAME_KEYS.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
var label = _dayNamesRow.Q<Label>($"day-name-{i}");
|
|
|
|
|
if (label != null)
|
|
|
|
|
{
|
|
|
|
|
label.text = LocalizationManager.Instance.GetString(DAY_NAME_KEYS[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Private Methods - Logic
|
|
|
|
|
private void NavigateMonth(int months)
|
|
|
|
|
{
|
|
|
|
|
_displayMonth = _displayMonth.AddMonths(months);
|
|
|
|
|
UpdateCalendar();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateCalendar()
|
|
|
|
|
{
|
|
|
|
|
if (_yearMonthLabel != null)
|
|
|
|
|
{
|
|
|
|
|
_yearMonthLabel.text = $"{_displayMonth.Year}년 {_displayMonth.Month:D2}월";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 첫째 날의 요일
|
|
|
|
|
DateTime firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
|
|
|
|
|
int startDayOfWeek = (int)firstDay.DayOfWeek; // 0=Sunday
|
|
|
|
|
|
|
|
|
|
// 해당 월의 일 수
|
|
|
|
|
int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
|
|
|
|
|
|
|
|
|
|
DateTime today = DateTime.Today;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < DAYS_IN_GRID; i++)
|
|
|
|
|
{
|
|
|
|
|
var btn = _dayButtons[i];
|
|
|
|
|
|
|
|
|
|
// 동적 클래스 초기화
|
|
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--sunday");
|
|
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--saturday");
|
|
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--today");
|
|
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--selected");
|
|
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--other-month");
|
2026-01-12 20:16:17 +09:00
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--range-start");
|
|
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--range-end");
|
|
|
|
|
btn.RemoveFromClassList("utk-date-picker__day-btn--in-range");
|
2026-01-08 20:15:57 +09:00
|
|
|
|
|
|
|
|
int dayNumber = i - startDayOfWeek + 1;
|
|
|
|
|
|
|
|
|
|
if (dayNumber < 1 || dayNumber > daysInMonth)
|
|
|
|
|
{
|
|
|
|
|
// 이번 달이 아닌 날
|
|
|
|
|
btn.text = "";
|
|
|
|
|
btn.SetEnabled(false);
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--other-month");
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
btn.text = dayNumber.ToString();
|
|
|
|
|
btn.SetEnabled(true);
|
|
|
|
|
|
|
|
|
|
DateTime currentDate = new DateTime(_displayMonth.Year, _displayMonth.Month, dayNumber);
|
|
|
|
|
DayOfWeek dow = currentDate.DayOfWeek;
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
// 범위 선택 모드 처리
|
|
|
|
|
if (IsRangeMode)
|
|
|
|
|
{
|
|
|
|
|
bool isRangeStart = _rangeStartDate.HasValue && currentDate.Date == _rangeStartDate.Value.Date;
|
|
|
|
|
bool isRangeEnd = _rangeEndDate.HasValue && currentDate.Date == _rangeEndDate.Value.Date;
|
|
|
|
|
bool isInRange = _rangeStartDate.HasValue && _rangeEndDate.HasValue &&
|
|
|
|
|
currentDate.Date > _rangeStartDate.Value.Date &&
|
|
|
|
|
currentDate.Date < _rangeEndDate.Value.Date;
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
if (isRangeStart)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--range-start");
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
if (isRangeEnd)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--range-end");
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
if (isInRange)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--in-range");
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
// 오늘 클래스 (범위에 포함되지 않은 경우에만)
|
|
|
|
|
bool isToday = currentDate.Date == today;
|
|
|
|
|
if (isToday && !isRangeStart && !isRangeEnd && !isInRange)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--today");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 요일별 클래스 (범위에 포함되지 않은 경우에만)
|
|
|
|
|
if (!isRangeStart && !isRangeEnd && !isInRange)
|
|
|
|
|
{
|
|
|
|
|
if (dow == DayOfWeek.Sunday)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--sunday");
|
|
|
|
|
}
|
|
|
|
|
else if (dow == DayOfWeek.Saturday)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--saturday");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-01-12 20:16:17 +09:00
|
|
|
// 단일 날짜 선택 모드
|
|
|
|
|
bool isSelected = currentDate.Date == _selectedDate.Date;
|
|
|
|
|
bool isToday = currentDate.Date == today;
|
|
|
|
|
|
|
|
|
|
if (isSelected)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--selected");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (isToday && !isSelected)
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-01-12 20:16:17 +09:00
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--today");
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
2026-01-12 20:16:17 +09:00
|
|
|
|
|
|
|
|
if (!isSelected)
|
2026-01-08 20:15:57 +09:00
|
|
|
{
|
2026-01-12 20:16:17 +09:00
|
|
|
if (dow == DayOfWeek.Sunday)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--sunday");
|
|
|
|
|
}
|
|
|
|
|
else if (dow == DayOfWeek.Saturday)
|
|
|
|
|
{
|
|
|
|
|
btn.AddToClassList("utk-date-picker__day-btn--saturday");
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDayClicked(int index)
|
|
|
|
|
{
|
|
|
|
|
// 첫째 날의 요일
|
|
|
|
|
DateTime firstDay = new DateTime(_displayMonth.Year, _displayMonth.Month, 1);
|
|
|
|
|
int startDayOfWeek = (int)firstDay.DayOfWeek;
|
|
|
|
|
|
|
|
|
|
int dayNumber = index - startDayOfWeek + 1;
|
|
|
|
|
int daysInMonth = DateTime.DaysInMonth(_displayMonth.Year, _displayMonth.Month);
|
|
|
|
|
|
|
|
|
|
if (dayNumber >= 1 && dayNumber <= daysInMonth)
|
|
|
|
|
{
|
|
|
|
|
// 시간 유지
|
|
|
|
|
int hour = _hourStepper?.Value ?? _selectedDate.Hour;
|
|
|
|
|
int minute = _minuteStepper?.Value ?? _selectedDate.Minute;
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
DateTime clickedDate = new DateTime(
|
2026-01-08 20:15:57 +09:00
|
|
|
_displayMonth.Year,
|
|
|
|
|
_displayMonth.Month,
|
|
|
|
|
dayNumber,
|
|
|
|
|
hour,
|
|
|
|
|
minute,
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-12 20:16:17 +09:00
|
|
|
if (IsRangeMode)
|
|
|
|
|
{
|
|
|
|
|
// 범위 선택 모드
|
|
|
|
|
if (_rangeState == RangeSelectionState.SelectingStart)
|
|
|
|
|
{
|
|
|
|
|
// 시작일 선택
|
|
|
|
|
_rangeStartDate = clickedDate;
|
|
|
|
|
_rangeEndDate = null;
|
|
|
|
|
_rangeState = RangeSelectionState.SelectingEnd;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 종료일 선택
|
|
|
|
|
if (_rangeStartDate.HasValue && clickedDate < _rangeStartDate.Value)
|
|
|
|
|
{
|
|
|
|
|
// 클릭한 날짜가 시작일보다 이전이면 시작일로 설정하고 기존 시작일을 종료일로
|
|
|
|
|
_rangeEndDate = _rangeStartDate;
|
|
|
|
|
_rangeStartDate = clickedDate;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
_rangeEndDate = clickedDate;
|
|
|
|
|
}
|
|
|
|
|
_rangeState = RangeSelectionState.SelectingStart;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UpdateRangeInfo();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 단일 날짜 선택 모드
|
|
|
|
|
_selectedDate = clickedDate;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-08 20:15:57 +09:00
|
|
|
UpdateCalendar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Confirm()
|
|
|
|
|
{
|
2026-01-12 20:16:17 +09:00
|
|
|
if (IsRangeMode)
|
|
|
|
|
{
|
|
|
|
|
// 범위 선택 모드
|
|
|
|
|
if (_rangeStartDate.HasValue && _rangeEndDate.HasValue)
|
|
|
|
|
{
|
|
|
|
|
OnDateRangeSelected?.Invoke(_rangeStartDate.Value, _rangeEndDate.Value);
|
|
|
|
|
}
|
|
|
|
|
else if (_rangeStartDate.HasValue)
|
|
|
|
|
{
|
|
|
|
|
// 종료일이 없으면 시작일을 종료일로도 사용
|
|
|
|
|
OnDateRangeSelected?.Invoke(_rangeStartDate.Value, _rangeStartDate.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
OnDateSelected?.Invoke(GetDate());
|
|
|
|
|
}
|
2026-01-08 20:15:57 +09:00
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetTitle(string title)
|
|
|
|
|
{
|
|
|
|
|
if (_titleLabel != null)
|
|
|
|
|
_titleLabel.text = title;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SetTimeVisible(bool visible)
|
|
|
|
|
{
|
|
|
|
|
if (_timeRow != null)
|
|
|
|
|
_timeRow.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateTimeFields()
|
|
|
|
|
{
|
|
|
|
|
if (_hourStepper != null)
|
|
|
|
|
_hourStepper.SetValue(_selectedDate.Hour, notify: false);
|
|
|
|
|
|
|
|
|
|
if (_minuteStepper != null)
|
|
|
|
|
_minuteStepper.SetValue(_selectedDate.Minute, notify: false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CenterOnScreen()
|
|
|
|
|
{
|
|
|
|
|
schedule.Execute(() =>
|
|
|
|
|
{
|
|
|
|
|
var parent = this.parent;
|
|
|
|
|
if (parent == null) return;
|
|
|
|
|
|
|
|
|
|
float parentWidth = parent.resolvedStyle.width;
|
|
|
|
|
float parentHeight = parent.resolvedStyle.height;
|
|
|
|
|
float selfWidth = resolvedStyle.width;
|
|
|
|
|
float selfHeight = resolvedStyle.height;
|
|
|
|
|
|
|
|
|
|
style.left = (parentWidth - selfWidth) / 2;
|
|
|
|
|
style.top = (parentHeight - selfHeight) / 2;
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-01-12 20:16:17 +09:00
|
|
|
|
|
|
|
|
private void SetRangeInfoVisible(bool visible)
|
|
|
|
|
{
|
|
|
|
|
if (_rangeInfoLabel != null)
|
|
|
|
|
{
|
|
|
|
|
_rangeInfoLabel.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateRangeInfo()
|
|
|
|
|
{
|
|
|
|
|
if (_rangeInfoLabel == null) return;
|
|
|
|
|
|
|
|
|
|
string startText = _rangeStartDate?.ToString("yyyy-MM-dd") ?? "---";
|
|
|
|
|
string endText = _rangeEndDate?.ToString("yyyy-MM-dd") ?? "---";
|
|
|
|
|
|
|
|
|
|
string stateIndicator = _rangeState == RangeSelectionState.SelectingStart ? "▶ " : " ";
|
|
|
|
|
string endStateIndicator = _rangeState == RangeSelectionState.SelectingEnd ? "▶ " : " ";
|
|
|
|
|
|
|
|
|
|
_rangeInfoLabel.text = $"{stateIndicator}시작: {startText} {endStateIndicator}종료: {endText}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 범위 선택 모드인지 확인
|
|
|
|
|
/// </summary>
|
|
|
|
|
private bool IsRangeMode => _mode == PickerMode.DateRange || _mode == PickerMode.DateTimeRange;
|
2026-01-08 20:15:57 +09:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region IDisposable
|
|
|
|
|
public void Dispose()
|
|
|
|
|
{
|
|
|
|
|
if (_disposed) return;
|
|
|
|
|
_disposed = true;
|
|
|
|
|
|
|
|
|
|
// 언어 변경 이벤트 구독 해제
|
|
|
|
|
LocalizationManager.Instance.OnLanguageChanged -= OnLanguageChanged;
|
|
|
|
|
|
|
|
|
|
_dayButtons.Clear();
|
|
|
|
|
|
|
|
|
|
OnDateSelected = null;
|
|
|
|
|
OnClosed = null;
|
|
|
|
|
|
|
|
|
|
_blocker = null;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
}
|
|
|
|
|
}
|