diff --git a/Assets/Data/UI/ui_atlas.csv b/Assets/Data/UI/ui_atlas.csv
new file mode 100644
index 00000000..7a63f625
--- /dev/null
+++ b/Assets/Data/UI/ui_atlas.csv
@@ -0,0 +1,6 @@
+kind,name,match,value,note
+theme,UI_Content,/gear/|/chest/|/logo_skill/|/gimmick/|ItemIcon|Icon_ShopItem,2048,화면별 콘텐츠 아이콘(장비·상자·스킬·기믹 타일·상점 상품) — 그 화면에서만 쓰임
+theme,UI_Common,*,4096,모든 화면 공통 크롬(틀·버튼·바·탭·재화·병과·작은 아이콘) — 위 줄에 안 걸린 테마 스프라이트 전부
+folder,WorldMap_UI,Assets/Resources/ChapterMap/UI,2048,월드맵 부품(깃발·띠·판·별·커서) — 폴더 전체
+option,single_area,,262144,이 넓이(px) 넘는 큰 그림(로비 바탕·키아트·로고·패턴)은 아틀라스에 넣지 않음 — 한 장 = 드로우콜 1 · 아틀라스 공간 낭비 방지
+option,padding,,4,스프라이트 간격(px)
diff --git a/Assets/Data/UI/ui_atlas.csv.meta b/Assets/Data/UI/ui_atlas.csv.meta
new file mode 100644
index 00000000..b7b5cd49
--- /dev/null
+++ b/Assets/Data/UI/ui_atlas.csv.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: dbc9f67710bed05499c84db6137a1454
+TextScriptImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/Editor/UIAtlasBuilder.cs b/Assets/Scripts/Editor/UIAtlasBuilder.cs
new file mode 100644
index 00000000..e0991bc0
--- /dev/null
+++ b/Assets/Scripts/Editor/UIAtlasBuilder.cs
@@ -0,0 +1,215 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using UnityEditor;
+using UnityEditor.U2D;
+using UnityEngine;
+using UnityEngine.U2D;
+
+namespace TKS.EditorTools
+{
+ ///
+ /// UI 스프라이트 아틀라스(PD 2026-11-10 「드로우콜 최소화 구조」) — 구성 SOT = Assets/Data/UI/ui_atlas.csv → Assets/UI/Atlases/<이름>.spriteatlasv2.
+ /// theme 줄 = UI 테마(UITheme.asset)가 쓰는 스프라이트를 match(경로 포함 · | = 또는 · * = 나머지 전부)로 나눔 · folder 줄 = 폴더 전체 ·
+ /// option single_area = 그보다 넓은 그림은 묶지 않음 · padding.
+ /// UI 테마 임포트가 끝날 때마다 자동으로 다시 맞춘다(새 납품 스프라이트 = 코드 변경 없이 아틀라스에 들어감) · 구성이 같으면 파일을 건드리지 않는다.
+ /// 패커 = Sprite Atlas V2(에디터 Play 에서도 묶음 · 프로젝트 설정 EditorSettings).
+ ///
+ public static class UIAtlasBuilder
+ {
+ public const string CsvPath = "Assets/Data/UI/ui_atlas.csv";
+ public const string OutDir = "Assets/UI/Atlases";
+
+ class Group
+ {
+ public string kind;
+ public string name;
+ public string[] match;
+ public int maxSize;
+ public readonly List paths = new List();
+ }
+
+ [MenuItem("TKS/Build UI Atlases (UI 아틀라스 갱신)")]
+ public static void Menu()
+ {
+ Build(AssetDatabase.LoadAssetAtPath(UIThemeImporter.AssetPath), true);
+ }
+
+ /// 표대로 아틀라스를 맞춘다 · 반환 = 오류 없이 끝났는가(오류면 아틀라스 미변경).
+ public static bool Build(UITheme theme, bool log = false)
+ {
+ if (!File.Exists(CsvPath))
+ {
+ Debug.LogError("[TKS] UI 아틀라스 표 없음: " + CsvPath);
+ return false;
+ }
+ var groups = new List();
+ long singleArea = long.MaxValue;
+ int padding = 4;
+ var errors = new List();
+ var lines = File.ReadAllText(CsvPath, Encoding.UTF8).TrimStart('').Replace("\r", "").Split('\n');
+ for (int n = 1; n < lines.Length; n++)
+ {
+ if (string.IsNullOrWhiteSpace(lines[n])) continue;
+ var f = lines[n].Split(',');
+ if (f.Length < 4)
+ {
+ errors.Add((n + 1) + "행: 열 부족");
+ continue;
+ }
+ string kind = f[0].Trim(), name = f[1].Trim(), match = f[2].Trim(), value = f[3].Trim();
+ int v;
+ switch (kind)
+ {
+ case "theme":
+ case "folder":
+ if (!int.TryParse(value, out v) || v < 128) errors.Add((n + 1) + "행 " + name + ": 최대 크기 " + value);
+ groups.Add(new Group { kind = kind, name = name, match = match.Split('|'), maxSize = v });
+ break;
+ case "option":
+ if (!int.TryParse(value, out v)) errors.Add((n + 1) + "행 " + name + ": 값 " + value);
+ else if (name == "single_area") singleArea = v;
+ else if (name == "padding") padding = v;
+ else errors.Add((n + 1) + "행: 모르는 옵션 " + name);
+ break;
+ default:
+ errors.Add((n + 1) + "행: 모르는 종류 " + kind);
+ break;
+ }
+ }
+
+ // theme 스프라이트 → 첫 번째로 맞는 theme 줄(* = 나머지)
+ var singles = new List();
+ foreach (var path in ThemeTextures(theme))
+ {
+ var tex = AssetDatabase.LoadAssetAtPath(path);
+ if (tex == null) continue;
+ if ((long)tex.width * tex.height > singleArea)
+ {
+ singles.Add(path);
+ continue;
+ }
+ Group hit = null;
+ foreach (var g in groups)
+ if (g.kind == "theme" && g.match.Any(m => m != "*" && path.Contains(m)))
+ {
+ hit = g;
+ break;
+ }
+ if (hit == null) hit = groups.FirstOrDefault(g => g.kind == "theme" && g.match.Contains("*"));
+ if (hit != null) hit.paths.Add(path);
+ }
+ foreach (var g in groups)
+ if (g.kind == "folder")
+ {
+ if (!AssetDatabase.IsValidFolder(g.match[0])) errors.Add(g.name + ": 폴더 없음 " + g.match[0]);
+ else g.paths.Add(g.match[0]);
+ }
+ if (errors.Count > 0)
+ {
+ Debug.LogError("[TKS] UI 아틀라스 표 오류 " + errors.Count + "건(아틀라스 미변경):\n" + string.Join("\n", errors));
+ return false;
+ }
+
+ // 패커 = Sprite Atlas V2(에디터 Play 에서도 묶음)
+ if (EditorSettings.spritePackerMode != SpritePackerMode.SpriteAtlasV2)
+ {
+ EditorSettings.spritePackerMode = SpritePackerMode.SpriteAtlasV2;
+ AssetDatabase.SaveAssets();
+ }
+ if (!AssetDatabase.IsValidFolder(OutDir)) AssetDatabase.CreateFolder(Path.GetDirectoryName(OutDir).Replace('\\', '/'), Path.GetFileName(OutDir));
+
+ int changed = 0;
+ var report = new StringBuilder();
+ foreach (var g in groups)
+ {
+ g.paths.Sort(System.StringComparer.Ordinal);
+ if (Write(g, padding)) changed++;
+ report.Append(" · ").Append(g.name).Append(' ').Append(g.paths.Count);
+ }
+ if (changed > 0) SpriteAtlasUtility.PackAllAtlases(EditorUserBuildSettings.activeBuildTarget);
+ if (log || changed > 0)
+ Debug.Log("[TKS] UI 아틀라스" + report + " · 큰 그림(묶지 않음) " + singles.Count + " · 갱신 " + changed + "장");
+ return true;
+ }
+
+ /// 아틀라스 1장을 표 구성대로 · 구성(목록·크기·간격) 서명이 같으면 건드리지 않는다.
+ static bool Write(Group g, int padding)
+ {
+ string path = OutDir + "/" + g.name + ".spriteatlasv2";
+ string sig = Hash128.Compute(g.maxSize + ";" + padding + ";" + string.Join("|", g.paths.Select(AssetDatabase.AssetPathToGUID))).ToString();
+ var imp = AssetImporter.GetAtPath(path) as SpriteAtlasImporter;
+ if (imp != null && imp.userData == sig) return false;
+
+ var asset = new SpriteAtlasAsset();
+ var objs = new List