# DataTools/generate_csharp_classes.py """노션 스키마 → Unity C# ScriptableObject 클래스 생성 (리스트 지원 버전)""" import json from pathlib import Path from collections import defaultdict GAMEDATA_DIR = Path(__file__).parent.parent / "GameData" UNITY_OUTPUT_DIR = Path(__file__).parent.parent / "Assets" / "Data" / "Scripts" / "DataClasses" def get_csharp_type(field_type): """Python 타입 → C# 타입 변환 (list 지원)""" # 리스트 여부 확인 (예: list:int, list:string) is_list = field_type.lower().startswith('list') # 기본 타입 추출 base_type = field_type.lower() if is_list and ':' in base_type: base_type = base_type.split(':')[1] elif is_list: # 'list'만 적혀있을 경우 기본적으로 string 리스트로 간주 base_type = 'string' type_map = { 'int': 'int', 'float': 'float', 'number': 'float', 'string': 'string', 'bool': 'bool', 'boolean': 'bool' } csharp_base = type_map.get(base_type, 'string') if is_list: return f"List<{csharp_base}>" return csharp_base def to_camel_case(snake_str): """snake_case → camelCase""" components = snake_str.split('_') return components[0] + ''.join(x.title() for x in components[1:]) def to_pascal_case(snake_str): """snake_case → PascalCase""" return ''.join(x.title() for x in snake_str.split('_')) def group_fields_by_condition(schema): """필드를 조건별로 그룹화""" groups = defaultdict(list) for field in schema: if field.get('condition'): condition_value = field['condition']['value'] groups[condition_value].append(field) else: groups['common'].append(field) return groups def generate_class(schema_name, schema): """스키마 → C# 클래스 코드 생성""" class_name = schema_name + "Data" field_groups = group_fields_by_condition(schema) condition_field = None for field in schema: if field.get('condition'): condition_field = field['condition']['field'] break lines = [] lines.append("// 이 파일은 자동 생성되었습니다. 직접 수정하지 마세요!") lines.append("// 생성 스크립트: DataTools/generate_csharp_classes.py") lines.append("") lines.append("using UnityEngine;") lines.append("using System.Collections.Generic; // 리스트 지원을 위해 추가") lines.append("") lines.append("namespace Northbound.Data") lines.append("{") lines.append(f' [CreateAssetMenu(fileName = "{class_name}", menuName = "Northbound/{schema_name} Data")]') lines.append(f" public partial class {class_name} : ScriptableObject") lines.append(" {") # 공통 필드 if 'common' in field_groups and field_groups['common']: lines.append(" [Header(\"기본 정보\")]") for field in field_groups['common']: add_field(lines, field, nullable=False) lines.append("") # 조건부 필드 (그룹별) for condition_value, fields in field_groups.items(): if condition_value == 'common': continue header_name = condition_value.capitalize() lines.append(f" [Header(\"{header_name} 전용\")]") for field in fields: add_field(lines, field, nullable=True) lines.append("") # 유틸리티 메서드 if condition_field: lines.append(" // ===== 유틸리티 메서드 =====") lines.append("") for condition_value in field_groups.keys(): if condition_value == 'common': continue property_name = f"Is{to_pascal_case(condition_value)}" field_name = to_camel_case(condition_field) lines.append(f' public bool {property_name} => {field_name} == "{condition_value}";') lines.append("") for condition_value, fields in field_groups.items(): if condition_value == 'common': continue for field in fields: field_name = to_camel_case(field['name']) method_name = f"Get{to_pascal_case(field['name'])}" field_type = get_csharp_type(field['type']) # 리스트 여부에 따른 기본값 처리 if "List<" in field_type: default_value = f"new {field_type}()" elif field_type == 'int': default_value = '0' elif field_type == 'float': default_value = '0f' elif field_type == 'bool': default_value = 'false' else: default_value = '""' lines.append(f" public {field_type} {method_name}() => {field_name} ?? {default_value};") lines.append(" }") lines.append("}") return "\n".join(lines) def add_field(lines, field, nullable=False): """필드 정의 추가""" field_name = to_camel_case(field['name']) field_type = get_csharp_type(field['type']) is_list = "List<" in field_type if field.get('description'): description = field['description'].replace('\\n', ' ').replace('\n', ' ') lines.append(f" /// {description}") # 필드 선언 if is_list: # 리스트는 인스펙터에서 바로 보이도록 초기화 lines.append(f" public {field_type} {field_name} = new {field_type}();") elif nullable and field_type not in ['string']: # string은 원래 nullable이므로 제외, 값 타입만 ? 적용 lines.append(f" public {field_type}? {field_name};") else: lines.append(f" public {field_type} {field_name};") def generate_all_classes(): """모든 스키마에 대해 C# 클래스 생성""" UNITY_OUTPUT_DIR.mkdir(parents=True, exist_ok=True) schema_files = list(GAMEDATA_DIR.glob(".*_schema.json")) if not schema_files: print("⚠️ 스키마 파일을 찾을 수 없습니다.") return print("=" * 60) print("🔧 C# 클래스 자동 생성 (List 지원)") print("=" * 60) generated_count = 0 for schema_file in schema_files: schema_name = schema_file.stem.replace("_schema", "").lstrip(".") try: with open(schema_file, 'r', encoding='utf-8') as f: schema = json.load(f) csharp_code = generate_class(schema_name, schema) output_file = UNITY_OUTPUT_DIR / f"{schema_name}Data.cs" with open(output_file, 'w', encoding='utf-8') as f: f.write(csharp_code) print(f" ✅ 생성: {schema_name}Data.cs") generated_count += 1 except Exception as e: print(f" ❌ {schema_name} 실패: {e}") print("=" * 60) print(f"🎉 완료: {generated_count}개 클래스 생성") if __name__ == "__main__": generate_all_classes()