Files
Northbound/Assets/Editor/DataImporter/CSVToSOImporter.cs
2026-01-30 13:40:31 +09:00

207 lines
7.8 KiB
C#

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<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;
}
// 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<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();
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}";
}
}
}