데이터 파이프라인 이식

This commit is contained in:
2026-01-24 08:41:22 +09:00
parent 84d4decd3a
commit fb6570a992
20 changed files with 3277 additions and 0 deletions

8
Assets/Data.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5f7bdf031c4165f4194ad3c9f0c80356
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Assets/Data/Scripts.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5380ca0483c112049895ebc9c6794d73
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1d366afae3759bc4c83aebc631a3ac4a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

8
Assets/Editor.meta Normal file
View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 208723e08bdfdc347bc1af53f4be5c3c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f4724b7534e0a443a8eabbe6f5483ad
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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("=== 디버그 완료 ===");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 95247e4274c204446ac0f9c45c334c56

View 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}";
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 79f372743463a5e4cb3a9b915de9bebe

View 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);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 017c665c9c855fa43b55f1d61c238903