348 lines
11 KiB
C#
348 lines
11 KiB
C#
// 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<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)
|
|
{
|
|
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}";
|
|
}
|
|
}
|
|
} |