using Cysharp.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.UI; using UVC.Log; namespace UVC.UI.Modal { /// /// 🖼️ 모달 창의 실제 '모습'을 담당하는 친구예요. Unity 에디터에서 만든 UI 요소들(버튼, 글상자 등)을 가지고 있어요. /// `ModalContent`라는 레시피를 받아서, 그 내용대로 화면에 그림을 그려주는 역할을 해요. /// 이 스크립트는 모달로 사용할 프리팹(Prefab)의 가장 바깥쪽 부모 게임 오브젝트에 붙여줘야 해요. /// /// /// 만약 입력창이 있는 특별한 모달을 만들고 싶다면, 이 ModalView를 상속받아서 만들 수 있어요: /// /// public class InputModalView : ModalView /// { /// [Header("My Special UI")] /// public TMP_InputField myInputField; // Unity 에디터에서 연결해줘야 해요. /// private string _inputValue = ""; /// /// public override async UniTask OnOpen(ModalContent content) /// { /// await base.OnOpen(content); // 부모의 OnOpen을 먼저 호출해서 기본 UI를 설정해요. /// ULog.Debug("입력 모달이 열렸어요! 입력창을 초기화합니다."); /// if (myInputField != null) /// { /// myInputField.text = ""; // 입력창 비우기 /// // 입력창에 변화가 있을 때마다 _inputValue를 업데이트하도록 설정할 수 있어요. /// myInputField.onValueChanged.AddListener(OnInputChanged); /// } /// } /// /// private void OnInputChanged(string newValue) /// { /// _inputValue = newValue; /// ULog.Debug($"입력된 값: {newValue}"); /// } /// /// public override object GetResult() /// { /// // 이 모달이 닫힐 때, 입력된 글자를 결과로 돌려줘요. /// return _inputValue; /// } /// /// public override async UniTask OnClose(ModalContent content) /// { /// ULog.Debug("입력 모달이 닫힙니다."); /// if (myInputField != null) /// { /// myInputField.onValueChanged.RemoveListener(OnInputChanged); // 리스너 정리 /// } /// await base.OnClose(content); /// } /// } /// /// // 이 InputModalView를 사용하는 방법: /// // 1. InputModalView 스크립트가 붙어있는 프리팹을 만들고, myInputField를 연결해요. /// // 2. ModalContent 만들 때 이 프리팹 경로를 사용해요. /// // var inputContent = new ModalContent("Prefabs/UI/MyInputModalPrefab") { Title = "이름 입력", Message = "이름을 입력해주세요." }; /// // string enteredName = await Modal.Open(inputContent); /// // if (!string.IsNullOrEmpty(enteredName)) { ULog.Debug($"환영합니다, {enteredName}님!"); } /// /// public class ModalView : MonoBehaviour { [Header("UI Elements")] /// /// 🏷️ 모달 창의 제목을 보여줄 글상자(TextMeshProUGUI)예요. /// Unity 에디터의 인스펙터 창에서 실제 UI 요소를 끌어다 연결해줘야 해요. /// public TextMeshProUGUI titleText; /// /// 💬 모달 창의 주요 메시지를 보여줄 글상자예요. 이것도 연결해주세요! /// public TextMeshProUGUI messageText; /// /// ✅ '확인' 버튼이에요. 연결 필수! /// public Button confirmButton; /// /// 확인 버튼 안에 있는 글상자예요. 확인 버튼 글자를 바꿀 때 사용돼요. /// public TextMeshProUGUI confirmButtonText; /// /// ❌ '취소' 버튼이에요. 이것도 연결해주세요! /// public Button cancelButton; /// /// 취소 버튼 안에 있는 글상자예요. 취소 버튼 글자를 바꿀 때 사용돼요. /// public TextMeshProUGUI cancelButtonText; /// /// ✖️ 모달 창을 닫는 (보통 오른쪽 위에 있는 X 모양) 버튼이에요. /// public Button closeButton; // 닫기 버튼 // 필요에 따라 다른 UI 요소들을 추가할 수 있습니다. // 예: public Image backgroundImage; // 예: public InputField inputField; /// /// 🚀 모달 창이 화면에 나타날 때 `Modal` 클래스가 호출하는 마법이에요! (비동기 작업 가능) /// `ModalContent` 레시피를 받아서, 이 `ModalView`의 UI 요소들(제목, 메시지, 버튼 등)을 레시피대로 설정해요. /// `ModalContent.OnOpen()`이 호출된 *후에* 실행돼요. /// /// 모달에 보여줄 내용과 설정을 담은 '레시피' (`ModalContent` 객체)예요. public virtual async UniTask OnOpen(ModalContent content) { //ULog.Debug($"[ModalView] {gameObject.name} OnOpen called."); // ModalContent 레시피에 적힌 대로 UI 요소들을 설정해요. if (titleText != null && content != null) { titleText.text = content.Title; } if (messageText != null && content != null) { messageText.text = content.Message; } // 확인 버튼 설정 if (confirmButton != null && content != null) { confirmButton.gameObject.SetActive(content.ShowConfirmButton); if (content.ShowConfirmButton && confirmButtonText != null && !string.IsNullOrEmpty(content.ConfirmButtonText)) { confirmButtonText.text = content.ConfirmButtonText; } } // 취소 버튼 설정 if (cancelButton != null && content != null) { cancelButton.gameObject.SetActive(content.ShowCancelButton); if (content.ShowCancelButton && cancelButtonText != null && !string.IsNullOrEmpty(content.CancelButtonText)) { cancelButtonText.text = content.CancelButtonText; } } // 닫기 버튼 설정 if (closeButton != null && content != null) { closeButton.gameObject.SetActive(content.ShowCloseButton); } // 버튼 위치를 예쁘게 조정해요 (예: 버튼이 하나만 있으면 가운데로). AdjustButtonPositions(); await UniTask.CompletedTask; // 비동기 메서드라서 마지막에 이걸 붙여줘요. } /// /// 📐 활성화된 버튼(확인/취소)의 수에 따라 버튼 위치를 보기 좋게 조정해요. /// 예를 들어, 버튼이 하나만 있다면 화면 가운데에 오도록 할 수 있어요. /// 이 메서드는 `OnOpen`에서 호출돼요. /// protected virtual void AdjustButtonPositions() { bool isConfirmActive = confirmButton != null && confirmButton.gameObject.activeSelf; bool isCancelActive = cancelButton != null && cancelButton.gameObject.activeSelf; // 확인 버튼만 활성화되어 있다면, if (isConfirmActive && !isCancelActive) { RectTransform confirmButtonRect = confirmButton.GetComponent(); if (confirmButtonRect != null) { // 예시: 확인 버튼을 부모 UI 요소의 가로 중앙으로 이동시켜요. // (정확한 값은 여러분의 UI 레이아웃에 따라 달라질 수 있어요!) confirmButtonRect.anchorMin = new Vector2(0.5f, confirmButtonRect.anchorMin.y); confirmButtonRect.anchorMax = new Vector2(0.5f, confirmButtonRect.anchorMax.y); confirmButtonRect.pivot = new Vector2(0.5f, confirmButtonRect.pivot.y); confirmButtonRect.anchoredPosition = new Vector2(0, confirmButtonRect.anchoredPosition.y); } } // 취소 버튼만 활성화되어 있다면, else if (!isConfirmActive && isCancelActive) { RectTransform cancelButtonRect = cancelButton.GetComponent(); if (cancelButtonRect != null) { // 예시: 취소 버튼도 부모 UI 요소의 가로 중앙으로 이동시켜요. cancelButtonRect.anchorMin = new Vector2(0.5f, cancelButtonRect.anchorMin.y); cancelButtonRect.anchorMax = new Vector2(0.5f, cancelButtonRect.anchorMax.y); cancelButtonRect.pivot = new Vector2(0.5f, cancelButtonRect.pivot.y); cancelButtonRect.anchoredPosition = new Vector2(0, cancelButtonRect.anchoredPosition.y); } } // 두 버튼이 모두 보이거나 모두 안 보일 때는 특별히 위치를 바꾸지 않아요 (기본 레이아웃 사용). // 필요하다면 이 부분에 다른 정렬 로직을 추가할 수도 있어요! } /// /// 🎬 모달 창이 화면에서 사라질 때 `Modal` 클래스가 호출하는 마법이에요! (비동기 작업 가능) /// `ModalContent.OnClose()`가 호출된 *후에* 실행돼요. /// 모달이 닫히면서 특별히 정리해야 할 작업이 있다면 여기에 작성해요. /// /// 이 모달을 열 때 사용했던 '레시피' (`ModalContent` 객체)예요. public virtual async UniTask OnClose(ModalContent content) { //ULog.Debug($"[ModalView] {gameObject.name} OnClose called."); // 예: 모달에서 사용했던 리소스를 해제하거나, UI 상태를 초기화하는 코드를 여기에 넣을 수 있어요. await UniTask.CompletedTask; } /// /// 🎁 모달 창이 닫힐 때, 이 모달이 어떤 '결과'를 만들었는지 알려주는 함수예요. /// 기본적으로는 아무것도 안 알려줘요 (`null` 반환). /// 만약 모달에서 사용자가 뭔가를 선택하거나 입력했다면, 이 함수를 **재정의(override)**해서 /// 그 선택/입력 값을 돌려주도록 만들 수 있어요. /// `Modal.Open()`를 호출할 때 `T`에 지정한 타입으로 이 결과가 변환돼요. /// /// 모달의 처리 결과예요. (예: 사용자가 입력한 글자, 선택한 아이템, 또는 그냥 true/false) /// /// 예를 들어, '예'/'아니오'를 선택하는 간단한 확인 모달이라면 이렇게 할 수 있어요: /// /// public class ConfirmModalView : ModalView /// { /// private bool _wasConfirmed = false; /// /// // (OnOpen 등 다른 메서드들은 필요에 따라 구현) /// /// // 확인 버튼이 눌렸을 때 호출될 메서드 (Modal.cs에서 연결해줌) /// public void HandleConfirm() /// { /// _wasConfirmed = true; /// // 실제로는 Modal.cs의 HandleModalActionAsync가 호출되어 모달이 닫힙니다. /// } /// /// // 취소 버튼이 눌렸을 때 호출될 메서드 /// public void HandleCancel() /// { /// _wasConfirmed = false; /// } /// /// public override object GetResult() /// { /// // 사용자가 '확인'을 눌렀는지 여부를 bool 값으로 반환해요. /// return _wasConfirmed; /// } /// } /// // Modal.Open(...) 이렇게 호출하면, GetResult()가 반환한 bool 값을 받을 수 있어요. /// /// 또는, 입력 필드가 있는 모달이라면 입력된 텍스트를 반환할 수 있어요: /// /// public class InputModalView : ModalView /// { /// public TMP_InputField inputField; /// // (OnOpen에서 inputField 초기화 및 이벤트 연결) /// /// public override object GetResult() /// { /// return inputField != null ? inputField.text : string.Empty; /// } /// } /// // Modal.Open(...) 이렇게 호출하면, 입력된 문자열을 받을 수 있어요. /// /// public virtual object GetResult() { return null; } /// /// 💡 모달 안에 있는 모든 주요 버튼들(확인, 취소, 닫기)을 한꺼번에 클릭할 수 있게 하거나 못하게 만들어요. /// 예를 들어, 모달이 뭔가 중요한 작업을 처리하는 동안에는 버튼을 잠시 못 누르게 할 때 사용해요. /// /// true로 설정하면 버튼들을 누를 수 있고, false면 못 눌러요. public virtual void SetAllButtonsInteractable(bool interactable) { if (confirmButton != null) confirmButton.interactable = interactable; if (cancelButton != null) cancelButton.interactable = interactable; if (closeButton != null) closeButton.interactable = interactable; } } }