using UnityEngine; using UnityEditor; using System; using System.IO; using System.Reflection; using System.Collections; // 추가: 리스트 처리를 위해 필요 using System.Collections.Generic; namespace Northbound.Editor { public static class CSVToSOImporter { private static readonly string GAMEDATA_PATH = Path.Combine(Application.dataPath, "..", "GameData"); private static readonly string SO_BASE_PATH = "Assets/Data/ScriptableObjects"; [MenuItem("Tools/Data/Import All CSV")] // 메뉴 추가 (편의성) public static void ImportAll() { Debug.Log("=== 전체 데이터 Import 시작 ==="); if (!Directory.Exists(GAMEDATA_PATH)) { Debug.LogError("GameData 폴더가 없습니다."); return; } var csvFiles = Directory.GetFiles(GAMEDATA_PATH, "*.csv"); int totalSuccess = 0; int totalFail = 0; foreach (var csvPath in csvFiles) { if (csvPath.Contains("Backups")) continue; string schemaName = Path.GetFileNameWithoutExtension(csvPath); if (schemaName.StartsWith(".")) continue; if (ImportSchema(schemaName)) totalSuccess++; else totalFail++; } Debug.Log($"\n🎉 Import 완료: 성공 {totalSuccess}, 실패 {totalFail}"); } public static bool ImportSchema(string schemaName) { string csvPath = Path.Combine(GAMEDATA_PATH, $"{schemaName}.csv"); string outputPath = Path.Combine(SO_BASE_PATH, schemaName); if (!File.Exists(csvPath)) return false; if (!Directory.Exists(outputPath)) Directory.CreateDirectory(outputPath); string className = schemaName + "Data"; Type dataType = FindScriptableObjectType(className); if (dataType == null) return false; try { var lines = File.ReadAllLines(csvPath, System.Text.Encoding.UTF8); if (lines.Length < 2) return false; var headers = ParseCSVLine(lines[0]); for (int lineIndex = 1; lineIndex < lines.Length; lineIndex++) { string line = lines[lineIndex].Trim(); if (string.IsNullOrEmpty(line)) continue; var values = ParseCSVLine(line); var so = ScriptableObject.CreateInstance(dataType); for (int col = 0; col < headers.Length; col++) { if (col >= values.Length) break; string fieldName = ToCamelCase(headers[col]); FieldInfo field = dataType.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance); if (field != null) SetFieldValue(so, field, values[col]); } string assetName = GetAssetName(so, lineIndex); AssetDatabase.CreateAsset(so, Path.Combine(outputPath, $"{assetName}.asset")); } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); return true; } catch (Exception e) { Debug.LogError($"{schemaName} 오류: {e.Message}"); return false; } } private static void SetFieldValue(object obj, FieldInfo field, string value) { Type fieldType = field.FieldType; // 1. 리스트 타입 처리 (List) if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) { Type elementType = fieldType.GetGenericArguments()[0]; IList list = (IList)field.GetValue(obj); if (list == null) { list = (IList)Activator.CreateInstance(fieldType); field.SetValue(obj, list); } list.Clear(); if (!string.IsNullOrWhiteSpace(value)) { // 쉼표(,) 혹은 세미콜론(;)으로 분할 가능하게 설정 string[] items = value.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); foreach (var item in items) { object convertedItem = ConvertValue(item.Trim(), elementType); if (convertedItem != null) list.Add(convertedItem); } } return; } if (string.IsNullOrWhiteSpace(value)) { field.SetValue(obj, null); return; } // 2. Nullable 처리 if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) { Type underlyingType = Nullable.GetUnderlyingType(fieldType); field.SetValue(obj, ConvertValue(value, underlyingType)); } else { // 3. 일반 타입 처리 field.SetValue(obj, ConvertValue(value, fieldType)); } } private static object ConvertValue(string value, Type targetType) { if (string.IsNullOrWhiteSpace(value)) return null; try { if (targetType == typeof(int)) return int.Parse(value); if (targetType == typeof(float)) return float.Parse(value); if (targetType == typeof(bool)) { string l = value.ToLower(); return l == "true" || l == "1" || l == "yes"; } if (targetType == typeof(string)) return value.Replace("\\n", "\n"); return Convert.ChangeType(value, targetType); } catch { return null; } } // --- 유틸리티 메서드 (기존과 동일) --- private static string[] ParseCSVLine(string line) { var result = new List(); bool inQuotes = false; string current = ""; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '"') inQuotes = !inQuotes; else if (c == ',' && !inQuotes) { result.Add(current.Trim()); current = ""; } else current += c; } result.Add(current.Trim()); return result.ToArray(); } private static Type FindScriptableObjectType(string className) { string fullName = $"Northbound.Data.{className}"; foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { Type type = assembly.GetType(fullName); if (type != null && type.IsSubclassOf(typeof(ScriptableObject))) return type; } return null; } private static string ToCamelCase(string snakeCase) { if (string.IsNullOrEmpty(snakeCase)) return snakeCase; var parts = snakeCase.Split('_'); if (parts.Length == 1) return snakeCase; string result = parts[0]; for (int i = 1; i < parts.Length; i++) if (parts[i].Length > 0) result += char.ToUpper(parts[i][0]) + parts[i].Substring(1); return result; } private static string GetAssetName(object so, int lineNumber) { Type type = so.GetType(); FieldInfo idField = type.GetField("id", BindingFlags.Public | BindingFlags.Instance); if (idField != null) { var val = idField.GetValue(so); if (val != null) return $"{type.Name.Replace("Data", "")}{val:D3}"; } return $"Data_Row{lineNumber}"; } } }