데이터파이프 라인에서 리스트 타입 지원

list:int, list:bool 등
This commit is contained in:
2026-01-30 13:40:31 +09:00
parent 23f200348f
commit 6f358a4aef
3 changed files with 238 additions and 732 deletions

View File

@@ -1,347 +1,206 @@
// Assets/Editor/DataImporter/CSVToSOImporter.cs
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 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;
if (csvPath.Contains("Backups")) continue;
string schemaName = Path.GetFileNameWithoutExtension(csvPath);
if (schemaName.StartsWith(".")) continue;
if (schemaName.StartsWith("."))
continue;
Debug.Log($"\n📋 {schemaName} Import 시작...");
if (ImportSchema(schemaName))
{
totalSuccess++;
}
else
{
totalFail++;
}
if (ImportSchema(schemaName)) totalSuccess++;
else totalFail++;
}
Debug.Log("\n" + new string('=', 60));
Debug.Log($"🎉 전체 Import 완료: 성공 {totalSuccess}개, 실패 {totalFail}개");
Debug.Log(new string('=', 60));
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))
{
Debug.LogError($"❌ CSV 파일을 찾을 수 없습니다: {csvPath}");
return false;
}
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}
if (!File.Exists(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;
if (dataType == null) return false;
try
{
var lines = File.ReadAllLines(csvPath, System.Text.Encoding.UTF8);
if (lines.Length < 2)
{
Debug.LogError("❌ CSV 파일에 데이터가 없습니다.");
return false;
}
if (lines.Length < 2) 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;
if (string.IsNullOrEmpty(line))
continue;
var values = ParseCSVLine(line);
var so = ScriptableObject.CreateInstance(dataType);
try
for (int col = 0; col < headers.Length; col++)
{
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}");
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();
Debug.Log($" 🎉 {schemaName} Import 완료: {importCount}개");
return true;
}
catch (Exception e)
{
Debug.LogError($"{schemaName} Import 실패: {e.Message}");
Debug.LogException(e);
Debug.LogError($"{schemaName} 오류: {e.Message}");
return false;
}
}
private static string[] ParseCSVLine(string line)
{
var result = new List<string>();
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)
{
Type fieldType = field.FieldType;
// 1. 리스트 타입 처리 (List<T>)
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;
}
Type fieldType = field.FieldType;
// 2. Nullable 처리
if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
Type underlyingType = Nullable.GetUnderlyingType(fieldType);
object convertedValue = ConvertValue(value, underlyingType);
field.SetValue(obj, convertedValue);
field.SetValue(obj, ConvertValue(value, underlyingType));
}
else
{
object convertedValue = ConvertValue(value, fieldType);
field.SetValue(obj, convertedValue);
// 3. 일반 타입 처리
field.SetValue(obj, ConvertValue(value, fieldType));
}
}
private static object ConvertValue(string value, Type targetType)
{
if (string.IsNullOrWhiteSpace(value))
return null;
if (string.IsNullOrWhiteSpace(value)) return null;
if (targetType == typeof(int))
try
{
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");
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; }
}
return value;
// --- 유틸리티 메서드 (기존과 동일) ---
private static string[] ParseCSVLine(string line)
{
var result = new List<string>();
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();
// ⭐ 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}";
}
var val = idField.GetValue(so);
if (val != null) return $"{type.Name.Replace("Data", "")}{val: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}";
}
}