데이터 파이프라인 이식
This commit is contained in:
8
Assets/Editor/DataImporter.meta
Normal file
8
Assets/Editor/DataImporter.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f4724b7534e0a443a8eabbe6f5483ad
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
94
Assets/Editor/DataImporter/CSVDebugger.cs
Normal file
94
Assets/Editor/DataImporter/CSVDebugger.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
// Assets/Editor/DataImporter/CSVDebugger.cs
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public static class CSVDebugger
|
||||
{
|
||||
[MenuItem("Northbound/Debug CSV Files")]
|
||||
public static void DebugCSVFiles()
|
||||
{
|
||||
string gameDataPath = Path.Combine(Application.dataPath, "..", "GameData");
|
||||
var csvFiles = Directory.GetFiles(gameDataPath, "*.csv");
|
||||
|
||||
Debug.Log("=== CSV 파일 디버그 ===\n");
|
||||
|
||||
foreach (var csvPath in csvFiles)
|
||||
{
|
||||
string fileName = Path.GetFileName(csvPath);
|
||||
|
||||
if (fileName.StartsWith("."))
|
||||
continue;
|
||||
|
||||
Debug.Log($"📄 파일: {fileName}");
|
||||
|
||||
// 파일 크기
|
||||
FileInfo fileInfo = new FileInfo(csvPath);
|
||||
Debug.Log($" 📊 크기: {fileInfo.Length} bytes");
|
||||
|
||||
// 인코딩 테스트
|
||||
try
|
||||
{
|
||||
// UTF-8로 읽기
|
||||
var lines = File.ReadAllLines(csvPath, Encoding.UTF8);
|
||||
Debug.Log($" ✅ UTF-8 읽기 성공: {lines.Length}줄");
|
||||
|
||||
// ⭐ 모든 줄 출력
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
string line = lines[i];
|
||||
Debug.Log($" 📋 [{i}] 길이:{line.Length} | 내용: '{line}'");
|
||||
|
||||
// 특수문자 확인
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
Debug.Log($" ⚠️ 빈 줄 또는 공백만 있음");
|
||||
}
|
||||
|
||||
// 바이트 출력 (첫 20바이트만)
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(line);
|
||||
string byteStr = "";
|
||||
for (int j = 0; j < Mathf.Min(bytes.Length, 20); j++)
|
||||
{
|
||||
byteStr += $"{bytes[j]:X2} ";
|
||||
}
|
||||
if (bytes.Length > 20) byteStr += "...";
|
||||
Debug.Log($" 바이트: {byteStr}");
|
||||
}
|
||||
|
||||
// BOM 체크
|
||||
byte[] fileBytes = File.ReadAllBytes(csvPath);
|
||||
if (fileBytes.Length >= 3 && fileBytes[0] == 0xEF && fileBytes[1] == 0xBB && fileBytes[2] == 0xBF)
|
||||
{
|
||||
Debug.Log($" ℹ️ UTF-8 BOM 있음");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($" ℹ️ BOM 없음");
|
||||
}
|
||||
|
||||
// 전체 파일 바이트 (처음 100바이트만)
|
||||
string fileBytesStr = "";
|
||||
for (int i = 0; i < Mathf.Min(fileBytes.Length, 100); i++)
|
||||
{
|
||||
fileBytesStr += $"{fileBytes[i]:X2} ";
|
||||
if ((i + 1) % 20 == 0) fileBytesStr += "\n ";
|
||||
}
|
||||
Debug.Log($" 📦 파일 바이트 (처음 100):\n {fileBytesStr}");
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
Debug.LogError($" ❌ 읽기 실패: {e.Message}");
|
||||
}
|
||||
|
||||
Debug.Log("");
|
||||
}
|
||||
|
||||
Debug.Log("=== 디버그 완료 ===");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/DataImporter/CSVDebugger.cs.meta
Normal file
2
Assets/Editor/DataImporter/CSVDebugger.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 95247e4274c204446ac0f9c45c334c56
|
||||
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 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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/DataImporter/CSVToSOImporter.cs.meta
Normal file
2
Assets/Editor/DataImporter/CSVToSOImporter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79f372743463a5e4cb3a9b915de9bebe
|
||||
122
Assets/Editor/DataImporter/ImporterWindow.cs
Normal file
122
Assets/Editor/DataImporter/ImporterWindow.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// Assets/Editor/DataImporter/ImporterWindow.cs
|
||||
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Northbound.Editor
|
||||
{
|
||||
public class ImporterWindow : EditorWindow
|
||||
{
|
||||
private Vector2 scrollPosition;
|
||||
|
||||
[MenuItem("Northbound/Data Importer")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<ImporterWindow>("Data Importer");
|
||||
window.minSize = new Vector2(400, 300);
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUILayout.Label("CSV → ScriptableObject Importer", EditorStyles.boldLabel);
|
||||
GUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"GameData 폴더의 CSV 파일을 ScriptableObject로 변환합니다.\n" +
|
||||
"자동 생성된 C# 클래스를 사용합니다.",
|
||||
MessageType.Info
|
||||
);
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
scrollPosition = GUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
string gameDataPath = Path.Combine(Application.dataPath, "..", "GameData");
|
||||
|
||||
if (!Directory.Exists(gameDataPath))
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"GameData 폴더를 찾을 수 없습니다.",
|
||||
MessageType.Warning
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
var csvFiles = Directory.GetFiles(gameDataPath, "*.csv")
|
||||
.Where(f => !f.Contains("Backups") && !Path.GetFileName(f).StartsWith("."))
|
||||
.ToArray();
|
||||
|
||||
if (csvFiles.Length == 0)
|
||||
{
|
||||
EditorGUILayout.HelpBox(
|
||||
"CSV 파일이 없습니다.\n" +
|
||||
"sync-from-notion.ps1을 먼저 실행하세요.",
|
||||
MessageType.Warning
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
GUILayout.Label("발견된 CSV 파일:", EditorStyles.boldLabel);
|
||||
GUILayout.Space(5);
|
||||
|
||||
foreach (var filePath in csvFiles)
|
||||
{
|
||||
string fileName = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
GUILayout.BeginHorizontal();
|
||||
|
||||
GUILayout.Label($"📊 {fileName}", GUILayout.Width(200));
|
||||
|
||||
if (GUILayout.Button("Import", GUILayout.Width(100)))
|
||||
{
|
||||
ImportSingle(fileName);
|
||||
}
|
||||
|
||||
GUILayout.EndHorizontal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
GUILayout.Space(20);
|
||||
|
||||
GUI.backgroundColor = Color.green;
|
||||
if (GUILayout.Button("Import All Data", GUILayout.Height(50)))
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
"전체 데이터 Import",
|
||||
"모든 CSV 파일을 읽어서 ScriptableObject를 생성합니다.\n" +
|
||||
"기존 파일은 덮어씌워집니다.",
|
||||
"Import All",
|
||||
"Cancel"))
|
||||
{
|
||||
CSVToSOImporter.ImportAll();
|
||||
}
|
||||
}
|
||||
GUI.backgroundColor = Color.white;
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
EditorGUILayout.HelpBox(
|
||||
"Import 후 Assets/Data/ScriptableObjects 폴더를 확인하세요.",
|
||||
MessageType.None
|
||||
);
|
||||
}
|
||||
|
||||
private void ImportSingle(string schemaName)
|
||||
{
|
||||
if (EditorUtility.DisplayDialog(
|
||||
$"{schemaName} Import",
|
||||
$"{schemaName}.csv를 읽어서 ScriptableObject를 생성합니다.\n" +
|
||||
"기존 파일은 덮어씌워집니다.",
|
||||
"Import",
|
||||
"Cancel"))
|
||||
{
|
||||
CSVToSOImporter.ImportSchema(schemaName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Editor/DataImporter/ImporterWindow.cs.meta
Normal file
2
Assets/Editor/DataImporter/ImporterWindow.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 017c665c9c855fa43b55f1d61c238903
|
||||
Reference in New Issue
Block a user