UTKInput Validation 기능 추가

This commit is contained in:
logonkhi
2026-02-12 18:04:38 +09:00
parent df6d3e3b5a
commit 93e1423525
54 changed files with 2900 additions and 188 deletions

View File

@@ -16,6 +16,10 @@ namespace UVC.UIToolkit
/// <para>- Center: 경계 상자의 중심점 (Vector3)</para>
/// <para>- Extents: 중심에서 각 축 방향으로의 거리 (Vector3), Size의 절반 값</para>
/// <para>- Size: 경계 상자의 전체 크기 (Extents * 2)</para>
/// <list type="bullet">
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
@@ -52,6 +56,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Bounds(Vector3.zero, Vector3.one);
/// readOnlyField.IsReadOnly = true;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var boundsField = new UTKBoundsField("경계");
/// boundsField.ErrorMessage = "크기는 양수여야 합니다.";
/// boundsField.Validation = () => boundsField.Value.size.x > 0 &amp;&amp; boundsField.Value.size.y > 0 &amp;&amp; boundsField.Value.size.z > 0;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = boundsField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// boundsField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
/// boundsField.ErrorMessage = "서버 오류가 발생했습니다.";
/// boundsField.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <!-- 네임스페이스 선언 -->
@@ -70,6 +94,9 @@ namespace UVC.UIToolkit
///
/// <!-- 읽기 전용 -->
/// <utk:UTKBoundsField label="고정 경계" is-readonly="true" />
///
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
/// <utk:UTKBoundsField label="경계" error-message="크기는 양수여야 합니다." />
/// </UXML>
/// </code>
/// <para><b>실제 활용 예시:</b></para>
@@ -102,6 +129,9 @@ namespace UVC.UIToolkit
private string _xLabel = "X";
private string _yLabel = "Y";
private string _zLabel = "Z";
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -202,6 +232,27 @@ namespace UVC.UIToolkit
EnableInClassList("utk-boundsfield--readonly", value);
}
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-boundsfield--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
#endregion
#region Constructor
@@ -238,6 +289,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<Bounds>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -303,6 +355,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-boundsfield--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-boundsfield--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-boundsfield__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -314,8 +433,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<Bounds>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// <item><description>과학 계산, 금융 데이터, 정밀 측정값에 사용</description></item>
/// <item><description>float보다 약 2배 정밀도 (유효숫자 15-17자리)</description></item>
/// <item><description>일반 게임 로직에는 UTKFloatField로 충분</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// <para><b>float vs double:</b></para>
/// <list type="bullet">
@@ -44,6 +46,20 @@ namespace UVC.UIToolkit
/// double currentValue = doubleField.Value;
/// doubleField.Value = 127.9780;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var precisionField = new UTKDoubleField("정밀 값", 0);
/// precisionField.ErrorMessage = "값은 0보다 커야 합니다.";
/// precisionField.Validation = () => precisionField.Value > 0;
/// // → FocusOut 시 자동으로 검증
///
/// // 강제 검증 호출
/// bool isValid = precisionField.Validate();
///
/// // 에러 수동 해제
/// precisionField.ClearError();
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 Double 필드 -->
@@ -72,6 +88,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -87,6 +106,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-double-field--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
/// <summary>활성화 상태</summary>
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -133,6 +173,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<double>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -165,6 +207,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
private void OnKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return)
{
RunValidation();
}
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-double-field--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-double-field--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-double-field__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -176,8 +293,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<double>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
UnregisterCallback<KeyDownEvent>(OnKeyDown);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -18,6 +18,8 @@ namespace UVC.UIToolkit
/// <item><description>가격, 속도, 거리 등 실수 값에 적합</description></item>
/// <item><description>더 높은 정밀도가 필요하면 UTKDoubleField 사용</description></item>
/// <item><description>정수만 필요하면 UTKIntegerField 사용</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// <para><b>주의:</b> float 리터럴은 숫자 뒤에 'f'를 붙여야 합니다 (예: 3.14f)</para>
/// </remarks>
@@ -44,6 +46,31 @@ namespace UVC.UIToolkit
/// // 비활성화
/// floatField.IsEnabled = false;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var speedField = new UTKFloatField("속도", 0f);
/// speedField.ErrorMessage = "속도는 0보다 커야 합니다.";
/// speedField.Validation = () => speedField.Value > 0f;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 범위 검증
/// var temperatureField = new UTKFloatField("온도", 20f);
/// temperatureField.ErrorMessage = "온도는 -40 ~ 60 사이여야 합니다.";
/// temperatureField.Validation = () => temperatureField.Value >= -40f &amp;&amp; temperatureField.Value &lt;= 60f;
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = speedField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// speedField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
/// floatField.ErrorMessage = "서버 오류가 발생했습니다.";
/// floatField.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 실수 필드 -->
@@ -56,6 +83,8 @@ namespace UVC.UIToolkit
/// <code>
/// // 캐릭터 이동 속도 편집
/// var moveSpeedField = new UTKFloatField("이동 속도", character.MoveSpeed);
/// moveSpeedField.ErrorMessage = "이동 속도는 0 이상이어야 합니다.";
/// moveSpeedField.Validation = () => moveSpeedField.Value >= 0f;
/// moveSpeedField.OnValueChanged += (newSpeed) => {
/// character.MoveSpeed = Mathf.Clamp(newSpeed, 0f, 100f);
/// };
@@ -71,6 +100,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -86,6 +118,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-float-field--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
/// <summary>활성화 상태</summary>
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -132,6 +185,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<float>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -164,6 +219,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
private void OnKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return)
{
RunValidation();
}
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-float-field--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-float-field--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-float-field__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -175,8 +305,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<float>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
UnregisterCallback<KeyDownEvent>(OnKeyDown);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -25,6 +25,12 @@ namespace UVC.UIToolkit
/// <item><description>증감 단위 설정 (Step)</description></item>
/// <item><description>순환 모드 (WrapAround) - 최대에서 최소로, 최소에서 최대로</description></item>
/// </list>
/// <para><b>Validation (유효성 검사):</b></para>
/// <para>
/// <see cref="Validation"/> 속성에 검증 함수를 설정하면,
/// 포커스 아웃 시 자동으로 유효성 검사를 수행합니다.
/// 검증 실패 시 <see cref="ErrorMessage"/>에 설정된 메시지가 표시됩니다.
/// </para>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
@@ -56,6 +62,12 @@ namespace UVC.UIToolkit
/// var readOnlyStepper = new UTKFloatStepper(0f, 10f, 5f, 0.1f);
/// readOnlyStepper.IsReadOnly = true;
/// </code>
/// <para><b>Validation 사용:</b></para>
/// <code>
/// var volumeStepper = new UTKFloatStepper(0f, 1f, 0.5f, 0.1f);
/// volumeStepper.ErrorMessage = "볼륨은 0~1 사이여야 합니다.";
/// volumeStepper.Validation = () => volumeStepper.Value >= 0f &amp;&amp; volumeStepper.Value &lt;= 1f;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 스테퍼 -->
@@ -69,6 +81,9 @@ namespace UVC.UIToolkit
///
/// <!-- 읽기 전용 -->
/// <utk:UTKFloatStepper value="5.0" is-readonly="true" />
///
/// <!-- 에러 메시지 설정 -->
/// <utk:UTKFloatStepper value="0.5" error-message="볼륨은 0~1 사이여야 합니다." />
/// ]]></code>
/// <para><b>실제 활용 예시 (볼륨 조절):</b></para>
/// <code>
@@ -142,6 +157,20 @@ namespace UVC.UIToolkit
EnableInClassList("utk-number-stepper--readonly", value);
}
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-number-stepper--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
#endregion
#region Fields
@@ -154,10 +183,29 @@ namespace UVC.UIToolkit
private bool _wrapAround;
private bool _isUpdating;
private bool _isHovered;
private string _errorMessage = "";
private TextField? _textField;
private Button? _upButton;
private Button? _downButton;
/// <summary>유효성 검사 함수. true 반환 시 유효, false 반환 시 에러 표시</summary>
private Func<bool>? _validation;
/// <summary>에러 메시지 표시용 레이블</summary>
private Label? _errorLabel;
#endregion
#region Properties
/// <summary>
/// 유효성 검사 함수.
/// 포커스 아웃 시 자동으로 호출되며, false 반환 시 에러 스타일과 메시지가 표시됩니다.
/// </summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
#endregion
#region Events
@@ -241,6 +289,7 @@ namespace UVC.UIToolkit
if (notify)
{
OnValueChanged?.Invoke(_value);
RunValidation();
}
}
}
@@ -288,6 +337,18 @@ namespace UVC.UIToolkit
{
_textField?.Focus();
}
/// <summary>강제로 Validation을 실행하여 에러 상태를 업데이트합니다.</summary>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
#endregion
#region Private Methods - UI Creation
@@ -344,6 +405,7 @@ namespace UVC.UIToolkit
_textField?.RegisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.RegisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
_textField?.RegisterCallback<FocusOutEvent>(OnTextFieldFocusOut);
RegisterCallback<MouseEnterEvent>(OnMouseEnter);
RegisterCallback<MouseLeaveEvent>(OnMouseLeave);
@@ -397,6 +459,12 @@ namespace UVC.UIToolkit
}
}
/// <summary>텍스트 필드 포커스 아웃 시 유효성 검사 실행</summary>
private void OnTextFieldFocusOut(FocusOutEvent evt)
{
RunValidation();
}
private void OnMouseEnter(MouseEnterEvent evt) => _isHovered = true;
private void OnMouseLeave(MouseLeaveEvent evt) => _isHovered = false;
@@ -433,9 +501,25 @@ namespace UVC.UIToolkit
private float ClampValueInternal(float value)
{
// Step 소수점 자릿수 기준으로 반올림하여 부동소수점 오차 제거
// 예: step=0.1 → digits=1, 0.7+0.1=0.8000001 → 0.8
int digits = GetDecimalDigits(_step);
if (digits > 0)
{
value = (float)Math.Round(value, digits);
}
return Mathf.Clamp(value, _minValue, _maxValue);
}
/// <summary>소수점 이하 유효 자릿수를 반환합니다.</summary>
private static int GetDecimalDigits(float value)
{
// 최대 7자리까지 검사 (float 정밀도 한계)
string s = value.ToString("G7");
int dotIndex = s.IndexOf('.');
return dotIndex < 0 ? 0 : s.Length - dotIndex - 1;
}
private void UpdateReadOnlyState()
{
if (_textField != null)
@@ -453,6 +537,47 @@ namespace UVC.UIToolkit
_downButton.SetEnabled(!_isReadOnly);
}
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
EnableInClassList("utk-number-stepper--error", false);
UpdateErrorLabel(null);
}
else
{
EnableInClassList("utk-number-stepper--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-number-stepper__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -471,6 +596,7 @@ namespace UVC.UIToolkit
_textField?.UnregisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.UnregisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
_textField?.UnregisterCallback<FocusOutEvent>(OnTextFieldFocusOut);
UnregisterCallback<MouseEnterEvent>(OnMouseEnter);
UnregisterCallback<MouseLeaveEvent>(OnMouseLeave);
@@ -481,6 +607,10 @@ namespace UVC.UIToolkit
OnTabPressed = null;
OnShiftTabPressed = null;
// Validation 정리
_validation = null;
_errorLabel = null;
// UI 참조 정리
_textField = null;
_upButton = null;

View File

@@ -9,6 +9,15 @@ namespace UVC.UIToolkit
/// 입력 필드 컴포넌트.
/// Unity TextField를 래핑하여 커스텀 스타일을 적용합니다.
/// </summary>
/// <remarks>
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item><description>플레이스홀더, 비밀번호, 멀티라인 지원</description></item>
/// <item><description>스타일 변형 (Default, Filled, Outlined)</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (Submit/FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
/// <code>
@@ -17,39 +26,62 @@ namespace UVC.UIToolkit
/// input.label = "이름";
/// input.Placeholder = "이름을 입력하세요";
/// input.OnValueChanged += (value) => Debug.Log($"입력값: {value}");
///
///
/// // 비밀번호 입력 필드
/// var password = new UTKInputField();
/// password.label = "비밀번호";
/// password.isPasswordField = true;
///
/// // 검증 오류 표시
/// input.ErrorMessage = "이름은 필수입니다.";
/// // 오류 제거
/// input.ErrorMessage = "";
///
///
/// // 변형 스타일
/// input.Variant = UTKInputField.InputFieldVariant.Outlined;
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var emailInput = new UTKInputField("이메일", "example@email.com");
/// emailInput.ErrorMessage = "올바른 이메일 형식이 아닙니다.";
/// emailInput.Validation = () => emailInput.Value.Contains("@");
/// // → Submit(Enter) 또는 FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 필수 입력 검증
/// var nameInput = new UTKInputField("이름");
/// nameInput.ErrorMessage = "이름은 필수 항목입니다.";
/// nameInput.Validation = () => !string.IsNullOrWhiteSpace(nameInput.Value);
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = nameInput.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// nameInput.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이)
/// input.ErrorMessage = "서버 오류가 발생했습니다.";
/// input.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <ui:UXML xmlns:utk="UVC.UIToolkit">
/// <!-- 기본 입력 필드 -->
/// <utk:UTKInputField label="이름" />
///
///
/// <!-- 플레이스홀더 -->
/// <utk:UTKInputField label="이메일" placeholder="example@email.com" />
///
///
/// <!-- 비밀번호 필드 -->
/// <utk:UTKInputField label="비밀번호" is-password-field="true" />
///
///
/// <!-- 여러 줄 입력 -->
/// <utk:UTKInputField label="설명" multiline="true" />
///
///
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
/// <utk:UTKInputField label="이메일" error-message="올바른 이메일 형식이 아닙니다." />
///
/// <!-- 비활성화 -->
/// <utk:UTKInputField label="읽기전용" is-enabled="false" value="수정 불가" />
/// </ui:UXML>
/// </code>
/// ]]></code>
/// </example>
[UxmlElement]
public partial class UTKInputField : TextField, IDisposable
@@ -63,6 +95,8 @@ namespace UVC.UIToolkit
private bool _isEnabled = true;
private string _errorMessage = "";
private InputFieldVariant _variant = InputFieldVariant.Default;
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -92,7 +126,7 @@ namespace UVC.UIToolkit
set => textEdition.placeholder = value;
}
/// <summary>에러 메시지</summary>
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
@@ -100,10 +134,19 @@ namespace UVC.UIToolkit
set
{
_errorMessage = value;
EnableInClassList("utk-input--error", !string.IsNullOrEmpty(value));
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-input--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. Submit/FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
/// <summary>활성화 상태</summary>
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -199,26 +242,9 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<string>>(OnTextValueChanged);
RegisterCallback<FocusInEvent>(_ =>
{
EnableInClassList("utk-input--focused", true);
OnFocused?.Invoke();
});
RegisterCallback<FocusOutEvent>(_ =>
{
EnableInClassList("utk-input--focused", false);
OnBlurred?.Invoke();
});
RegisterCallback<KeyDownEvent>(evt =>
{
if (evt.keyCode == KeyCode.Return && !multiline)
{
OnSubmit?.Invoke(value);
}
}, TrickleDown.TrickleDown);
RegisterCallback<FocusInEvent>(OnFocusIn);
RegisterCallback<FocusOutEvent>(OnFocusOut);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -263,6 +289,28 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusIn(FocusInEvent evt)
{
EnableInClassList("utk-input--focused", true);
OnFocused?.Invoke();
}
private void OnFocusOut(FocusOutEvent evt)
{
EnableInClassList("utk-input--focused", false);
OnBlurred?.Invoke();
RunValidation();
}
private void OnKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return && !multiline)
{
OnSubmit?.Invoke(value);
RunValidation();
}
}
#endregion
#region Methods
@@ -283,6 +331,15 @@ namespace UVC.UIToolkit
}
}
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>
/// 선택 영역 설정
/// </summary>
@@ -291,6 +348,57 @@ namespace UVC.UIToolkit
textSelection.SelectAll();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-input--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-input--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-input__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
private void UpdateVariant()
{
RemoveFromClassList("utk-input--default");
@@ -318,10 +426,16 @@ namespace UVC.UIToolkit
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
UnregisterCallback<ChangeEvent<string>>(OnTextValueChanged);
UnregisterCallback<FocusInEvent>(OnFocusIn);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
UnregisterCallback<KeyDownEvent>(OnKeyDown);
OnValueChanged = null;
OnFocused = null;
OnBlurred = null;
OnSubmit = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -24,6 +24,8 @@ namespace UVC.UIToolkit
/// <item><description>최소/최대값 제한 (MinValue, MaxValue)</description></item>
/// <item><description>증감 단위 설정 (Step)</description></item>
/// <item><description>순환 모드 (WrapAround) - 최대에서 최소로, 최소에서 최대로</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// </remarks>
/// <example>
@@ -79,6 +81,15 @@ namespace UVC.UIToolkit
/// UpdateCalendar(month);
/// };
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// var monthStepper = new UTKIntStepper(1, 12, 1, 1);
/// monthStepper.ErrorMessage = "유효하지 않은 월입니다.";
/// monthStepper.Validation = () => monthStepper.Value >= 1 &amp;&amp; monthStepper.Value &lt;= 12;
///
/// bool isValid = monthStepper.Validate();
/// monthStepper.ClearError();
/// </code>
/// </example>
[UxmlElement]
public partial class UTKIntStepper : VisualElement, IDisposable
@@ -143,6 +154,20 @@ namespace UVC.UIToolkit
EnableInClassList("utk-number-stepper--readonly", value);
}
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-number-stepper--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
#endregion
#region Fields
@@ -156,11 +181,24 @@ namespace UVC.UIToolkit
private bool _isUpdating;
private bool _isHovered;
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
private TextField? _textField;
private Button? _upButton;
private Button? _downButton;
#endregion
#region Properties
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
#endregion
#region Events
/// <summary>값이 변경될 때 발생</summary>
public event Action<int>? OnValueChanged;
@@ -242,6 +280,7 @@ namespace UVC.UIToolkit
if (notify)
{
OnValueChanged?.Invoke(_value);
RunValidation();
}
}
}
@@ -289,6 +328,18 @@ namespace UVC.UIToolkit
{
_textField?.Focus();
}
/// <summary>강제로 Validation을 실행하여 에러 상태를 업데이트합니다.</summary>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
#endregion
#region Private Methods - UI Creation
@@ -345,6 +396,7 @@ namespace UVC.UIToolkit
_textField?.RegisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.RegisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
_textField?.RegisterCallback<FocusOutEvent>(OnTextFieldFocusOut);
RegisterCallback<MouseEnterEvent>(OnMouseEnter);
RegisterCallback<MouseLeaveEvent>(OnMouseLeave);
@@ -371,6 +423,11 @@ namespace UVC.UIToolkit
}
}
private void OnTextFieldFocusOut(FocusOutEvent evt)
{
RunValidation();
}
private void OnTextFieldKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.UpArrow)
@@ -454,6 +511,47 @@ namespace UVC.UIToolkit
_downButton.SetEnabled(!_isReadOnly);
}
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
EnableInClassList("utk-number-stepper--error", false);
UpdateErrorLabel(null);
}
else
{
EnableInClassList("utk-number-stepper--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-number-stepper__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -472,6 +570,7 @@ namespace UVC.UIToolkit
_textField?.UnregisterCallback<ChangeEvent<string>>(OnTextFieldChanged);
_textField?.UnregisterCallback<KeyDownEvent>(OnTextFieldKeyDown, TrickleDown.TrickleDown);
_textField?.UnregisterCallback<FocusOutEvent>(OnTextFieldFocusOut);
UnregisterCallback<MouseEnterEvent>(OnMouseEnter);
UnregisterCallback<MouseLeaveEvent>(OnMouseLeave);
@@ -483,6 +582,8 @@ namespace UVC.UIToolkit
OnShiftTabPressed = null;
// UI 참조 정리
_validation = null;
_errorLabel = null;
_textField = null;
_upButton = null;
_downButton = null;

View File

@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// <item><description>개수, 수량, 인덱스 등 정수 값에 사용</description></item>
/// <item><description>큰 숫자가 필요하면 UTKLongField 사용</description></item>
/// <item><description>소수점이 필요하면 UTKFloatField 사용</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// </remarks>
/// <example>
@@ -42,6 +44,31 @@ namespace UVC.UIToolkit
/// // 비활성화
/// intField.IsEnabled = false;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var ageField = new UTKIntegerField("나이", 0);
/// ageField.ErrorMessage = "나이는 1~150 사이여야 합니다.";
/// ageField.Validation = () => ageField.Value >= 1 &amp;&amp; ageField.Value &lt;= 150;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 양수만 허용
/// var quantityField = new UTKIntegerField("수량", 0);
/// quantityField.ErrorMessage = "수량은 0보다 커야 합니다.";
/// quantityField.Validation = () => quantityField.Value > 0;
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = ageField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// ageField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
/// intField.ErrorMessage = "서버 오류가 발생했습니다.";
/// intField.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 정수 필드 -->
@@ -54,8 +81,10 @@ namespace UVC.UIToolkit
/// <code>
/// // 인벤토리 아이템 수량 편집
/// var quantityField = new UTKIntegerField("보유 수량", item.Quantity);
/// quantityField.ErrorMessage = "수량은 0 이상이어야 합니다.";
/// quantityField.Validation = () => quantityField.Value >= 0;
/// quantityField.OnValueChanged += (newQty) => {
/// item.Quantity = Mathf.Max(0, newQty); // 음수 방지
/// item.Quantity = newQty;
/// UpdateInventoryUI();
/// };
/// </code>
@@ -70,6 +99,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -85,6 +117,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-integer-field--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
/// <summary>활성화 상태</summary>
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -131,6 +184,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<int>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -163,6 +218,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
private void OnKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return)
{
RunValidation();
}
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-integer-field--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-integer-field--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-integer-field__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -174,8 +304,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<int>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
UnregisterCallback<KeyDownEvent>(OnKeyDown);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// <item><description>파일 크기(바이트), 타임스탬프, 고유 ID 등에 사용</description></item>
/// <item><description>int 범위(-21억~21억)를 초과하는 값에 적합</description></item>
/// <item><description>일반 정수는 UTKIntegerField 사용 권장 (메모리 효율)</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// </remarks>
/// <example>
@@ -39,6 +41,26 @@ namespace UVC.UIToolkit
/// long currentValue = longField.Value;
/// longField.Value = 5000000000L; // 50억
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var fileSizeField = new UTKLongField("파일 크기", 0);
/// fileSizeField.ErrorMessage = "파일 크기는 0보다 커야 합니다.";
/// fileSizeField.Validation = () => fileSizeField.Value > 0;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = fileSizeField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// fileSizeField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
/// longField.ErrorMessage = "서버 오류가 발생했습니다.";
/// longField.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 Long 필드 -->
@@ -64,6 +86,9 @@ namespace UVC.UIToolkit
#region Fields
private bool _disposed;
private bool _isEnabled = true;
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -79,6 +104,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-long-field--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
/// <summary>활성화 상태</summary>
[UxmlAttribute("is-enabled")]
public bool IsEnabled
@@ -125,6 +171,8 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<long>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
RegisterCallback<KeyDownEvent>(OnKeyDown, TrickleDown.TrickleDown);
}
private void SubscribeToThemeChanges()
@@ -157,6 +205,81 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
private void OnKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Return)
{
RunValidation();
}
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-long-field--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-long-field--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-long-field__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -168,8 +291,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<long>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
UnregisterCallback<KeyDownEvent>(OnKeyDown);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -17,6 +17,8 @@ namespace UVC.UIToolkit
/// <item><description><b>X, Y</b>: 사각형의 왼쪽 상단 모서리 위치</description></item>
/// <item><description><b>Width</b>: 사각형의 너비</description></item>
/// <item><description><b>Height</b>: 사각형의 높이</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// <para>주로 UI 요소의 위치/크기, 스프라이트 영역, 화면 좌표 등에 사용됩니다.</para>
/// </remarks>
@@ -47,6 +49,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Rect(10, 10, 200, 100);
/// readOnlyField.IsReadOnly = true;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var areaField = new UTKRectField("영역");
/// areaField.ErrorMessage = "너비와 높이는 0보다 커야 합니다.";
/// areaField.Validation = () => areaField.Value.width > 0 &amp;&amp; areaField.Value.height > 0;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = areaField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// areaField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
/// areaField.ErrorMessage = "서버 오류가 발생했습니다.";
/// areaField.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 Rect 필드 -->
@@ -68,6 +90,8 @@ namespace UVC.UIToolkit
/// // 스프라이트 UV 영역 편집기
/// var uvField = new UTKRectField("UV 영역");
/// uvField.Value = sprite.rect;
/// uvField.ErrorMessage = "UV 영역은 텍스처 범위를 초과할 수 없습니다.";
/// uvField.Validation = () => uvField.Value.xMax &lt;= texture.width &amp;&amp; uvField.Value.yMax &lt;= texture.height;
/// uvField.OnValueChanged += (newRect) => {
/// // 스프라이트 영역 업데이트
/// UpdateSpriteRect(sprite, newRect);
@@ -89,6 +113,9 @@ namespace UVC.UIToolkit
private string _yLabel = "Y";
private string _wLabel = "W";
private string _hLabel = "H";
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -117,6 +144,27 @@ namespace UVC.UIToolkit
set => this.value = value;
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-rectfield--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
/// <summary>X 라벨</summary>
[UxmlAttribute("x-label")]
public string XLabel
@@ -221,6 +269,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<Rect>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -276,6 +325,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-rectfield--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-rectfield--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-rectfield__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -287,8 +403,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<Rect>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -16,6 +16,13 @@ namespace UVC.UIToolkit
/// <para>- X: 수평(가로) 방향의 값</para>
/// <para>- Y: 수직(세로) 방향의 값</para>
/// <para>주로 2D 게임의 위치, UI 크기, 텍스처 좌표(UV) 등에 사용됩니다.</para>
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item><description>X, Y 축 라벨 커스터마이징</description></item>
/// <item><description>읽기 전용 모드</description></item>
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
@@ -44,6 +51,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Vector2(100, 50);
/// readOnlyField.IsReadOnly = true;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var sizeField = new UTKVector2Field("크기");
/// sizeField.ErrorMessage = "크기는 양수여야 합니다.";
/// sizeField.Validation = () => sizeField.Value.x > 0 && sizeField.Value.y > 0;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = sizeField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// sizeField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이)
/// sizeField.ErrorMessage = "서버 오류가 발생했습니다.";
/// sizeField.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <!-- 네임스페이스 선언 -->
@@ -59,6 +86,9 @@ namespace UVC.UIToolkit
///
/// <!-- 읽기 전용 -->
/// <utk:UTKVector2Field label="고정 크기" is-readonly="true" />
///
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
/// <utk:UTKVector2Field label="크기" error-message="크기는 양수여야 합니다." />
/// </UXML>
/// </code>
/// <para><b>실제 활용 예시:</b></para>
@@ -90,6 +120,9 @@ namespace UVC.UIToolkit
private bool _isReadOnly = false;
private string _xLabel = "X";
private string _yLabel = "Y";
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -154,6 +187,27 @@ namespace UVC.UIToolkit
EnableInClassList("utk-vector2-field--readonly", value);
}
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-vector2-field--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
#endregion
#region Constructor
@@ -198,6 +252,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<Vector2>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -251,6 +306,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-vector2-field--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-vector2-field--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-vector2-field__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -262,8 +384,12 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<Vector2>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -52,6 +52,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Vector3(1, 2, 3);
/// readOnlyField.IsReadOnly = true;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var posField = new UTKVector3Field("위치");
/// posField.ErrorMessage = "Y값은 0 이상이어야 합니다.";
/// posField.Validation = () => posField.Value.y >= 0;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = posField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// posField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
/// posField.ErrorMessage = "유효하지 않은 좌표입니다.";
/// posField.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code><![CDATA[
/// <!-- 기본 Vector3 필드 -->
@@ -66,6 +86,9 @@ namespace UVC.UIToolkit
///
/// <!-- 읽기 전용 -->
/// <utk:UTKVector3Field label="고정 위치" is-readonly="true" />
///
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
/// <utk:UTKVector3Field label="위치" error-message="유효하지 않은 좌표입니다." />
/// ]]></code>
/// <para><b>실제 활용 예시 (Transform 편집기):</b></para>
/// <code>
@@ -97,6 +120,9 @@ namespace UVC.UIToolkit
private string _xLabel = "X";
private string _yLabel = "Y";
private string _zLabel = "Z";
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -173,6 +199,36 @@ namespace UVC.UIToolkit
EnableInClassList("utk-vector3-field--readonly", value);
}
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-vector3-field--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>
/// 검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시.
/// </summary>
/// <example>
/// <code>
/// var posField = new UTKVector3Field("위치");
/// posField.ErrorMessage = "Y값은 0 이상이어야 합니다.";
/// posField.Validation = () => posField.Value.y >= 0;
/// </code>
/// </example>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
#endregion
#region Constructor
@@ -217,6 +273,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<Vector3>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -271,6 +328,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-vector3-field--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-vector3-field--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-vector3-field__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -282,8 +406,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<Vector3>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -18,6 +18,10 @@ namespace UVC.UIToolkit
/// <para>- 색상(RGBA): Red, Green, Blue, Alpha 값 (0~1 범위)</para>
/// <para>- 쉐이더 파라미터: 커스텀 쉐이더에 전달하는 4개의 값</para>
/// <para>- 사원수(Quaternion)와 유사한 구조로 회전 데이터 저장</para>
/// <list type="bullet">
/// <item><description>Validation 함수를 통한 입력 검증 (FocusOut 시 자동 호출)</description></item>
/// <item><description>에러 상태 시 붉은 외곽선 + 에러 메시지 표시</description></item>
/// </list>
/// </remarks>
/// <example>
/// <para><b>C# 코드에서 사용:</b></para>
@@ -48,6 +52,26 @@ namespace UVC.UIToolkit
/// readOnlyField.Value = new Vector4(1, 0.5f, 0.5f, 1);
/// readOnlyField.IsReadOnly = true;
/// </code>
/// <para><b>Validation (입력 검증):</b></para>
/// <code>
/// // 검증 함수 설정 (Func&lt;bool&gt;)
/// var colorField = new UTKVector4Field("색상");
/// colorField.ErrorMessage = "알파 값은 0~1 사이여야 합니다.";
/// colorField.Validation = () => colorField.Value.w >= 0 &amp;&amp; colorField.Value.w &lt;= 1;
/// // → FocusOut 시 자동으로 검증
/// // → 실패 시 붉은 외곽선 + 에러 메시지 표시, 통과 시 자동 해제
///
/// // 강제 검증 호출 (예: 폼 제출 버튼 클릭 시)
/// bool isValid = colorField.Validate();
/// if (!isValid) return; // 검증 실패
///
/// // 에러 수동 해제
/// colorField.ClearError();
///
/// // 에러 메시지 직접 설정 (Validation 없이, 서버 오류 등)
/// vec4Field.ErrorMessage = "서버 오류가 발생했습니다.";
/// vec4Field.ErrorMessage = ""; // 오류 제거
/// </code>
/// <para><b>UXML에서 사용:</b></para>
/// <code>
/// <!-- 네임스페이스 선언 -->
@@ -64,6 +88,9 @@ namespace UVC.UIToolkit
///
/// <!-- 읽기 전용 -->
/// <utk:UTKVector4Field label="고정값" is-readonly="true" />
///
/// <!-- 에러 메시지 (C#에서 Validation 설정 권장) -->
/// <utk:UTKVector4Field label="색상" error-message="알파 값은 0~1 사이여야 합니다." />
/// </UXML>
/// </code>
/// <para><b>실제 활용 예시:</b></para>
@@ -95,6 +122,9 @@ namespace UVC.UIToolkit
private string _yLabel = "Y";
private string _zLabel = "Z";
private string _wLabel = "W";
private string _errorMessage = "";
private Func<bool>? _validation;
private Label? _errorLabel;
#endregion
#region Events
@@ -183,6 +213,27 @@ namespace UVC.UIToolkit
EnableInClassList("utk-vector4field--readonly", value);
}
}
/// <summary>에러 메시지. 비어있지 않으면 에러 상태로 표시</summary>
[UxmlAttribute("error-message")]
public string ErrorMessage
{
get => _errorMessage;
set
{
_errorMessage = value;
var hasError = !string.IsNullOrEmpty(value);
EnableInClassList("utk-vector4field--error", hasError);
UpdateErrorLabel(hasError ? value : null);
}
}
/// <summary>검증 함수. FocusOut 시 호출되어 false 반환 시 ErrorMessage 표시</summary>
public Func<bool>? Validation
{
get => _validation;
set => _validation = value;
}
#endregion
#region Constructor
@@ -227,6 +278,7 @@ namespace UVC.UIToolkit
private void SetupEvents()
{
RegisterCallback<ChangeEvent<Vector4>>(OnFieldValueChanged);
RegisterCallback<FocusOutEvent>(OnFocusOut);
}
private void SubscribeToThemeChanges()
@@ -282,6 +334,73 @@ namespace UVC.UIToolkit
{
OnValueChanged?.Invoke(evt.newValue);
}
private void OnFocusOut(FocusOutEvent evt)
{
RunValidation();
}
#endregion
#region Methods
/// <summary>
/// 강제로 Validation을 실행하여 에러 상태를 업데이트합니다.
/// </summary>
/// <returns>Validation이 null이면 true, 아니면 Validation 결과</returns>
public bool Validate()
{
return RunValidation();
}
/// <summary>에러 상태를 수동으로 해제합니다.</summary>
public void ClearError()
{
ErrorMessage = "";
}
private bool RunValidation()
{
if (_validation == null) return true;
var isValid = _validation.Invoke();
if (isValid)
{
// 검증 통과 시 에러 상태 해제
EnableInClassList("utk-vector4field--error", false);
UpdateErrorLabel(null);
}
else
{
// 검증 실패 시 에러 상태 표시
EnableInClassList("utk-vector4field--error", true);
UpdateErrorLabel(_errorMessage);
}
return isValid;
}
private void UpdateErrorLabel(string? message)
{
if (string.IsNullOrEmpty(message))
{
// 에러 라벨 숨기기 (존재하면)
if (_errorLabel != null)
{
_errorLabel.style.display = DisplayStyle.None;
}
return;
}
// 에러 라벨 생성 (지연 생성 - 필요할 때만)
if (_errorLabel == null)
{
_errorLabel = new Label();
_errorLabel.AddToClassList("utk-vector4field__error-message");
_errorLabel.style.display = DisplayStyle.None;
Add(_errorLabel);
}
_errorLabel.text = message;
_errorLabel.style.display = DisplayStyle.Flex;
}
#endregion
#region IDisposable
@@ -293,8 +412,11 @@ namespace UVC.UIToolkit
UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
OnValueChanged = null;
UnregisterCallback<ChangeEvent<Vector4>>(OnFieldValueChanged);
UnregisterCallback<FocusOutEvent>(OnFocusOut);
OnValueChanged = null;
_validation = null;
_errorLabel = null;
}
#endregion
}

View File

@@ -976,10 +976,13 @@ namespace UVC.UIToolkit
// TreeView 갱신은 ToggleGroupExpanded에서 처리
}
private void OnItemValueChanged(IUTKPropertyItem item, object? oldValue, object? newValue)
private void OnItemValueChanged(IUTKPropertyItem item, object? oldValue, object? newValue, bool notify)
{
var args = new UTKPropertyValueChangedEventArgs(item, oldValue, newValue);
OnPropertyValueChanged?.Invoke(args);
if (notify)
{
var args = new UTKPropertyValueChangedEventArgs(item, oldValue, newValue);
OnPropertyValueChanged?.Invoke(args);
}
}
private void OnSearch(string newValue)

View File

@@ -38,7 +38,7 @@ namespace UVC.UIToolkit
string? GroupId { get; set; }
/// <summary>값 변경 이벤트</summary>
event Action<IUTKPropertyItem, object?, object?>? OnValueChanged;
event Action<IUTKPropertyItem, object?, object?, bool>? OnValueChanged;
/// <summary>상태(ReadOnly 등) 변경 이벤트. View가 구독하여 UI를 갱신합니다.</summary>
event Action<IUTKPropertyItem>? OnStateChanged;

View File

@@ -124,7 +124,7 @@ namespace UVC.UIToolkit
#region Events
/// <summary>값 변경 이벤트 (object 타입)</summary>
public event Action<IUTKPropertyItem, object?, object?>? OnValueChanged;
public event Action<IUTKPropertyItem, object?, object?, bool>? OnValueChanged;
/// <summary>값 변경 이벤트 (제네릭 타입)</summary>
public event Action<IUTKPropertyItem<T>, T, T>? OnTypedValueChanged;
@@ -159,35 +159,34 @@ namespace UVC.UIToolkit
{
if (default(T) == null)
{
if (notifyChangeEvent)
{
Value = default!;
}
else
if (!Equals(_value, value))
{
var oldValue = _value;
_value = default!;
NotifyValueChanged(oldValue, default!, notifyChangeEvent);
}
}
}
else if (value is T typedValue)
{
if (notifyChangeEvent)
{
Value = typedValue;
}
else
if (!Equals(_value, typedValue))
{
var oldValue = _value;
_value = typedValue;
NotifyValueChanged(oldValue, typedValue, notifyChangeEvent);
}
}
else
{
try
{
if (notifyChangeEvent)
Value = (T)Convert.ChangeType(value, typeof(T));
else
_value = (T)Convert.ChangeType(value, typeof(T));
var v = (T)Convert.ChangeType(value, typeof(T));
if (!Equals(_value, v))
{
var oldValue = _value;
_value = v;
NotifyValueChanged(oldValue, v, notifyChangeEvent);
}
}
catch (Exception ex)
{
@@ -199,10 +198,10 @@ namespace UVC.UIToolkit
#region Protected Methods
/// <summary>값 변경을 알립니다.</summary>
protected void NotifyValueChanged(T oldValue, T newValue)
protected void NotifyValueChanged(T oldValue, T newValue, bool notifyChangeEvent = false)
{
OnTypedValueChanged?.Invoke(this, oldValue, newValue);
OnValueChanged?.Invoke(this, oldValue, newValue);
OnValueChanged?.Invoke(this, oldValue, newValue, notifyChangeEvent);
}
#endregion

View File

@@ -14,7 +14,7 @@ namespace UVC.UIToolkit
/// <para><b>사용법 (Data 바인딩):</b></para>
/// <code>
/// var data = new UTKFloatDropdownPropertyItem("id", "Label", 1.5f,
/// new List&lt;string&gt; { "A", "B", "C" }, "A");
/// new List<string> { "A", "B", "C" }, "A");
/// var view = new UTKFloatDropdownPropertyItemView();
/// view.Bind(data);
/// </code>

View File

@@ -128,8 +128,8 @@ namespace UVC.UIToolkit
/// private void SubscribeToThemeChanges()
/// {
/// UTKThemeManager.Instance.OnThemeChanged += OnThemeChanged;
/// RegisterCallback&lt;AttachToPanelEvent&gt;(OnAttachToPanelForTheme);
/// RegisterCallback&lt;DetachFromPanelEvent&gt;(OnDetachFromPanelForTheme);
/// RegisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
/// RegisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
/// }
///
/// private void OnAttachToPanelForTheme(AttachToPanelEvent evt)
@@ -154,8 +154,8 @@ namespace UVC.UIToolkit
/// if (_disposed) return;
/// _disposed = true;
/// UTKThemeManager.Instance.OnThemeChanged -= OnThemeChanged;
/// UnregisterCallback&lt;AttachToPanelEvent&gt;(OnAttachToPanelForTheme);
/// UnregisterCallback&lt;DetachFromPanelEvent&gt;(OnDetachFromPanelForTheme);
/// UnregisterCallback<AttachToPanelEvent>(OnAttachToPanelForTheme);
/// UnregisterCallback<DetachFromPanelEvent>(OnDetachFromPanelForTheme);
/// }
/// }
/// </code>

View File

@@ -51,7 +51,7 @@ namespace UVC.UIToolkit
/// };
///
/// // 3. 데이터 로드 (그룹 + 개별 아이템 혼합)
/// var entries = new List&lt;IUTKPropertyEntry&gt;();
/// var entries = new List<IUTKPropertyEntry>();
/// entries.Add(new UTKStringPropertyItem("name", "이름", "기본값"));
///
/// var group = new UTKPropertyGroup("transform", "Transform");