데이터를 정리한 엑셀파일을 CSV 로 저장 - Resource 파일에 저장 - CSV를 읽어 파싱 - 리스트에 저장
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 특정한 테이블의 ROW 객체를 ID를 통해 저장 및 조회할 수 있는 클래스
/// </summary>
/// <typeparam name="TKey">테이블의 ID값</typeparam>
/// <typeparam name="TRow">실제 ROW타입 데이터</typeparam>
public class Table<TKey, TRow> where TRow : TableBase
{
private readonly Dictionary<TKey, TRow> _data;
private readonly string _tableName;
private readonly string _idColumName;
public IReadOnlyDictionary<TKey, TRow> Data => _data;
public Table(string tableName, string idColumnName, Dictionary<TKey, TRow> data)
{
_tableName = tableName;
_idColumName = idColumnName;
_data = data;
}
public TRow this[TKey id] // 인덱싱
{
get
{
if(_data.TryGetValue(id, out TRow row))
{
return row;
}
Debug.LogError($"존재하지 않는 ID : {id}");
return null;
}
}
}
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using System;
using System.Reflection;
/// <summary>
/// CSV 파일을 파싱하고 정의된 ROW클래스에 데이터를 채워넣는 클래스
/// </summary>
public class TableParser
{
// 실제 컬럼 이름이 적혀있는 행의 인덱스
private const int HeaderRowIndex = 1;
// 데이터가 시작되는 첫 줄의 인덱스
private const int DataStartRowIndex = 3;
public static Table<TKey, TRow> Parse<TKey, TRow>(
TextAsset csvFile,
string idColumName
) where TRow : TableBase, new()
{
// 입력 검사 : csv 파일이 null 이면 Log 남기고 null 반환
if (csvFile == null)
{
Debug.LogError("CSV 파일이 null 입니다.");
return null;
}
// 1. CSV 내용 읽기 및 줄/셀 분리
List<string[]> rows;
try
{
// 줄 분리, 분리된 줄 가져와서 ',' 단위 분리
rows = csvFile.text.
Split(new[] { '\r', '\n' }, System.StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Split(',').Select(cell => cell.Trim()).ToArray())
.ToList();
}
catch (Exception e)
{
Debug.LogError($"CSV 파일 읽기 오류");
Debug.LogError(e.Message);
return null;
}
// 2. 컬럼 헤더 추출
if (rows.Count <= HeaderRowIndex)
{
// 헤더 행 인덱스가 전체 줄 개수보다 크거나 같다면 헤더가 없다는 의미
Debug.LogError($"CSV에 컬럼 헤더 {HeaderRowIndex}가 없습니다.");
return null;
}
// 헤더 행을 컬럼명 배열로 취득
string[] columnNames = rows[HeaderRowIndex];
// 사용자가 지정한 ID 컬럼 이름이 헤더에 있는지 확인
int idIndex = Array.IndexOf(columnNames, idColumName);
if (idIndex < 0)
{
Debug.LogError($"CSV에 id 컬럼 헤더 {HeaderRowIndex}가 없습니다.");
return null;
}
// 3. TRow 클래스 속성 및 필드 컬럼명 맵핑
Type rowType = typeof(TRow);
Dictionary<string, MemberInfo> memberMap = new Dictionary<string, MemberInfo>();
foreach (var name in columnNames)
{
var prop = rowType.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
if (prop != null)
{
memberMap.Add(name, prop);
continue;
}
var field = rowType.GetField(name, BindingFlags.Public | BindingFlags.Instance);
if (field != null)
{
memberMap.Add(name, field);
continue;
}
}
// 4. 데이터 파싱 및 TRow 인스턴스 생성
// 최정적으로 결과가 저장될 장소
Dictionary<TKey, TRow> allRowData = new Dictionary<TKey, TRow>();
// 데이터 시작 인덱스부터 끝까지 한줄씩 처리 진행
for (int i = DataStartRowIndex; i < rows.Count; i++)
{
// 현재 처리하고자 하는 데이터 행의 셀 배열
string[] rowValues = rows[i];
// 행의 셀 갯수가 헤더의 컬럼 갯수보다 작을 경우
if (rowValues.Length < columnNames.Length)
{
if (rowValues.All(string.IsNullOrEmpty))
{
// 빈 것 건너뛰기
continue;
}
// ID 추출 및 TKey 타입으로 변환
// id컬럼 문자열 값
string idString = rowValues[idIndex];
// id가 비었다는 것은 유효한 데이터가 아니라는 의미로 해석
if (string.IsNullOrEmpty(idString))
{
continue;
}
TKey idValue = default;
try
{
idValue = (TKey)Convert.ChangeType(idString, typeof(TKey));
}
catch (Exception e)
{
Debug.LogError($"id {idString} 변환 실패");
Debug.LogError(e.Message);
}
if (allRowData.ContainsKey(idValue))
{
Debug.LogError($"{idValue} 중복");
{
continue;
}
}
// id 처리 완료
// 새로운 TRow 인스턴스 생성
TRow newRow = new TRow();
// 인스턴스에 값 채우기
for (int j = 0; j < columnNames.Length; j++)
{
// 컬럼 이름
string columnName = columnNames[j];
// 읽어온 문자열 값
string stringValue = rowValues[j];
// TRow 에 멤버가 존재하는지 체크해서 진행
if (memberMap.TryGetValue(columnName, out MemberInfo member))
{
Type targetType = (member is PropertyInfo prop) ? prop.PropertyType : ((FieldInfo)member).FieldType;
try
{
// 현재는 문자열 값으로 되어있으니까 테이블에서 정의한 타입으로 변환해서 매칭 시켜주기
object convertedValue = ConvertValue(stringValue, targetType);
if (member is PropertyInfo propInfo)
{
propInfo.SetValue(newRow, convertedValue);
}
else if (member is FieldInfo fieldInfo)
{
fieldInfo.SetValue(newRow, convertedValue);
}
}
catch (Exception e)
{
Debug.LogError($"값 변환 오류: 칼럼 '{columnName}' 의 값 '{stringValue}' 을(를) {targetType} 타입으로 변환할 수 없습니다. 오류: {e.Message}");
}
}
}
// 모든 컬럼에 대한 처리가 끝나면 딕셔너리에 추가
allRowData.Add(idValue, newRow);
}
}
// 정제된 데이터를 테이블로 변환
string tableName = csvFile.name;
return new Table<TKey, TRow>(tableName, idColumName, allRowData);
}
private static object ConvertValue(string value, Type targetType)
{
value = value.Trim();
// nullable 이나 비어있는 값에 대한 처리
if (string.IsNullOrEmpty(value))
{
if (targetType.IsValueType && Nullable.GetUnderlyingType(targetType) == null)
{
return Activator.CreateInstance(targetType);
}
return null;
}
// bool 값에 대한 처리 현재는 0, 1 로 표시된 경우만 처리중
if (targetType == typeof(bool))
{
if (int.TryParse(value, out int intValue))
{
return intValue != 0;
}
}
// 그 외에는 변환해서 반환하면 끝
return Convert.ChangeType(value, targetType);
}
}
- CSV 파일을 읽어서 줄과 셀 분리
- 헤더를 분석해서 ID 컬럼 위치를 찾고, TRow 의 멤버와 컬럼 이름을 매핑
- 데이터 줄 순회하면서 TRow 생성 , CSV 문자열 값을 해당 멤버의 타입에 맞춰 변환하여 할당
- ID 를 키로 사용해서 데이터를 찾을 수 있는 Table 객체를 반환
'📖TIL' 카테고리의 다른 글
| 251210 StringBuilder (0) | 2025.12.10 |
|---|---|
| 251210 Github Issue & Github Projects (0) | 2025.12.10 |
| 251208 옵저버 패턴 복습 (0) | 2025.12.08 |
| 251204 (0) | 2025.12.04 |
| 251203 (0) | 2025.12.03 |