using System.Collections.Generic; using System.IO; using System.Reflection; using BlueWater; using BlueWater.Interfaces; using BlueWater.Items; using BlueWater.Utility; using DDD.ScriptableObjects; using Newtonsoft.Json; using UnityEditor; using UnityEngine; public class JsonHelperEditor : EditorWindow { // Json파일이 추가될 때 마다, DataType을 추가하고, // LoadData의 switch문에 LoadData<>를 추가합니다. private enum DataType { None = 0, ItemDataTable, CraftRecipeData } private string _jsonFilePath = "Assets/Resources/Json/FileName"; private string _assetFilePath = "Assets/02.Scripts/DDD/ScriptableObject"; private DataType _dataType = DataType.None; [MenuItem("Tools/02. json -> ScriptableObject")] public static void ShowWindow() { GetWindow("02. json -> ScriptableObject"); } private void OnGUI() { GUILayout.Label("Json파일 ScriptableObject로 자동 변환", EditorStyles.boldLabel); GUILayout.Space(10); EditorGUILayout.HelpBox("Assets 폴더를 시작으로 전체 Path가 필요하며, Json 파일이 있는 Path를 입력해야 합니다." + "\n파일 확장자는 자동으로 .json이 들어갑니다." + "\n예시: Assets/Resources/Json/최종 Json 파일 이름", MessageType.Info); _jsonFilePath = EditorGUILayout.TextField("Json 파일 위치", _jsonFilePath); EditorGUILayout.HelpBox("Assets 폴더를 시작으로 전체 Path가 필요하며, File을 저장할 폴더의 전체 Path가 필요합니다." + "\n파일명은 Json파일의 이름과 동일하게 자동으로 적용됩니다." + "\n예시1: Assets/02.Scripts/ScriptableObject" + "\n예시2: Assets/02.Scripts/ScriptableObject/최종 폴더 이름", MessageType.Info); _assetFilePath = EditorGUILayout.TextField("저장할 폴더 위치", _assetFilePath); _dataType = (DataType)EditorGUILayout.EnumPopup("데이터 종류", _dataType); if (GUILayout.Button("변환하기")) { LoadDataFromJson(); } } private void LoadDataFromJson() { if (string.IsNullOrEmpty(_jsonFilePath) || string.IsNullOrEmpty(_assetFilePath)) { EditorUtility.DisplayDialog("경고 메세지", "빈 목록을 채워주세요.", "확인"); Debug.LogError("빈 목록을 채워주세요."); return; } switch (_dataType) { case DataType.None: EditorUtility.DisplayDialog("경고 메세지", "데이터 타입이 None인지 확인해주세요.", "확인"); Debug.LogError("데이터 타입이 None인지 확인해주세요."); return; case DataType.ItemDataTable: LoadData(); break; case DataType.CraftRecipeData: LoadData(); break; break; default: EditorUtility.DisplayDialog("경고 메세지", "데이터 타입이 제대로 설정되어있는지 확인해주세요.", "OK"); Debug.LogError("데이터 타입이 제대로 설정되어있는지 확인해주세요."); return; } } private void LoadData() where T : class, IIdx where U : ScriptableObject, IDataContainer, new() { var fullJsonFilePath = _jsonFilePath + ".json"; if (!File.Exists(fullJsonFilePath)) { EditorUtility.DisplayDialog("경고 메세지", "Json 파일을 찾을 수 없습니다.\n" + fullJsonFilePath, "확인"); Debug.LogError("Json 파일을 찾을 수 없습니다.\n" + fullJsonFilePath); return; } var newDataList = JsonHelper.LoadJsonData(fullJsonFilePath); if (newDataList == null || newDataList.Count == 0) { EditorUtility.DisplayDialog("경고 메세지", "Json 파일 데이터가 비어있거나, 불러올 수 없습니다.\n" + fullJsonFilePath, "확인"); return; } var newDataKeyValueList = new List>(); foreach (var data in newDataList) { if (data != null) { newDataKeyValueList.Add(new SerializableKeyValuePair { Key = data.Idx, Value = data }); } } var assetFileName = Path.GetFileNameWithoutExtension(_jsonFilePath) + ".asset"; var assetPath = Path.Combine(_assetFilePath, assetFileName); var so = AssetDatabase.LoadAssetAtPath(assetPath); if (so == null) { so = CreateInstance(); if (so is IDataContainer container) { container.SetSerializedDataList(newDataKeyValueList); } AssetDatabase.CreateAsset(so, assetPath); EditorUtility.DisplayDialog("알림 메세지", "새로운 ScriptableObject가 생성되었습니다.", "확인"); } else { if (so is IDataContainer container) { var existingData = container.GetSerializedDataList(); MergeData(existingData, newDataKeyValueList); //existingData.Sort((x, y) => x.Idx.CompareTo(y.Idx)); container.SetSerializedDataList(existingData); } EditorUtility.DisplayDialog("알림 메세지", "기존 ScriptableObject가 업데이트되었습니다.", "확인"); } EditorUtility.SetDirty(so); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private void MergeData(List> existingData, List> newData) where T : class, IIdx { existingData ??= new List>(); var newDataIdxSet = new HashSet(); foreach (var newDataItem in newData) { if (newDataItem != null && newDataItem.Value != null) { newDataIdxSet.Add(newDataItem.Key); // Key는 Idx에 해당 } } // 기존 데이터에서 JSON에 없는 항목 제거 existingData.RemoveAll(item => item == null || !newDataIdxSet.Contains(item.Key)); // 기존 데이터를 Dictionary로 변환 var existingDataDict = new Dictionary>(); foreach (var data in existingData) { if (data != null && data.Value != null) { existingDataDict[data.Key] = data; } } // 새로운 데이터를 처리 foreach (var newDataItem in newData) { if (newDataItem == null || newDataItem.Value == null) continue; if (existingDataDict.TryGetValue(newDataItem.Key, out var existingItem)) { // 기존 데이터가 있으면 업데이트 var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); foreach (var property in properties) { // JSON 데이터에서 덮어쓸 프로퍼티만 처리 if (property.GetCustomAttribute() != null) { var newValue = property.GetValue(newDataItem.Value); var existingValue = property.GetValue(existingItem.Value); // 덮어쓰기 조건 확인 if (ShouldOverwriteProperty(newValue, existingValue, property)) { property.SetValue(existingItem.Value, newValue); } } } } else { // 기존 데이터에 없으면 새로 추가 existingDataDict[newDataItem.Key] = newDataItem; } } // 기존 데이터를 유지하고 새로운 데이터를 덮어쓰거나 추가한 결과로 갱신 existingData.Clear(); existingData.AddRange(existingDataDict.Values); } /// /// 속성 덮어쓰기 여부를 결정하는 일반적인 로직 /// private bool ShouldOverwriteProperty(object newValue, object existingValue, PropertyInfo property) { // 값이 없으면 덮어쓰지 않음 if (newValue == null) return false; // Enum 타입의 경우, 값을 비교하여 덮어쓸지 결정 if (property.PropertyType.IsEnum) { return !newValue.Equals(existingValue); // 기존 enum 값과 다를 경우에만 덮어씀 } if (property.PropertyType.IsValueType) { return !newValue.Equals(existingValue); // 기존 값과 다를 경우 덮어씀 } // 참조 타입인 경우 return true; // 참조 타입은 항상 덮어씀 (null 체크는 상위에서 수행됨) } }