/* ══════════════════════════════════════════════════
   F-4: PWA Service Worker 등록
══════════════════════════════════════════════════ */
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/static/sw.js', { scope: '/' })
      .then(reg => {
        console.log('[GUARDiA PWA] SW 등록 성공:', reg.scope);
        // 새 버전 감지 시 사용자에게 알림
        reg.addEventListener('updatefound', () => {
          const newSW = reg.installing;
          newSW.addEventListener('statechange', () => {
            if (newSW.state === 'installed' && navigator.serviceWorker.controller) {
              console.log('[GUARDiA PWA] 새 버전 사용 가능 — 새로고침하면 업데이트됩니다.');
            }
          });
        });
      })
      .catch(err => console.warn('[GUARDiA PWA] SW 등록 실패:', err));
  });
}

/* ══════════════════════════════════════════════════
   Nifty 사이드바 계층 메뉴 토글
══════════════════════════════════════════════════ */
function toggleNavGroup(header) {
  const body = header.nextElementSibling;
  const isOpen = body.classList.contains('open');
  body.classList.toggle('open', !isOpen);
  header.setAttribute('aria-expanded', String(!isOpen));
}

// 현재 URL에 해당하는 메뉴 자동 열기
(function autoOpenNavGroup() {
  document.querySelectorAll('.nav-group-body .nav-sub-item').forEach(item => {
    const href = item.getAttribute('href') || '';
    if (href && location.pathname.startsWith(href.split('?')[0])) {
      const body   = item.closest('.nav-group-body');
      const header = body?.previousElementSibling;
      if (body && header) {
        body.classList.add('open');
        header.setAttribute('aria-expanded', 'true');
        item.classList.add('active');
      }
    }
  });
})();

/* ══════════════════════════════════════════════════
   테마 관리  (스크립트 최상단 — FOUC 방지)
══════════════════════════════════════════════════ */
function applyTheme(key) {
  document.body.dataset.theme = key;
  localStorage.setItem("guardia_theme", key);
  document.querySelectorAll(".theme-swatch").forEach(b =>
    b.classList.toggle("active", b.dataset.theme === key)
  );
}

/* ─── Auth ───────────────────────────────────────── */
const _token    = localStorage.getItem("guardia_token");
const _userInfo = JSON.parse(localStorage.getItem("guardia_userinfo") || "{}");

// 로그인 안 된 경우 → /login으로
if (!_token) {
  window.location.replace("/login");
}
// 비밀번호 변경 필요 → /change-password로
if (_userInfo.must_change_pw) {
  window.location.replace("/change-password");
}

/**
 * Bearer 토큰을 자동으로 포함하는 fetch 래퍼.
 * 401 응답 시 로그인 페이지로 리다이렉트.
 */
async function authFetch(url, opts = {}) {
  const headers = { "Content-Type": "application/json", ...(opts.headers || {}) };
  if (_token) headers["Authorization"] = `Bearer ${_token}`;
  const res = await fetch(url, { ...opts, headers });
  if (res.status === 401) {
    localStorage.removeItem("guardia_token");
    localStorage.removeItem("guardia_userinfo");
    window.location.replace("/login");
    throw new Error("Unauthorized");
  }
  return res;
}

/** 로그아웃 */
function logout() {
  localStorage.removeItem("guardia_token");
  localStorage.removeItem("guardia_userinfo");
  window.location.replace("/login");
}

/* ─── State ─────────────────────────────────────── */
let currentView    = "dashboard";
let srCache        = [];
let statsCache     = {};
let workloadCache  = [];
let dashCache      = {};   // 역할별 대시보드 데이터

/* ─── Status labels ─────────────────────────────── */
const STATUS_LABEL = {
  RECEIVED:              "접수",
  PARSED:                "파싱 완료",
  PENDING_APPROVAL:      "승인 대기",
  APPROVED:              "승인됨",
  IN_PROGRESS:           "진행 중",
  PENDING_PM_VALIDATION: "PM 검증 대기",
  COMPLETED:             "완료",
  FAILED_ROLLBACK:       "롤백 실패",
  REJECTED:              "반려",
};

const TYPE_LABEL = {
  DEPLOY: "배포", RESTART: "재기동", LOG: "로그",
  INQUIRY: "문의", OTHER: "기타",
};

const PRIORITY_LABEL = { CRITICAL: "긴급", HIGH: "높음", MEDIUM: "보통", LOW: "낮음" };

const KANBAN_COLS = [
  { key: "RECEIVED",              label: "접수" },
  { key: "PENDING_APPROVAL",      label: "승인 대기" },
  { key: "APPROVED",              label: "승인됨" },
  { key: "IN_PROGRESS",           label: "진행 중" },
  { key: "PENDING_PM_VALIDATION", label: "PM 검증" },
  { key: "COMPLETED",             label: "완료" },
  { key: "FAILED_ROLLBACK",       label: "롤백 실패" },
  { key: "REJECTED",              label: "반려" },
];

const STATUS_COLORS = {
  RECEIVED:              "#8b949e",
  PARSED:                "#79c0ff",
  PENDING_APPROVAL:      "#e3b341",
  APPROVED:              "#56d364",
  IN_PROGRESS:           "#58a6ff",
  PENDING_PM_VALIDATION: "#bc8cff",
  COMPLETED:             "#3fb950",
  FAILED_ROLLBACK:       "#f85149",
  REJECTED:              "#da3633",
};

/* ─── Init ──────────────────────────────────────── */
window.addEventListener("DOMContentLoaded", async () => {
  // 사용자 정보 표시
  const roleLabel = { ADMIN:"관리자", ENGINEER:"엔지니어", PM:"PM", CUSTOMER:"고객" };
  const roleColor = {
    ADMIN:"#818cf8", ENGINEER:"#34d399", PM:"#fbbf24", CUSTOMER:"#a78bfa"
  };
  const role = _userInfo.role || "";
  document.getElementById("user-display-name").textContent =
    _userInfo.display_name || _userInfo.username || "사용자";
  const roleBadge = document.getElementById("user-role-badge");
  roleBadge.textContent           = roleLabel[role] || role;
  roleBadge.style.background      = (roleColor[role] || "#64748b") + "22";
  roleBadge.style.color           = roleColor[role] || "#64748b";

  // 테마 버튼 active 상태 동기화
  applyTheme(document.body.dataset.theme || "dark");

  setupNav();
  setupNewSR();
  setupListFilters();
  initChat();
  await loadAll();
  initSSE();          // 실시간 이벤트 연결
  startPoll();        // 30초 폴백 폴링
});

async function loadAll() {
  await Promise.all([loadDashboardMe(), loadSRs(), loadWorkload()]);
  renderCurrentView();
}

/* ══════════════════════════════════════════════════
   SSE 실시간 연결
══════════════════════════════════════════════════ */
let _sseSource = null;
let _sseRetry  = 0;
let _refreshPending = false;
let _pollTimer = null;

function initSSE() {
  if (!_token) return;
  if (_sseSource) { _sseSource.close(); _sseSource = null; }

  const url = `/api/dashboard/events?token=${encodeURIComponent(_token)}`;
  _sseSource = new EventSource(url);

  _sseSource.onopen = () => {
    _sseRetry = 0;
    setSseDot(true);
  };

  _sseSource.onmessage = (e) => {
    if (!e.data || e.data.trim() === "") return;
    try { handleSSEEvent(JSON.parse(e.data)); } catch {}
  };

  _sseSource.onerror = () => {
    setSseDot(false);
    _sseSource.close();
    _sseSource = null;
    _sseRetry++;
    const delay = Math.min(3000 * _sseRetry, 60000);
    setTimeout(initSSE, delay);
  };
}

function setSseDot(connected) {
  const dot = document.getElementById("sse-dot");
  const lbl = document.getElementById("sse-label");
  if (!dot) return;
  dot.className = `sse-dot ${connected ? "green" : "red"}`;
  if (lbl) lbl.textContent = connected ? "실시간 연결" : "재연결 중…";
}

async function handleSSEEvent(evt) {
  switch (evt.type) {
    case "sr_created":
      showToast(`🆕 새 SR 접수: ${evt.title || evt.sr_id}`, "info");
      debouncedRefresh();
      break;
    case "sr_updated":
      showToast(`📋 ${evt.sr_id} → ${STATUS_LABEL[evt.new_status] || evt.new_status}`, "info");
      debouncedRefresh();
      break;
    case "approval_done": {
      const icon = evt.result === "APPROVED" ? "✅" : "❌";
      showToast(`${icon} ${evt.sr_id} 결재 완료 (${evt.approver})`,
                evt.result === "APPROVED" ? "success" : "warning");
      debouncedRefresh();
      break;
    }
    case "stats":
      await loadDashboardMe();
      if (currentView === "dashboard") renderDashboard();
      break;
  }
}

function debouncedRefresh() {
  if (_refreshPending) return;
  _refreshPending = true;
  setTimeout(async () => {
    await loadAll();
    if (["ADMIN","PM"].includes(_userInfo.role)) loadTrend();
    _refreshPending = false;
    showRefreshIndicator();
  }, 600);
}

function showRefreshIndicator() {
  const el = document.getElementById("topbar-refresh-indicator");
  if (!el) return;
  el.classList.remove("hidden");
  setTimeout(() => el.classList.add("hidden"), 2500);
}

/* 폴백 폴링 (SSE 장애 대비, 30초) */
function startPoll() {
  if (_pollTimer) clearInterval(_pollTimer);
  _pollTimer = setInterval(async () => {
    if (_sseSource && _sseSource.readyState === EventSource.OPEN) return;
    await loadDashboardMe();
    if (currentView === "dashboard") renderDashboard();
  }, 30000);
}

/* ══════════════════════════════════════════════════
   토스트 알림
══════════════════════════════════════════════════ */
function showToast(msg, type = "info") {
  const el = document.createElement("div");
  el.className = `toast toast-${type}`;
  el.textContent = msg;
  document.body.appendChild(el);
  requestAnimationFrame(() => {
    requestAnimationFrame(() => el.classList.add("show"));
  });
  setTimeout(() => {
    el.classList.remove("show");
    setTimeout(() => el.remove(), 350);
  }, 3500);
}

async function loadDashboardMe() {
  try {
    const r = await authFetch("/api/dashboard/me");
    dashCache = await r.json();
    // stats도 함께 채워두기 (호환성)
    if (dashCache.kpi) {
      statsCache = {
        total: dashCache.kpi.total,
        by_status: {},
        by_type: {},
      };
    }
  } catch { dashCache = {}; }
}

/* ─── Nav ───────────────────────────────────────── */
function setupNav() {
  document.querySelectorAll(".nav-item").forEach(el => {
    el.addEventListener("click", () => {
      const view = el.dataset.view;
      switchView(view);
    });
  });
}

function switchView(view) {
  currentView = view;
  document.querySelectorAll(".nav-item").forEach(el =>
    el.classList.toggle("active", el.dataset.view === view)
  );
  document.querySelectorAll(".view").forEach(el =>
    el.classList.toggle("active", el.id === `view-${view}`)
  );
  const titles = {
    dashboard: "대시보드", board: "칸반 보드",
    list: "SR 목록", audit: "감사 로그", cmdb: "CMDB",
    kb: "기술 문서 KB",
    institutions: "기관 관리", scripts: "스크립트 관리",
    timetable: "작업 타임테이블",
  };
  document.getElementById("page-title").textContent = titles[view] || view;
  renderCurrentView();
}

function renderCurrentView() {
  if (currentView === "dashboard")    renderDashboard();
  else if (currentView === "board")   renderKanban();
  else if (currentView === "list")    renderList();
  else if (currentView === "audit")   loadAudit();
  else if (currentView === "cmdb")    loadCmdb();
  else if (currentView === "kb")      loadKBView();
  else if (currentView === "institutions") loadInstitutions();
  else if (currentView === "scripts")     loadScripts();
  else if (currentView === "timetable")   loadTimetable();
}

/* ─── Data loading ──────────────────────────────── */
async function loadWorkload() {
  try {
    const r = await authFetch("/api/assign/workload");
    workloadCache = await r.json();
    renderWorkload();
  } catch { workloadCache = []; }
}

async function loadStats() {
  try {
    const r = await authFetch("/api/tasks/stats");
    statsCache = await r.json();
  } catch { statsCache = {}; }
}

async function loadSRs(params = {}) {
  const qs = new URLSearchParams(params).toString();
  const r  = await authFetch(`/api/tasks?limit=200${qs ? "&" + qs : ""}`);
  srCache  = await r.json();
}

/* ══════════════════════════════════════════════════
   7일 추이 차트  (순수 SVG)
══════════════════════════════════════════════════ */
async function loadTrend() {
  try {
    const r    = await authFetch("/api/dashboard/stats/trend");
    const data = await r.json();
    renderTrendChart(data);
    const lbl = document.getElementById("trend-last-updated");
    if (lbl) lbl.textContent = new Date().toLocaleTimeString("ko-KR", { hour:"2-digit", minute:"2-digit" }) + " 기준";
  } catch {}
}

function renderTrendChart(days) {
  const card = document.getElementById("trend-chart-card");
  const el   = document.getElementById("trend-chart");
  if (!card || !el || !days?.length) return;
  card.style.display = "";

  const W   = el.clientWidth || 520;
  const H   = 130;
  const pad = { t: 16, r: 16, b: 32, l: 36 };
  const cw  = W - pad.l - pad.r;
  const ch  = H - pad.t - pad.b;

  const maxVal = Math.max(...days.flatMap(d => [d.created, d.completed]), 1);
  const step   = cw / days.length;
  const bw     = Math.floor(step * 0.32);

  // Y 격자선 + 레이블
  const yLines = [0, .5, 1].map(f => {
    const y = pad.t + ch * (1 - f);
    const v = Math.round(maxVal * f);
    return `<line x1="${pad.l}" y1="${y}" x2="${pad.l + cw}" y2="${y}" stroke="rgba(255,255,255,.05)" stroke-width="1"/>
            <text x="${pad.l - 4}" y="${y + 4}" fill="#64748b" font-size="9" text-anchor="end">${v}</text>`;
  }).join("");

  // 막대 + X 레이블
  const bars = days.map((d, i) => {
    const cx = pad.l + i * step + step * 0.5;
    const x1 = cx - bw - 1;
    const x2 = cx + 1;
    const h1 = Math.max(d.created   / maxVal * ch, d.created   ? 2 : 0);
    const h2 = Math.max(d.completed / maxVal * ch, d.completed ? 2 : 0);
    const y1 = pad.t + ch - h1;
    const y2 = pad.t + ch - h2;
    return `
      <rect x="${x1}" y="${y1}" width="${bw}" height="${h1}" fill="#818cf8" opacity=".85" rx="3"/>
      <rect x="${x2}" y="${y2}" width="${bw}" height="${h2}" fill="#34d399" opacity=".85" rx="3"/>
      <text x="${cx}" y="${H - 4}" fill="#64748b" font-size="9" text-anchor="middle">${d.label}</text>`;
  }).join("");

  el.innerHTML = `
    <svg width="100%" height="${H}" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" style="overflow:visible">
      ${yLines}
      <line x1="${pad.l}" y1="${pad.t}" x2="${pad.l}" y2="${pad.t + ch}" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
      <line x1="${pad.l}" y1="${pad.t + ch}" x2="${pad.l + cw}" y2="${pad.t + ch}" stroke="rgba(255,255,255,.08)" stroke-width="1"/>
      ${bars}
    </svg>`;
}

/* ─── Dashboard (역할별 분기) ─────────────────────── */
function renderDashboard() {
  const role = _userInfo.role || "ADMIN";
  const d    = dashCache || {};
  const view = document.getElementById("view-dashboard");
  if (!view) return;

  // 역할 전환 시 이전 역할 전용 동적 요소 정리
  document.getElementById("eng-wl-bar")?.remove();
  document.getElementById("workload-mini-card")?.remove();

  if (role === "ADMIN")    renderDashboardAdmin(d);
  else if (role === "ENGINEER") renderDashboardEngineer(d);
  else if (role === "PM")  renderDashboardPM(d);
  else if (role === "CUSTOMER") renderDashboardCustomer(d);
  else                     renderDashboardAdmin(d);
}

/* ── 공통 헬퍼 ─────────────────────────────────── */
function _srRow(sr, extra = "") {
  return `
    <div class="recent-row" onclick="openDetail('${sr.sr_id}')">
      <span class="recent-sr-id">${sr.sr_id}</span>
      <span class="recent-title">${esc(sr.title)}</span>
      <span class="badge badge-${sr.status}">${STATUS_LABEL[sr.status] || sr.status}</span>
      ${extra}
    </div>`;
}

function _statCard(value, label, color = "accent", sub = "", icon = "") {
  return `<div class="stat-card ${color}">
    ${icon ? `<div class="stat-icon">${icon}</div>` : ""}
    <div class="stat-value">${value}</div>
    <div class="stat-label">${label}</div>
    ${sub ? `<div class="stat-sub">${sub}</div>` : ""}
  </div>`;
}

function _statusBarChart(byStatus) {
  const total = Object.values(byStatus).reduce((a, b) => a + b, 0) || 1;
  return `<div class="status-bar-list">` +
    Object.entries(byStatus).sort((a,b) => b[1]-a[1]).map(([k,v]) => `
      <div class="status-bar-item">
        <div class="status-bar-label">
          <span>${STATUS_LABEL[k]||k}</span><span>${v}</span>
        </div>
        <div class="status-bar-track">
          <div class="status-bar-fill" style="width:${Math.round(v/total*100)}%;background:${STATUS_COLORS[k]||'#8b949e'}"></div>
        </div>
      </div>`).join("") + `</div>`;
}

/* ── ADMIN 대시보드 ─────────────────────────────── */
function renderDashboardAdmin(d) {
  const kpi = d.kpi || {};
  document.getElementById("dash-greeting")?.remove();

  document.getElementById("stats-row").innerHTML = `
    ${_statCard(kpi.total || 0,              "전체 SR",     "accent",  "",          "📋")}
    ${_statCard(kpi.pending_approval || 0,   "승인 대기",   "yellow",  "",          "🔔")}
    ${_statCard(kpi.in_progress || 0,        "진행 중",     "cyan",    "",          "⚙️")}
    ${_statCard(kpi.completed_today || 0,    "오늘 완료",   "green",   "",          "✅")}
    ${_statCard(kpi.failed || 0,             "롤백 실패",   "red",     "",          "⚠️")}
    ${_statCard((kpi.auto_rate || 0) + "%",  "AI 자동처리율","purple", "",          "🤖")}
  `;

  // 최근 SR
  const recent = (d.recent_srs || []).slice(0, 10);
  document.getElementById("recent-list").innerHTML = recent.map(sr => _srRow(sr,
    `<span class="recent-time">${fmtDate(sr.created_at)}</span>`
  )).join("") || '<div style="padding:12px 18px;color:var(--text-muted)">SR이 없습니다.</div>';

  // 상태별 차트 (srCache 기반)
  const bs = {};
  srCache.forEach(sr => { bs[sr.status] = (bs[sr.status]||0)+1; });
  document.getElementById("status-chart").innerHTML = _statusBarChart(bs);

  // 7일 추이 차트
  loadTrend();

  // 승인 대기 목록 (workload 카드에 표시)
  const pendEl = document.getElementById("workload-body");
  const pendList = (d.pending_srs || []);
  const pendHTML = pendList.length
    ? pendList.map(sr => `
        <div class="recent-row pending-row" onclick="openDetail('${sr.sr_id}')">
          <span class="recent-sr-id">${sr.sr_id}</span>
          <span class="recent-title">${esc(sr.title)}</span>
          <span class="badge badge-priority-${sr.priority}">${PRIORITY_LABEL[sr.priority]||sr.priority}</span>
          <span style="font-size:11px;color:var(--text-muted)">${esc(sr.requested_by||"")}</span>
        </div>`).join("")
    : '<div style="color:var(--text-muted);font-size:13px;padding:8px">승인 대기 SR 없음</div>';

  // workload 카드 헤더 변경
  const wlCard = document.getElementById("workload-card");
  if (wlCard) {
    wlCard.querySelector(".card-header").innerHTML = `
      <span>🟡 승인 대기 (${pendList.length}건)</span>
      <span style="font-size:12px;color:var(--text-muted);font-weight:400">클릭하여 상세 확인</span>`;
    if (pendEl) pendEl.innerHTML = pendHTML;
  }

  // 엔지니어 워크로드 mini (status-chart 하단에 추가)
  renderWorkloadMini(d.workload || workloadCache);
}

function renderWorkloadMini(wl) {
  // 기존 mini 제거
  document.getElementById("workload-mini-card")?.remove();
  const main = document.getElementById("view-dashboard");
  if (!main || !wl?.length) return;
  const div = document.createElement("div");
  div.id = "workload-mini-card";
  div.className = "card";
  div.style.marginTop = "16px";
  div.innerHTML = `
    <div class="card-header">👷 엔지니어 워크로드</div>
    <div class="card-body" style="padding:12px 16px">
      <div class="workload-grid">
        ${wl.map(e => {
          const pct   = e.utilization || 0;
          const color = pct >= 80 ? "#f85149" : pct >= 50 ? "#e3b341" : "#3fb950";
          return `
            <div class="workload-eng-card" onclick="openAutoAssignModal(event,'${e.username}')">
              <div class="workload-eng-header">
                <div class="eng-avatar">${(e.display_name||"?").charAt(0)}</div>
                <div>
                  <div class="eng-name">${esc(e.display_name||e.username)}</div>
                  <div style="font-size:10px;color:var(--text-muted)">${(e.skill_types||"").split(",").filter(Boolean).join("·")}</div>
                </div>
                <div class="eng-count" style="color:${color}">${e.active}<span style="color:var(--text-muted);font-size:11px">/${e.max_workload}</span></div>
              </div>
              <div class="workload-bar-track">
                <div class="workload-bar-fill" style="width:${pct}%;background:${color}"></div>
              </div>
            </div>`;
        }).join("")}
      </div>
    </div>`;
  main.appendChild(div);
}

/* ── ENGINEER 대시보드 ──────────────────────────── */
function renderDashboardEngineer(d) {
  const wl  = d.my_workload || {};
  const pct = wl.utilization || 0;
  const barColor = pct >= 80 ? "#f85149" : pct >= 50 ? "#e3b341" : "#3fb950";
  const skills   = (wl.skills || "").split(",").filter(Boolean)
                     .map(s => ({ DEPLOY:"배포", RESTART:"재기동", LOG:"로그", INQUIRY:"문의" }[s]||s));

  document.getElementById("stats-row").innerHTML = `
    <div class="stat-card accent" style="flex:2;min-width:200px">
      <div style="display:flex;align-items:center;gap:12px">
        <div class="eng-avatar" style="width:44px;height:44px;font-size:20px">${(_userInfo.display_name||"E").charAt(0)}</div>
        <div>
          <div class="stat-value" style="font-size:16px">${esc(d.greeting||"")}</div>
          <div class="stat-sub" style="margin-top:4px">${skills.map(s=>`<span class="skill-tag">${s}</span>`).join("")}</div>
        </div>
      </div>
    </div>
    ${_statCard(wl.active||0, "담당 중", "accent", `최대 ${wl.max||5}건`)}
    ${_statCard(wl.completed_month||0, "이번 달 완료", "green")}
    ${_statCard(wl.completed_total||0, "누적 완료", "purple")}
  `;

  // 워크로드 바 (중복 방지)
  document.getElementById("eng-wl-bar")?.remove();
  const statsRowEl = document.getElementById("stats-row");
  statsRowEl.insertAdjacentHTML("afterend", `
    <div id="eng-wl-bar" class="eng-workload-bar-wrap">
      <div style="display:flex;justify-content:space-between;font-size:12px;color:var(--text-muted);margin-bottom:4px">
        <span>현재 워크로드</span><span>${wl.active||0}/${wl.max||5}건 (${pct}%)</span>
      </div>
      <div class="workload-bar-track" style="height:8px">
        <div class="workload-bar-fill" style="width:${pct}%;background:${barColor};height:8px"></div>
      </div>
    </div>`);

  // 처리 대기 (APPROVED — 바로 시작 가능)
  const ready = d.action_required || [];
  document.getElementById("recent-list").innerHTML = `
    <div style="font-size:11px;font-weight:700;color:var(--accent);text-transform:uppercase;
                letter-spacing:.05em;padding:8px 16px;border-bottom:1px solid var(--border)">
      ⚡ 즉시 실행 가능 (APPROVED, ${ready.length}건)
    </div>` +
    (ready.map(sr => `
      <div class="recent-row" onclick="openDetail('${sr.sr_id}')">
        <span class="recent-sr-id">${sr.sr_id}</span>
        <span class="recent-title">${esc(sr.title)}</span>
        <span class="badge badge-priority-${sr.priority}">${PRIORITY_LABEL[sr.priority]||sr.priority}</span>
        <button class="btn btn-primary" style="font-size:11px;padding:2px 10px;margin-left:auto"
                onclick="event.stopPropagation();runSimulate('${sr.sr_id}')">▶ 실행</button>
      </div>`).join("") ||
      '<div style="padding:12px 16px;color:var(--text-muted);font-size:13px">처리 대기 SR 없음</div>');

  // 진행 중
  const inprog = d.in_progress || [];
  document.getElementById("status-chart").innerHTML = `
    <div style="font-size:11px;font-weight:700;color:var(--text-muted);text-transform:uppercase;
                letter-spacing:.05em;padding:8px 16px 4px;border-bottom:1px solid var(--border)">
      🔄 진행 중 (${inprog.length}건)
    </div>` +
    (inprog.map(sr => _srRow(sr, `<span class="recent-time">${fmtDate(sr.updated_at)}</span>`)).join("") ||
      '<div style="padding:12px 16px;color:var(--text-muted);font-size:13px">진행 중 SR 없음</div>');

  // 워크로드 카드: 최근 완료
  const done = d.recent_completed || [];
  const wlEl = document.getElementById("workload-card");
  if (wlEl) {
    wlEl.querySelector(".card-header").innerHTML = `<span>✅ 최근 완료 (${done.length}건)</span>`;
    const body = document.getElementById("workload-body");
    if (body) body.innerHTML = done.map(sr =>
      _srRow(sr, `<span class="recent-time">${fmtDate(sr.updated_at)}</span>`)
    ).join("") || '<div style="padding:12px 16px;color:var(--text-muted);font-size:13px">완료 이력 없음</div>';
  }
}

/* ── PM 대시보드 ────────────────────────────────── */
function renderDashboardPM(d) {
  const pendCnt = d.pending_count || 0;
  document.getElementById("stats-row").innerHTML = `
    <div class="stat-card yellow" style="flex:1.5">
      <div class="stat-value">${pendCnt}</div>
      <div class="stat-label">승인 대기</div>
      ${pendCnt > 0 ? '<div class="stat-sub" style="color:#e3b341">즉시 처리 필요</div>' : ""}
    </div>
    ${(d.inst_stats||[]).map(i => _statCard(i.total, i.inst_name, "accent", `진행 ${i.active} · 완료 ${i.completed}`)).join("")}
  `;

  // 승인 대기 큐 (메인 카드)
  const pend = d.pending_srs || [];
  document.getElementById("recent-list").innerHTML = `
    <div style="font-size:11px;font-weight:700;color:var(--yellow);text-transform:uppercase;
                letter-spacing:.05em;padding:8px 16px;border-bottom:1px solid var(--border)">
      🔔 승인 대기 큐 — 결재 필요
    </div>` +
    pend.map(sr => `
      <div class="recent-row" onclick="openDetail('${sr.sr_id}')">
        <span class="recent-sr-id">${sr.sr_id}</span>
        <span class="recent-title">${esc(sr.title)}</span>
        <span class="badge badge-priority-${sr.priority}">${PRIORITY_LABEL[sr.priority]||sr.priority}</span>
        <span style="font-size:11px;color:var(--text-muted);flex-shrink:0">${esc(sr.requested_by||"")}</span>
        <div style="display:flex;gap:4px;margin-left:auto" onclick="event.stopPropagation()">
          <button class="btn btn-approve" style="font-size:11px;padding:2px 8px"
                  onclick="quickApprove('${sr.sr_id}','APPROVED')">✅</button>
          <button class="btn btn-reject" style="font-size:11px;padding:2px 8px"
                  onclick="quickApprove('${sr.sr_id}','REJECTED')">❌</button>
        </div>
      </div>`).join("") ||
    '<div style="padding:12px 16px;color:var(--text-muted);font-size:13px">승인 대기 SR 없음 ✨</div>';

  // 엔지니어 워크로드
  const wlEl = document.getElementById("workload-card");
  if (wlEl) {
    wlEl.querySelector(".card-header").innerHTML = `<span>👷 엔지니어 워크로드</span>`;
    const body = document.getElementById("workload-body");
    if (body) body.innerHTML = `<div class="workload-grid">` +
      (d.workload||[]).map(e => {
        const pct = e.utilization||0;
        const color = pct>=80?"#f85149":pct>=50?"#e3b341":"#3fb950";
        return `<div class="workload-eng-card">
          <div class="workload-eng-header">
            <div class="eng-avatar">${(e.display_name||"?").charAt(0)}</div>
            <div><div class="eng-name">${esc(e.display_name||e.username)}</div></div>
            <div class="eng-count" style="color:${color}">${e.active}<span style="color:var(--text-muted);font-size:11px">/${e.max_workload}</span></div>
          </div>
          <div class="workload-bar-track"><div class="workload-bar-fill" style="width:${pct}%;background:${color}"></div></div>
        </div>`;
      }).join("") + `</div>`;
  }

  // 상태 차트
  const bs = {};
  srCache.forEach(sr => { bs[sr.status] = (bs[sr.status]||0)+1; });
  document.getElementById("status-chart").innerHTML = _statusBarChart(bs);

  // PM도 추이 차트 표시
  loadTrend();
}

/* ── CUSTOMER 대시보드 ──────────────────────────── */
function renderDashboardCustomer(d) {
  const st = d.stats || {};
  document.getElementById("stats-row").innerHTML = `
    <div class="stat-card accent" style="flex:2">
      <div style="font-size:13px;color:var(--text-muted);margin-bottom:4px">소속 기관</div>
      <div class="stat-value" style="font-size:18px">${esc(d.inst_name||d.inst_code||"—")}</div>
    </div>
    ${_statCard(st.total||0, "전체 SR")}
    ${_statCard(st.active||0, "진행 중", "accent")}
    ${_statCard(st.completed||0, "완료", "green")}
    ${d.avg_rating != null ? _statCard("★".repeat(Math.round(d.avg_rating||0)) + ` ${d.avg_rating}`, "평균 만족도", "yellow") : ""}
  `;

  // 진행 중 SR
  const activeSRs = d.active_srs || [];
  document.getElementById("recent-list").innerHTML = `
    <div style="font-size:11px;font-weight:700;color:var(--accent);text-transform:uppercase;
                letter-spacing:.05em;padding:8px 16px;border-bottom:1px solid var(--border)">
      🔄 진행 중인 요청 (${activeSRs.length}건)
    </div>` +
    activeSRs.map(sr => `
      <div class="recent-row" onclick="openDetail('${sr.sr_id}')">
        <span class="recent-sr-id">${sr.sr_id}</span>
        <span class="recent-title">${esc(sr.title)}</span>
        <span class="badge badge-${sr.status}">${STATUS_LABEL[sr.status]||sr.status}</span>
        <span style="font-size:11px;color:var(--text-muted)">${esc(sr.assigned_to||"처리 중")}</span>
      </div>`).join("") ||
    '<div style="padding:12px 16px;color:var(--text-muted);font-size:13px">진행 중인 요청이 없습니다.</div>';

  // 상태 현황
  const bs = {};
  srCache.forEach(sr => { bs[sr.status] = (bs[sr.status]||0)+1; });
  document.getElementById("status-chart").innerHTML = _statusBarChart(bs);

  // 워크로드 카드: 최근 완료
  const done = d.recent_completed || [];
  const wlEl = document.getElementById("workload-card");
  if (wlEl) {
    wlEl.querySelector(".card-header").innerHTML = `<span>✅ 최근 완료된 요청</span>`;
    const body = document.getElementById("workload-body");
    if (body) body.innerHTML = done.map(sr => `
      <div class="recent-row" onclick="openDetail('${sr.sr_id}')">
        <span class="recent-sr-id">${sr.sr_id}</span>
        <span class="recent-title">${esc(sr.title)}</span>
        <span class="badge badge-COMPLETED">완료</span>
        <span class="recent-time">${fmtDate(sr.updated_at)}</span>
      </div>`).join("") ||
      '<div style="padding:12px 16px;color:var(--text-muted);font-size:13px">완료된 요청 없음</div>';
  }
}

/* ── PM 빠른 승인/반려 ──────────────────────────── */
async function quickApprove(srId, result) {
  const actor = _userInfo.display_name || _userInfo.username || "PM";
  const r = await authFetch(`/api/approvals/${srId}`, {
    method: "POST",
    body: JSON.stringify({ approver: actor, result, comment: "대시보드 빠른 처리" }),
  });
  if (r.ok) { await loadAll(); }
  else { const e = await r.json().catch(()=>({})); alert(e.detail||"처리 실패"); }
}

/* ─── 엔지니어 워크로드 패널 ─────────────────────── */
function renderWorkload() {
  const el = document.getElementById("workload-body");
  if (!el) return;

  if (!workloadCache.length) {
    el.innerHTML = '<div style="color:var(--text-muted);font-size:13px;padding:8px 0">등록된 엔지니어 프로필 없음</div>';
    return;
  }

  const skillLabel = { DEPLOY:"배포", RESTART:"재기동", LOG:"로그", INQUIRY:"문의", OTHER:"기타" };
  const canAssign  = ["ADMIN","PM"].includes(_userInfo.role || "");

  el.innerHTML = `
    <div class="workload-grid">
      ${workloadCache.map(eng => {
        const pct   = eng.utilization;
        const color = pct >= 80 ? "#f85149" : pct >= 50 ? "#e3b341" : "#3fb950";
        const skills = (eng.skill_types || "").split(",").filter(Boolean)
                         .map(s => `<span class="skill-tag">${skillLabel[s]||s}</span>`).join("");
        const aff    = (eng.inst_affinity || "").split(",").filter(Boolean)
                         .map(a => `<span class="skill-tag inst-tag">${a}</span>`).join("");
        const assignBtn = canAssign
          ? `<button class="btn btn-secondary" style="font-size:11px;padding:3px 8px;margin-top:4px"
               onclick="openAutoAssignModal(event,'${eng.username}')">배정 중인 SR 보기</button>`
          : "";
        return `
          <div class="workload-eng-card">
            <div class="workload-eng-header">
              <div class="eng-avatar">${eng.display_name.charAt(0)}</div>
              <div>
                <div class="eng-name">${esc(eng.display_name)}</div>
                <div style="font-size:11px;color:var(--text-muted)">${eng.username}</div>
              </div>
              <div class="eng-count" style="color:${color}">
                ${eng.active}<span style="color:var(--text-muted);font-size:11px">/${eng.max_workload}</span>
              </div>
            </div>
            <div class="workload-bar-track">
              <div class="workload-bar-fill" style="width:${pct}%;background:${color}"></div>
            </div>
            <div class="workload-tags">${skills}${aff}</div>
            <div style="font-size:11px;color:var(--text-muted);margin-top:2px">완료 ${eng.completed}건</div>
            ${assignBtn}
          </div>`;
      }).join("")}
    </div>`;
}

function openAutoAssignModal(e, username) {
  e.stopPropagation();
  const eng    = workloadCache.find(e => e.username === username);
  if (!eng) return;
  const active = srCache.filter(sr =>
    sr.assigned_to === username &&
    !["COMPLETED","FAILED_ROLLBACK","REJECTED"].includes(sr.status)
  );
  const html = active.length
    ? active.map(sr => `
        <div class="recent-row" onclick="openDetail('${sr.sr_id}')">
          <span class="recent-sr-id">${sr.sr_id}</span>
          <span class="recent-title">${esc(sr.title)}</span>
          <span class="badge badge-${sr.status}">${STATUS_LABEL[sr.status]||sr.status}</span>
        </div>`).join("")
    : '<div style="color:var(--text-muted);font-size:13px;padding:12px">현재 담당 중인 SR 없음</div>';

  document.getElementById("modal-body").innerHTML = `
    <div class="modal-title">👷 ${esc(eng.display_name)} 담당 SR</div>
    <div style="font-size:13px;color:var(--text-muted);margin-bottom:12px">
      활성 ${eng.active}건 | 완료 ${eng.completed}건 | 이용률 ${eng.utilization}%
    </div>
    ${html}`;
  document.getElementById("modal-overlay").classList.remove("hidden");
}

/* ─── Kanban ────────────────────────────────────── */
function renderKanban() {
  const board = document.getElementById("kanban-board");
  board.innerHTML = "";
  KANBAN_COLS.forEach(col => {
    const cards = srCache.filter(sr => sr.status === col.key);
    const colEl = document.createElement("div");
    colEl.className = "kanban-col";
    colEl.innerHTML = `
      <div class="kanban-col-header">
        <span class="badge badge-${col.key}">${col.label}</span>
        <span class="col-count">${cards.length}</span>
      </div>
      <div class="kanban-cards" id="col-${col.key}"></div>`;
    board.appendChild(colEl);

    const cardsEl = colEl.querySelector(`#col-${col.key}`);
    cards.forEach(sr => {
      const card = document.createElement("div");
      card.className = "kanban-card";
      card.innerHTML = `
        <div class="kanban-card-id">${sr.sr_id}</div>
        <div class="kanban-card-title">${esc(sr.title)}</div>
        <div class="kanban-card-meta">
          <span class="badge badge-type-${sr.sr_type}">${TYPE_LABEL[sr.sr_type] || sr.sr_type}</span>
          <span class="badge badge-priority-${sr.priority}">${PRIORITY_LABEL[sr.priority] || sr.priority}</span>
        </div>`;
      card.addEventListener("click", () => openDetail(sr.sr_id));
      cardsEl.appendChild(card);
    });
  });
}

/* ─── SR List ───────────────────────────────────── */
function setupListFilters() {
  document.getElementById("search-input").addEventListener("input", renderList);
  document.getElementById("filter-status").addEventListener("change", renderList);
  document.getElementById("filter-type").addEventListener("change", renderList);
}

function renderList() {
  const keyword  = document.getElementById("search-input").value.toLowerCase();
  const fStatus  = document.getElementById("filter-status").value;
  const fType    = document.getElementById("filter-type").value;

  let rows = srCache;
  if (keyword) rows = rows.filter(r => r.title.toLowerCase().includes(keyword) || r.sr_id.toLowerCase().includes(keyword));
  if (fStatus) rows = rows.filter(r => r.status === fStatus);
  if (fType)   rows = rows.filter(r => r.sr_type === fType);

  document.getElementById("sr-tbody").innerHTML = rows.map(sr => {
    const engInfo = workloadCache.find(e => e.username === sr.assigned_to);
    const engChip = sr.assigned_to
      ? `<span class="eng-chip">${esc(engInfo?.display_name || sr.assigned_to)}</span>`
      : `<span style="color:var(--text-muted);font-size:12px">미배정</span>`;
    return `
    <tr onclick="openDetail('${sr.sr_id}')">
      <td><code style="font-size:11px">${sr.sr_id}</code></td>
      <td><span class="badge badge-type-${sr.sr_type}">${TYPE_LABEL[sr.sr_type] || sr.sr_type}</span></td>
      <td>${esc(sr.title)}</td>
      <td><span class="badge badge-${sr.status}">${STATUS_LABEL[sr.status] || sr.status}</span></td>
      <td><span class="badge badge-priority-${sr.priority}">${PRIORITY_LABEL[sr.priority] || sr.priority}</span></td>
      <td>${engChip}</td>
      <td>${esc(sr.requested_by || "")}</td>
      <td style="font-size:12px;color:var(--text-muted)">${fmtDate(sr.created_at)}</td>
    </tr>`;
  }).join("") || `<tr><td colspan="8" style="color:var(--text-muted);text-align:center;padding:20px">결과 없음</td></tr>`;
}

const WORK_ACTION_LABEL = {
  CMDB_CHECK:"🔍 자산 확인", SSH_CONNECT:"🔗 SSH 접속",
  SSH_EXEC:"⚡ 명령 실행", SOURCE_MOD:"📦 파일 배포",
  HEALTH_CHECK:"💓 헬스체크", RESULT:"📋 결과 기록", COMPLETE:"✅ 완료 처리",
};

/* ─── 첨부파일 헬퍼 ────────────────────────────── */
function _fileIcon(name) {
  const ext = (name.split(".").pop() || "").toLowerCase();
  return {
    pdf:"📄", png:"🖼️", jpg:"🖼️", jpeg:"🖼️", gif:"🖼️", webp:"🖼️",
    xlsx:"📊", xls:"📊", docx:"📝", doc:"📝", pptx:"📊", ppt:"📊",
    zip:"🗜️", tar:"🗜️", gz:"🗜️",
    sh:"⚙️", sql:"🗄️", json:"📋", yaml:"📋", yml:"📋", md:"📋",
    log:"📜", txt:"📜", csv:"📊",
  }[ext] || "📎";
}

function _fmtSize(bytes) {
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
  return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}

function _renderAttachments(atts, srId) {
  if (!atts || atts.length === 0)
    return `<div style="color:var(--text-muted);font-size:13px">첨부파일 없음</div>`;
  return `<div class="attachment-list">` + atts.map(a => `
    <div class="attachment-item">
      <span class="att-icon">${_fileIcon(a.original_name)}</span>
      <a class="att-name" href="/api/tasks/${srId}/attachments/${a.id}/download"
         download="${esc(a.original_name)}" target="_blank">${esc(a.original_name)}</a>
      <span class="att-size">${_fmtSize(a.file_size)}</span>
      <span class="att-uploader">${esc(a.uploaded_by)}</span>
      <span class="att-date">${fmtDate(a.created_at)}</span>
      <button class="att-del" title="삭제" onclick="deleteAttachment('${srId}',${a.id},this)">🗑️</button>
    </div>`).join("") + `</div>`;
}

async function uploadAttachments(srId, input) {
  if (!input.files.length) return;
  const ffd = new FormData();
  for (const f of input.files) ffd.append("files", f);
  const r = await authFetch(`/api/tasks/${srId}/attachments`, { method: "POST", body: ffd });
  if (r.ok) {
    const newAtts = await authFetch(`/api/tasks/${srId}/attachments`).then(x => x.json()).catch(() => []);
    const el = document.getElementById(`att-list-${srId}`);
    if (el) el.innerHTML = _renderAttachments(newAtts, srId);
  } else {
    const err = await r.json().catch(() => ({}));
    alert(err.detail || "파일 업로드 실패");
  }
  input.value = "";
}

async function deleteAttachment(srId, attId, btn) {
  if (!confirm("첨부파일을 삭제하시겠습니까?")) return;
  const r = await authFetch(`/api/tasks/${srId}/attachments/${attId}`, { method: "DELETE" });
  if (r.ok) {
    const newAtts = await authFetch(`/api/tasks/${srId}/attachments`).then(x => x.json()).catch(() => []);
    const el = document.getElementById(`att-list-${srId}`);
    if (el) el.innerHTML = _renderAttachments(newAtts, srId);
  } else {
    const err = await r.json().catch(() => ({}));
    alert(err.detail || "삭제 실패");
  }
}

/* ─── SR Detail Modal ───────────────────────────── */
async function openDetail(srId) {
  const sr   = srCache.find(s => s.sr_id === srId);
  if (!sr) return;

  const [approvalsRes, auditRes, workRes, ratingRes, attachmentsRes] = await Promise.all([
    authFetch(`/api/approvals/${srId}`).then(r => r.json()).catch(() => []),
    authFetch(`/api/audit?sr_id=${srId}`).then(r => r.json()).catch(() => []),
    authFetch(`/api/work/${srId}`).then(r => r.json()).catch(() => []),
    authFetch(`/api/rating/${srId}`).then(r => r.ok ? r.json() : null).catch(() => null),
    authFetch(`/api/tasks/${srId}/attachments`).then(r => r.json()).catch(() => []),
  ]);

  const approvalHTML = approvalsRes.length
    ? approvalsRes.map(a => `
        <div style="font-size:13px;padding:6px 0;border-bottom:1px solid var(--border)">
          <strong>${esc(a.approver)}</strong>
          <span class="badge badge-${a.result === "APPROVED" ? "COMPLETED" : a.result === "REJECTED" ? "REJECTED" : "PENDING_APPROVAL"}" style="margin-left:8px">
            ${a.result === "APPROVED" ? "승인" : a.result === "REJECTED" ? "반려" : "대기"}
          </span>
          ${a.comment ? `<span style="color:var(--text-muted);margin-left:8px">${esc(a.comment)}</span>` : ""}
        </div>`).join("")
    : '<div style="color:var(--text-muted);font-size:13px">승인 기록 없음</div>';

  const auditHTML = auditRes.slice(0, 10).map(log => `
    <div class="timeline-item done">
      <div class="timeline-action">${esc(log.action)} <span style="color:var(--text-muted);font-weight:400">by ${esc(log.actor || "system")}</span></div>
      <div class="timeline-detail">${esc(log.detail || "")} &nbsp;<span class="hash-code">#${(log.log_hash || "").slice(0, 12)}</span></div>
    </div>`).join("") || '<div style="color:var(--text-muted);font-size:13px">기록 없음</div>';

  const canApprove   = sr.status === "PENDING_APPROVAL";
  const canSimulate  = !["COMPLETED","FAILED_ROLLBACK","REJECTED"].includes(sr.status);
  const canAssign    = ["ADMIN","PM"].includes(_userInfo.role || "");

  /* 작업 로그 HTML */
  const workHTML = workRes.length
    ? `<div class="timeline">` + workRes.map(w => `
        <div class="timeline-item done">
          <div class="timeline-action">${WORK_ACTION_LABEL[w.action_type] || w.action_type}
            <span style="color:var(--text-muted);font-weight:400;font-size:11px"> by ${esc(w.engineer||"AI")}</span>
          </div>
          <div class="timeline-detail">${esc(w.content||"")}
            ${w.result ? `<br><code style="font-size:11px;color:#58a6ff">${esc(w.result.slice(0,120))}</code>` : ""}
          </div>
        </div>`).join("") + `</div>`
    : `<div style="color:var(--text-muted);font-size:13px">작업 이력 없음</div>`;

  /* 별점 HTML */
  const ratingHTML = ratingRes
    ? `<div style="font-size:13px;padding:6px 0;color:#e3b341">${"★".repeat(ratingRes.stars)}${"☆".repeat(5-ratingRes.stars)}
         <span style="color:var(--text-muted);margin-left:8px">${esc(ratingRes.customer||"")}
         ${ratingRes.comment ? `— ${esc(ratingRes.comment)}` : ""}</span></div>`
    : sr.status === "COMPLETED"
      ? `<div id="star-widget" style="margin-top:8px">
           <div style="font-size:12px;color:var(--text-muted);margin-bottom:6px">고객 만족도 평가</div>
           <div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center">
             ${[1,2,3,4,5].map(n=>`<button class="btn btn-secondary" style="font-size:18px;padding:4px 8px" onclick="rateFromModal('${srId}',${n})">★${n}</button>`).join("")}
           </div>
         </div>`
      : "";

  document.getElementById("modal-body").innerHTML = `
    <div class="modal-title">${esc(sr.title)}</div>
    <div class="modal-meta">
      <span class="badge badge-${sr.status}">${STATUS_LABEL[sr.status] || sr.status}</span>
      <span class="badge badge-type-${sr.sr_type}">${TYPE_LABEL[sr.sr_type] || sr.sr_type}</span>
      <span class="badge badge-priority-${sr.priority}">${PRIORITY_LABEL[sr.priority] || sr.priority}</span>
      <code style="font-size:11px;color:var(--text-muted)">${sr.sr_id}</code>
    </div>

    <div class="modal-section">
      <div class="modal-section-title">요약 정보</div>
      <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:13px">
        <div><span style="color:var(--text-muted)">요청자:</span> ${esc(sr.requested_by || "")}</div>
        <div id="assign-cell">
          <span style="color:var(--text-muted)">담당 엔지니어:</span>
          ${sr.assigned_to
            ? `<span class="eng-chip" style="margin-left:4px">${esc(workloadCache.find(e=>e.username===sr.assigned_to)?.display_name || sr.assigned_to)}</span>`
            : `<span style="color:var(--text-muted);margin-left:4px">미배정</span>`}
          ${canAssign ? `<button class="btn btn-secondary" style="font-size:11px;padding:2px 8px;margin-left:6px"
            onclick="openReassignPanel('${sr.sr_id}')">재배정</button>` : ""}
        </div>
        <div><span style="color:var(--text-muted)">대상 서버:</span> ${esc(sr.target_server || "-")}</div>
        <div><span style="color:var(--text-muted)">생성일:</span> ${fmtDate(sr.created_at)}</div>
      </div>
    </div>

    ${sr.description ? `
    <div class="modal-section">
      <div class="modal-section-title">설명</div>
      <div class="modal-desc">${esc(sr.description)}</div>
    </div>` : ""}

    <div class="modal-section">
      <div class="modal-section-title">승인 현황</div>
      ${approvalHTML}
    </div>

    <div class="modal-section">
      <div class="modal-section-title">🛠️ 작업 실행 이력</div>
      ${workHTML}
      ${canSimulate ? `
      <div style="margin-top:10px">
        <button class="btn btn-primary" id="btn-simulate-${srId}"
                onclick="runSimulate('${srId}')">⚡ AI 작업 실행 시뮬레이션</button>
        <span id="sim-status-${srId}" style="font-size:12px;color:var(--text-muted);margin-left:8px"></span>
      </div>` : ""}
    </div>

    ${ratingHTML ? `
    <div class="modal-section">
      <div class="modal-section-title">⭐ 고객 만족도</div>
      ${ratingHTML}
    </div>` : ""}

    <div class="modal-section">
      <div class="modal-section-title" style="display:flex;align-items:center;justify-content:space-between">
        <span>📎 첨부파일</span>
        <label class="btn btn-secondary" style="font-size:11px;padding:2px 10px;cursor:pointer">
          파일 추가
          <input type="file" multiple style="display:none"
            accept=".pdf,.txt,.log,.csv,.png,.jpg,.jpeg,.xlsx,.docx,.zip,.sh,.sql,.json,.yaml,.md"
            onchange="uploadAttachments('${srId}', this)">
        </label>
      </div>
      <div id="att-list-${srId}">${_renderAttachments(attachmentsRes, srId)}</div>
    </div>

    <!-- KB 추천 섹션 (비동기 로드) -->
    <div class="modal-section">
      <div class="modal-section-title" style="display:flex;align-items:center;justify-content:space-between">
        <span>📚 관련 기술 문서 추천</span>
        <button class="btn btn-secondary" style="font-size:11px;padding:2px 8px"
                onclick="loadKBSuggestForSR('${srId}')">새로고침</button>
      </div>
      <div id="kb-suggest-section">
        <div style="color:var(--text-muted);font-size:12px">로딩 중…</div>
      </div>
    </div>

    <div class="modal-section">
      <div class="modal-section-title">감사 로그</div>
      <div class="timeline">${auditHTML}</div>
    </div>

    ${canApprove ? `
    <div class="modal-actions" id="approval-actions">
      <input type="text" id="approver-name" placeholder="승인자 이름" class="search-box" style="max-width:160px">
      <input type="text" id="approver-comment" placeholder="코멘트(선택)" class="search-box" style="max-width:200px">
      <button class="btn btn-approve" onclick="doApproval('${srId}', 'APPROVED')">✅ 승인</button>
      <button class="btn btn-reject"  onclick="doApproval('${srId}', 'REJECTED')">❌ 반려</button>
    </div>` : ""}
  `;

  document.getElementById("modal-overlay").classList.remove("hidden");

  // KB 추천 비동기 로드 (모달 렌더 후)
  setTimeout(() => loadKBSuggestForSR(srId), 100);
}

async function doApproval(srId, result) {
  const approver = document.getElementById("approver-name").value.trim();
  const comment  = document.getElementById("approver-comment").value.trim();
  if (!approver) { alert("승인자 이름을 입력하세요."); return; }

  const r = await authFetch(`/api/approvals/${srId}`, {
    method: "POST",
    body: JSON.stringify({ approver, result, comment: comment || null }),
  });
  if (r.ok) {
    document.getElementById("modal-overlay").classList.add("hidden");
    await loadAll();
  } else {
    const err = await r.json().catch(() => ({}));
    alert(err.detail || "처리 중 오류 발생");
  }
}

/* ─── Simulate + Rating from modal ─────────────── */
async function runSimulate(srId) {
  const btn = document.getElementById(`btn-simulate-${srId}`);
  const lbl = document.getElementById(`sim-status-${srId}`);
  if (btn) { btn.disabled = true; btn.textContent = "⏳ 실행 중…"; }
  if (lbl) lbl.textContent = "CMDB 확인 → SSH 접속 → 작업 수행 중…";

  const r = await authFetch(`/api/work/${srId}/simulate`, { method: "POST" });
  if (r.ok) {
    if (lbl) lbl.textContent = "✅ 완료! 메신저에 알림이 전송됩니다.";
    await loadAll();
    setTimeout(() => openDetail(srId), 600);
  } else {
    const err = await r.json().catch(() => ({}));
    if (lbl) lbl.textContent = "❌ " + (err.detail || "오류 발생");
    if (btn) { btn.disabled = false; btn.textContent = "⚡ AI 작업 실행 시뮬레이션"; }
  }
}

async function rateFromModal(srId, stars) {
  const customer = prompt("평가자 이름을 입력하세요:", "고객");
  if (!customer) return;
  const r = await authFetch(`/api/rating/${srId}`, {
    method: "POST",
    body: JSON.stringify({ customer, stars, comment: null }),
  });
  if (r.ok) {
    const widget = document.getElementById("star-widget");
    if (widget) widget.innerHTML = `<div style="color:#e3b341;font-size:14px">${"★".repeat(stars)}${"☆".repeat(5-stars)} — 평가 감사합니다!</div>`;
  }
}

document.getElementById("modal-close-btn").addEventListener("click", () =>
  document.getElementById("modal-overlay").classList.add("hidden")
);
document.getElementById("modal-overlay").addEventListener("click", e => {
  if (e.target === e.currentTarget) e.currentTarget.classList.add("hidden");
});

/* ─── 엔지니어 재배정 패널 ───────────────────────── */
async function openReassignPanel(srId) {
  let engineers = [];
  try {
    const r = await authFetch("/api/assign/engineers");
    engineers = await r.json();
  } catch {}

  const panel = document.createElement("div");
  panel.id = "reassign-panel";
  panel.style.cssText = "margin-top:10px;padding:10px;background:var(--surface-2);border-radius:6px;border:1px solid var(--border)";
  panel.innerHTML = `
    <div style="font-size:12px;color:var(--text-muted);margin-bottom:8px">담당 엔지니어 변경</div>
    <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center">
      <select id="reassign-select" class="filter-select" style="flex:1;min-width:120px">
        <option value="">— 자동 배정 —</option>
        ${engineers.map(e => `<option value="${e.username}">${esc(e.display_name)}</option>`).join("")}
      </select>
      <button class="btn btn-primary" style="font-size:12px;padding:5px 12px"
              onclick="submitReassign('${srId}')">배정</button>
      <button class="btn btn-secondary" style="font-size:12px;padding:5px 10px"
              onclick="document.getElementById('reassign-panel')?.remove()">취소</button>
    </div>`;

  // 기존 패널 있으면 제거
  document.getElementById("reassign-panel")?.remove();
  const assignCell = document.getElementById("assign-cell");
  if (assignCell) assignCell.appendChild(panel);
}

async function submitReassign(srId) {
  const sel = document.getElementById("reassign-select");
  const eng = sel ? sel.value : "";  // "" → 자동 배정

  const url  = `/api/assign/${srId}${eng ? `?engineer=${encodeURIComponent(eng)}` : ""}`;
  const r    = await authFetch(url, { method: "POST" });
  if (r.ok) {
    const data = await r.json();
    document.getElementById("reassign-panel")?.remove();
    await loadWorkload();
    await loadAll();
    openDetail(srId);
  } else {
    const err = await r.json().catch(() => ({}));
    alert(err.detail || "배정 실패");
  }
}

/* ─── New SR Modal ──────────────────────────────── */
function setupNewSR() {
  document.getElementById("btn-new-sr").addEventListener("click", () =>
    document.getElementById("new-sr-overlay").classList.remove("hidden")
  );
  document.getElementById("new-sr-close").addEventListener("click", () =>
    document.getElementById("new-sr-overlay").classList.add("hidden")
  );
  document.getElementById("new-sr-overlay").addEventListener("click", e => {
    if (e.target === e.currentTarget) e.currentTarget.classList.add("hidden");
  });
  // 파일 선택 미리보기
  document.getElementById("sr-file-input").addEventListener("change", e => {
    const files = Array.from(e.target.files || []);
    const txt   = document.getElementById("file-upload-text");
    const prev  = document.getElementById("file-preview-list");
    txt.textContent = files.length
      ? `${files.length}개 파일 선택됨`
      : "첨부파일 선택 (선택사항, 최대 10개 · 파일당 20 MB)";
    prev.innerHTML = files.map(f => `
      <div class="file-preview-item">
        <span class="file-preview-icon">${_fileIcon(f.name)}</span>
        <span class="file-preview-name">${esc(f.name)}</span>
        <span class="file-preview-size">${_fmtSize(f.size)}</span>
      </div>`).join("");
  });

  document.getElementById("new-sr-form").addEventListener("submit", async e => {
    e.preventDefault();
    const fd = new FormData(e.target);
    const payload = Object.fromEntries(fd.entries());
    // remove empty optional fields
    if (!payload.description) delete payload.description;
    if (!payload.target_server) delete payload.target_server;
    if (!payload.inst_code) delete payload.inst_code;

    const r = await authFetch("/api/tasks", {
      method: "POST",
      body: JSON.stringify(payload),
    });
    if (!r.ok) {
      const err = await r.json().catch(() => ({}));
      alert(err.detail || "SR 생성 실패");
      return;
    }
    const sr = await r.json();

    // 파일 업로드 (선택된 경우)
    const fileInput = document.getElementById("sr-file-input");
    if (fileInput.files.length > 0) {
      const ffd = new FormData();
      for (const f of fileInput.files) ffd.append("files", f);
      const ur = await authFetch(`/api/tasks/${sr.sr_id}/attachments`, {
        method: "POST",
        body: ffd,
      });
      if (!ur.ok) {
        const uerr = await ur.json().catch(() => ({}));
        alert(`SR이 생성되었으나 파일 업로드 실패: ${uerr.detail || "알 수 없는 오류"}`);
      }
    }

    document.getElementById("new-sr-overlay").classList.add("hidden");
    e.target.reset();
    document.getElementById("file-upload-text").textContent = "첨부파일 선택 (선택사항, 최대 10개 · 파일당 20 MB)";
    document.getElementById("file-preview-list").innerHTML = "";
    await loadAll();
  });
}

/* ─── Audit ─────────────────────────────────────── */
async function loadAudit() {
  const data = await authFetch("/api/audit?limit=100").then(r => r.json()).catch(() => []);
  document.getElementById("audit-tbody").innerHTML = data.map((log, i) => `
    <tr>
      <td style="color:var(--text-muted)">${i + 1}</td>
      <td><code style="font-size:11px">${esc(log.sr_id || "—")}</code></td>
      <td>${esc(log.actor || "system")}</td>
      <td><strong>${esc(log.action)}</strong></td>
      <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(log.detail || "")}</td>
      <td class="hash-code">${(log.log_hash || "").slice(0, 12)}</td>
      <td style="font-size:12px;color:var(--text-muted)">${fmtDate(log.created_at)}</td>
    </tr>`).join("") || `<tr><td colspan="7" style="text-align:center;padding:20px;color:var(--text-muted)">기록 없음</td></tr>`;

  document.getElementById("btn-verify").addEventListener("click", async () => {
    const res = await authFetch("/api/audit/verify").then(r => r.json());
    const el  = document.getElementById("verify-result");
    if (res.intact) {
      el.textContent = "✅ 체인 무결성 확인됨";
      el.className = "ok";
    } else {
      el.textContent = `❌ 변조 감지 (ID: ${res.broken_at_id})`;
      el.className = "fail";
    }
  });
}

/* ─── CMDB ───────────────────────────────────────── */
async function loadCmdb() {
  const institutions = await authFetch("/api/cmdb/institutions").then(r => r.json()).catch(() => []);
  const grid = document.getElementById("cmdb-grid");
  grid.innerHTML = "";

  await Promise.all(institutions.map(async inst => {
    const servers = await authFetch(`/api/cmdb/institutions/${inst.inst_code}/servers`)
                          .then(r => r.json()).catch(() => []);
    const card = document.createElement("div");
    card.className = "cmdb-card";
    card.innerHTML = `
      <div class="cmdb-card-header">
        <span>${esc(inst.inst_name)}</span>
        <span style="font-size:11px;color:var(--text-muted)">${esc(inst.inst_code)}</span>
      </div>
      <div class="cmdb-servers">
        ${servers.map(s => `
          <div class="cmdb-server-row">
            <span class="server-role-badge role-${s.server_role}">${s.server_role}</span>
            <span>${esc(s.server_name)}</span>
            <span style="font-size:11px;color:var(--text-muted)">${esc(s.os_type || "")}</span>
            <span class="${s.is_active ? "server-active" : "server-inactive"}">${s.is_active ? "● 정상" : "● 비활성"}</span>
          </div>`).join("") || '<div style="padding:8px 16px;color:var(--text-muted);font-size:12px">서버 없음</div>'}
      </div>
      ${inst.contact_pm ? `<div style="padding:8px 16px;font-size:12px;color:var(--text-muted);border-top:1px solid var(--border)">PM: ${esc(inst.contact_pm)}</div>` : ""}
    `;
    grid.appendChild(card);
  }));
}

/* ─── Knowledge Base ────────────────────────────── */
const KB_CAT_COLOR = {
  JAVA: "#f0883e", MIDDLEWARE: "#58a6ff", DB: "#e3b341",
  WEB: "#3fb950", OS: "#bc8cff", SECURITY: "#f85149",
};

async function loadKBView() {
  // 초기 로드: 전체 목록 표시
  const cat = document.getElementById("kb-category-filter")?.value || "";
  const url = `/api/kb/list${cat ? `?category=${encodeURIComponent(cat)}` : ""}`;
  try {
    const docs = await authFetch(url).then(r => r.json());
    renderKBDocs(docs.map(d => ({ doc: d, score: null, matched_keywords: [] })));
  } catch { document.getElementById("kb-results").innerHTML = '<div style="color:var(--text-muted);padding:20px">로드 실패</div>'; }

  // 검색 이벤트 연결 (한 번만)
  const inp = document.getElementById("kb-search-input");
  if (inp && !inp._bound) {
    inp._bound = true;
    inp.addEventListener("keydown", e => { if (e.key === "Enter") searchKB(); });
  }
}

async function searchKB() {
  const q   = document.getElementById("kb-search-input")?.value.trim();
  const cat = document.getElementById("kb-category-filter")?.value || "";
  const el  = document.getElementById("kb-results");

  if (!q) { loadKBView(); return; }

  el.innerHTML = '<div style="color:var(--text-muted);padding:20px">검색 중…</div>';
  try {
    const url = `/api/kb?q=${encodeURIComponent(q)}&limit=10`;
    let hits  = await authFetch(url).then(r => r.json());
    if (cat) hits = hits.filter(h => h.doc.category === cat);
    renderKBDocs(hits);
  } catch { el.innerHTML = '<div style="color:var(--text-muted);padding:20px">검색 실패</div>'; }
}

function renderKBDocs(hits) {
  const el = document.getElementById("kb-results");
  if (!hits.length) {
    el.innerHTML = '<div style="color:var(--text-muted);padding:20px;text-align:center">관련 문서 없음</div>';
    return;
  }
  el.innerHTML = hits.map(h => renderKBCard(h, false)).join("");
}

function renderKBCard(h, compact = false) {
  const d     = h.doc;
  const color = KB_CAT_COLOR[d.category] || "#8b949e";
  const scoreHTML = h.score !== null
    ? `<span class="kb-score">관련도 ${Math.round(h.score * 100)}%</span>`
    : "";
  const kwHTML = h.matched_keywords?.length
    ? `<div class="kb-keywords">${h.matched_keywords.map(k => `<code>${esc(k)}</code>`).join(" ")}</div>`
    : "";

  if (compact) {
    // 모달 내 축약형
    return `
      <div class="kb-card-compact" onclick="openKBDetail('${d.doc_id}')">
        <div class="kb-compact-header">
          <span class="kb-cat-badge" style="background:${color}22;color:${color};border-color:${color}44">${d.category}</span>
          <span class="kb-compact-title">${esc(d.title)}</span>
          ${scoreHTML}
        </div>
        ${kwHTML}
      </div>`;
  }

  // 전체 카드
  return `
    <div class="kb-card" id="kb-card-${d.doc_id}">
      <div class="kb-card-header" onclick="toggleKBCard('${d.doc_id}')">
        <div style="display:flex;align-items:center;gap:10px;flex:1;min-width:0">
          <span class="kb-cat-badge" style="background:${color}22;color:${color};border-color:${color}44">${d.category}</span>
          <span class="kb-card-title">${esc(d.title)}</span>
        </div>
        <div style="display:flex;align-items:center;gap:10px;flex-shrink:0">
          ${scoreHTML}
          <span class="kb-toggle-icon">▼</span>
        </div>
      </div>
      ${kwHTML ? `<div style="padding:6px 16px 0">${kwHTML}</div>` : ""}
      <div class="kb-card-body" id="kb-body-${d.doc_id}" style="display:none">
        <div class="kb-section">
          <div class="kb-section-label">🔍 증상</div>
          <div class="kb-section-text">${esc(d.symptoms || "-")}</div>
        </div>
        <div class="kb-section">
          <div class="kb-section-label">💡 원인</div>
          <div class="kb-section-text">${esc(d.cause || "-")}</div>
        </div>
        <div class="kb-section">
          <div class="kb-section-label">🛠️ 해결 절차</div>
          <pre class="kb-pre">${esc(d.solution || "-")}</pre>
        </div>
        ${d.commands ? `
        <div class="kb-section">
          <div class="kb-section-label">⚡ 점검 명령어</div>
          <pre class="kb-pre cmd">${esc(d.commands)}</pre>
        </div>` : ""}
        <div style="padding-top:6px">
          <span class="kb-doc-id">${d.doc_id}</span>
        </div>
      </div>
    </div>`;
}

function toggleKBCard(docId) {
  const body = document.getElementById(`kb-body-${docId}`);
  const icon = document.querySelector(`#kb-card-${docId} .kb-toggle-icon`);
  if (!body) return;
  const open = body.style.display === "none";
  body.style.display = open ? "block" : "none";
  if (icon) icon.textContent = open ? "▲" : "▼";
}

async function openKBDetail(docId) {
  // KB 뷰로 이동 후 해당 카드 펼치기
  switchView("kb");
  await loadKBView();
  setTimeout(() => {
    const card = document.getElementById(`kb-card-${docId}`);
    if (card) {
      card.scrollIntoView({ behavior: "smooth", block: "center" });
      const body = document.getElementById(`kb-body-${docId}`);
      if (body && body.style.display === "none") toggleKBCard(docId);
    }
  }, 400);
}

/* SR 모달 내 KB 추천 렌더링 */
async function loadKBSuggestForSR(srId) {
  const el = document.getElementById("kb-suggest-section");
  if (!el) return;
  el.innerHTML = '<div style="color:var(--text-muted);font-size:12px">분석 중…</div>';
  try {
    const hits = await authFetch(`/api/kb/suggest/${srId}`).then(r => r.json());
    if (!hits.length) {
      el.innerHTML = '<div style="color:var(--text-muted);font-size:12px">관련 문서 없음</div>';
      return;
    }
    el.innerHTML = hits.map(h => renderKBCard(h, true)).join("");
  } catch {
    el.innerHTML = '<div style="color:var(--text-muted);font-size:12px">추천 로드 실패</div>';
  }
}

/* ─── AI 채팅 어시스턴트 ─────────────────────────── */
const _CHAT_GREET = [
  "안녕하세요! GUARDiA AI 어시스턴트입니다.",
  "자연어로 ITSM 업무를 처리할 수 있습니다.",
  "예시 명령을 클릭하거나 직접 입력해 보세요.",
];
const _CHAT_EXAMPLES = [
  "승인 대기 SR 목록 보여줘",
  "엔지니어 워크로드 현황",
  "KB에서 OOM 검색해줘",
  "전체 현황 요약해줘",
  "긴급 SR 있어?",
];

let _chatHistory = [];

function initChat() {
  const fab   = document.getElementById("ai-chat-fab");
  const panel = document.getElementById("ai-chat-panel");
  const close = document.getElementById("ai-chat-close");
  const inp   = document.getElementById("ai-chat-input");
  const send  = document.getElementById("ai-chat-send");

  fab.addEventListener("click", () => {
    const open = panel.classList.toggle("hidden");
    if (!open && _chatHistory.length === 0) {
      // 첫 오픈 시 인사 메시지
      appendChatMsg("ai", _CHAT_GREET.join("\n"));
      renderSuggestions(_CHAT_EXAMPLES);
    }
  });
  close.addEventListener("click", () => panel.classList.add("hidden"));

  send.addEventListener("click", sendChat);
  inp.addEventListener("keydown", e => {
    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendChat(); }
  });
}

function appendChatMsg(role, text, data) {
  const el  = document.getElementById("ai-chat-messages");
  const div = document.createElement("div");
  div.className = role === "user" ? "chat-msg chat-user" : "chat-msg chat-ai";

  // 마크다운-라이크 렌더링 (bold, bullet)
  const rendered = text
    .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
    .replace(/`(.+?)`/g, '<code>$1</code>')
    .replace(/^• /gm, '<span class="chat-bullet">▪</span> ')
    .replace(/\n/g, '<br>');

  div.innerHTML = rendered;

  // 데이터 링크 (SR 클릭 가능)
  if (data?.length) {
    const links = data.filter(d => d.sr_id).map(d =>
      `<span class="chat-sr-link" onclick="openDetail('${d.sr_id}')">${d.sr_id}</span>`
    );
    if (links.length) {
      const linksDiv = document.createElement("div");
      linksDiv.className = "chat-links";
      linksDiv.innerHTML = links.join("");
      div.appendChild(linksDiv);
    }
  }

  el.appendChild(div);
  el.scrollTop = el.scrollHeight;
  _chatHistory.push({ role, text });
}

function renderSuggestions(items) {
  const el = document.getElementById("ai-chat-suggestions");
  el.innerHTML = items.map(s =>
    `<button class="chat-suggestion" onclick="sendChatText('${s.replace(/'/g, "\\'")}')">${esc(s)}</button>`
  ).join("");
}

function sendChatText(text) {
  document.getElementById("ai-chat-input").value = text;
  sendChat();
}

async function sendChat() {
  const inp  = document.getElementById("ai-chat-input");
  const text = inp.value.trim();
  if (!text) return;
  inp.value = "";

  document.getElementById("ai-chat-suggestions").innerHTML = "";
  appendChatMsg("user", text);

  // 로딩 표시
  const loadId = "chat-loading-" + Date.now();
  const el = document.getElementById("ai-chat-messages");
  el.insertAdjacentHTML("beforeend",
    `<div id="${loadId}" class="chat-msg chat-ai" style="color:var(--text-muted)">⏳ 처리 중…</div>`
  );
  el.scrollTop = el.scrollHeight;

  try {
    const r = await authFetch("/api/nlcmd", {
      method: "POST",
      body: JSON.stringify({ text }),
    });
    const data = await r.json();
    document.getElementById(loadId)?.remove();

    appendChatMsg("ai", data.response, data.data);

    // 액션 수행 시 데이터 갱신
    if (data.action_taken) {
      await loadAll();
    }

    // 후속 제안
    if (data.suggestions?.length) {
      renderSuggestions(data.suggestions);
    } else if (data.intent === "QUERY_SR_LIST" && data.data?.length) {
      const sample = data.data[0];
      renderSuggestions([
        `${sample.sr_id} 상태 알려줘`,
        `${sample.sr_id} 자동 배정해줘`,
      ]);
    } else if (data.intent === "SEARCH_KB" && data.data?.length) {
      renderSuggestions(["KB 뷰에서 전체 문서 보기"]);
    }
  } catch {
    document.getElementById(loadId)?.remove();
    appendChatMsg("ai", "❌ 명령 처리 중 오류가 발생했습니다.");
  }
}

/* ══════════════════════════════════════════════════
   기관 관리 뷰
══════════════════════════════════════════════════ */
let _instCache = [];
let _instCurrentCode = null;

async function loadInstitutions() {
  try {
    const r = await authFetch("/api/institutions");
    _instCache = await r.json();
    renderInstitutionTable(_instCache);
  } catch { _instCache = []; }
}

function filterInstitutions() {
  const kw = (document.getElementById("inst-search")?.value || "").toLowerCase();
  const region = document.getElementById("inst-region-filter")?.value || "";
  const filtered = _instCache.filter(i => {
    const matchKw = !kw || i.inst_name?.toLowerCase().includes(kw) || i.inst_code?.toLowerCase().includes(kw);
    const matchRegion = !region || i.region === region;
    return matchKw && matchRegion;
  });
  renderInstitutionTable(filtered);
}

function renderInstitutionTable(list) {
  const tbody = document.getElementById("inst-tbody");
  if (!tbody) return;
  // CUSTOMER 역할이면 버튼 숨김
  const canEdit = ["ADMIN", "PM"].includes(_userInfo.role || "");
  if (!list.length) {
    tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:var(--text-muted);padding:24px">등록된 기관이 없습니다.</td></tr>';
    return;
  }
  tbody.innerHTML = list.map(inst => {
    const expiry = inst.contract_end ? new Date(inst.contract_end) : null;
    const today  = new Date();
    const daysLeft = expiry ? Math.ceil((expiry - today) / 86400000) : null;
    let expiryBadge = expiry ? `<span class="badge" style="background:rgba(99,102,241,.15);color:#818cf8">${inst.contract_end}</span>` : "-";
    if (daysLeft !== null && daysLeft <= 30 && daysLeft > 7) expiryBadge = `<span class="badge badge-PENDING_APPROVAL">D-${daysLeft} ⚠</span>`;
    if (daysLeft !== null && daysLeft <= 7)  expiryBadge = `<span class="badge badge-FAILED_ROLLBACK">D-${daysLeft} 🔴</span>`;
    return `<tr onclick="openInstDetail('${esc(inst.inst_code)}')" style="cursor:pointer">
      <td><strong>${esc(inst.inst_code)}</strong></td>
      <td>${esc(inst.inst_name)}</td>
      <td>${esc(inst.region || "-")}</td>
      <td>${expiryBadge}</td>
      <td>${inst.sla_hours}h</td>
      <td style="color:var(--text-muted)">${inst.server_count ?? "-"}대</td>
      <td style="color:var(--text-muted)">${inst.contact_count ?? "-"}명</td>
      <td><span class="badge ${inst.is_active ? "badge-COMPLETED" : "badge-REJECTED"}">${inst.is_active ? "활성" : "비활성"}</span></td>
      <td onclick="event.stopPropagation()">
        ${canEdit ? `<button class="btn btn-secondary" style="font-size:11px;padding:3px 8px" onclick="openInstModal('${esc(inst.inst_code)}')">수정</button>` : ""}
      </td>
    </tr>`;
  }).join("");
}

async function openInstDetail(instCode) {
  _instCurrentCode = instCode;
  const inst = _instCache.find(i => i.inst_code === instCode);
  if (!inst) return;
  // 담당자 목록 로드 후 상세 모달 생성
  let contacts = [];
  try {
    const r = await authFetch(`/api/institutions/${instCode}/contacts`);
    contacts = await r.json();
  } catch {}
  // 상세는 SR 상세 모달 재활용
  const canEdit = ["ADMIN", "PM"].includes(_userInfo.role || "");
  const roleLabel = {MANAGER:"담당자",ENGINEER:"엔지니어",PM:"PM",SECURITY:"보안",HELPDESK:"헬프데스크"};
  const html = `
    <div class="modal-section-title">🏢 ${esc(inst.inst_name)} (${esc(inst.inst_code)})</div>
    <div class="detail-grid">
      <div><span class="detail-label">지역</span><span>${esc(inst.region||"-")}</span></div>
      <div><span class="detail-label">SLA</span><span>${inst.sla_hours}시간</span></div>
      <div><span class="detail-label">전화</span><span>${esc(inst.phone||"-")}</span></div>
      <div><span class="detail-label">계약 기간</span><span>${inst.contract_start||"?"} ~ ${inst.contract_end||"?"}</span></div>
      <div><span class="detail-label">주소</span><span>${esc(inst.address||"-")}</span></div>
      <div><span class="detail-label">비고</span><span>${esc(inst.note||"-")}</span></div>
    </div>
    <div class="modal-section-title" style="margin-top:16px;display:flex;align-items:center;justify-content:space-between">
      <span>👤 담당자 (${contacts.length}명)</span>
      ${canEdit ? `<button class="btn btn-primary" style="font-size:12px;padding:4px 12px" onclick="openContactModal('${esc(instCode)}')">+ 담당자 추가</button>` : ""}
    </div>
    ${contacts.length ? `
    <table class="sr-table" style="margin:0">
      <thead><tr><th>이름</th><th>역할</th><th>부서</th><th>이메일</th><th>전화</th><th>주담</th></tr></thead>
      <tbody>
        ${contacts.map(c => `<tr>
          <td>${esc(c.contact_name)}</td>
          <td><span class="badge" style="background:rgba(99,102,241,.15);color:#818cf8">${roleLabel[c.role]||c.role}</span></td>
          <td style="color:var(--text-muted)">${esc(c.dept||"-")}</td>
          <td>${esc(c.email||"-")}</td>
          <td>${esc(c.phone||c.mobile||"-")}</td>
          <td style="text-align:center">${c.is_primary ? "★" : ""}</td>
        </tr>`).join("")}
      </tbody>
    </table>` : `<div style="color:var(--text-muted);font-size:13px;padding:12px">등록된 담당자가 없습니다.</div>`}
  `;
  document.getElementById("modal-body").innerHTML = html;
  document.getElementById("modal-overlay").classList.remove("hidden");
}

// 기관 등록/수정 모달
let _instEditCode = null;
function openInstModal(instCode = null) {
  _instEditCode = instCode;
  const overlay = document.getElementById("inst-modal-overlay");
  const form = document.getElementById("inst-form");
  form.reset();
  document.getElementById("inst-modal-title").textContent = instCode ? "기관 수정" : "기관 등록";
  if (instCode) {
    const inst = _instCache.find(i => i.inst_code === instCode);
    if (inst) {
      Object.keys(inst).forEach(k => {
        const el = form.elements[k];
        if (el) el.value = inst[k] ?? "";
      });
    }
  }
  overlay.classList.remove("hidden");
}
function closeInstModal() { document.getElementById("inst-modal-overlay").classList.add("hidden"); }

async function submitInstForm(e) {
  e.preventDefault();
  const form = e.target;
  const data = Object.fromEntries(new FormData(form));
  // 숫자 변환
  if (data.sla_hours) data.sla_hours = parseInt(data.sla_hours);
  // 빈 문자열 null로
  Object.keys(data).forEach(k => { if (data[k] === "") data[k] = null; });
  try {
    let r;
    if (_instEditCode) {
      r = await authFetch(`/api/institutions/${_instEditCode}`, {
        method: "PATCH", body: JSON.stringify(data),
      });
    } else {
      r = await authFetch("/api/institutions", {
        method: "POST", body: JSON.stringify(data),
      });
    }
    if (!r.ok) { const err = await r.json(); showToast(err.detail || "저장 실패", "error"); return; }
    showToast(_instEditCode ? "기관 정보가 수정됐습니다." : "기관이 등록됐습니다.", "success");
    closeInstModal();
    loadInstitutions();
  } catch { showToast("저장 중 오류가 발생했습니다.", "error"); }
}

// 담당자 등록 모달
let _contactInstCode = null;
function openContactModal(instCode) {
  _contactInstCode = instCode;
  document.getElementById("contact-form").reset();
  document.getElementById("contact-modal-overlay").classList.remove("hidden");
}
function closeContactModal() { document.getElementById("contact-modal-overlay").classList.add("hidden"); }

async function submitContactForm(e) {
  e.preventDefault();
  const form = e.target;
  const data = Object.fromEntries(new FormData(form));
  data.is_primary = form.elements.is_primary?.checked || false;
  Object.keys(data).forEach(k => { if (data[k] === "") data[k] = null; });
  try {
    const r = await authFetch(`/api/institutions/${_contactInstCode}/contacts`, {
      method: "POST", body: JSON.stringify(data),
    });
    if (!r.ok) { const err = await r.json(); showToast(err.detail || "저장 실패", "error"); return; }
    showToast("담당자가 등록됐습니다.", "success");
    closeContactModal();
    openInstDetail(_contactInstCode);
  } catch { showToast("저장 중 오류가 발생했습니다.", "error"); }
}

/* ══════════════════════════════════════════════════
   스크립트 관리 뷰
══════════════════════════════════════════════════ */
let _scriptCache = [];

const SCRIPT_CATEGORY_KO = {
  SM: "SM", REGULAR: "정기점검", ADHOC: "수시점검",
  DEPLOY: "배포", SECURITY: "보안", MONITORING: "모니터링",
};
const SCRIPT_CATEGORY_COLOR = {
  SM: "#818cf8", REGULAR: "#34d399", ADHOC: "#fbbf24",
  DEPLOY: "#38bdf8", SECURITY: "#f87171", MONITORING: "#a78bfa",
};

async function loadScripts() {
  try {
    const r = await authFetch("/api/shell-scripts?limit=200");
    _scriptCache = await r.json();
    renderScriptList(_scriptCache);
  } catch { _scriptCache = []; }
}

function filterScripts() {
  const kw = (document.getElementById("script-search")?.value || "").toLowerCase();
  const cat = document.getElementById("script-category-filter")?.value || "";
  const layer = document.getElementById("script-layer-filter")?.value || "";
  const filtered = _scriptCache.filter(s => {
    const matchKw = !kw || s.script_name?.toLowerCase().includes(kw)
      || s.description?.toLowerCase().includes(kw)
      || (s.tags||"").toLowerCase().includes(kw);
    const matchCat = !cat || s.category === cat;
    const matchLayer = !layer || s.target_layer === layer || s.target_layer === "ALL";
    return matchKw && matchCat && matchLayer;
  });
  renderScriptList(filtered);
}

function renderScriptList(list) {
  const body = document.getElementById("script-list-body");
  if (!body) return;
  const canEdit = ["ADMIN", "PM", "ENGINEER"].includes(_userInfo.role || "");
  if (!list.length) {
    body.innerHTML = '<div style="padding:24px;color:var(--text-muted);text-align:center">등록된 스크립트가 없습니다.</div>';
    return;
  }
  body.innerHTML = list.map(s => {
    const catColor = SCRIPT_CATEGORY_COLOR[s.category] || "#818cf8";
    const dangerBadge = s.is_dangerous
      ? `<span class="badge" style="background:rgba(248,113,113,.15);color:#f87171">⚠ 위험</span>` : "";
    const approvalBadge = s.requires_approval
      ? `<span class="badge" style="background:rgba(251,191,36,.15);color:#fbbf24">승인필요</span>` : "";
    return `
    <div class="script-card" onclick="openScriptDetail(${s.id})">
      <div class="script-card-header">
        <div class="script-card-name">${esc(s.script_name)}</div>
        <div style="display:flex;gap:6px;align-items:center">
          <span class="badge" style="background:${catColor}22;color:${catColor}">${SCRIPT_CATEGORY_KO[s.category]||s.category}</span>
          <span class="badge" style="background:rgba(56,189,248,.15);color:#38bdf8">${esc(s.target_layer)}</span>
          ${dangerBadge}${approvalBadge}
          ${canEdit ? `<button class="btn btn-secondary" style="font-size:11px;padding:2px 8px" onclick="event.stopPropagation();openScriptModal(${s.id})">수정</button>` : ""}
        </div>
      </div>
      <div class="script-card-desc">${esc(s.description)}</div>
      <div class="script-card-meta">
        <span>버전 ${esc(s.version)}</span>
        <span>사용 ${s.use_count}회</span>
        ${s.tags ? `<span>${esc(s.tags).split(",").map(t=>`#${t.trim()}`).join(" ")}</span>` : ""}
      </div>
    </div>`;
  }).join("");
}

function openScriptDetail(scriptId) {
  const s = _scriptCache.find(x => x.id === scriptId);
  if (!s) return;
  const catColor = SCRIPT_CATEGORY_COLOR[s.category] || "#818cf8";
  const html = `
    <div class="modal-section-title">📜 ${esc(s.script_name)}</div>
    <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px">
      <span class="badge" style="background:${catColor}22;color:${catColor}">${SCRIPT_CATEGORY_KO[s.category]||s.category}</span>
      <span class="badge" style="background:rgba(56,189,248,.15);color:#38bdf8">${esc(s.target_layer)}</span>
      <span class="badge" style="background:rgba(99,102,241,.15);color:#818cf8">${esc(s.os_type)}</span>
      ${s.is_dangerous ? `<span class="badge" style="background:rgba(248,113,113,.15);color:#f87171">⚠ 위험 명령 포함</span>` : ""}
      ${s.requires_approval ? `<span class="badge" style="background:rgba(251,191,36,.15);color:#fbbf24">실행 전 승인 필요</span>` : ""}
    </div>
    <div class="detail-grid">
      <div><span class="detail-label">설명</span><span>${esc(s.description)}</span></div>
      <div><span class="detail-label">버전</span><span>${esc(s.version)}</span></div>
      <div><span class="detail-label">작성자</span><span>${esc(s.author||"-")}</span></div>
      <div><span class="detail-label">사용 횟수</span><span>${s.use_count}회</span></div>
    </div>
    <div class="modal-section-title" style="margin-top:16px">스크립트 내용</div>
    <pre class="code-block">${esc(s.script_body)}</pre>
    ${s.sample_output ? `<div class="modal-section-title" style="margin-top:12px">예상 출력</div><pre class="code-block">${esc(s.sample_output)}</pre>` : ""}
  `;
  document.getElementById("modal-body").innerHTML = html;
  document.getElementById("modal-overlay").classList.remove("hidden");
}

let _scriptEditId = null;
function openScriptModal(scriptId = null) {
  _scriptEditId = scriptId;
  const overlay = document.getElementById("script-modal-overlay");
  const form = document.getElementById("script-form");
  form.reset();
  document.getElementById("script-modal-title").textContent = scriptId ? "스크립트 수정" : "스크립트 등록";
  if (scriptId) {
    const s = _scriptCache.find(x => x.id === scriptId);
    if (s) {
      Object.keys(s).forEach(k => {
        const el = form.elements[k];
        if (el) el.value = s[k] ?? "";
      });
      if (s.is_dangerous) form.elements.is_dangerous.checked = true;
      if (s.requires_approval) form.elements.requires_approval.checked = true;
    }
  }
  overlay.classList.remove("hidden");
}
function closeScriptModal() { document.getElementById("script-modal-overlay").classList.add("hidden"); }

async function submitScriptForm(e) {
  e.preventDefault();
  const form = e.target;
  const data = Object.fromEntries(new FormData(form));
  data.is_dangerous = form.elements.is_dangerous?.checked || false;
  data.requires_approval = form.elements.requires_approval?.checked || false;
  Object.keys(data).forEach(k => { if (data[k] === "") data[k] = null; });
  try {
    let r;
    if (_scriptEditId) {
      r = await authFetch(`/api/shell-scripts/${_scriptEditId}`, {
        method: "PATCH", body: JSON.stringify(data),
      });
    } else {
      r = await authFetch("/api/shell-scripts", {
        method: "POST", body: JSON.stringify(data),
      });
    }
    if (!r.ok) { const err = await r.json(); showToast(err.detail || "저장 실패", "error"); return; }
    showToast("스크립트가 저장됐습니다.", "success");
    closeScriptModal();
    loadScripts();
  } catch { showToast("저장 중 오류가 발생했습니다.", "error"); }
}

/* ══════════════════════════════════════════════════
   작업 타임테이블 뷰
══════════════════════════════════════════════════ */
let _ttCache = [];

const WORK_TYPE_KO = {
  REGULAR_CHECK: "정기점검", PM: "예방정비", SR: "SR작업",
  ADHOC: "수시점검", DEPLOY: "배포", EMERGENCY: "긴급대응",
};
const WORK_TYPE_COLOR = {
  REGULAR_CHECK: "#34d399", PM: "#818cf8", SR: "#38bdf8",
  ADHOC: "#fbbf24", DEPLOY: "#a78bfa", EMERGENCY: "#f87171",
};
const RESULT_STATUS_KO = {
  PENDING: "예정", SUCCESS: "완료", FAILED: "실패",
  PARTIAL: "부분완료", CANCELLED: "취소",
};
const RESULT_STATUS_BADGE = {
  PENDING: "badge-PARSED",
  SUCCESS: "badge-COMPLETED",
  FAILED:  "badge-FAILED_ROLLBACK",
  PARTIAL: "badge-PENDING_APPROVAL",
  CANCELLED: "badge-REJECTED",
};

async function loadTimetable() {
  try {
    const r = await authFetch("/api/timetable?limit=200");
    _ttCache = await r.json();
    // 기관·스크립트 목록 로드 (모달 select 채우기)
    _populateTimetableSelects();
    renderTimetableTable(_ttCache);
  } catch { _ttCache = []; }
}

async function _populateTimetableSelects() {
  // 기관 select
  const instSel = document.getElementById("tt-inst-select");
  if (instSel && instSel.options.length <= 1) {
    if (!_instCache.length) {
      try { const r = await authFetch("/api/institutions"); _instCache = await r.json(); } catch {}
    }
    _instCache.forEach(i => {
      const opt = document.createElement("option");
      opt.value = i.id; opt.textContent = `${i.inst_code} ${i.inst_name}`;
      instSel.appendChild(opt);
    });
  }
  // 스크립트 select
  const scriptSel = document.getElementById("tt-script-select");
  if (scriptSel && scriptSel.options.length <= 1) {
    if (!_scriptCache.length) {
      try { const r = await authFetch("/api/shell-scripts?limit=200"); _scriptCache = await r.json(); } catch {}
    }
    _scriptCache.forEach(s => {
      const opt = document.createElement("option");
      opt.value = s.id; opt.textContent = `[${s.category}] ${s.script_name}`;
      opt.dataset.body = s.script_body;
      scriptSel.appendChild(opt);
    });
  }
}

function fillScriptBody(sel) {
  const opt = sel.options[sel.selectedIndex];
  const ta  = document.querySelector("#tt-form textarea[name='command_or_shell']");
  if (ta && opt?.dataset.body) ta.value = opt.dataset.body;
  else if (ta && !opt?.dataset.body) ta.value = "";
}

function filterTimetable() {
  const kw = (document.getElementById("tt-search")?.value || "").toLowerCase();
  const type = document.getElementById("tt-type-filter")?.value || "";
  const status = document.getElementById("tt-status-filter")?.value || "";
  const filtered = _ttCache.filter(t => {
    const matchKw = !kw || t.title?.toLowerCase().includes(kw) || (t.content||"").toLowerCase().includes(kw);
    const matchType = !type || t.work_type === type;
    const matchStatus = !status || t.result_status === status;
    return matchKw && matchType && matchStatus;
  });
  renderTimetableTable(filtered);
}

function renderTimetableTable(list) {
  const tbody = document.getElementById("tt-tbody");
  if (!tbody) return;
  const canEdit = ["ADMIN", "PM", "ENGINEER"].includes(_userInfo.role || "");
  if (!list.length) {
    tbody.innerHTML = '<tr><td colspan="9" style="text-align:center;color:var(--text-muted);padding:24px">등록된 작업이 없습니다.</td></tr>';
    return;
  }
  const instMap = {};
  _instCache.forEach(i => { instMap[i.id] = i.inst_name; });
  tbody.innerHTML = list.map(t => {
    const typeColor = WORK_TYPE_COLOR[t.work_type] || "#818cf8";
    const instName = t.inst_id ? (instMap[t.inst_id] || "-") : "-";
    return `<tr onclick="openTimetableDetail(${t.id})" style="cursor:pointer">
      <td><span class="badge" style="background:${typeColor}22;color:${typeColor}">${WORK_TYPE_KO[t.work_type]||t.work_type}</span></td>
      <td>${esc(t.title)}</td>
      <td style="color:var(--text-muted)">${esc(instName)}</td>
      <td style="color:var(--text-muted)">${fmtDate(t.scheduled_at)}</td>
      <td style="color:var(--text-muted)">${t.completed_at ? fmtDate(t.completed_at) : "-"}</td>
      <td><span class="badge ${RESULT_STATUS_BADGE[t.result_status]||''}">${RESULT_STATUS_KO[t.result_status]||t.result_status}</span></td>
      <td style="color:var(--text-muted)">${esc(t.assignee||"-")}</td>
      <td style="color:var(--text-muted)">${t.sr_id ? `<a href="#" onclick="event.stopPropagation();openDetail('${esc(t.sr_id)}')">${esc(t.sr_id)}</a>` : "-"}</td>
      <td onclick="event.stopPropagation()">
        ${canEdit ? `<button class="btn btn-secondary" style="font-size:11px;padding:3px 8px" onclick="openTimetableModal(${t.id})">수정</button>` : ""}
      </td>
    </tr>`;
  }).join("");
}

function openTimetableDetail(id) {
  const t = _ttCache.find(x => x.id === id);
  if (!t) return;
  const typeColor = WORK_TYPE_COLOR[t.work_type] || "#818cf8";
  const instMap = {};
  _instCache.forEach(i => { instMap[i.id] = i.inst_name; });
  const instName = t.inst_id ? (instMap[t.inst_id] || "-") : "-";
  const duration = (t.started_at && t.completed_at)
    ? `${Math.ceil((new Date(t.completed_at) - new Date(t.started_at)) / 60000)}분` : "-";
  const html = `
    <div class="modal-section-title">📅 ${esc(t.title)}</div>
    <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px">
      <span class="badge" style="background:${typeColor}22;color:${typeColor}">${WORK_TYPE_KO[t.work_type]||t.work_type}</span>
      <span class="badge ${RESULT_STATUS_BADGE[t.result_status]||''}">${RESULT_STATUS_KO[t.result_status]||t.result_status}</span>
    </div>
    <div class="detail-grid">
      <div><span class="detail-label">기관</span><span>${esc(instName)}</span></div>
      <div><span class="detail-label">처리예정</span><span>${fmtDate(t.scheduled_at)}</span></div>
      <div><span class="detail-label">시작</span><span>${t.started_at ? fmtDate(t.started_at) : "-"}</span></div>
      <div><span class="detail-label">완료</span><span>${t.completed_at ? fmtDate(t.completed_at) : "-"}</span></div>
      <div><span class="detail-label">소요</span><span>${duration}</span></div>
      <div><span class="detail-label">담당자</span><span>${esc(t.assignee||"-")}</span></div>
      <div><span class="detail-label">검토자</span><span>${esc(t.reviewer||"-")}</span></div>
      ${t.sr_id ? `<div><span class="detail-label">SR</span><span>${esc(t.sr_id)}</span></div>` : ""}
    </div>
    <div class="modal-section-title" style="margin-top:14px">처리내용</div>
    <div class="detail-text-block">${esc(t.content)}</div>
    ${t.command_or_shell ? `<div class="modal-section-title" style="margin-top:14px">명령어/쉘</div><pre class="code-block">${esc(t.command_or_shell)}</pre>` : ""}
    ${t.result ? `<div class="modal-section-title" style="margin-top:14px">처리결과</div><div class="detail-text-block">${esc(t.result)}</div>` : ""}
    ${t.note ? `<div class="modal-section-title" style="margin-top:12px">비고</div><div style="color:var(--text-muted);font-size:13px">${esc(t.note)}</div>` : ""}
  `;
  document.getElementById("modal-body").innerHTML = html;
  document.getElementById("modal-overlay").classList.remove("hidden");
}

let _ttEditId = null;
function openTimetableModal(ttId = null) {
  _ttEditId = ttId;
  const overlay = document.getElementById("tt-modal-overlay");
  const form = document.getElementById("tt-form");
  form.reset();
  document.getElementById("tt-modal-title").textContent = ttId ? "작업 수정" : "작업 등록";
  _populateTimetableSelects();
  if (ttId) {
    const t = _ttCache.find(x => x.id === ttId);
    if (t) {
      Object.keys(t).forEach(k => {
        const el = form.elements[k];
        if (!el) return;
        if (k === "scheduled_at" || k === "started_at" || k === "completed_at") {
          el.value = t[k] ? t[k].slice(0,16) : "";
        } else {
          el.value = t[k] ?? "";
        }
      });
    }
  } else {
    // 기본값: 지금부터 1시간 후
    const dt = new Date(Date.now() + 3600000);
    const iso = dt.toISOString().slice(0,16);
    const el = form.elements.scheduled_at;
    if (el) el.value = iso;
  }
  overlay.classList.remove("hidden");
}
function closeTimetableModal() { document.getElementById("tt-modal-overlay").classList.add("hidden"); }

async function submitTimetableForm(e) {
  e.preventDefault();
  const form = e.target;
  const data = Object.fromEntries(new FormData(form));
  // 빈 문자열 → null, 숫자 변환
  Object.keys(data).forEach(k => { if (data[k] === "") data[k] = null; });
  if (data.inst_id) data.inst_id = parseInt(data.inst_id);
  if (data.script_id) data.script_id = parseInt(data.script_id);
  try {
    let r;
    if (_ttEditId) {
      r = await authFetch(`/api/timetable/${_ttEditId}`, {
        method: "PATCH", body: JSON.stringify(data),
      });
    } else {
      r = await authFetch("/api/timetable", {
        method: "POST", body: JSON.stringify(data),
      });
    }
    if (!r.ok) { const err = await r.json(); showToast(err.detail || "저장 실패", "error"); return; }
    showToast("작업이 저장됐습니다.", "success");
    closeTimetableModal();
    loadTimetable();
  } catch { showToast("저장 중 오류가 발생했습니다.", "error"); }
}

async function exportTimetableExcel() {
  const type = document.getElementById("tt-type-filter")?.value || "";
  const status = document.getElementById("tt-status-filter")?.value || "";
  const params = new URLSearchParams();
  if (type)   params.set("work_type", type);
  if (status) params.set("result_status", status);
  try {
    const r = await authFetch(`/api/timetable/export/excel?${params.toString()}`);
    if (!r.ok) { showToast("Excel 생성 실패", "error"); return; }
    const blob = await r.blob();
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement("a");
    const cd   = r.headers.get("Content-Disposition") || "";
    const match = cd.match(/filename\*=UTF-8''(.+)/);
    a.download = match ? decodeURIComponent(match[1]) : "GUARDiA_작업이력.xlsx";
    a.href = url;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
    showToast("Excel 다운로드 완료", "success");
  } catch { showToast("다운로드 중 오류가 발생했습니다.", "error"); }
}

/* ─── Helpers ───────────────────────────────────── */
function esc(s) {
  return String(s ?? "")
    .replace(/&/g, "&amp;").replace(/</g, "&lt;")
    .replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}

function fmtDate(iso) {
  if (!iso) return "";
  try {
    return new Date(iso).toLocaleString("ko-KR", {
      month: "2-digit", day: "2-digit",
      hour: "2-digit", minute: "2-digit",
    });
  } catch { return iso; }
}
