modal 개발 중

This commit is contained in:
logonkhi
2025-06-12 19:25:33 +09:00
parent 466dbdbcad
commit e8d52b3e90
434 changed files with 822421 additions and 451 deletions

View File

@@ -0,0 +1,359 @@
using Cysharp.Threading.Tasks;
using System; // System.Type 사용을 위해 추가
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
using UVC.Log;
namespace UVC.UI.Modal
{
public static class Modal
{
private static GameObject currentModalInstance;
private static GameObject currentBlockerInstance;
private static IUniTaskSource activeTcs;
private static Type activeResultType;
private const string DefaultBlockerPrefabPath = "Prefabs/UI/Modal/ModalBlocker";
public static async UniTask<T> Open<T>(ModalContent content, string blockerPrefabPath = DefaultBlockerPrefabPath)
{
if (currentModalInstance != null)
{
ULog.Warning("[Modal] 다른 모달이 이미 열려있습니다. 새 모달을 열기 전에 기존 모달을 닫아주세요.");
return default(T);
}
var tcs = new UniTaskCompletionSource<T>();
activeTcs = tcs;
activeResultType = typeof(T);
CanvasGroup blockerCanvasGroup = null;
GameObject blockerPrefabObj = await Resources.LoadAsync<GameObject>(blockerPrefabPath) as GameObject;
if (blockerPrefabObj != null)
{
Canvas mainCanvasForBlocker = UnityEngine.Object.FindFirstObjectByType<Canvas>();
if (mainCanvasForBlocker != null)
{
currentBlockerInstance = UnityEngine.Object.Instantiate(blockerPrefabObj, mainCanvasForBlocker.transform);
currentBlockerInstance.transform.SetAsLastSibling();
blockerCanvasGroup = currentBlockerInstance.GetComponent<CanvasGroup>();
if (blockerCanvasGroup == null) blockerCanvasGroup = currentBlockerInstance.AddComponent<CanvasGroup>();
blockerCanvasGroup.alpha = 0f;
_ = FadeUI(blockerCanvasGroup, 0.7f, 0.3f, true);
}
else ULog.Error("[Modal] UIBlocker를 표시할 Canvas를 찾을 수 없습니다.");
}
else ULog.Warning($"[Modal] UIBlocker 프리팹을 다음 경로에서 찾을 수 없습니다: {blockerPrefabPath}");
GameObject modalPrefabObj = await Resources.LoadAsync<GameObject>(content.PrefabPath) as GameObject;
if (modalPrefabObj == null)
{
ULog.Error($"[Modal] 모달 프리팹을 다음 경로에서 찾을 수 없습니다: {content.PrefabPath}");
await CleanupCurrentModalResources(null, false); // ModalView가 없으므로 null 전달
tcs.TrySetResult(default(T));
return await tcs.Task;
}
Canvas mainCanvasForModal = UnityEngine.Object.FindObjectOfType<Canvas>();
if (mainCanvasForModal == null)
{
ULog.Error("[Modal] 모달을 표시할 Canvas를 찾을 수 없습니다.");
await CleanupCurrentModalResources(null, false);
tcs.TrySetResult(default(T));
return await tcs.Task;
}
currentModalInstance = UnityEngine.Object.Instantiate(modalPrefabObj, mainCanvasForModal.transform);
currentModalInstance.transform.SetAsLastSibling();
ModalView modalView = currentModalInstance.GetComponent<ModalView>();
if (modalView == null)
{
ULog.Error($"[Modal] 모달 프리팹에 ModalView 컴포넌트가 없습니다: {content.PrefabPath}");
await CleanupCurrentModalResources(null, false);
tcs.TrySetResult(default(T));
return await tcs.Task;
}
var modalDestroyToken = currentModalInstance.GetCancellationTokenOnDestroy();
modalDestroyToken.RegisterWithoutCaptureExecutionContext(async () => {
if (Modal.activeTcs == tcs && tcs.Task.Status == UniTaskStatus.Pending)
{
ULog.Debug("[Modal] 활성 모달 인스턴스가 외부에서 파괴되어 취소로 처리합니다.");
// modalView 참조가 유효하다면 전달, 아니면 null
ModalView viewOnDestroy = currentModalInstance != null ? currentModalInstance.GetComponent<ModalView>() : null;
await CleanupCurrentModalResources(viewOnDestroy, false, true, tcs, typeof(T));
}
});
if (modalView.titleText != null) modalView.titleText.text = content.Title;
if (modalView.messageText != null) modalView.messageText.text = content.Message;
SetupButton(modalView.confirmButton, modalView.confirmButtonText, content.ConfirmButtonText, content.ShowConfirmButton,
() => _ = HandleModalActionAsync(tcs, true, modalView));
SetupButton(modalView.cancelButton, modalView.cancelButtonText, content.CancelButtonText, content.ShowCancelButton,
() => _ = HandleModalActionAsync(tcs, false, modalView));
SetupButton(modalView.closeButton, null, null, true,
() => _ = HandleModalActionAsync(tcs, false, modalView));
// OnOpen 호출
modalView.OnOpen();
return await tcs.Task;
}
private static void SetupButton(Button button, TMPro.TextMeshProUGUI buttonTextComponent, string text, bool showButton, Action onClickAction)
{
if (button == null) return;
button.gameObject.SetActive(showButton);
if (showButton)
{
if (buttonTextComponent != null && !string.IsNullOrEmpty(text)) buttonTextComponent.text = text;
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() => onClickAction?.Invoke());
}
}
private static void SetButtonsInteractable(ModalView view, bool interactable)
{
if (view == null) return;
if (view.confirmButton != null) view.confirmButton.interactable = interactable;
if (view.cancelButton != null) view.cancelButton.interactable = interactable;
if (view.closeButton != null) view.closeButton.interactable = interactable;
}
private static async UniTaskVoid HandleModalActionAsync<T>(
UniTaskCompletionSource<T> tcs,
bool isConfirmAction,
ModalView modalViewContext)
{
if (tcs != activeTcs || currentModalInstance == null || modalViewContext == null)
{
return;
}
SetButtonsInteractable(modalViewContext, false);
// GetResult 호출 시점 변경: CleanupCurrentModalResources 내부에서 호출하도록 변경하거나,
// 여기서 결과를 받고 Cleanup에는 결과값 자체를 넘겨주는 방식도 고려 가능.
// 현재는 Cleanup 내부에서 ModalView를 통해 GetResult를 호출하도록 유지.
await CleanupCurrentModalResources(modalViewContext, isConfirmAction, false, tcs, typeof(T));
}
private static async UniTask CleanupCurrentModalResources(
ModalView modalViewToClose, // ModalView 인스턴스 전달
bool confirmOrCancel,
bool wasExternallyDestroyed = false,
IUniTaskSource tcsToResolve = null,
Type resultTypeForTcs = null)
{
var blockerInstanceToDestroy = currentBlockerInstance;
var modalInstanceToDestroy = currentModalInstance; // 이 시점의 currentModalInstance 사용
if (tcsToResolve == null) tcsToResolve = activeTcs;
if (resultTypeForTcs == null) resultTypeForTcs = activeResultType;
currentModalInstance = null;
currentBlockerInstance = null;
activeTcs = null;
activeResultType = null;
if (blockerInstanceToDestroy != null)
{
var blockerCG = blockerInstanceToDestroy.GetComponent<CanvasGroup>();
if (blockerCG != null) await FadeUI(blockerCG, 0f, 0.3f, false);
UnityEngine.Object.Destroy(blockerInstanceToDestroy);
}
if (tcsToResolve != null && tcsToResolve.GetStatus(0) == UniTaskStatus.Pending)
{
if (resultTypeForTcs == typeof(bool))
{
if (tcsToResolve is UniTaskCompletionSource<bool> boolCompletionSource)
{
boolCompletionSource.TrySetResult(confirmOrCancel);
}
}
else // bool이 아닌 다른 타입의 경우 ModalView.GetResult() 사용
{
object resultFromView = null;
if (modalViewToClose != null) // modalViewToClose가 유효할 때만 GetResult 호출
{
resultFromView = modalViewToClose.GetResult();
}
if (tcsToResolve.GetType().IsGenericType &&
tcsToResolve.GetType().GetGenericTypeDefinition() == typeof(UniTaskCompletionSource<>))
{
var genericArg = tcsToResolve.GetType().GetGenericArguments()[0];
if (genericArg == resultTypeForTcs)
{
object resultToSet;
if (resultFromView != null && resultTypeForTcs.IsAssignableFrom(resultFromView.GetType()))
{
resultToSet = resultFromView;
}
else
{
if (resultFromView != null) // 타입은 안맞지만 null은 아닐 때 경고
{
ULog.Warning($"[Modal] GetResult() 반환 타입({resultFromView.GetType()})과 모달 결과 타입({resultTypeForTcs})이 일치하지 않습니다. 기본값을 사용합니다.");
}
resultToSet = GetDefault(resultTypeForTcs);
}
var trySetResultHelperMethod = typeof(Modal).GetMethod(nameof(TrySetGenericResultHelper), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
.MakeGenericMethod(resultTypeForTcs);
trySetResultHelperMethod.Invoke(null, new object[] { tcsToResolve, resultToSet });
}
}
}
}
if (modalInstanceToDestroy != null) // modalInstanceToDestroy는 Cleanup 시작 시점의 currentModalInstance
{
// OnClose 호출
// modalViewToClose가 null일 수 있으므로, modalInstanceToDestroy에서 다시 가져오거나 확인
ModalView viewForOnClose = modalInstanceToDestroy.GetComponent<ModalView>();
if (viewForOnClose != null)
{
viewForOnClose.OnClose();
}
if (!wasExternallyDestroyed)
{
UnityEngine.Object.Destroy(modalInstanceToDestroy);
}
}
}
// Helper for setting generic result
private static void TrySetGenericResultHelper<T>(IUniTaskSource tcs, object result)
{
if (tcs is UniTaskCompletionSource<T> genericTcs)
{
try
{
genericTcs.TrySetResult((T)result);
}
catch (InvalidCastException ex)
{
ULog.Error($"[Modal] 결과를 {typeof(T)}로 캐스팅하는데 실패했습니다: {ex.Message}. 기본값을 사용합니다.", ex);
genericTcs.TrySetResult(default(T));
}
}
}
public static async UniTask Close()
{
if (currentModalInstance == null && activeTcs == null) return;
IUniTaskSource tcsToClose = activeTcs;
Type resultTypeToClose = activeResultType;
ModalView view = currentModalInstance?.GetComponent<ModalView>();
if (view != null) SetButtonsInteractable(view, false);
var localModalInstance = currentModalInstance;
var localBlockerInstance = currentBlockerInstance;
currentModalInstance = null;
currentBlockerInstance = null;
activeTcs = null;
activeResultType = null;
if (tcsToClose != null && tcsToClose.GetStatus(0) == UniTaskStatus.Pending)
{
if (resultTypeToClose == typeof(bool) && tcsToClose is UniTaskCompletionSource<bool> boolTcs)
{
boolTcs.TrySetResult(false); // Close는 항상 false(취소)로 간주
}
else if (resultTypeToClose != null && tcsToClose.GetType().IsGenericType && tcsToClose.GetType().GetGenericTypeDefinition() == typeof(UniTaskCompletionSource<>))
{
// Close 시에는 GetResult()를 사용하지 않고 기본값(취소)으로 처리
var genericArg = tcsToClose.GetType().GetGenericArguments()[0];
if (genericArg == resultTypeToClose)
{
var trySetDefaultResultMethod = typeof(Modal).GetMethod(nameof(TrySetDefaultResultHelper), System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)
.MakeGenericMethod(genericArg);
trySetDefaultResultMethod.Invoke(null, new object[] { tcsToClose });
}
}
}
if (localBlockerInstance != null)
{
var blockerCG = localBlockerInstance.GetComponent<CanvasGroup>();
if (blockerCG != null) await FadeUI(blockerCG, 0f, 0.3f, false);
UnityEngine.Object.Destroy(localBlockerInstance);
}
if (localModalInstance != null)
{
// OnClose 호출
ModalView viewForOnClose = localModalInstance.GetComponent<ModalView>();
if (viewForOnClose != null)
{
viewForOnClose.SendMessage("OnClose", SendMessageOptions.DontRequireReceiver);
}
UnityEngine.Object.Destroy(localModalInstance);
}
}
private static void TrySetDefaultResultHelper<T>(IUniTaskSource tcs)
{
if (tcs is UniTaskCompletionSource<T> genericTcs)
{
genericTcs.TrySetResult(default(T));
}
}
private static async UniTask FadeUI(CanvasGroup canvasGroup, float targetAlpha, float duration, bool fadeIn)
{
if (canvasGroup == null) return;
CancellationToken cancellationToken;
try
{
cancellationToken = canvasGroup.GetCancellationTokenOnDestroy();
}
catch (Exception ex)
{
ULog.Error($"[Modal] CanvasGroup에 대한 취소 토큰을 가져오는 중 오류 발생: {ex.Message}", ex);
return;
}
float startAlpha = canvasGroup.alpha;
float time = 0;
if (fadeIn)
{
canvasGroup.interactable = true;
canvasGroup.blocksRaycasts = true;
}
while (time < duration)
{
if (cancellationToken.IsCancellationRequested || canvasGroup == null) return;
canvasGroup.alpha = Mathf.Lerp(startAlpha, targetAlpha, time / duration);
time += Time.deltaTime;
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken).SuppressCancellationThrow();
}
if (cancellationToken.IsCancellationRequested || canvasGroup == null) return;
canvasGroup.alpha = targetAlpha;
if (!fadeIn)
{
canvasGroup.interactable = false;
canvasGroup.blocksRaycasts = false;
}
}
private static object GetDefault(Type type)
{
if (type == null) return null;
if (type.IsValueType) return Activator.CreateInstance(type);
return null;
}
}
}