Projext/Assets/Editor/ProjectOrganizer.cs
2026-02-02 17:30:23 +09:00

526 lines
17 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Unity 프로젝트 폴더 자동 정리 스크립트 V3 (최종 완성판)
/// - 공백이 있는 파일명 지원
/// - 5.TestScript 폴더 구조 완벽 인식
/// - 모든 변형 파일명 검색
/// </summary>
public class ProjectOrganizerV3 : EditorWindow
{
private const string TARGET_FOLDER = "Assets/Scripts";
private bool _createBackup = true;
private bool _showDetailedLog = true;
private Vector2 _scrollPosition;
private List<string> _manualMoveList = new List<string>();
[MenuItem("Tools/Project Organizer V3 (Final)")]
public static void ShowWindow()
{
GetWindow<ProjectOrganizerV3>("프로젝트 정리 V3");
}
private void OnGUI()
{
_scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
EditorGUILayout.Space(10);
EditorGUILayout.LabelField("🗂️ 프로젝트 폴더 자동 정리 V3 (최종판)", EditorStyles.boldLabel);
EditorGUILayout.Space(5);
EditorGUILayout.HelpBox(
"✨ V3 개선사항:\n" +
"- 공백이 있는 파일명 지원 (예: 'HP UI bar.cs')\n" +
"- 5.TestScript 폴더 구조 완벽 인식\n" +
"- Heal/, Level_Scripts/, Optimizaion/ 등 모든 폴더 검색\n" +
"- 수동 이동 목록 제공\n\n" +
"⚠️ 실행 전 백업 필수!",
MessageType.Info
);
EditorGUILayout.Space(10);
_createBackup = EditorGUILayout.Toggle("백업 자동 생성", _createBackup);
_showDetailedLog = EditorGUILayout.Toggle("상세 로그 출력", _showDetailedLog);
EditorGUILayout.Space(10);
if (GUILayout.Button("🔍 파일 스캔 + 수동 이동 목록 생성", GUILayout.Height(40)))
{
ScanFilesWithManualList();
}
EditorGUILayout.Space(5);
if (GUILayout.Button("🚀 폴더 구조 생성하기", GUILayout.Height(35)))
{
if (EditorUtility.DisplayDialog(
"폴더 구조 생성",
"새로운 폴더 구조를 생성하시겠습니까?",
"실행", "취소"))
{
CreateFolderStructure();
}
}
EditorGUILayout.Space(5);
if (GUILayout.Button("📦 찾은 파일 자동 이동하기", GUILayout.Height(40)))
{
if (EditorUtility.DisplayDialog(
"파일 자동 이동",
"⚠️ 찾은 파일들을 새 위치로 이동합니다.\n\n계속하시겠습니까?",
"실행", "취소"))
{
if (_createBackup)
{
CreateBackup();
}
OrganizeFilesRecursive();
}
}
EditorGUILayout.Space(5);
if (GUILayout.Button("🔙 백업 생성만 하기", GUILayout.Height(30)))
{
CreateBackup();
}
EditorGUILayout.Space(10);
// 수동 이동 목록 표시
if (_manualMoveList.Count > 0)
{
EditorGUILayout.HelpBox(
$"⚠️ 수동 이동 필요: {_manualMoveList.Count}개\n아래 Console 로그 참고",
MessageType.Warning
);
}
EditorGUILayout.HelpBox(
"💡 권장 순서:\n" +
"1. '파일 스캔' 클릭 → Console에서 결과 확인\n" +
"2. '백업 생성'\n" +
"3. '폴더 구조 생성'\n" +
"4. '찾은 파일 자동 이동'\n" +
"5. Console에 나온 수동 이동 목록 처리\n" +
"6. Unity 컴파일 대기 후 테스트",
MessageType.Info
);
EditorGUILayout.EndScrollView();
}
private void ScanFilesWithManualList()
{
Log("🔍 파일 스캔 시작...");
_manualMoveList.Clear();
var fileMap = GetFileMappings();
int foundCount = 0;
int notFoundCount = 0;
List<string> notFoundFiles = new List<string>();
foreach (var kvp in fileMap)
{
string fileName = kvp.Key;
string targetFolder = kvp.Value;
string[] foundFiles = FindScriptAdvanced(fileName);
if (foundFiles.Length > 0)
{
foundCount++;
Log($" ✅ 찾음: {fileName} → {foundFiles[0]}");
}
else
{
notFoundCount++;
notFoundFiles.Add(fileName);
Log($" ❌ 못 찾음: {fileName}");
}
}
// 수동 이동 가이드 생성
Log("\n" + new string('=', 60));
Log("📋 수동 이동 가이드");
Log(new string('=', 60));
if (notFoundFiles.Count > 0)
{
Log("\n⚠ 다음 파일들을 수동으로 이동해주세요:\n");
foreach (string fileName in notFoundFiles)
{
if (fileMap.ContainsKey(fileName))
{
string targetPath = fileMap[fileName];
Log($"[ ] {fileName}.cs");
Log($" → Assets/Scripts/{targetPath}/");
Log("");
_manualMoveList.Add($"{fileName} → Scripts/{targetPath}/");
}
}
Log("\n💡 수동 이동 방법:");
Log("1. Project 창에서 't:Script [파일명]' 으로 검색");
Log("2. 찾은 파일을 드래그해서 위 경로로 이동");
Log("3. Unity가 자동으로 참조 업데이트");
}
string result = $"스캔 완료!\n✅ 찾음: {foundCount}개\n❌ 못 찾음: {notFoundCount}개";
if (notFoundCount > 0)
{
result += $"\n\n⚠ Console 로그에서 수동 이동 가이드 확인!";
}
Log(result);
EditorUtility.DisplayDialog("스캔 완료", result, "확인");
}
private void CreateBackup()
{
Log("🔙 백업 생성 시작...");
string timestamp = System.DateTime.Now.ToString("yyyyMMdd_HHmmss");
string backupPath = $"Assets/Scripts_Backup_{timestamp}";
// 모든 스크립트 폴더 찾기
string[] scriptFolders = new string[]
{
"Assets/5.TestScript",
"Assets/Scripts",
"Assets/Code" // 혹시 있을 수 있는 다른 폴더
};
AssetDatabase.CreateFolder("Assets", $"Scripts_Backup_{timestamp}");
foreach (string folder in scriptFolders)
{
if (Directory.Exists(folder))
{
string folderName = Path.GetFileName(folder);
string destFolder = Path.Combine(backupPath, folderName);
CopyDirectorySafe(folder, destFolder);
Log($" 백업: {folder}");
}
}
AssetDatabase.Refresh();
Log($"✅ 백업 완료: {backupPath}");
EditorUtility.DisplayDialog("백업 완료", $"백업이 생성되었습니다:\n{backupPath}", "확인");
}
private void CopyDirectorySafe(string sourceDir, string destDir)
{
try
{
Directory.CreateDirectory(destDir);
foreach (string file in Directory.GetFiles(sourceDir))
{
string fileName = Path.GetFileName(file);
if (fileName.EndsWith(".meta")) continue;
string destFile = Path.Combine(destDir, fileName);
File.Copy(file, destFile, true);
}
foreach (string subDir in Directory.GetDirectories(sourceDir))
{
string dirName = Path.GetFileName(subDir);
string destSubDir = Path.Combine(destDir, dirName);
CopyDirectorySafe(subDir, destSubDir);
}
}
catch (System.Exception ex)
{
Log($"❌ 백업 실패: {sourceDir} - {ex.Message}");
}
}
private void CreateFolderStructure()
{
Log("📁 폴더 구조 생성 시작...");
string[] folders = new string[]
{
"Core",
"Player/Controller",
"Player/Animation",
"Player/Combat",
"Player/Stats",
"Player/Upgrade/Data",
"Player/Upgrade/UI",
"Player/Equipment",
"Player/Interaction",
"Combat/Interfaces",
"Combat/Components",
"Combat/Debug",
"Enemy/Spawner",
"Enemy/Types",
"Items/Pickups",
"Items/Interactive",
"Items/VFX",
"UI/HUD",
"UI/Enemy",
"UI/Level",
"Camera/Effects",
"Systems/ObjectPool",
"Systems/Scene",
"Systems/Settings",
"Systems/Optimization/Editor",
"Data/Stats",
"Utilities"
};
int createdCount = 0;
foreach (string folder in folders)
{
string fullPath = $"{TARGET_FOLDER}/{folder}";
if (CreateFolderRecursive(fullPath))
{
createdCount++;
}
}
AssetDatabase.Refresh();
Log($"✅ 폴더 구조 생성 완료! ({createdCount}개 폴더)");
EditorUtility.DisplayDialog("완료", $"{createdCount}개의 폴더가 생성되었습니다.", "확인");
}
private bool CreateFolderRecursive(string path)
{
string[] parts = path.Replace("Assets/", "").Split('/');
string currentPath = "Assets";
bool created = false;
for (int i = 0; i < parts.Length; i++)
{
string nextPath = currentPath + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(nextPath))
{
AssetDatabase.CreateFolder(currentPath, parts[i]);
Log($" 생성됨: {nextPath}");
created = true;
}
currentPath = nextPath;
}
return created;
}
private void OrganizeFilesRecursive()
{
Log("📦 파일 이동 시작...");
var fileMap = GetFileMappings();
int movedCount = 0;
int failedCount = 0;
int skippedCount = 0;
List<string> notFoundFiles = new List<string>();
foreach (var kvp in fileMap)
{
string fileName = kvp.Key;
string targetFolder = kvp.Value;
string[] foundFiles = FindScriptAdvanced(fileName);
if (foundFiles.Length == 0)
{
notFoundFiles.Add(fileName);
continue;
}
string oldPath = foundFiles[0];
string newPath = $"{TARGET_FOLDER}/{targetFolder}/{Path.GetFileName(oldPath)}";
// 이미 목적지에 있으면 스킵
if (oldPath == newPath)
{
Log($" ⏭️ 스킵: {fileName} (이미 올바른 위치)");
skippedCount++;
continue;
}
// 파일 이동
string result = AssetDatabase.MoveAsset(oldPath, newPath);
if (string.IsNullOrEmpty(result))
{
Log($" ✅ 이동: {Path.GetFileName(oldPath)}");
Log($" {oldPath} → {newPath}");
movedCount++;
}
else
{
Log($" ❌ 실패: {fileName} - {result}");
failedCount++;
}
}
AssetDatabase.Refresh();
// 결과 출력
string resultMessage = $"✅ 이동 완료: {movedCount}개\n";
if (skippedCount > 0)
{
resultMessage += $"⏭️ 스킵: {skippedCount}개 (이미 올바른 위치)\n";
}
if (failedCount > 0)
{
resultMessage += $"❌ 실패: {failedCount}개\n";
}
if (notFoundFiles.Count > 0)
{
resultMessage += $"\n⚠ 못 찾음: {notFoundFiles.Count}개\n";
resultMessage += "→ Console에서 수동 이동 가이드 확인\n";
}
Log($"\n✅ 파일 정리 완료!\n{resultMessage}");
EditorUtility.DisplayDialog("완료", resultMessage, "확인");
}
/// <summary>
/// 고급 스크립트 검색 (공백, 언더스코어, 변형 모두 지원)
/// </summary>
private string[] FindScriptAdvanced(string fileName)
{
List<string> results = new List<string>();
// 1단계: 정확한 이름으로 검색
string[] guids = AssetDatabase.FindAssets($"{fileName} t:Script");
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
string scriptName = Path.GetFileNameWithoutExtension(path);
if (scriptName == fileName)
{
results.Add(path);
}
}
if (results.Count > 0) return results.ToArray();
// 2단계: 변형 검색
List<string> variations = new List<string>
{
fileName,
fileName.Replace("_", " "), // HP_UI_bar → HP UI bar
fileName.Replace("_", ""), // HP_UI_bar → HPUIbar
fileName.Replace(" ", "_"), // 반대
fileName.Replace(" ", ""), // 공백 제거
fileName + "UI", // 끝에 UI 붙은 버전
fileName.Replace("UI", ""), // UI 제거
};
// 부분 검색
string[] allGuids = AssetDatabase.FindAssets("t:Script");
foreach (string guid in allGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
// 5.TestScript 폴더 우선
if (!path.Contains("5.TestScript") && !path.Contains("Scripts")) continue;
string scriptName = Path.GetFileNameWithoutExtension(path);
foreach (string variation in variations)
{
if (scriptName.Equals(variation, System.StringComparison.OrdinalIgnoreCase))
{
results.Add(path);
return results.ToArray();
}
}
}
return results.ToArray();
}
private Dictionary<string, string> GetFileMappings()
{
return new Dictionary<string, string>()
{
// Player
{"PlayerMovement", "Player/Controller"},
{"PlayerInput", "Player/Controller"},
{"Animation", "Player/Animation"},
{"Attack", "Player/Combat"},
{"Health", "Combat/Components"},
{"Stats", "Player/Stats"},
{"PlayerLevelSystem", "Player/Stats"},
{"StatType", "Player/Stats"},
{"CardData", "Player/Upgrade/Data"},
{"RandomStatCardData", "Player/Upgrade/Data"},
{"RandomStatCardUIstar", "Player/Upgrade/UI"},
{"RandomStatCardUIStar", "Player/Upgrade/UI"},
{"CardUI", "Player/Upgrade/UI"},
{"EquipItem", "Player/Equipment"},
{"PlayerInteraction", "Player/Interaction"},
// Combat
{"IDamageable", "Combat/Interfaces"},
{"WeaponHitBox", "Combat/Components"},
{"WePonHitBox", "Combat/Components"},
{"DamageBot", "Combat/Debug"},
// Enemy
{"MonsterSpawner", "Enemy/Spawner"},
{"Monster Spawner", "Enemy/Spawner"},
{"MonsterUpdateManager", "Enemy/Spawner"},
{"DummyBot", "Enemy/Types"},
// Items
{"HealthPotion", "Items/Pickups"},
{"HealthAltar", "Items/Interactive"},
{"ItemHighlight", "Items/VFX"},
// UI
{"CrossHairUI", "UI/HUD"},
{"HP_UI_bar", "UI/HUD"},
{"HP UI bar", "UI/HUD"},
{"HPUIbar", "UI/HUD"},
{"PlayerStat_UI", "UI/HUD"},
{"PlayerStat UI", "UI/HUD"},
{"PlayerStatUI", "UI/HUD"},
{"EnemyHP_UI", "UI/Enemy"},
{"EnemyHP UI", "UI/Enemy"},
{"EnemyHPUI", "UI/Enemy"},
{"LevelUIManager", "UI/Level"},
// Camera
{"CamShake", "Camera/Effects"},
// Systems
{"GenericObjectPool", "Systems/ObjectPool"},
{"LoadScene", "Systems/Scene"},
{"SettingResolution", "Systems/Settings"},
// Data
{"StatsConfig", "Data/Stats"},
// Optimization
{"PlayerRangeManager", "Systems/Optimization"},
{"RenderGroup", "Systems/Optimization"},
{"RenderOptimizationConfig", "Systems/Optimization"},
{"PlayerRangeManagerEditor", "Systems/Optimization/Editor"}
};
}
private void Log(string message)
{
if (_showDetailedLog)
{
Debug.Log($"[ProjectOrganizerV3] {message}");
}
}
}