데이터 파이프라인 구축
This commit is contained in:
348
Assets/Editor/DataImporter/CSVToSOImporter.cs
Normal file
348
Assets/Editor/DataImporter/CSVToSOImporter.cs
Normal file
@@ -0,0 +1,348 @@
|
||||
// Assets/Editor/DataImporter/CSVToSOImporter.cs
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace DigAndDefend.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 = $"DigAndDefend.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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user