AX Agent 도구·스킬 정합성 재구성 및 실행 품질 보강

변경 목적:
- AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다.
- claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다.

핵심 수정사항:
- 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다.
- ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다.
- Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다.
- 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다.
- 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
This commit is contained in:
2026-04-14 17:52:46 +09:00
parent fa33b98f7e
commit 8cb08576d5
200 changed files with 13522 additions and 5764 deletions

View File

@@ -22,8 +22,33 @@ public static class TemplateService
new("corporate", "기업 공식", "🏢", "보수적인 레이아웃, 로고 영역, 페이지 번호 — 사내 공식 보고서"),
new("magazine", "매거진", "📰", "멀티 컬럼, 큰 히어로 헤더, 인용 강조 — 뉴스레터·매거진"),
new("dashboard", "대시보드", "📈", "KPI 카드, 차트 영역, 그리드 레이아웃 — 데이터 대시보드"),
new("seminar", "세미나", "🎓", "다크/라이트 테마 전환, 그라데이션 히어로, 파이프라인 다이어그램 — 기술 세미나·발표 자료"),
new("seminar-toc", "세미나 (사이드 목차)", "📑", "좌측 고정 사이드바 목차 + 세미나 스타일 — 긴 기술 문서·레퍼런스"),
];
/// <summary>테마 전환 + 플로팅 TOC JS 스크립트 (HTML에 삽입용).</summary>
public const string ThemeToggleScript = """
<script>
function axToggleTheme(){
var h=document.documentElement,t=h.getAttribute('data-theme')==='dark'?'light':'dark';
h.setAttribute('data-theme',t);
var b=document.querySelector('.ax-theme-toggle');
if(b) b.innerHTML=t==='dark'?'&#127769;':'&#9728;&#65039;';
try{localStorage.setItem('ax-doc-theme',t);}catch(e){}
}
(function(){
try{var s=localStorage.getItem('ax-doc-theme');
if(s){document.documentElement.setAttribute('data-theme',s);
var b=document.querySelector('.ax-theme-toggle');
if(b) b.innerHTML=s==='dark'?'&#127769;':'&#9728;&#65039;';}}catch(e){}
var fab=document.getElementById('axFabToc');
if(fab){window.addEventListener('scroll',function(){
fab.classList.toggle('visible',window.scrollY>300);
});}
})();
</script>
""";
// ── 커스텀 무드 저장소 ──
private static readonly Dictionary<string, Models.CustomMoodEntry> _customMoods = new(StringComparer.OrdinalIgnoreCase);
@@ -67,6 +92,8 @@ public static class TemplateService
"corporate" => CssCorporate,
"magazine" => CssMagazine,
"dashboard" => CssDashboard,
"seminar" => CssSeminar,
"seminar-toc" => CssSeminarSidebar,
_ => CssModern,
};
return moodCss + "\n" + CssShared;
@@ -100,18 +127,21 @@ public static class TemplateService
.Build();
var bodyHtml = Markdown.ToHtml(markdown, pipeline);
var css = GetCss(moodKey);
var defaultTheme = moodKey is "dark" or "seminar" or "seminar-toc" or "dashboard" ? "dark" : "light";
return $"""
<!DOCTYPE html>
<html lang="ko">
<html lang="ko" data-theme="{defaultTheme}">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>{css}</style>
</head>
<body>
<button class="ax-theme-toggle" onclick="axToggleTheme()" title="테마 전환">&#127769;</button>
<div class="container">
{bodyHtml}
</div>
{ThemeToggleScript}
</body>
</html>
""";
@@ -167,6 +197,8 @@ public static class TemplateService
"corporate" => new("#f3f4f6", "#ffffff", "#1f2937", "#6b7280", "#1e40af", "#e5e7eb"),
"magazine" => new("#f9fafb", "#ffffff", "#111827", "#6b7280", "#dc2626", "#f3f4f6"),
"dashboard" => new("#0f172a", "#1e293b", "#f1f5f9", "#94a3b8", "#3b82f6", "#334155"),
"seminar" => new("#0f1117", "#161822", "#e2e8f0", "#8892a8", "#6C8EEF", "#2a2d3e"),
"seminar-toc" => new("#0f1117", "#161822", "#e2e8f0", "#8892a8", "#6C8EEF", "#2a2d3e"),
_ => new("#f5f5f7", "#ffffff", "#1d1d1f", "#6e6e73", "#0066cc", "#e5e5e7"),
};
}
@@ -595,6 +627,240 @@ public static class TemplateService
""";
#endregion
#region Seminar
private const string CssSeminar = """
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
:root {
--accent: #6C8EEF; --accent2: #A78BFA; --green: #34D399; --amber: #FBBF24;
--red: #F87171; --cyan: #22D3EE; --transition: 0.3s ease;
}
[data-theme="dark"] {
--bg: #0f1117; --bg2: #0b0d13; --surface: #161822; --surface2: #1c1f2e; --surface3: #22253a;
--border: #2a2d3e; --text: #e2e8f0; --text-dim: #8892a8; --text-inv: #1a1a2e;
--hero-grad1: #161822; --hero-grad2: #0f1117; --hero-glow: rgba(108,142,239,0.12);
--shadow: rgba(0,0,0,0.3); --code-bg: rgba(108,142,239,0.1); --code-border: rgba(108,142,239,0.15);
}
[data-theme="light"] {
--bg: #f8fafc; --bg2: #f1f5f9; --surface: #ffffff; --surface2: #f1f5f9; --surface3: #e8ecf2;
--border: #e2e8f0; --text: #1e293b; --text-dim: #64748b; --text-inv: #ffffff;
--hero-grad1: #eef2ff; --hero-grad2: #f8fafc; --hero-glow: rgba(108,142,239,0.08);
--shadow: rgba(0,0,0,0.06); --code-bg: rgba(108,142,239,0.06); --code-border: rgba(108,142,239,0.12);
--accent: #4f6fd9; --accent2: #8b6fc0; --green: #16a34a; --amber: #d97706; --red: #dc2626; --cyan: #0891b2;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { font-family: 'Inter', 'Segoe UI', 'Noto Sans KR', sans-serif;
background: var(--bg); color: var(--text); line-height: 1.75; padding: 0;
-webkit-font-smoothing: antialiased; transition: background var(--transition), color var(--transition); }
.container { max-width: 980px; margin: 0 auto; padding: 0 28px; background: transparent; }
/* ── Hero ── */
.cover-page, .header-bar { position: relative; padding: 60px 40px 48px; text-align: center; overflow: hidden;
background: linear-gradient(180deg, var(--hero-grad1) 0%, var(--hero-grad2) 100%);
border-radius: 0; margin: 0 -28px 32px; transition: background var(--transition); }
.cover-page::before, .header-bar::before { content: ''; position: absolute; top: -120px; left: 50%;
transform: translateX(-50%); width: 600px; height: 600px;
background: radial-gradient(circle, var(--hero-glow) 0%, transparent 70%); pointer-events: none; }
.cover-page h1, .header-bar h1 { font-size: 36px; font-weight: 800; letter-spacing: -0.5px;
background: linear-gradient(135deg, var(--text), var(--accent), var(--accent2));
-webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 8px; color: var(--text); }
.cover-page .cover-subtitle { font-size: 16px; color: var(--text-dim); }
.cover-page .cover-meta, .meta { font-size: 12px; color: var(--text-dim); margin-bottom: 24px; }
.header-bar .meta { margin-bottom: 0; margin-top: 8px; }
/* ── Headings ── */
h1 { font-size: 28px; font-weight: 800; color: var(--text); margin-bottom: 4px; }
h2 { font-size: 20px; font-weight: 700; margin: 40px 0 16px; color: var(--text);
padding-bottom: 12px; border-bottom: 2px solid var(--border);
display: flex; align-items: center; gap: 12px; transition: border-color var(--transition); }
h3 { font-size: 16px; font-weight: 700; color: var(--accent); margin: 28px 0 10px; }
h4 { font-size: 14px; font-weight: 600; color: var(--text); margin: 16px 0 8px;
border-left: 3px solid var(--accent); padding-left: 10px; }
/* ── Text ── */
p { margin: 10px 0; font-size: 14px; }
ul, ol { margin: 8px 0 12px 20px; font-size: 14px; }
li { margin: 4px 0; }
li::marker { color: var(--text-dim); }
strong { font-weight: 700; }
em { color: var(--accent); font-style: normal; font-weight: 600; }
/* ── Code ── */
code { font-family: 'Cascadia Code', 'Fira Code', Consolas, monospace;
background: var(--code-bg); border: 1px solid var(--code-border);
border-radius: 4px; padding: 1px 6px; font-size: 12.5px; color: var(--cyan); }
pre { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 18px; overflow-x: auto; font-size: 12.5px; margin: 14px 0; line-height: 1.55;
transition: all var(--transition); }
pre code { background: transparent; border: none; padding: 0; color: var(--text-dim); }
/* ── Cards ── */
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 20px 24px; margin-bottom: 16px; transition: all var(--transition); }
.card:hover { box-shadow: 0 4px 16px var(--shadow); }
.card-header { font-size: 14px; font-weight: 700; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
/* ── Tables ── */
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13px; }
th { background: var(--surface2); padding: 10px 14px; text-align: left; font-weight: 700;
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim);
border-bottom: 2px solid var(--border); transition: all var(--transition); }
td { padding: 10px 14px; border-bottom: 1px solid var(--border); vertical-align: top;
transition: all var(--transition); }
tr:last-child td { border-bottom: none; }
tr:hover td { background: var(--surface2); }
/* ── Badges ── */
.badge { font-size: 10px; font-weight: 700; padding: 2px 8px; border-radius: 4px;
text-transform: uppercase; letter-spacing: 0.5px; display: inline-block; margin: 2px 4px 2px 0; }
.badge-green { background: rgba(52,211,153,0.15); color: var(--green); }
.badge-amber, .badge-yellow { background: rgba(251,191,36,0.15); color: var(--amber); }
.badge-blue { background: rgba(108,142,239,0.15); color: var(--accent); }
.badge-purple { background: rgba(167,139,250,0.15); color: var(--accent2); }
.badge-red { background: rgba(248,113,113,0.15); color: var(--red); }
.badge-cyan { background: rgba(34,211,238,0.15); color: var(--cyan); }
/* ── Info boxes ── */
.callout { border-radius: 10px; padding: 16px 20px; margin: 16px 0; font-size: 13.5px;
display: flex; gap: 12px; align-items: flex-start; border-left: 4px solid;
transition: all var(--transition); }
.callout::before { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
.callout-info { background: rgba(108,142,239,0.07); border-color: var(--accent); }
.callout-info::before { content: '💡'; }
.callout-tip { background: rgba(52,211,153,0.07); border-color: var(--green); }
.callout-tip::before { content: '✅'; }
.callout-warning { background: rgba(251,191,36,0.07); border-color: var(--amber); }
.callout-warning::before { content: ''; }
.callout-danger { background: rgba(248,113,113,0.07); border-color: var(--red); }
.callout-danger::before { content: '🚨'; }
.callout-note { background: rgba(167,139,250,0.07); border-color: var(--accent2); }
.callout-note::before { content: '📝'; }
/* ── Flow diagram ── */
.flow { display: flex; align-items: center; gap: 0; margin: 20px 0; flex-wrap: wrap; justify-content: center; }
.flow-step { background: var(--surface2); border: 1px solid var(--border); border-radius: 10px;
padding: 12px 18px; text-align: center; min-width: 120px; transition: all var(--transition); }
.flow-step .num { font-size: 10px; font-weight: 700; color: var(--accent); text-transform: uppercase;
letter-spacing: 1px; margin-bottom: 4px; }
.flow-step .label { font-size: 13px; font-weight: 600; }
.flow-step .desc { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
.flow-arrow { color: var(--text-dim); font-size: 18px; margin: 0 6px; flex-shrink: 0; }
/* ── Pipeline ── */
.pipeline { margin: 20px 0; }
.pipeline-stage { display: flex; align-items: flex-start; gap: 16px; padding: 14px 0;
border-left: 2px solid var(--border); margin-left: 14px; padding-left: 24px; position: relative; }
.pipeline-stage::before { content: ''; position: absolute; left: -7px; top: 18px;
width: 12px; height: 12px; border-radius: 50%;
background: var(--surface); border: 2px solid var(--accent); transition: all var(--transition); }
.pipeline-stage:last-child { border-left-color: transparent; }
.pipeline-stage .stage-num { font-size: 10px; font-weight: 700; color: var(--accent);
text-transform: uppercase; letter-spacing: 1px; min-width: 60px; padding-top: 2px; }
.pipeline-stage .stage-body { flex: 1; }
.pipeline-stage .stage-title { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.pipeline-stage .stage-desc { font-size: 12.5px; color: var(--text-dim); }
/* ── Stat cards ── */
.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
padding: 18px 14px; text-align: center; transition: all var(--transition); }
.stat-card .number { font-size: 30px; font-weight: 800; color: var(--accent); }
.stat-card .label { font-size: 11px; color: var(--text-dim); margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
/* ── Grids ── */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; margin: 16px 0; }
.grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; margin: 16px 0; }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin: 16px 0; }
/* ── Profile cards ── */
.profile-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin: 16px 0; }
.profile-card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 18px 20px; transition: all var(--transition); }
.profile-card:hover { box-shadow: 0 4px 16px var(--shadow); }
.profile-card .name { font-size: 14px; font-weight: 700; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
.profile-card .desc { font-size: 12px; color: var(--text-dim); margin-bottom: 10px; }
.profile-card .props { font-size: 12px; }
.profile-card .props dt { color: var(--text-dim); float: left; width: 110px; }
.profile-card .props dd { margin-bottom: 3px; }
/* ── Arch diagram ── */
.arch-diagram { background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
padding: 24px 28px; margin: 20px 0; font-family: 'Cascadia Code', monospace;
font-size: 11.5px; line-height: 1.55; overflow-x: auto; white-space: pre;
color: var(--text-dim); transition: all var(--transition); }
.arch-diagram .hl { color: var(--accent); font-weight: 600; }
.arch-diagram .g { color: var(--green); }
.arch-diagram .a { color: var(--amber); }
.arch-diagram .r { color: var(--red); }
.arch-diagram .p { color: var(--accent2); }
.arch-diagram .c { color: var(--cyan); }
/* ── Tags ── */
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
.tag { display: inline-block; padding: 3px 10px; border-radius: 6px; font-size: 11.5px;
font-weight: 500; background: var(--surface2); border: 1px solid var(--border);
color: var(--text-dim); transition: all var(--transition); }
.tag.accent { border-color: rgba(108,142,239,0.3); color: var(--accent); background: rgba(108,142,239,0.06); }
/* ── Blockquote ── */
blockquote { border-left: 3px solid var(--accent); padding: 12px 20px; margin: 16px 0;
background: rgba(108,142,239,0.05); border-radius: 0 10px 10px 0; font-size: 14px;
color: var(--text-dim); transition: all var(--transition); }
/* ── Separator ── */
.divider, hr { border: none; height: 1px;
background: linear-gradient(90deg, transparent, var(--border), transparent); margin: 40px 0; }
/* ── Responsive ── */
@media (max-width: 720px) {
.grid-2, .profile-grid { grid-template-columns: 1fr; }
.grid-3, .grid-4 { grid-template-columns: 1fr 1fr; }
.flow { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
.cover-page h1, .header-bar h1 { font-size: 24px; }
.arch-diagram { font-size: 10px; padding: 16px; }
}
""";
private const string CssSeminarSidebar = CssSeminar + """
/* ═══════ Sidebar TOC Layout Override ═══════ */
.page-wrapper { display: flex; min-height: 100vh; }
.container { max-width: 980px; padding: 0 28px; margin-left: 280px; margin-right: auto; }
@supports (margin-left: max(0px, 0px)) {
.container { margin-left: max(280px, calc((100vw + 280px - 980px) / 2)); }
}
.toc {
position: fixed; top: 0; left: 0; width: 280px; height: 100vh;
overflow-y: auto; background: var(--surface); border-right: 1px solid var(--border);
padding: 24px 16px; z-index: 100; box-shadow: 2px 0 16px var(--shadow);
transition: all var(--transition);
}
.toc h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 2px;
color: var(--text-dim); margin-bottom: 14px; font-weight: 700; padding: 0 8px; }
.toc-grid { display: flex; flex-direction: column; gap: 2px; }
.toc a { display: flex; align-items: center; gap: 8px; text-decoration: none; color: var(--text);
padding: 6px 8px; border-radius: 6px; font-size: 12.5px; transition: all 0.15s; }
.toc a:hover { background: var(--surface2); color: var(--accent); }
.toc a.active { background: rgba(108,142,239,0.1); color: var(--accent); font-weight: 600; }
.toc-num { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center;
border-radius: 5px; font-size: 10px; font-weight: 700;
background: rgba(108,142,239,0.1); color: var(--accent); flex-shrink: 0; }
.toc-sub { padding-left: 28px; font-size: 11.5px; color: var(--text-dim); }
.toc-sub:hover { color: var(--accent); }
.toc-divider { height: 1px; background: var(--border); margin: 6px 0; }
.toc::-webkit-scrollbar { width: 4px; }
.toc::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
@media (max-width: 960px) {
.toc { position: static; width: 100%; height: auto; max-height: none;
border-right: none; border-bottom: 1px solid var(--border);
box-shadow: 0 4px 16px var(--shadow); }
.container { margin-left: 0; }
.page-wrapper { flex-direction: column; }
}
""";
#endregion
// ════════════════════════════════════════════════════════════════════
// 공통 CSS 컴포넌트 (모든 무드에 자동 첨부)
// ════════════════════════════════════════════════════════════════════
@@ -602,6 +868,66 @@ public static class TemplateService
#region Shared
private const string CssShared = """
/* ── 범용 다크 모드 (CSS 변수 미사용 무드용) ── */
[data-theme="dark"] body { background: #0f172a; color: #e2e8f0; }
[data-theme="dark"] .container { background: #1e293b; color: #e2e8f0; border-color: #334155; }
[data-theme="dark"] h1, [data-theme="dark"] h2, [data-theme="dark"] h3 { color: #e2e8f0; }
[data-theme="dark"] p, [data-theme="dark"] li { color: #cbd5e1; }
[data-theme="dark"] th { background: #334155; color: #e2e8f0; border-color: #475569; }
[data-theme="dark"] td { border-color: #334155; color: #cbd5e1; }
[data-theme="dark"] tr:hover td { background: #1e293b; }
[data-theme="dark"] tr:nth-child(even) td { background: #1e293b; }
[data-theme="dark"] code { background: rgba(99,102,241,0.15); color: #a5b4fc; border-color: rgba(99,102,241,0.2); }
[data-theme="dark"] pre { background: #0f172a; color: #e2e8f0; }
[data-theme="dark"] blockquote { background: rgba(99,102,241,0.08); color: #cbd5e1; }
[data-theme="dark"] .card { background: #1e293b; border-color: #334155; color: #e2e8f0; }
[data-theme="dark"] nav.toc { background: #1e293b; border-color: #334155; }
[data-theme="dark"] nav.toc a { color: #818cf8; }
[data-theme="dark"] .callout { background: rgba(99,102,241,0.08); border-color: #6366f1; color: #cbd5e1; }
[data-theme="dark"] .callout-info { background: rgba(59,130,246,0.1); border-color: #3b82f6; }
[data-theme="dark"] .callout-warning { background: rgba(245,158,11,0.1); border-color: #f59e0b; }
[data-theme="dark"] .callout-tip { background: rgba(34,197,94,0.1); border-color: #22c55e; }
[data-theme="dark"] .callout-danger { background: rgba(239,68,68,0.1); border-color: #ef4444; }
[data-theme="dark"] .highlight, [data-theme="dark"] .highlight-box { background: rgba(99,102,241,0.1); }
[data-theme="dark"] .cover-page { background: linear-gradient(135deg, #312e81 0%, #4c1d95 100%); }
[data-theme="dark"] .header-bar { border-color: #334155; }
[data-theme="dark"] .meta { color: #94a3b8; }
[data-theme="dark"] .chart-bar .bar-track { background: #334155; }
[data-theme="dark"] .divider, [data-theme="dark"] .divider-thick { border-color: #334155; }
[data-theme="dark"] hr { background: #334155; }
[data-theme="dark"] .kpi-card, [data-theme="dark"] .chart-area { background: #1e293b; border-color: #334155; color: #e2e8f0; }
[data-theme="dark"] .kpi-card .kpi-value { color: #e2e8f0; }
[data-theme="dark"] .kpi-card .kpi-label { color: #94a3b8; }
[data-theme="dark"] .badge-blue { background: rgba(59,130,246,0.15); color: #60a5fa; }
[data-theme="dark"] .badge-green { background: rgba(34,197,94,0.15); color: #4ade80; }
[data-theme="dark"] .badge-red { background: rgba(239,68,68,0.15); color: #f87171; }
[data-theme="dark"] .badge-yellow { background: rgba(245,158,11,0.15); color: #fbbf24; }
[data-theme="dark"] .badge-purple { background: rgba(139,92,246,0.15); color: #a78bfa; }
[data-theme="dark"] .badge-gray { background: rgba(107,114,128,0.15); color: #9ca3af; }
[data-theme="light"] body { } /* light 기본값 — 각 무드 CSS가 우선 */
/* ── 테마 토글 버튼 ── */
.ax-theme-toggle { position: fixed; top: 16px; right: 16px; z-index: 1000;
width: 40px; height: 40px; border-radius: 50%; border: 1px solid #d1d5db;
background: #fff; color: #374151; font-size: 18px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s ease; }
.ax-theme-toggle:hover { transform: scale(1.1); box-shadow: 0 4px 16px rgba(0,0,0,0.15); }
[data-theme="dark"] .ax-theme-toggle { background: #1e293b; color: #e2e8f0; border-color: #334155; }
/* ── 플로팅 TOC 버튼 ── */
.ax-fab-toc { position: fixed; bottom: 24px; right: 24px; z-index: 999;
width: 44px; height: 44px; border-radius: 12px; border: 1px solid #d1d5db;
background: #fff; color: #4b5efc; font-size: 20px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
box-shadow: 0 4px 16px rgba(0,0,0,0.1); transition: all 0.3s ease;
opacity: 0; pointer-events: none; transform: translateY(12px); }
.ax-fab-toc.visible { opacity: 1; pointer-events: auto; transform: translateY(0); }
.ax-fab-toc:hover { background: #4b5efc; color: #fff; transform: translateY(-2px);
box-shadow: 0 6px 24px rgba(75,94,252,0.3); }
[data-theme="dark"] .ax-fab-toc { background: #1e293b; border-color: #334155; }
[data-theme="dark"] .ax-fab-toc:hover { background: #4b5efc; }
/* ── 목차 (TOC) ── */
nav.toc { background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 10px;
padding: 20px 28px; margin: 24px 0 32px; }
@@ -718,6 +1044,7 @@ public static class TemplateService
/* ── 인쇄/PDF 최적화 ── */
@media print {
.ax-theme-toggle, .ax-fab-toc { display: none !important; }
body { background: #fff !important; padding: 0 !important; }
.container { box-shadow: none !important; border: none !important;
max-width: none !important; padding: 20px !important; }