// Assets/Editor/DataImporter/CSVToSOImporter.cs using UnityEngine; using UnityEditor; using System; using System.IO; using System.Reflection; 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"; public static void ImportAll() { Debug.Log("=== 전체 데이터 Import 시작 ==="); 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; Debug.Log($"\n📋 {schemaName} Import 시작..."); if (ImportSchema(schemaName)) { totalSuccess++; } else { totalFail++; } } Debug.Log("\n" + new string('=', 60)); Debug.Log($"🎉 전체 Import 완료: 성공 {totalSuccess}개, 실패 {totalFail}개"); Debug.Log(new string('=', 60)); } 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)) { Debug.LogError($"❌ CSV 파일을 찾을 수 없습니다: {csvPath}"); return false; } if (!Directory.Exists(outputPath)) { Directory.CreateDirectory(outputPath); } string className = schemaName + "Data"; Type dataType = FindScriptableObjectType(className); if (dataType == null) { Debug.LogError($"❌ ScriptableObject 클래스를 찾을 수 없습니다: {className}"); Debug.LogError($"💡 generate_csharp_classes.py를 먼저 실행하세요."); return false; } Debug.Log($" ✅ 클래스 발견: {dataType.FullName}"); int importCount = 0; try { var lines = File.ReadAllLines(csvPath, System.Text.Encoding.UTF8); if (lines.Length < 2) { Debug.LogError("❌ CSV 파일에 데이터가 없습니다."); return false; } var headers = ParseCSVLine(lines[0]); Debug.Log($" 📊 {lines.Length - 1}개 행, {headers.Length}개 컬럼"); for (int lineIndex = 1; lineIndex < lines.Length; lineIndex++) { string line = lines[lineIndex].Trim(); if (string.IsNullOrEmpty(line)) continue; try { var values = ParseCSVLine(line); if (values.Length != headers.Length) { Debug.LogWarning($" ⚠️ 행 {lineIndex + 1}: 컬럼 개수 불일치"); continue; } var so = ScriptableObject.CreateInstance(dataType); for (int col = 0; col < headers.Length; col++) { string fieldName = headers[col]; if (string.IsNullOrEmpty(fieldName)) continue; string camelFieldName = ToCamelCase(fieldName); Debug.Log(" 🔍 필드 매핑: " + camelFieldName); FieldInfo field = dataType.GetField(camelFieldName, BindingFlags.Public | BindingFlags.Instance); if (field == null) { if (lineIndex == 1) { Debug.LogWarning($" ⚠️ 필드를 찾을 수 없습니다: {camelFieldName}"); } continue; } string cellValue = values[col]; SetFieldValue(so, field, cellValue); } string assetName = GetAssetName(so, lineIndex); string fileName = assetName + ".asset"; string assetPath = Path.Combine(outputPath, fileName); AssetDatabase.CreateAsset(so, assetPath); importCount++; Debug.Log($" ✅ {assetName}"); } catch (Exception e) { Debug.LogError($" ❌ 행 {lineIndex + 1} Import 실패: {e.Message}"); } } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($" 🎉 {schemaName} Import 완료: {importCount}개"); return true; } catch (Exception e) { Debug.LogError($"❌ {schemaName} Import 실패: {e.Message}"); Debug.LogException(e); return false; } } private static string[] ParseCSVLine(string line) { var result = new List(); bool inQuotes = false; string currentValue = ""; for (int i = 0; i < line.Length; i++) { char c = line[i]; if (c == '"') { inQuotes = !inQuotes; } else if (c == ',' && !inQuotes) { result.Add(currentValue.Trim()); currentValue = ""; } else { currentValue += c; } } result.Add(currentValue.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 void SetFieldValue(object obj, FieldInfo field, string value) { if (string.IsNullOrWhiteSpace(value)) { field.SetValue(obj, null); return; } Type fieldType = field.FieldType; if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>)) { Type underlyingType = Nullable.GetUnderlyingType(fieldType); object convertedValue = ConvertValue(value, underlyingType); field.SetValue(obj, convertedValue); } else { object convertedValue = ConvertValue(value, fieldType); field.SetValue(obj, convertedValue); } } private static object ConvertValue(string value, Type targetType) { if (string.IsNullOrWhiteSpace(value)) return null; if (targetType == typeof(int)) { if (int.TryParse(value, out int result)) return result; return 0; } else if (targetType == typeof(float)) { if (float.TryParse(value, out float result)) return result; return 0f; } else if (targetType == typeof(double)) { if (double.TryParse(value, out double result)) return result; return 0.0; } else if (targetType == typeof(bool)) { string lower = value.ToLower(); return lower == "true" || lower == "1" || lower == "yes"; } else if (targetType == typeof(string)) { // ⭐ 이스케이프된 줄바꿈 복원 return value.Replace("\\n", "\n"); } return value; } private static string GetAssetName(object so, int lineNumber) { Type type = so.GetType(); // ⭐ 1순위: id 필드 찾기 FieldInfo idField = type.GetField("id", BindingFlags.Public | BindingFlags.Instance); if (idField != null) { var idValue = idField.GetValue(so); if (idValue != null) { // 스키마 이름 추출 (TowersData → Towers, TowerData → Tower) string typeName = type.Name; if (typeName.EndsWith("Data")) { typeName = typeName.Substring(0, typeName.Length - 4); } // Tower1, Enemy5 형식 return $"{typeName}{idValue:D3}"; } } // ⭐ 2순위: name 필드 찾기 FieldInfo nameField = type.GetField("name", BindingFlags.Public | BindingFlags.Instance); if (nameField != null) { var nameValue = nameField.GetValue(so); if (nameValue != null && !string.IsNullOrWhiteSpace(nameValue.ToString())) { string name = nameValue.ToString(); name = name.Replace(" ", ""); name = System.Text.RegularExpressions.Regex.Replace(name, @"[^a-zA-Z0-9_가-힣]", ""); return name; } } // 3순위: 행 번호 사용 return $"Data_Row{lineNumber}"; } } }