diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop.meta b/Assets/Scripts/Streamingle/StreamingleControl/Prop.meta
new file mode 100644
index 00000000..51d62e7c
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ef5f7eb69221f2b498b9a1cc56a4d794
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor.meta b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor.meta
new file mode 100644
index 00000000..83ee3505
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 69ed6044e4ba275409c5c0a75325bf03
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropBrowserWindow.cs b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropBrowserWindow.cs
new file mode 100644
index 00000000..aae77b43
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropBrowserWindow.cs
@@ -0,0 +1,1394 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using UnityEditor;
+using UnityEngine;
+
+namespace Streamingle.Prop.Editor
+{
+ ///
+ /// 개별 프리펩 표시용 데이터
+ ///
+ public class PrefabDisplayItem
+ {
+ public string prefabName; // 프리펩 이름
+ public string prefabPath; // 프리펩 경로
+ public string folderName; // 폴더 이름
+ public PropInfo parentProp; // 부모 PropInfo 참조
+ public string thumbnailPath; // 썸네일 경로
+
+ public string DisplayName => prefabName;
+ }
+
+ ///
+ /// 프랍 브라우저 에디터 윈도우
+ ///
+ public class PropBrowserWindow : EditorWindow
+ {
+ private const string PROP_PATH = "Assets/ResourcesData/Prop";
+ private const string DATABASE_PATH = "Assets/Resources/Settings/PropDatabase.asset";
+ private const string THUMBNAIL_FOLDER = "Thumbnail";
+ private const int THUMBNAIL_SIZE = 128;
+ private const int THUMBNAIL_CAPTURE_SIZE = 256;
+ private const int GRID_PADDING = 10;
+
+ private PropDatabase _database;
+ private List _filteredProps;
+ private List _displayItems; // 개별 프리펩 표시용
+ private Dictionary _thumbnailCache = new Dictionary();
+
+ private Vector2 _scrollPosition;
+ private string _searchFilter = "";
+ private int _viewMode = 0; // 0: 그리드, 1: 리스트
+
+ private static readonly string[] VIEW_MODE_OPTIONS = { "그리드", "리스트" };
+ private static readonly Color SELECTED_COLOR = new Color(0.3f, 0.6f, 1f, 0.3f);
+ private static readonly Color HOVER_COLOR = new Color(1f, 1f, 1f, 0.1f);
+
+ [MenuItem("Streamingle/Prop Browser %#p")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("프랍 브라우저");
+ window.minSize = new Vector2(400, 500);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ LoadOrCreateDatabase();
+ }
+
+ private void OnDisable()
+ {
+ ClearThumbnailCache();
+ }
+
+ private void OnGUI()
+ {
+ DrawToolbar();
+ DrawContent();
+ }
+
+ private void DrawToolbar()
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
+
+ // 새로고침 버튼
+ if (GUILayout.Button("새로고침", EditorStyles.toolbarButton, GUILayout.Width(60)))
+ {
+ RefreshPropList();
+ }
+
+ GUILayout.Space(10);
+
+ // 검색
+ EditorGUILayout.LabelField("검색:", GUILayout.Width(35));
+ string newSearch = EditorGUILayout.TextField(_searchFilter, EditorStyles.toolbarSearchField, GUILayout.Width(150));
+ if (newSearch != _searchFilter)
+ {
+ _searchFilter = newSearch;
+ ApplyFilter();
+ }
+
+ if (GUILayout.Button("X", EditorStyles.toolbarButton, GUILayout.Width(20)))
+ {
+ _searchFilter = "";
+ ApplyFilter();
+ GUI.FocusControl(null);
+ }
+
+ GUILayout.FlexibleSpace();
+
+ // 뷰 모드
+ _viewMode = GUILayout.Toolbar(_viewMode, VIEW_MODE_OPTIONS, EditorStyles.toolbarButton, GUILayout.Width(100));
+
+ EditorGUILayout.EndHorizontal();
+
+ // 정보 바
+ EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
+ int totalPrefabCount = _database?.props.Sum(p => Math.Max(1, p.prefabPaths.Count)) ?? 0;
+ int filteredCount = _displayItems?.Count ?? 0;
+ string countText = string.IsNullOrEmpty(_searchFilter)
+ ? $"총 {totalPrefabCount}개 프리펩"
+ : $"{filteredCount}/{totalPrefabCount}개 프리펩";
+ EditorGUILayout.LabelField(countText);
+
+ GUILayout.FlexibleSpace();
+
+ // 썸네일 생성
+ if (GUILayout.Button("썸네일 생성", GUILayout.Width(80)))
+ {
+ ShowThumbnailMenu();
+ }
+
+ // 웹 업로드
+ if (GUILayout.Button("웹 업로드", GUILayout.Width(80)))
+ {
+ ShowWebUploadMenu();
+ }
+
+ // 폴더 열기
+ if (GUILayout.Button("폴더", GUILayout.Width(40)))
+ {
+ EditorUtility.RevealInFinder(PROP_PATH);
+ }
+
+ EditorGUILayout.EndHorizontal();
+ }
+
+ private void ShowThumbnailMenu()
+ {
+ var menu = new GenericMenu();
+ menu.AddItem(new GUIContent("모든 프리펩 썸네일 생성 (개별)"), false, () => GenerateAllPrefabThumbnails());
+ menu.AddItem(new GUIContent("썸네일 없는 프리펩만 생성"), false, () => GenerateMissingPrefabThumbnails());
+ menu.AddSeparator("");
+ menu.AddItem(new GUIContent("폴더 단위로 생성 (기존 방식)/모든 폴더"), false, () => GenerateAllThumbnails());
+ menu.AddItem(new GUIContent("폴더 단위로 생성 (기존 방식)/선택한 폴더"), false, () => GenerateThumbnailsForSelected());
+ menu.ShowAsContext();
+ }
+
+ private void DrawContent()
+ {
+ if (_database == null || _displayItems == null)
+ {
+ EditorGUILayout.HelpBox("데이터베이스를 로드할 수 없습니다. 새로고침을 눌러주세요.", MessageType.Warning);
+ return;
+ }
+
+ if (_displayItems.Count == 0)
+ {
+ EditorGUILayout.HelpBox("프리펩이 없습니다.", MessageType.Info);
+ return;
+ }
+
+ _scrollPosition = EditorGUILayout.BeginScrollView(_scrollPosition);
+
+ if (_viewMode == 0)
+ {
+ DrawGridView();
+ }
+ else
+ {
+ DrawListView();
+ }
+
+ EditorGUILayout.EndScrollView();
+ }
+
+ private void DrawGridView()
+ {
+ float windowWidth = position.width - 30;
+ float itemWidth = THUMBNAIL_SIZE + GRID_PADDING;
+ float itemHeight = THUMBNAIL_SIZE + 40; // 폴더명 표시를 위해 높이 증가
+ int columnsCount = Mathf.Max(1, (int)(windowWidth / itemWidth));
+
+ int rowCount = Mathf.CeilToInt((float)_displayItems.Count / columnsCount);
+ float gridHeight = rowCount * itemHeight;
+
+ Rect gridRect = GUILayoutUtility.GetRect(windowWidth, gridHeight);
+ gridRect.x += GRID_PADDING;
+
+ for (int i = 0; i < _displayItems.Count; i++)
+ {
+ int row = i / columnsCount;
+ int col = i % columnsCount;
+
+ Rect itemRect = new Rect(
+ gridRect.x + col * itemWidth,
+ gridRect.y + row * itemHeight,
+ THUMBNAIL_SIZE,
+ itemHeight
+ );
+
+ DrawGridItem(_displayItems[i], itemRect);
+ }
+ }
+
+ private void DrawGridItem(PrefabDisplayItem item, Rect rect)
+ {
+ // 호버 효과
+ if (rect.Contains(Event.current.mousePosition))
+ {
+ EditorGUI.DrawRect(rect, HOVER_COLOR);
+ Repaint();
+ }
+
+ // 썸네일
+ var thumbnailRect = new Rect(rect.x + 2, rect.y + 2, THUMBNAIL_SIZE - 4, THUMBNAIL_SIZE - 4);
+ var thumbnail = GetThumbnailForItem(item);
+
+ if (thumbnail != null)
+ {
+ GUI.DrawTexture(thumbnailRect, thumbnail, ScaleMode.ScaleToFit);
+ }
+ else
+ {
+ EditorGUI.DrawRect(thumbnailRect, new Color(0.2f, 0.2f, 0.2f));
+
+ // 프리펩 미리보기
+ if (!string.IsNullOrEmpty(item.prefabPath))
+ {
+ var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
+ if (prefab != null)
+ {
+ var preview = AssetPreview.GetAssetPreview(prefab);
+ if (preview != null)
+ {
+ GUI.DrawTexture(thumbnailRect, preview, ScaleMode.ScaleToFit);
+ }
+ else
+ {
+ AssetPreview.SetPreviewTextureCacheSize(256);
+ GUI.Label(thumbnailRect, "Loading...", new GUIStyle(EditorStyles.centeredGreyMiniLabel) { alignment = TextAnchor.MiddleCenter });
+ }
+ }
+ }
+ else
+ {
+ GUI.Label(thumbnailRect, "No\nPrefab", new GUIStyle(EditorStyles.centeredGreyMiniLabel) { wordWrap = true, alignment = TextAnchor.MiddleCenter });
+ }
+ }
+
+ // 폴더 이름 배지 (프리펩 이름과 다를 경우)
+ if (!string.IsNullOrEmpty(item.folderName) && item.folderName != item.prefabName)
+ {
+ var folderBadgeRect = new Rect(rect.x + 4, rect.y + THUMBNAIL_SIZE - 18, THUMBNAIL_SIZE - 8, 14);
+ EditorGUI.DrawRect(folderBadgeRect, new Color(0, 0, 0, 0.7f));
+ GUI.Label(folderBadgeRect, item.folderName, new GUIStyle(EditorStyles.miniLabel) {
+ alignment = TextAnchor.MiddleCenter,
+ normal = { textColor = new Color(1, 1, 1, 0.9f) },
+ fontSize = 9,
+ clipping = TextClipping.Clip
+ });
+ }
+
+ // 프리펩 이름
+ var labelRect = new Rect(rect.x, rect.y + THUMBNAIL_SIZE, THUMBNAIL_SIZE, 35);
+ GUI.Label(labelRect, item.prefabName, new GUIStyle(EditorStyles.miniLabel) {
+ alignment = TextAnchor.UpperCenter,
+ wordWrap = true,
+ clipping = TextClipping.Clip
+ });
+
+ // 클릭 이벤트
+ if (Event.current.type == EventType.MouseDown && rect.Contains(Event.current.mousePosition))
+ {
+ if (Event.current.clickCount == 2)
+ {
+ // 더블클릭: 프리펩 선택
+ SelectPrefabInProject(item);
+ }
+ else if (Event.current.button == 0)
+ {
+ // 싱글클릭: 드래그 시작
+ HandleDragStartForItem(item);
+ }
+ else if (Event.current.button == 1)
+ {
+ // 우클릭: 컨텍스트 메뉴
+ ShowContextMenuForItem(item);
+ }
+ Event.current.Use();
+ }
+ }
+
+ private void DrawListView()
+ {
+ foreach (var item in _displayItems)
+ {
+ DrawListItem(item);
+ }
+ }
+
+ private void DrawListItem(PrefabDisplayItem item)
+ {
+ EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
+
+ // 썸네일 (작게)
+ var thumbnail = GetThumbnailForItem(item);
+ var thumbRect = GUILayoutUtility.GetRect(50, 50, GUILayout.Width(50), GUILayout.Height(50));
+
+ if (thumbnail != null)
+ {
+ GUI.DrawTexture(thumbRect, thumbnail, ScaleMode.ScaleToFit);
+ }
+ else
+ {
+ EditorGUI.DrawRect(thumbRect, new Color(0.2f, 0.2f, 0.2f));
+
+ // 프리펩 미리보기
+ if (!string.IsNullOrEmpty(item.prefabPath))
+ {
+ var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
+ if (prefab != null)
+ {
+ var preview = AssetPreview.GetAssetPreview(prefab);
+ if (preview != null)
+ {
+ GUI.DrawTexture(thumbRect, preview, ScaleMode.ScaleToFit);
+ }
+ }
+ }
+ }
+
+ GUILayout.Space(10);
+
+ // 프리펩 정보
+ EditorGUILayout.BeginVertical();
+
+ EditorGUILayout.LabelField(item.prefabName, EditorStyles.boldLabel);
+
+ // 폴더 이름 (다르면 표시)
+ if (!string.IsNullOrEmpty(item.folderName) && item.folderName != item.prefabName)
+ {
+ EditorGUILayout.LabelField($"폴더: {item.folderName}", EditorStyles.miniLabel);
+ }
+
+ EditorGUILayout.EndVertical();
+
+ GUILayout.FlexibleSpace();
+
+ // 버튼들
+ EditorGUILayout.BeginVertical();
+
+ if (GUILayout.Button("선택", GUILayout.Width(50)))
+ {
+ SelectPrefabInProject(item);
+ }
+
+ if (GUILayout.Button("...", GUILayout.Width(50)))
+ {
+ ShowContextMenuForItem(item);
+ }
+
+ EditorGUILayout.EndVertical();
+
+ EditorGUILayout.EndHorizontal();
+
+ // 드래그 처리
+ var lastRect = GUILayoutUtility.GetLastRect();
+ if (Event.current.type == EventType.MouseDown && lastRect.Contains(Event.current.mousePosition))
+ {
+ if (Event.current.button == 0)
+ {
+ HandleDragStartForItem(item);
+ }
+ }
+ }
+
+ private void HandleDragStart(PropInfo propInfo)
+ {
+ if (propInfo.prefabPaths.Count == 0) return;
+
+ var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
+ if (prefab == null) return;
+
+ DragAndDrop.PrepareStartDrag();
+ DragAndDrop.objectReferences = new UnityEngine.Object[] { prefab };
+ DragAndDrop.paths = new string[] { propInfo.MainPrefabPath };
+ DragAndDrop.StartDrag(propInfo.propName);
+ }
+
+ private void HandleDragStartForItem(PrefabDisplayItem item)
+ {
+ if (string.IsNullOrEmpty(item.prefabPath)) return;
+
+ var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
+ if (prefab == null) return;
+
+ DragAndDrop.PrepareStartDrag();
+ DragAndDrop.objectReferences = new UnityEngine.Object[] { prefab };
+ DragAndDrop.paths = new string[] { item.prefabPath };
+ DragAndDrop.StartDrag(item.prefabName);
+ }
+
+ private void SelectPrefabInProject(PrefabDisplayItem item)
+ {
+ if (!string.IsNullOrEmpty(item.prefabPath))
+ {
+ var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
+ if (prefab != null)
+ {
+ Selection.activeObject = prefab;
+ EditorGUIUtility.PingObject(prefab);
+ return;
+ }
+ }
+
+ // 프리펩이 없으면 폴더 선택
+ if (item.parentProp != null)
+ {
+ var folder = AssetDatabase.LoadAssetAtPath(item.parentProp.folderPath);
+ if (folder != null)
+ {
+ Selection.activeObject = folder;
+ EditorGUIUtility.PingObject(folder);
+ }
+ }
+ }
+
+ private void ShowContextMenuForItem(PrefabDisplayItem item)
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("프로젝트에서 선택"), false, () => SelectPrefabInProject(item));
+
+ if (item.parentProp != null)
+ {
+ menu.AddItem(new GUIContent("폴더 열기"), false, () => RevealInFinder(item.parentProp));
+ }
+
+ menu.AddSeparator("");
+
+ menu.AddItem(new GUIContent("씬에 배치"), false, () => InstantiatePrefabFromItem(item));
+
+ menu.AddSeparator("");
+
+ menu.AddItem(new GUIContent("이 프리펩 썸네일 생성"), false, () => GenerateThumbnailForItem(item));
+
+ menu.ShowAsContext();
+ }
+
+ private void InstantiatePrefabFromItem(PrefabDisplayItem item)
+ {
+ if (string.IsNullOrEmpty(item.prefabPath))
+ {
+ EditorUtility.DisplayDialog("오류", "프리펩이 없습니다.", "확인");
+ return;
+ }
+
+ var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
+ if (prefab == null)
+ {
+ EditorUtility.DisplayDialog("오류", "프리펩을 로드할 수 없습니다.", "확인");
+ return;
+ }
+
+ var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
+ if (instance != null)
+ {
+ if (SceneView.lastActiveSceneView != null)
+ {
+ instance.transform.position = SceneView.lastActiveSceneView.pivot;
+ }
+
+ Selection.activeGameObject = instance;
+ Undo.RegisterCreatedObjectUndo(instance, "Instantiate Prop");
+ }
+ }
+
+ private Texture2D GetThumbnailForItem(PrefabDisplayItem item)
+ {
+ if (string.IsNullOrEmpty(item.thumbnailPath))
+ return null;
+
+ if (_thumbnailCache.TryGetValue(item.thumbnailPath, out var cached))
+ return cached;
+
+ var texture = AssetDatabase.LoadAssetAtPath(item.thumbnailPath);
+ if (texture != null)
+ {
+ _thumbnailCache[item.thumbnailPath] = texture;
+ }
+
+ return texture;
+ }
+
+ private void ShowContextMenu(PropInfo propInfo)
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("프로젝트에서 선택"), false, () => SelectPropInProject(propInfo));
+ menu.AddItem(new GUIContent("폴더 열기"), false, () => RevealInFinder(propInfo));
+
+ menu.AddSeparator("");
+
+ // 프리펩 목록
+ if (propInfo.prefabPaths.Count > 0)
+ {
+ foreach (var prefabPath in propInfo.prefabPaths)
+ {
+ string prefabName = Path.GetFileNameWithoutExtension(prefabPath);
+ menu.AddItem(new GUIContent($"프리펩/{prefabName}"), false, () =>
+ {
+ var prefab = AssetDatabase.LoadAssetAtPath(prefabPath);
+ if (prefab != null)
+ {
+ Selection.activeObject = prefab;
+ EditorGUIUtility.PingObject(prefab);
+ }
+ });
+ }
+ }
+ else
+ {
+ menu.AddDisabledItem(new GUIContent("프리펩 없음"));
+ }
+
+ menu.AddSeparator("");
+
+ // 모델 목록
+ if (propInfo.modelPaths.Count > 0)
+ {
+ foreach (var modelPath in propInfo.modelPaths)
+ {
+ string modelName = Path.GetFileName(modelPath);
+ menu.AddItem(new GUIContent($"모델/{modelName}"), false, () =>
+ {
+ var model = AssetDatabase.LoadAssetAtPath(modelPath);
+ if (model != null)
+ {
+ Selection.activeObject = model;
+ EditorGUIUtility.PingObject(model);
+ }
+ });
+ }
+ }
+
+ menu.AddSeparator("");
+
+ menu.AddItem(new GUIContent("씬에 배치"), false, () => InstantiatePrefab(propInfo));
+
+ menu.AddSeparator("");
+
+ menu.AddItem(new GUIContent("썸네일 생성"), false, () => GenerateThumbnail(propInfo));
+
+ menu.ShowAsContext();
+ }
+
+ private void SelectPropInProject(PropInfo propInfo)
+ {
+ if (propInfo.prefabPaths.Count > 0)
+ {
+ var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
+ if (prefab != null)
+ {
+ Selection.activeObject = prefab;
+ EditorGUIUtility.PingObject(prefab);
+ return;
+ }
+ }
+
+ // 프리펩이 없으면 폴더 선택
+ var folder = AssetDatabase.LoadAssetAtPath(propInfo.folderPath);
+ if (folder != null)
+ {
+ Selection.activeObject = folder;
+ EditorGUIUtility.PingObject(folder);
+ }
+ }
+
+ private void RevealInFinder(PropInfo propInfo)
+ {
+ EditorUtility.RevealInFinder(propInfo.folderPath);
+ }
+
+ private void InstantiatePrefab(PropInfo propInfo)
+ {
+ if (propInfo.prefabPaths.Count == 0)
+ {
+ EditorUtility.DisplayDialog("오류", "프리펩이 없습니다.", "확인");
+ return;
+ }
+
+ var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
+ if (prefab == null)
+ {
+ EditorUtility.DisplayDialog("오류", "프리펩을 로드할 수 없습니다.", "확인");
+ return;
+ }
+
+ var instance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
+ if (instance != null)
+ {
+ // 씬 뷰 중앙에 배치
+ if (SceneView.lastActiveSceneView != null)
+ {
+ instance.transform.position = SceneView.lastActiveSceneView.pivot;
+ }
+
+ Selection.activeGameObject = instance;
+ Undo.RegisterCreatedObjectUndo(instance, "Instantiate Prop");
+ }
+ }
+
+ private Texture2D GetThumbnail(PropInfo propInfo)
+ {
+ if (string.IsNullOrEmpty(propInfo.thumbnailPath))
+ return null;
+
+ if (_thumbnailCache.TryGetValue(propInfo.thumbnailPath, out var cached))
+ return cached;
+
+ var texture = AssetDatabase.LoadAssetAtPath(propInfo.thumbnailPath);
+ if (texture != null)
+ {
+ _thumbnailCache[propInfo.thumbnailPath] = texture;
+ }
+
+ return texture;
+ }
+
+ private void ClearThumbnailCache()
+ {
+ _thumbnailCache.Clear();
+ }
+
+ private void LoadOrCreateDatabase()
+ {
+ _database = AssetDatabase.LoadAssetAtPath(DATABASE_PATH);
+
+ if (_database == null)
+ {
+ // 폴더 생성
+ if (!AssetDatabase.IsValidFolder("Assets/Resources"))
+ AssetDatabase.CreateFolder("Assets", "Resources");
+ if (!AssetDatabase.IsValidFolder("Assets/Resources/Settings"))
+ AssetDatabase.CreateFolder("Assets/Resources", "Settings");
+
+ _database = CreateInstance();
+ AssetDatabase.CreateAsset(_database, DATABASE_PATH);
+ AssetDatabase.SaveAssets();
+ }
+
+ RefreshPropList();
+ }
+
+ private void RefreshPropList()
+ {
+ if (_database == null) return;
+
+ _database.props.Clear();
+
+ if (!Directory.Exists(PROP_PATH))
+ {
+ UnityEngine.Debug.LogWarning($"Prop 폴더가 없습니다: {PROP_PATH}");
+ ApplyFilter();
+ return;
+ }
+
+ // Prop 하위 폴더들 검색 (Glb, Prop Prefab 제외)
+ var propFolders = Directory.GetDirectories(PROP_PATH)
+ .Where(f => !f.EndsWith("Glb") && !f.EndsWith("Prop Prefab"))
+ .ToList();
+
+ foreach (var folderPath in propFolders)
+ {
+ string propName = Path.GetFileName(folderPath);
+ string assetPath = folderPath.Replace("\\", "/");
+
+ // Assets/ 형식으로 변환
+ int assetsIndex = assetPath.IndexOf("Assets/");
+ if (assetsIndex >= 0)
+ {
+ assetPath = assetPath.Substring(assetsIndex);
+ }
+
+ var propInfo = new PropInfo
+ {
+ propName = propName,
+ folderPath = assetPath
+ };
+
+ // 파일 스캔
+ ScanPropFiles(propInfo, folderPath);
+
+ _database.props.Add(propInfo);
+ }
+
+ // 이름순 정렬
+ _database.props = _database.props.OrderBy(p => p.propName).ToList();
+
+ EditorUtility.SetDirty(_database);
+ AssetDatabase.SaveAssets();
+
+ ApplyFilter();
+
+ UnityEngine.Debug.Log($"[PropBrowser] {_database.props.Count}개 프랍 스캔 완료");
+ }
+
+ private void ScanPropFiles(PropInfo propInfo, string folderPath)
+ {
+ var allFiles = Directory.GetFiles(folderPath, "*.*", SearchOption.AllDirectories)
+ .Where(f => !f.EndsWith(".meta"))
+ .ToList();
+
+ foreach (var file in allFiles)
+ {
+ string ext = Path.GetExtension(file).ToLower();
+ string assetPath = file.Replace("\\", "/");
+
+ int assetsIndex = assetPath.IndexOf("Assets/");
+ if (assetsIndex >= 0)
+ {
+ assetPath = assetPath.Substring(assetsIndex);
+ }
+
+ if (ext == ".prefab")
+ {
+ propInfo.prefabPaths.Add(assetPath);
+ }
+ else if (ext == ".glb" || ext == ".fbx" || ext == ".obj")
+ {
+ propInfo.modelPaths.Add(assetPath);
+ }
+ else if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".tga" || ext == ".psd")
+ {
+ propInfo.textureCount++;
+
+ // 썸네일 찾기 (thumbnail 또는 _thumb 포함)
+ string fileName = Path.GetFileNameWithoutExtension(file).ToLower();
+ if (fileName.Contains("thumbnail") || fileName.Contains("_thumb"))
+ {
+ propInfo.thumbnailPath = assetPath;
+ }
+ }
+ else if (ext == ".mat")
+ {
+ propInfo.materialCount++;
+ }
+ }
+
+ // 프리펩 이름순 정렬
+ propInfo.prefabPaths = propInfo.prefabPaths.OrderBy(p => Path.GetFileName(p)).ToList();
+ }
+
+ private void ApplyFilter()
+ {
+ if (_database == null)
+ {
+ _filteredProps = new List();
+ _displayItems = new List();
+ return;
+ }
+
+ // 기존 필터링
+ if (string.IsNullOrEmpty(_searchFilter))
+ {
+ _filteredProps = new List(_database.props);
+ }
+ else
+ {
+ string filter = _searchFilter.ToLower();
+ _filteredProps = _database.props
+ .Where(p => p.propName.ToLower().Contains(filter) ||
+ p.prefabPaths.Any(pp => Path.GetFileNameWithoutExtension(pp).ToLower().Contains(filter)))
+ .ToList();
+ }
+
+ // 개별 프리펩 단위로 표시 아이템 생성
+ _displayItems = new List();
+
+ foreach (var prop in _filteredProps)
+ {
+ if (prop.prefabPaths.Count > 0)
+ {
+ // 각 프리펩을 개별 아이템으로 추가
+ foreach (var prefabPath in prop.prefabPaths)
+ {
+ string prefabName = Path.GetFileNameWithoutExtension(prefabPath);
+
+ // 검색 필터 추가 적용
+ if (!string.IsNullOrEmpty(_searchFilter))
+ {
+ string filter = _searchFilter.ToLower();
+ if (!prefabName.ToLower().Contains(filter) && !prop.propName.ToLower().Contains(filter))
+ continue;
+ }
+
+ // 개별 프리펩 썸네일 경로 찾기
+ string prefabThumbnailPath = $"{prop.folderPath}/{THUMBNAIL_FOLDER}/{prefabName}_thumbnail.png";
+ string prefabThumbnailFullPath = prefabThumbnailPath.Replace("Assets/", Application.dataPath + "/");
+
+ // 개별 썸네일이 없으면 폴더 썸네일 사용
+ string thumbnailToUse = File.Exists(prefabThumbnailFullPath) ? prefabThumbnailPath : prop.thumbnailPath;
+
+ _displayItems.Add(new PrefabDisplayItem
+ {
+ prefabName = prefabName,
+ prefabPath = prefabPath,
+ folderName = prop.propName,
+ parentProp = prop,
+ thumbnailPath = thumbnailToUse
+ });
+ }
+ }
+ else
+ {
+ // 프리펩이 없는 폴더도 표시
+ _displayItems.Add(new PrefabDisplayItem
+ {
+ prefabName = prop.propName,
+ prefabPath = null,
+ folderName = prop.propName,
+ parentProp = prop,
+ thumbnailPath = prop.thumbnailPath
+ });
+ }
+ }
+
+ // 이름순 정렬
+ _displayItems = _displayItems.OrderBy(d => d.prefabName).ToList();
+ }
+
+ #region Thumbnail Generation
+
+ ///
+ /// 개별 프리펩에 대한 썸네일 생성
+ ///
+ private bool GenerateThumbnailForItem(PrefabDisplayItem item)
+ {
+ if (string.IsNullOrEmpty(item.prefabPath) || item.parentProp == null)
+ {
+ UnityEngine.Debug.LogWarning($"[PropBrowser] {item.prefabName}: 프리펩 경로가 없습니다.");
+ return false;
+ }
+
+ var prefab = AssetDatabase.LoadAssetAtPath(item.prefabPath);
+ if (prefab == null)
+ {
+ UnityEngine.Debug.LogWarning($"[PropBrowser] {item.prefabName}: 프리펩을 로드할 수 없습니다.");
+ return false;
+ }
+
+ // 썸네일 폴더 경로
+ string thumbnailFolderPath = $"{item.parentProp.folderPath}/{THUMBNAIL_FOLDER}";
+ string thumbnailFileName = $"{item.prefabName}_thumbnail.png";
+ string thumbnailPath = $"{thumbnailFolderPath}/{thumbnailFileName}";
+
+ // 폴더 생성
+ if (!AssetDatabase.IsValidFolder(thumbnailFolderPath))
+ {
+ AssetDatabase.CreateFolder(item.parentProp.folderPath, THUMBNAIL_FOLDER);
+ }
+
+ var previewTexture = GeneratePreviewTexture(prefab);
+ if (previewTexture == null)
+ {
+ UnityEngine.Debug.LogWarning($"[PropBrowser] {item.prefabName}: 미리보기를 생성할 수 없습니다.");
+ return false;
+ }
+
+ try
+ {
+ byte[] pngData = previewTexture.EncodeToPNG();
+ string fullPath = thumbnailPath.Replace("Assets/", Application.dataPath + "/");
+
+ string directory = Path.GetDirectoryName(fullPath);
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ File.WriteAllBytes(fullPath, pngData);
+ DestroyImmediate(previewTexture);
+
+ AssetDatabase.ImportAsset(thumbnailPath);
+
+ // item의 썸네일 경로 업데이트
+ item.thumbnailPath = thumbnailPath;
+
+ UnityEngine.Debug.Log($"[PropBrowser] {item.prefabName}: 썸네일 생성 완료");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ UnityEngine.Debug.LogError($"[PropBrowser] {item.prefabName}: 썸네일 저장 실패 - {ex.Message}");
+ return false;
+ }
+ }
+
+ ///
+ /// 모든 프리펩에 대해 개별 썸네일 생성
+ ///
+ private void GenerateAllPrefabThumbnails()
+ {
+ // 모든 프리펩 아이템 수집 (필터 없이)
+ var allItems = new List();
+ foreach (var prop in _database.props)
+ {
+ foreach (var prefabPath in prop.prefabPaths)
+ {
+ allItems.Add(new PrefabDisplayItem
+ {
+ prefabName = Path.GetFileNameWithoutExtension(prefabPath),
+ prefabPath = prefabPath,
+ folderName = prop.propName,
+ parentProp = prop
+ });
+ }
+ }
+
+ if (allItems.Count == 0)
+ {
+ EditorUtility.DisplayDialog("알림", "생성할 프리펩이 없습니다.", "확인");
+ return;
+ }
+
+ if (!EditorUtility.DisplayDialog("확인",
+ $"모든 프리펩 ({allItems.Count}개)의 썸네일을 개별 생성합니다.\n계속하시겠습니까?",
+ "생성", "취소"))
+ {
+ return;
+ }
+
+ GeneratePrefabThumbnails(allItems, "모든 프리펩");
+ }
+
+ ///
+ /// 썸네일이 없는 프리펩만 개별 썸네일 생성
+ ///
+ private void GenerateMissingPrefabThumbnails()
+ {
+ var itemsWithoutThumbnail = new List();
+
+ foreach (var prop in _database.props)
+ {
+ foreach (var prefabPath in prop.prefabPaths)
+ {
+ string prefabName = Path.GetFileNameWithoutExtension(prefabPath);
+ string expectedThumbnailPath = $"{prop.folderPath}/{THUMBNAIL_FOLDER}/{prefabName}_thumbnail.png";
+ string fullPath = expectedThumbnailPath.Replace("Assets/", Application.dataPath + "/");
+
+ if (!File.Exists(fullPath))
+ {
+ itemsWithoutThumbnail.Add(new PrefabDisplayItem
+ {
+ prefabName = prefabName,
+ prefabPath = prefabPath,
+ folderName = prop.propName,
+ parentProp = prop
+ });
+ }
+ }
+ }
+
+ if (itemsWithoutThumbnail.Count == 0)
+ {
+ EditorUtility.DisplayDialog("알림", "모든 프리펩에 썸네일이 있습니다.", "확인");
+ return;
+ }
+
+ GeneratePrefabThumbnails(itemsWithoutThumbnail, $"썸네일 없는 {itemsWithoutThumbnail.Count}개 프리펩");
+ }
+
+ ///
+ /// PrefabDisplayItem 리스트에 대해 썸네일 일괄 생성
+ ///
+ private void GeneratePrefabThumbnails(List items, string description)
+ {
+ int successCount = 0;
+ int failCount = 0;
+
+ try
+ {
+ for (int i = 0; i < items.Count; i++)
+ {
+ var item = items[i];
+
+ EditorUtility.DisplayProgressBar("썸네일 생성 중",
+ $"{item.prefabName} ({i + 1}/{items.Count})",
+ (float)i / items.Count);
+
+ if (GenerateThumbnailForItem(item))
+ {
+ successCount++;
+ }
+ else
+ {
+ failCount++;
+ }
+ }
+ }
+ finally
+ {
+ EditorUtility.ClearProgressBar();
+ }
+
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ ClearThumbnailCache();
+ ApplyFilter(); // displayItems 다시 생성하여 썸네일 경로 반영
+ Repaint();
+
+ EditorUtility.DisplayDialog("완료",
+ $"{description} 썸네일 생성 완료\n성공: {successCount}개\n실패: {failCount}개",
+ "확인");
+ }
+
+ private void GenerateThumbnailsForSelected()
+ {
+ // 현재 선택된 프랍 (Selection에서)
+ var selectedObjects = Selection.objects;
+ var propsToGenerate = new List();
+
+ foreach (var obj in selectedObjects)
+ {
+ string path = AssetDatabase.GetAssetPath(obj);
+ if (string.IsNullOrEmpty(path)) continue;
+
+ var prop = _database.props.Find(p => path.StartsWith(p.folderPath));
+ if (prop != null && !propsToGenerate.Contains(prop))
+ {
+ propsToGenerate.Add(prop);
+ }
+ }
+
+ if (propsToGenerate.Count == 0)
+ {
+ EditorUtility.DisplayDialog("알림", "선택된 프랍이 없습니다.\n프로젝트 창에서 프랍 폴더나 프리펩을 선택해주세요.", "확인");
+ return;
+ }
+
+ GenerateThumbnails(propsToGenerate, $"선택한 {propsToGenerate.Count}개 프랍");
+ }
+
+ private void GenerateAllThumbnails()
+ {
+ if (!EditorUtility.DisplayDialog("확인",
+ $"모든 프랍 ({_database.props.Count}개)의 썸네일을 생성합니다.\n계속하시겠습니까?",
+ "생성", "취소"))
+ {
+ return;
+ }
+
+ GenerateThumbnails(_database.props, "모든 프랍");
+ }
+
+ private void GenerateMissingThumbnails()
+ {
+ var propsWithoutThumbnail = _database.props
+ .Where(p => string.IsNullOrEmpty(p.thumbnailPath) ||
+ !File.Exists(p.thumbnailPath.Replace("Assets/", Application.dataPath + "/")))
+ .ToList();
+
+ if (propsWithoutThumbnail.Count == 0)
+ {
+ EditorUtility.DisplayDialog("알림", "모든 프랍에 썸네일이 있습니다.", "확인");
+ return;
+ }
+
+ GenerateThumbnails(propsWithoutThumbnail, $"썸네일 없는 {propsWithoutThumbnail.Count}개 프랍");
+ }
+
+ private void GenerateThumbnails(List props, string description)
+ {
+ int successCount = 0;
+ int failCount = 0;
+
+ try
+ {
+ for (int i = 0; i < props.Count; i++)
+ {
+ var prop = props[i];
+
+ EditorUtility.DisplayProgressBar("썸네일 생성 중",
+ $"{prop.propName} ({i + 1}/{props.Count})",
+ (float)i / props.Count);
+
+ if (GenerateThumbnail(prop))
+ {
+ successCount++;
+ }
+ else
+ {
+ failCount++;
+ }
+ }
+ }
+ finally
+ {
+ EditorUtility.ClearProgressBar();
+ }
+
+ EditorUtility.SetDirty(_database);
+ AssetDatabase.SaveAssets();
+ AssetDatabase.Refresh();
+
+ ClearThumbnailCache();
+ Repaint();
+
+ EditorUtility.DisplayDialog("완료",
+ $"{description} 썸네일 생성 완료\n성공: {successCount}개\n실패: {failCount}개",
+ "확인");
+ }
+
+ private bool GenerateThumbnail(PropInfo propInfo)
+ {
+ if (propInfo.prefabPaths.Count == 0)
+ {
+ UnityEngine.Debug.LogWarning($"[PropBrowser] {propInfo.propName}: 프리펩이 없어 썸네일을 생성할 수 없습니다.");
+ return false;
+ }
+
+ // 프리펩 로드
+ var prefab = AssetDatabase.LoadAssetAtPath(propInfo.MainPrefabPath);
+ if (prefab == null)
+ {
+ UnityEngine.Debug.LogWarning($"[PropBrowser] {propInfo.propName}: 프리펩을 로드할 수 없습니다.");
+ return false;
+ }
+
+ // 썸네일 폴더 경로
+ string thumbnailFolderPath = $"{propInfo.folderPath}/{THUMBNAIL_FOLDER}";
+ string thumbnailFileName = $"{propInfo.propName}_thumbnail.png";
+ string thumbnailPath = $"{thumbnailFolderPath}/{thumbnailFileName}";
+
+ // 폴더 생성
+ if (!AssetDatabase.IsValidFolder(thumbnailFolderPath))
+ {
+ AssetDatabase.CreateFolder(propInfo.folderPath, THUMBNAIL_FOLDER);
+ }
+
+ // 씬에서 직접 렌더링하여 썸네일 생성 (씬 조명 활용)
+ var previewTexture = GeneratePreviewTexture(prefab);
+
+ if (previewTexture == null)
+ {
+ UnityEngine.Debug.LogWarning($"[PropBrowser] {propInfo.propName}: 미리보기를 생성할 수 없습니다.");
+ return false;
+ }
+
+ // 텍스처 저장
+ try
+ {
+ // PNG로 저장
+ byte[] pngData = previewTexture.EncodeToPNG();
+ string fullPath = thumbnailPath.Replace("Assets/", Application.dataPath + "/");
+
+ string directory = Path.GetDirectoryName(fullPath);
+ if (!Directory.Exists(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ File.WriteAllBytes(fullPath, pngData);
+
+ DestroyImmediate(previewTexture);
+
+ // 에셋 임포트
+ AssetDatabase.ImportAsset(thumbnailPath);
+
+ // PropInfo 업데이트
+ propInfo.thumbnailPath = thumbnailPath;
+
+ UnityEngine.Debug.Log($"[PropBrowser] {propInfo.propName}: 썸네일 생성 완료");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ UnityEngine.Debug.LogError($"[PropBrowser] {propInfo.propName}: 썸네일 저장 실패 - {ex.Message}");
+ return false;
+ }
+ }
+
+ private Texture2D GeneratePreviewTexture(GameObject prefab)
+ {
+ // 씬에서 프리펩을 소환하여 씬 조명을 활용
+ var tempInstance = PrefabUtility.InstantiatePrefab(prefab) as GameObject;
+ if (tempInstance == null)
+ {
+ tempInstance = Instantiate(prefab);
+ }
+
+ // 썸네일 촬영용 임시 위치 설정 (씬 외곽)
+ Vector3 capturePosition = new Vector3(10000f, 10000f, 10000f);
+ tempInstance.transform.position = capturePosition;
+ tempInstance.name = "[ThumbnailCapture_Temp]";
+
+ try
+ {
+ // 바운드 계산 - 모든 렌더러 포함
+ Bounds bounds = CalculateTotalBounds(tempInstance);
+ if (bounds.size == Vector3.zero)
+ {
+ DestroyImmediate(tempInstance);
+ return null;
+ }
+
+ // 카메라 설정
+ var cameraGo = new GameObject("[ThumbnailCamera_Temp]");
+ var camera = cameraGo.AddComponent();
+ camera.backgroundColor = new Color(0.15f, 0.15f, 0.15f, 0f);
+ camera.clearFlags = CameraClearFlags.SolidColor;
+ camera.orthographic = false;
+ camera.fieldOfView = 30f; // 좁은 FOV로 왜곡 최소화
+ camera.nearClipPlane = 0.01f;
+ camera.farClipPlane = 10000f;
+
+ // 카메라 위치 계산 - 바운더리 기반으로 전체가 보이도록
+ PositionCameraToFitBounds(camera, bounds);
+
+ // 렌더 텍스처
+ var rt = new RenderTexture(THUMBNAIL_CAPTURE_SIZE, THUMBNAIL_CAPTURE_SIZE, 24, RenderTextureFormat.ARGB32);
+ rt.antiAliasing = 4; // 안티앨리어싱 추가
+ camera.targetTexture = rt;
+
+ // 씬 조명이 없는 경우를 대비한 보조 조명
+ GameObject backupLightGo = null;
+ var existingLights = FindObjectsByType(FindObjectsSortMode.None);
+ bool hasSceneLight = existingLights.Any(l => l.enabled && l.gameObject.activeInHierarchy && l.type == LightType.Directional);
+
+ if (!hasSceneLight)
+ {
+ // 씬에 디렉셔널 라이트가 없으면 임시 조명 추가
+ backupLightGo = new GameObject("[ThumbnailLight_Temp]");
+ var light = backupLightGo.AddComponent();
+ light.type = LightType.Directional;
+ light.transform.rotation = Quaternion.Euler(50, -30, 0);
+ light.intensity = 1.5f;
+ light.color = new Color(1f, 0.98f, 0.95f); // 약간 따뜻한 색
+
+ // 보조 조명 (필/림 라이트)
+ var fillLightGo = new GameObject("[ThumbnailFillLight_Temp]");
+ var fillLight = fillLightGo.AddComponent();
+ fillLight.type = LightType.Directional;
+ fillLight.transform.rotation = Quaternion.Euler(30, 150, 0);
+ fillLight.intensity = 0.5f;
+ fillLight.color = new Color(0.8f, 0.85f, 1f); // 약간 차가운 색
+ fillLightGo.transform.SetParent(backupLightGo.transform);
+ }
+
+ // 렌더링
+ camera.Render();
+
+ // 텍스처 읽기
+ var texture = new Texture2D(THUMBNAIL_CAPTURE_SIZE, THUMBNAIL_CAPTURE_SIZE, TextureFormat.RGBA32, false);
+ RenderTexture.active = rt;
+ texture.ReadPixels(new Rect(0, 0, THUMBNAIL_CAPTURE_SIZE, THUMBNAIL_CAPTURE_SIZE), 0, 0);
+ texture.Apply();
+ RenderTexture.active = null;
+
+ // 정리
+ if (backupLightGo != null) DestroyImmediate(backupLightGo);
+ DestroyImmediate(cameraGo);
+ DestroyImmediate(rt);
+ DestroyImmediate(tempInstance);
+
+ return texture;
+ }
+ catch (Exception ex)
+ {
+ UnityEngine.Debug.LogError($"[PropBrowser] GeneratePreviewTexture 오류: {ex.Message}");
+ DestroyImmediate(tempInstance);
+ return null;
+ }
+ }
+
+ ///
+ /// 오브젝트의 전체 바운드 계산 (모든 렌더러, 스킨드 메시, 파티클 포함)
+ ///
+ private Bounds CalculateTotalBounds(GameObject obj)
+ {
+ var renderers = obj.GetComponentsInChildren(true);
+ var skinnedMeshRenderers = obj.GetComponentsInChildren(true);
+ var meshFilters = obj.GetComponentsInChildren(true);
+
+ Bounds bounds = new Bounds(obj.transform.position, Vector3.zero);
+ bool hasBounds = false;
+
+ // 렌더러 바운드 수집
+ foreach (var renderer in renderers)
+ {
+ // 파티클 시스템은 제외 (불필요하게 큰 바운드 생성)
+ if (renderer is ParticleSystemRenderer) continue;
+
+ if (!hasBounds)
+ {
+ bounds = renderer.bounds;
+ hasBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(renderer.bounds);
+ }
+ }
+
+ // 바운드가 없으면 MeshFilter에서 직접 계산
+ if (!hasBounds)
+ {
+ foreach (var mf in meshFilters)
+ {
+ if (mf.sharedMesh == null) continue;
+
+ var meshBounds = mf.sharedMesh.bounds;
+ var worldBounds = new Bounds(
+ mf.transform.TransformPoint(meshBounds.center),
+ Vector3.Scale(meshBounds.size, mf.transform.lossyScale)
+ );
+
+ if (!hasBounds)
+ {
+ bounds = worldBounds;
+ hasBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(worldBounds);
+ }
+ }
+ }
+
+ return bounds;
+ }
+
+ ///
+ /// 카메라를 바운더리에 맞게 위치시켜 전체가 보이도록 함
+ ///
+ private void PositionCameraToFitBounds(Camera camera, Bounds bounds)
+ {
+ // 대각선 방향에서 촬영 (앞쪽에서 45도 각도)
+ // Z+ 방향이 앞쪽이므로 Z를 양수로 설정하여 앞에서 촬영
+ Vector3 viewDirection = new Vector3(-0.7f, 0.5f, 1f).normalized;
+
+ // 바운드 크기 계산
+ float boundsSphereRadius = bounds.extents.magnitude;
+
+ // FOV 기반 거리 계산 - 전체가 화면에 들어오도록
+ float fov = camera.fieldOfView * Mathf.Deg2Rad;
+ float distance = boundsSphereRadius / Mathf.Sin(fov / 2f);
+
+ // 약간의 여백 추가 (1.2배)
+ distance *= 1.2f;
+
+ // 카메라 위치 설정
+ camera.transform.position = bounds.center + viewDirection * distance;
+ camera.transform.LookAt(bounds.center);
+
+ // 클리핑 평면 조정
+ camera.nearClipPlane = Mathf.Max(0.01f, distance - boundsSphereRadius * 2f);
+ camera.farClipPlane = distance + boundsSphereRadius * 2f;
+ }
+
+ #endregion
+
+ #region Web Upload
+
+ private void ShowWebUploadMenu()
+ {
+ var menu = new GenericMenu();
+
+ menu.AddItem(new GUIContent("웹사이트에 업로드"), false, () => UploadToWebsite());
+ menu.AddItem(new GUIContent("업로드 설정 열기"), false, () => WebsitePropExporter.ShowWindow());
+
+ menu.ShowAsContext();
+ }
+
+ private void UploadToWebsite()
+ {
+ if (_database == null || _database.props.Count == 0)
+ {
+ EditorUtility.DisplayDialog("오류", "업로드할 프랍이 없습니다.", "확인");
+ return;
+ }
+
+ // WebsitePropExporter 윈도우 열고 바로 업로드
+ var exporter = EditorWindow.GetWindow("프랍 업로드");
+ exporter.UploadToWebsite();
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropBrowserWindow.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropBrowserWindow.cs.meta
new file mode 100644
index 00000000..10b5999f
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropBrowserWindow.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 57624d2ef1d480c48bd4af170df78730
\ No newline at end of file
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropSyncSettings.cs b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropSyncSettings.cs
new file mode 100644
index 00000000..a5496829
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropSyncSettings.cs
@@ -0,0 +1,62 @@
+using System;
+using UnityEngine;
+
+namespace Streamingle.Prop.Editor
+{
+ ///
+ /// 프랍 동기화 설정을 저장하는 ScriptableObject
+ ///
+ [CreateAssetMenu(fileName = "PropSyncSettings", menuName = "Streamingle/Prop Sync Settings")]
+ public class PropSyncSettings : ScriptableObject
+ {
+ [Header("웹사이트 API 설정")]
+ [Tooltip("프랍 API 엔드포인트 URL (예: https://minglestudio.co.kr/api/props)")]
+ public string apiEndpoint = "https://minglestudio.co.kr/api/props";
+
+ [Tooltip("API 인증 키 (선택사항)")]
+ public string apiKey = "";
+
+ [Header("Git 설정 (썸네일 URL용)")]
+ [Tooltip("Git 서버 URL (예: https://kindnick-git.duckdns.org)")]
+ public string gitServerUrl = "https://kindnick-git.duckdns.org";
+
+ [Tooltip("Git 리포지토리 경로 (예: kindnick/Streamingle_URP)")]
+ public string gitRepoPath = "kindnick/Streamingle_URP";
+
+ [Tooltip("Git 브랜치 (예: main)")]
+ public string gitBranch = "main";
+
+ [Header("웹사이트 설정")]
+ [Tooltip("프랍 페이지 URL (브라우저에서 열기용)")]
+ public string websiteUrl = "https://minglestudio.co.kr/props";
+
+ ///
+ /// Git Media 파일 URL 생성 (Gitea 형식)
+ ///
+ public string GetGitRawUrl(string assetPath)
+ {
+ // Assets/ResourcesData/Prop/... 형식의 경로를 Git Media URL로 변환
+ string relativePath = assetPath.Replace("\\", "/");
+
+ // 경로의 각 세그먼트를 URL 인코딩 (슬래시는 유지)
+ string[] segments = relativePath.Split('/');
+ for (int i = 0; i < segments.Length; i++)
+ {
+ segments[i] = Uri.EscapeDataString(segments[i]);
+ }
+ string encodedPath = string.Join("/", segments);
+
+ return $"{gitServerUrl}/{gitRepoPath}/media/branch/{gitBranch}/{encodedPath}";
+ }
+
+ ///
+ /// API 설정이 유효한지 확인
+ ///
+ public bool IsValid()
+ {
+ return !string.IsNullOrEmpty(apiEndpoint) &&
+ !string.IsNullOrEmpty(gitServerUrl) &&
+ !string.IsNullOrEmpty(gitRepoPath);
+ }
+ }
+}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropSyncSettings.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropSyncSettings.cs.meta
new file mode 100644
index 00000000..91c6507a
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/PropSyncSettings.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 98eaf226422c2a746ab5e281d74c76e8
\ No newline at end of file
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/WebsitePropExporter.cs b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/WebsitePropExporter.cs
new file mode 100644
index 00000000..51a878c2
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/WebsitePropExporter.cs
@@ -0,0 +1,409 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.Networking;
+using Newtonsoft.Json;
+
+namespace Streamingle.Prop.Editor
+{
+ ///
+ /// 프랍 데이터를 웹사이트 API로 업로드
+ /// 썸네일은 Git URL을 사용
+ ///
+ public class WebsitePropExporter : EditorWindow
+ {
+ private PropSyncSettings _settings;
+ private PropDatabase _database;
+ private string _statusMessage = "";
+ private MessageType _statusType = MessageType.Info;
+ private bool _isExporting;
+ private UnityWebRequestAsyncOperation _currentRequest;
+
+ private const string SETTINGS_PATH = "Assets/Resources/Settings/PropSyncSettings.asset";
+ private const string DATABASE_PATH = "Assets/Resources/Settings/PropDatabase.asset";
+
+ [MenuItem("Streamingle/Upload Props to Website")]
+ public static void ShowWindow()
+ {
+ var window = GetWindow("프랍 업로드");
+ window.minSize = new Vector2(450, 400);
+ window.Show();
+ }
+
+ private void OnEnable()
+ {
+ LoadSettings();
+ }
+
+ private void LoadSettings()
+ {
+ _settings = AssetDatabase.LoadAssetAtPath(SETTINGS_PATH);
+ _database = AssetDatabase.LoadAssetAtPath(DATABASE_PATH);
+ }
+
+ private void OnGUI()
+ {
+ EditorGUILayout.Space(10);
+ EditorGUILayout.LabelField("프랍 웹사이트 업로드", EditorStyles.boldLabel);
+ EditorGUILayout.Space(10);
+
+ // 설정 확인
+ if (_settings == null)
+ {
+ EditorGUILayout.HelpBox("PropSyncSettings을 찾을 수 없습니다.\nAssets/Resources/Settings/PropSyncSettings.asset 을 생성해주세요.", MessageType.Warning);
+
+ if (GUILayout.Button("설정 파일 생성"))
+ {
+ CreateSettingsAsset();
+ }
+ return;
+ }
+
+ if (_database == null)
+ {
+ EditorGUILayout.HelpBox("PropDatabase를 찾을 수 없습니다.\n프랍 브라우저에서 새로고침을 눌러주세요.", MessageType.Warning);
+ return;
+ }
+
+ // API 설정
+ EditorGUILayout.BeginVertical(EditorStyles.helpBox);
+ EditorGUILayout.LabelField("API 설정", EditorStyles.boldLabel);
+
+ EditorGUI.BeginChangeCheck();
+
+ _settings.apiEndpoint = EditorGUILayout.TextField("API 엔드포인트", _settings.apiEndpoint);
+ _settings.apiKey = EditorGUILayout.TextField("API 키 (선택)", _settings.apiKey);
+
+ EditorGUILayout.Space(5);
+ EditorGUILayout.LabelField("Git 설정 (썸네일 URL용)", EditorStyles.boldLabel);
+ _settings.gitServerUrl = EditorGUILayout.TextField("Git Server URL", _settings.gitServerUrl);
+ _settings.gitRepoPath = EditorGUILayout.TextField("Repo Path", _settings.gitRepoPath);
+ _settings.gitBranch = EditorGUILayout.TextField("Branch", _settings.gitBranch);
+
+ EditorGUILayout.Space(5);
+ _settings.websiteUrl = EditorGUILayout.TextField("웹사이트 URL", _settings.websiteUrl);
+
+ if (EditorGUI.EndChangeCheck())
+ {
+ EditorUtility.SetDirty(_settings);
+ AssetDatabase.SaveAssets();
+ }
+
+ EditorGUILayout.EndVertical();
+
+ // 설정 유효성 검사
+ EditorGUILayout.Space(10);
+
+ if (!_settings.IsValid())
+ {
+ EditorGUILayout.HelpBox("API 엔드포인트와 Git 설정을 입력해주세요.", MessageType.Warning);
+ }
+
+ // 썸네일 URL 예시
+ EditorGUILayout.Space(5);
+ EditorGUILayout.LabelField("썸네일 URL 예시:", EditorStyles.boldLabel);
+ string exampleUrl = _settings.GetGitRawUrl("Assets/ResourcesData/Prop/예시/Thumbnail/예시_thumbnail.png");
+ EditorGUILayout.SelectableLabel(exampleUrl, EditorStyles.miniLabel, GUILayout.Height(20));
+
+ // 데이터베이스 정보
+ EditorGUILayout.Space(10);
+ EditorGUILayout.LabelField($"프랍 수: {_database.props.Count}개", EditorStyles.miniLabel);
+
+ // 업로드 버튼
+ EditorGUILayout.Space(20);
+
+ GUI.enabled = _settings.IsValid() && !_isExporting;
+
+ if (GUILayout.Button("웹사이트에 업로드", GUILayout.Height(35)))
+ {
+ UploadToWebsite();
+ }
+
+ EditorGUILayout.Space(5);
+
+ if (GUILayout.Button("업로드 후 브라우저에서 열기", GUILayout.Height(30)))
+ {
+ UploadToWebsite(() =>
+ {
+ if (!string.IsNullOrEmpty(_settings.websiteUrl))
+ {
+ Application.OpenURL(_settings.websiteUrl);
+ }
+ });
+ }
+
+ EditorGUILayout.Space(5);
+
+ if (GUILayout.Button("API 연결 테스트", GUILayout.Height(25)))
+ {
+ TestApiConnection();
+ }
+
+ GUI.enabled = true;
+
+ // 상태 메시지
+ if (!string.IsNullOrEmpty(_statusMessage))
+ {
+ EditorGUILayout.Space(10);
+ EditorGUILayout.HelpBox(_statusMessage, _statusType);
+ }
+ }
+
+ private void CreateSettingsAsset()
+ {
+ // Settings 폴더 확인
+ if (!AssetDatabase.IsValidFolder("Assets/Resources"))
+ {
+ AssetDatabase.CreateFolder("Assets", "Resources");
+ }
+ if (!AssetDatabase.IsValidFolder("Assets/Resources/Settings"))
+ {
+ AssetDatabase.CreateFolder("Assets/Resources", "Settings");
+ }
+
+ // 새 설정 파일 생성
+ var settings = CreateInstance();
+ AssetDatabase.CreateAsset(settings, SETTINGS_PATH);
+ AssetDatabase.SaveAssets();
+
+ _settings = settings;
+ UnityEngine.Debug.Log("[WebsitePropExporter] PropSyncSettings 생성됨");
+ }
+
+ ///
+ /// 웹사이트에 프랍 데이터 업로드 (개별 프리펩 단위)
+ ///
+ public void UploadToWebsite(Action onSuccess = null)
+ {
+ _isExporting = true;
+ _statusMessage = "업로드 준비 중...";
+ _statusType = MessageType.Info;
+
+ try
+ {
+ // JSON 데이터 생성 - 개별 프리펩 단위로 내보내기
+ var exportData = new WebsitePropData
+ {
+ lastUpdated = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
+ props = new List()
+ };
+
+ foreach (var propInfo in _database.props)
+ {
+ // 폴더에 프리펩이 있으면 각 프리펩을 개별 항목으로 추가
+ if (propInfo.prefabPaths.Count > 0)
+ {
+ foreach (var prefabPath in propInfo.prefabPaths)
+ {
+ string prefabName = System.IO.Path.GetFileNameWithoutExtension(prefabPath);
+
+ // 개별 프리펩 썸네일 경로 찾기
+ string prefabThumbnailPath = $"{propInfo.folderPath}/Thumbnail/{prefabName}_thumbnail.png";
+ string prefabThumbnailFullPath = prefabThumbnailPath.Replace("Assets/", Application.dataPath + "/");
+
+ // 개별 썸네일이 있으면 사용, 없으면 폴더 썸네일 사용
+ string thumbnailPath = System.IO.File.Exists(prefabThumbnailFullPath)
+ ? prefabThumbnailPath
+ : propInfo.thumbnailPath;
+
+ var item = new WebsitePropItem
+ {
+ name = prefabName, // 프리펩 파일 이름 사용
+ folderPath = propInfo.folderPath,
+ folderName = propInfo.propName, // 원래 폴더 이름도 유지
+ prefabPath = prefabPath,
+ prefabCount = 1,
+ modelCount = propInfo.modelPaths.Count,
+ textureCount = propInfo.textureCount,
+ materialCount = propInfo.materialCount,
+ thumbnailUrl = !string.IsNullOrEmpty(thumbnailPath)
+ ? _settings.GetGitRawUrl(thumbnailPath)
+ : null
+ };
+
+ exportData.props.Add(item);
+ }
+ }
+ else
+ {
+ // 프리펩이 없는 경우 폴더명으로 추가
+ var item = new WebsitePropItem
+ {
+ name = propInfo.propName,
+ folderPath = propInfo.folderPath,
+ folderName = propInfo.propName,
+ prefabPath = null,
+ prefabCount = 0,
+ modelCount = propInfo.modelPaths.Count,
+ textureCount = propInfo.textureCount,
+ materialCount = propInfo.materialCount,
+ thumbnailUrl = !string.IsNullOrEmpty(propInfo.thumbnailPath)
+ ? _settings.GetGitRawUrl(propInfo.thumbnailPath)
+ : null
+ };
+
+ exportData.props.Add(item);
+ }
+ }
+
+ // JSON 문자열 생성
+ string json = JsonConvert.SerializeObject(exportData, Formatting.Indented);
+
+ // HTTP POST 요청
+ SendPostRequest(json, onSuccess);
+ }
+ catch (Exception ex)
+ {
+ _statusMessage = $"데이터 준비 실패: {ex.Message}";
+ _statusType = MessageType.Error;
+ _isExporting = false;
+ UnityEngine.Debug.LogError($"[WebsitePropExporter] 데이터 준비 실패: {ex.Message}");
+ }
+ }
+
+ private void SendPostRequest(string jsonData, Action onSuccess)
+ {
+ _statusMessage = "서버에 업로드 중...";
+
+ var request = new UnityWebRequest(_settings.apiEndpoint, "POST");
+ byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonData);
+ request.uploadHandler = new UploadHandlerRaw(bodyRaw);
+ request.downloadHandler = new DownloadHandlerBuffer();
+ request.SetRequestHeader("Content-Type", "application/json");
+
+ if (!string.IsNullOrEmpty(_settings.apiKey))
+ {
+ request.SetRequestHeader("X-API-Key", _settings.apiKey);
+ }
+
+ _currentRequest = request.SendWebRequest();
+ _currentRequest.completed += operation =>
+ {
+ HandleUploadResponse(request, onSuccess);
+ };
+
+ // 진행 상황 업데이트를 위한 에디터 갱신
+ EditorApplication.update += UpdateProgress;
+ }
+
+ private void UpdateProgress()
+ {
+ if (_currentRequest != null && !_currentRequest.isDone)
+ {
+ _statusMessage = $"업로드 중... {(_currentRequest.progress * 100):F0}%";
+ Repaint();
+ }
+ else
+ {
+ EditorApplication.update -= UpdateProgress;
+ }
+ }
+
+ private void HandleUploadResponse(UnityWebRequest request, Action onSuccess)
+ {
+ _isExporting = false;
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ try
+ {
+ var response = JsonConvert.DeserializeObject(request.downloadHandler.text);
+ if (response.success)
+ {
+ _statusMessage = $"업로드 완료!\n{response.message}\n업데이트: {response.lastUpdated}";
+ _statusType = MessageType.Info;
+ UnityEngine.Debug.Log($"[WebsitePropExporter] 업로드 성공: {response.message}");
+ onSuccess?.Invoke();
+ }
+ else
+ {
+ _statusMessage = $"서버 오류: {response.error}";
+ _statusType = MessageType.Error;
+ }
+ }
+ catch (Exception ex)
+ {
+ _statusMessage = $"응답 파싱 실패: {ex.Message}\n응답: {request.downloadHandler.text}";
+ _statusType = MessageType.Error;
+ }
+ }
+ else
+ {
+ _statusMessage = $"업로드 실패!\n{request.error}\n상태코드: {request.responseCode}";
+ _statusType = MessageType.Error;
+ UnityEngine.Debug.LogError($"[WebsitePropExporter] 업로드 실패: {request.error}");
+ }
+
+ request.Dispose();
+ Repaint();
+ }
+
+ private void TestApiConnection()
+ {
+ _statusMessage = "API 연결 테스트 중...";
+ _statusType = MessageType.Info;
+
+ var request = UnityWebRequest.Get(_settings.apiEndpoint);
+ var operation = request.SendWebRequest();
+
+ operation.completed += op =>
+ {
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ _statusMessage = $"API 연결 성공!\n응답: {request.downloadHandler.text.Substring(0, Math.Min(200, request.downloadHandler.text.Length))}...";
+ _statusType = MessageType.Info;
+ }
+ else
+ {
+ _statusMessage = $"API 연결 실패!\n{request.error}\n상태코드: {request.responseCode}";
+ _statusType = MessageType.Error;
+ }
+
+ request.Dispose();
+ Repaint();
+ };
+ }
+ }
+
+ ///
+ /// API 응답 구조
+ ///
+ [Serializable]
+ public class PropApiResponse
+ {
+ public bool success;
+ public string message;
+ public string error;
+ public string lastUpdated;
+ public int count;
+ }
+
+ ///
+ /// 웹사이트용 프랍 데이터 구조
+ ///
+ [Serializable]
+ public class WebsitePropData
+ {
+ public string lastUpdated;
+ public List props;
+ }
+
+ ///
+ /// 웹사이트용 개별 프랍 항목 (프리펩 단위)
+ ///
+ [Serializable]
+ public class WebsitePropItem
+ {
+ public string name; // 프리펩 이름
+ public string folderPath; // 폴더 경로
+ public string folderName; // 원래 폴더 이름
+ public string prefabPath; // 프리펩 경로
+ public int prefabCount;
+ public int modelCount;
+ public int textureCount;
+ public int materialCount;
+ public string thumbnailUrl;
+ }
+}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/WebsitePropExporter.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/WebsitePropExporter.cs.meta
new file mode 100644
index 00000000..3e4fdaa9
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/Editor/WebsitePropExporter.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: b7f64cb7ac7294a41a80919b89f3d527
\ No newline at end of file
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/PropData.cs b/Assets/Scripts/Streamingle/StreamingleControl/Prop/PropData.cs
new file mode 100644
index 00000000..5bdc420c
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/PropData.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Streamingle.Prop
+{
+ ///
+ /// 프랍 정보를 담는 데이터 클래스
+ ///
+ [Serializable]
+ public class PropInfo
+ {
+ public string propName; // 프랍 이름
+ public string folderPath; // 프랍 폴더 경로 (Assets/...)
+ public string thumbnailPath; // 썸네일 이미지 경로
+ public Texture2D thumbnail; // 로드된 썸네일 (런타임용)
+ public List prefabPaths = new List(); // 프리펩 경로들
+ public List modelPaths = new List(); // 모델 파일 경로들
+ public int textureCount; // 텍스처 파일 수
+ public int materialCount; // 머티리얼 파일 수
+
+ public string DisplayName => propName;
+
+ ///
+ /// 대표 프리펩 경로 (첫 번째)
+ ///
+ public string MainPrefabPath => prefabPaths.Count > 0 ? prefabPaths[0] : null;
+ }
+
+ ///
+ /// 프랍 데이터를 저장하는 ScriptableObject
+ ///
+ [CreateAssetMenu(fileName = "PropDatabase", menuName = "Streamingle/Prop Database")]
+ public class PropDatabase : ScriptableObject
+ {
+ public List props = new List();
+
+ ///
+ /// 프랍 이름으로 검색
+ ///
+ public PropInfo FindByName(string propName)
+ {
+ return props.Find(p => p.propName == propName);
+ }
+
+ ///
+ /// 폴더 경로로 검색
+ ///
+ public PropInfo FindByPath(string folderPath)
+ {
+ return props.Find(p => p.folderPath == folderPath);
+ }
+ }
+}
diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Prop/PropData.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Prop/PropData.cs.meta
new file mode 100644
index 00000000..304a7211
--- /dev/null
+++ b/Assets/Scripts/Streamingle/StreamingleControl/Prop/PropData.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: e148686fc073a10478d6beb39b4cd8d8
\ No newline at end of file