<!doctype html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <%- include('partials/favicon') %>
    <title>회의록 AI - NCue</title>
    <link rel="stylesheet" href="/public/styles.css" />
  </head>
  <body class="meeting-minutes-page">
    <% var mmNow = new Date(); var mmTodayIso = mmNow.getFullYear() + '-' + ('0' + (mmNow.getMonth() + 1)).slice(-2) + '-' + ('0' + mmNow.getDate()).slice(-2); %>
    <div class="app-shell">
      <%- include('partials/nav', { activeMenu: 'ai-explore', adminMode: typeof adminMode !== 'undefined' ? adminMode : false }) %>
      <div class="content-area">
        <header class="topbar">
          <h1>회의록 AI</h1>
          <a class="top-action-link" href="/ai-explore">AI 목록</a>
        </header>
        <main class="container meeting-minutes-main">
          <a href="/ai-explore" class="prompts-back" aria-label="AI 탐색으로 돌아가기">← AI</a>
          <% var hasEmail = typeof meetingUserEmail !== 'undefined' && meetingUserEmail; %>
          <% if (!hasEmail) { %>
          <p class="chat-api-warning" style="margin-bottom: 16px">
            이메일 인증(OPS <code>PROD</code>) 또는 DEV에서 <strong>관리자 모드</strong>, SUPER(데모)에서는 별도 로그인 없이 이용할 수 있습니다. (DEV 테스트: <code>MEETING_DEV_EMAIL</code>)
          </p>
          <% } %>
          <div class="mm-layout">
            <aside class="mm-sidebar panel">
              <h2 class="mm-sidebar-title">내 회의록</h2>
              <button type="button" class="btn-ghost mm-refresh" id="mmListRefresh" <%= hasEmail ? '' : 'disabled' %>>새로고침</button>
              <ul class="mm-meeting-list" id="mmMeetingList" role="list"></ul>
              <p class="mm-list-empty" id="mmListEmpty" hidden>저장된 회의록이 없습니다.</p>
            </aside>
            <div class="mm-workspace">
              <section class="panel mm-prompt-panel mm-prompt-collapsed" id="mmPromptPanel">
                <div class="mm-prompt-panel-head">
                  <h2 id="mmPromptHeading">출력 형식 (프롬프트)</h2>
                  <button type="button" class="mm-prompt-toggle" id="mmPromptToggle" aria-expanded="false" aria-controls="mmPromptBody" aria-labelledby="mmPromptHeading" title="펼치기·접기">
                    <span class="mm-prompt-toggle-icon" aria-hidden="true">▼</span>
                  </button>
                </div>
                <div class="mm-prompt-body" id="mmPromptBody" hidden>
                  <p class="subtitle">회의록 작성 시스템 프롬프트를 수정할 수 있습니다. 저장 후 업무 체크리스트 자동 연동은 액션·후속 항목에서 추출합니다.</p>
                  <div class="mm-prompt-form" id="mmPromptForm">
                    <div class="mm-custom-block">
                      <label class="mm-field-label" for="mmCustomInstr">추가 지시</label>
                      <textarea id="mmCustomInstr" class="mm-textarea mm-custom-instr-textarea" rows="9" placeholder="비워두면 시스템 기본 구조(6개 섹션)로 회의록을 작성합니다. 추가로 지시할 내용이 있을 때만 입력하세요."></textarea>
                    </div>
                    <div class="mm-prompt-actions">
                      <button type="button" class="top-action" id="mmSavePrompt" <%= hasEmail ? '' : 'disabled' %>>프롬프트 저장</button>
                    </div>
                  </div>
                </div>
              </section>

              <section class="panel">
                <div class="mm-tabs" role="tablist">
                  <button type="button" class="mm-tab is-active" id="mmTabText" data-tab="text" role="tab" aria-selected="true" aria-controls="mmPanelText">텍스트 입력</button>
                  <button type="button" class="mm-tab" id="mmTabAudio" data-tab="audio" role="tab" aria-selected="false" aria-controls="mmPanelAudio">음성 파일</button>
                </div>

                <div class="mm-tab-panel mm-field-stack" id="mmPanelText" role="tabpanel" aria-labelledby="mmTabText" aria-hidden="false">
                  <div class="mm-field mm-title-date-field">
                    <div class="mm-title-date-labels">
                      <span class="mm-field-label">제목</span>
                      <span class="mm-field-label">날짜</span>
                    </div>
                    <div class="mm-title-date-box">
                      <input type="text" id="mmTitleText" class="mm-input mm-title-input" placeholder="회의 제목" maxlength="500" <%= hasEmail ? '' : 'disabled' %> />
                      <div class="mm-date-wrap">
                        <input type="date" id="mmDateText" class="mm-date-native" value="<%= mmTodayIso %>" <%= hasEmail ? '' : 'disabled' %> />
                        <button type="button" class="mm-date-trigger" id="mmDateTextBtn" <%= hasEmail ? '' : 'disabled' %> aria-label="달력 열기">▼</button>
                      </div>
                    </div>
                  </div>
                  <div class="mm-field">
                    <div class="mm-minutes-header mm-source-text-header">
                      <label class="mm-field-label" for="mmSourceText">회의 원문</label>
                      <div class="mm-minutes-actions">
                        <button
                          type="button"
                          class="mm-minutes-copy"
                          id="mmSourceTextCopy"
                          title="회의 원문을 클립보드에 복사합니다."
                          aria-label="회의 원문을 클립보드에 복사합니다."
                          <%= hasEmail ? '' : 'disabled' %>
                        >복사</button>
                      </div>
                    </div>
                    <textarea id="mmSourceText" class="mm-textarea" rows="12" placeholder="회의 내용을 붙여 넣거나 입력하세요." <%= hasEmail ? '' : 'disabled' %> aria-label="회의 원문"></textarea>
                  </div>
                  <label class="mm-field mm-field-narrow">
                    <span class="mm-field-label">회의록 생성 모델</span>
                    <select id="mmModelText" class="mm-select" <%= hasEmail ? '' : 'disabled' %>>
                      <option value="gpt-5-mini">gpt-5-mini</option>
                      <option value="gpt-5.4" selected>gpt-5.4</option>
                    </select>
                  </label>
                  <div class="form-actions mm-form-actions">
                    <button type="button" class="top-action" id="mmGenText" <%= hasEmail ? '' : 'disabled' %>>회의록 생성</button>
                  </div>
                </div>

                <div class="mm-tab-panel mm-field-stack mm-audio-panel" id="mmPanelAudio" role="tabpanel" aria-labelledby="mmTabAudio" aria-hidden="true" hidden>
                  <div class="mm-audio-phase">
                    <h3 class="mm-section-heading">1. 음성 전사</h3>
                    <label class="mm-field">
                      <span class="mm-field-label">음성 파일</span>
                      <input type="file" id="mmAudioFile" class="mm-file-input" accept=".mp3,.mp4,.mpeg,.mpga,.m4a,.wav,.webm,.ogg,.flac" <%= hasEmail ? '' : 'disabled' %> />
                    </label>
                    <p class="mm-audio-hint">지원 포맷: mp3, m4a, wav 등. 파일당 최대 300MB입니다.</p>
                    <label class="mm-field mm-transcribe-model-only">
                      <span class="mm-field-label">전사 모델 (OpenAI)</span>
                      <select id="mmWhisperModel" class="mm-select" <%= hasEmail ? '' : 'disabled' %> title="음성→텍스트 API 모델">
                        <option value="gpt-4o-mini-transcribe">gpt-4o-mini-transcribe (경량·더 빠를 수 있음)</option>
                        <option value="gpt-4o-transcribe" selected>gpt-4o-transcribe (기본)</option>
                      </select>
                    </label>
                  </div>
                  <div class="mm-audio-phase">
                    <h3 class="mm-section-heading">2. 회의록 작성</h3>
                    <div class="mm-field mm-title-date-field">
                      <div class="mm-title-date-labels">
                        <span class="mm-field-label">제목</span>
                        <span class="mm-field-label">날짜</span>
                      </div>
                      <div class="mm-title-date-box">
                        <input type="text" id="mmTitleAudio" class="mm-input mm-title-input" placeholder="회의 제목" maxlength="500" <%= hasEmail ? '' : 'disabled' %> />
                        <div class="mm-date-wrap">
                          <input type="date" id="mmDateAudio" class="mm-date-native" value="<%= mmTodayIso %>" <%= hasEmail ? '' : 'disabled' %> />
                          <button type="button" class="mm-date-trigger" id="mmDateAudioBtn" <%= hasEmail ? '' : 'disabled' %> aria-label="달력 열기">▼</button>
                        </div>
                      </div>
                    </div>
                    <label class="mm-field mm-field-narrow">
                      <span class="mm-field-label">회의록 생성 모델</span>
                      <select id="mmModelAudio" class="mm-select" <%= hasEmail ? '' : 'disabled' %>>
                        <option value="gpt-5-mini">gpt-5-mini</option>
                        <option value="gpt-5.4" selected>gpt-5.4</option>
                      </select>
                    </label>
                    <p class="mm-audio-summary-hint">전사된 텍스트를 아래 프롬프트(출력 형식)에 맞게 요약·정리합니다.</p>
                    <div class="form-actions mm-form-actions">
                      <button type="button" class="top-action" id="mmGenAudio" <%= hasEmail ? '' : 'disabled' %>>전사 및 회의록 생성</button>
                    </div>
                  </div>
                </div>
              </section>

              <div id="mmGenProgress" class="mm-gen-progress" hidden role="status" aria-live="polite" aria-busy="false">
                <div id="mmGenAudioPipeline" class="mm-gen-audio-pipeline" hidden aria-label="음성 회의록 처리 단계">
                  <div class="mm-gen-pipeline-steps" role="list">
                    <span
                      class="mm-gen-pipeline-step"
                      id="mmGenStepUpload"
                      data-mm-step-name="업로드"
                      role="listitem"
                      ><strong class="mm-gen-pipeline-step-num">1</strong> 업로드</span>
                    <span class="mm-gen-pipeline-join" aria-hidden="true">&rarr;</span>
                    <span
                      class="mm-gen-pipeline-step"
                      id="mmGenStepTranscribe"
                      data-mm-step-name="전사"
                      role="listitem"
                      ><strong class="mm-gen-pipeline-step-num">2</strong> 전사</span>
                    <span class="mm-gen-pipeline-join" aria-hidden="true">&rarr;</span>
                    <span
                      class="mm-gen-pipeline-step"
                      id="mmGenStepTranslate"
                      data-mm-step-name="번역"
                      role="listitem"
                      ><strong class="mm-gen-pipeline-step-num">3</strong> 번역</span>
                  </div>
                  <p class="mm-gen-pipeline-note" id="mmGenTranslateNote" hidden>
                    「번역」단계에서는 전문 템플릿에 맞게 회의 내용 전체를 LLM으로 서술 형식 회의록으로 작성합니다 (속도 표시 아님).
                  </p>
                </div>
                <div class="mm-gen-progress-track-row">
                  <div class="mm-gen-progress-track" aria-hidden="true">
                    <div class="mm-gen-progress-bar"></div>
                  </div>
                  <span class="mm-gen-progress-pct" id="mmGenProgressPct" hidden></span>
                </div>
                <p class="mm-gen-progress-msg" id="mmGenProgressMsg">처리 중…</p>
              </div>

              <section class="panel mm-result-panel" id="mmResultSection" hidden>
                <div class="mm-result-head">
                  <h2 class="mm-result-title">생성 결과</h2>
                  <button type="button" class="top-action" id="mmSaveResult" disabled>저장</button>
                </div>
                <p class="mm-result-hint">
                  생성 결과에서 <strong>회의록</strong>을 수정한 뒤 <strong>저장</strong>하면 DB에 반영되며, 회의록 기준으로 액션 아이템·체크리스트가 업무 체크리스트 AI에 다시 연동됩니다. 음성 전사가 있는 회의는 <strong>음성 파일</strong> 탭을 선택하면 아래 <strong>전사 기록</strong>도 함께 수정할 수 있습니다. 텍스트 입력으로 만든 회의는 위쪽 <strong>회의 원문</strong>에서 원문을 고칩니다.
                </p>
                <div class="mm-result-split">
                  <div id="mmTranscriptPane" class="mm-result-transcript-pane" hidden aria-hidden="true">
                    <div class="mm-result-field">
                      <div class="mm-minutes-header">
                        <span class="mm-result-field-label" id="mmTranscriptLabel">전사 기록</span>
                        <div class="mm-minutes-actions">
                          <button
                            type="button"
                            class="mm-minutes-copy mm-transcript-copy"
                            id="mmTranscriptCopy"
                            title="전사 기록을 클립보드에 복사합니다."
                            aria-label="전사 기록을 클립보드에 복사합니다."
                          >복사</button>
                          <button type="button" class="top-action mm-minutes-apply" id="mmTranscriptSave" disabled>저장</button>
                        </div>
                      </div>
                      <textarea
                        id="mmTranscriptBody"
                        class="mm-result-body mm-result-textarea mm-result-textarea-half mm-transcript-textarea"
                        rows="14"
                        spellcheck="false"
                        placeholder="음성 전사 텍스트가 여기에 표시됩니다."
                      ></textarea>
                    </div>
                  </div>
                  <div class="mm-result-field">
                    <div class="mm-minutes-header">
                      <span class="mm-result-field-label">회의록</span>
                      <div class="mm-minutes-actions" id="mmMinutesActions" role="toolbar" aria-label="회의록 액션">
                        <button
                          type="button"
                          class="mm-minutes-copy"
                          id="mmMinutesCopy"
                          title="회의록을 복사합니다."
                          aria-label="회의록을 복사합니다."
                        >복사</button>
                        <button type="button" class="btn-ghost mm-minutes-edit" id="mmMinutesEdit">마크다운 편집</button>
                        <button type="button" class="btn-ghost mm-minutes-cancel" id="mmMinutesCancel" hidden>취소</button>
                      </div>
                    </div>
                    <div class="mm-minutes-editor-wrap">
                      <div
                        id="mmMinutesRendered"
                        class="mm-minutes-rendered mm-minutes-rendered-empty"
                        role="region"
                        aria-label="회의록 (마크다운 렌더링)"
                        tabindex="0"
                      >
                        회의록이 없습니다. 마크다운 편집으로 내용을 입력하세요.
                      </div>
                      <textarea
                        id="mmResultBody"
                        class="mm-result-body mm-result-textarea mm-result-textarea-half mm-minutes-source"
                        rows="14"
                        spellcheck="false"
                        hidden
                        placeholder="마크다운으로 회의록을 편집합니다. 「저장」으로 뷰로 돌아가거나 「취소」로 편집 전 내용으로 되돌립니다."
                      ></textarea>
                    </div>
                  </div>
                </div>
              </section>
            </div>
          </div>
        </main>
      </div>
    </div>
    <script src="/vendor/marked/marked.umd.js"></script>
    <script src="/vendor/dompurify/purify.min.js"></script>
    <script>
      (function () {
        var hasEmail = <%= hasEmail ? 'true' : 'false' %>;
        if (!hasEmail) return;

        var listEl = document.getElementById('mmMeetingList');
        var emptyEl = document.getElementById('mmListEmpty');
        var resultSection = document.getElementById('mmResultSection');
        var resultBody = document.getElementById('mmResultBody');
        var minutesRenderedEl = document.getElementById('mmMinutesRendered');
        var minutesEditBtn = document.getElementById('mmMinutesEdit');
        var minutesCopyBtn = document.getElementById('mmMinutesCopy');
        var minutesCancelBtn = document.getElementById('mmMinutesCancel');
        /** 편집 모드 진입 시점의 회의록 원문(취소 시 복원) */
        var minutesEditSnapshot = '';
        var minutesInEditMode = false;
        var transcriptBody = document.getElementById('mmTranscriptBody');
        var transcriptCopyBtn = document.getElementById('mmTranscriptCopy');
        var sourceTextCopyBtn = document.getElementById('mmSourceTextCopy');
        var mmSourceTextEl = document.getElementById('mmSourceText');
        var transcriptSaveBtn = document.getElementById('mmTranscriptSave');
        var transcriptLabel = document.getElementById('mmTranscriptLabel');
        /** true: 음성 전사(transcript_text) 편집, false: 텍스트 회의 원문(source_text) 편집 */
        var lastMeetingTranscriptIsAudio = false;
        var saveResultBtn = document.getElementById('mmSaveResult');
        var genProgressEl = document.getElementById('mmGenProgress');
        var genProgressMsg = document.getElementById('mmGenProgressMsg');
        var genProgressBar = genProgressEl ? genProgressEl.querySelector('.mm-gen-progress-bar') : null;
        var genProgressPct = document.getElementById('mmGenProgressPct');
        var mmAudioPipeEl = document.getElementById('mmGenAudioPipeline');
        var mmGenTranslateNoteEl = document.getElementById('mmGenTranslateNote');
        /** 음성 「전사 및 회의록 생성」 진행 중 디버그 스냅샷 — 콘솔: getMeetingMinutesAudioDiag() */
        var mmAudioJobDiag = { active: false };
        var mmAudioLastUiPhase = 'upload';
        var currentMeetingId = null;

        function escapeHtmlMm(s) {
          var div = document.createElement('div');
          div.textContent = s;
          return div.innerHTML;
        }
        var minutesMarkdownConfigured = false;
        function configureMinutesMarkdown() {
          if (minutesMarkdownConfigured) return;
          minutesMarkdownConfigured = true;
          if (typeof marked !== 'undefined') {
            marked.setOptions({ async: false, breaks: true, gfm: true });
          }
          var domPurify = typeof window !== 'undefined' && window.DOMPurify ? window.DOMPurify : typeof DOMPurify !== 'undefined' ? DOMPurify : null;
          if (domPurify) {
            domPurify.addHook('afterSanitizeAttributes', function (node) {
              if (node.tagName === 'A' && node.hasAttribute('href')) {
                var href = node.getAttribute('href');
                try {
                  var u = new URL(href, window.location.href);
                  if (u.protocol !== 'http:' && u.protocol !== 'https:') {
                    node.removeAttribute('href');
                    return;
                  }
                } catch (e) {
                  node.removeAttribute('href');
                  return;
                }
                node.setAttribute('target', '_blank');
                node.setAttribute('rel', 'noopener noreferrer');
              }
            });
          }
        }
        function getDomPurify() {
          return typeof window !== 'undefined' && window.DOMPurify
            ? window.DOMPurify
            : typeof DOMPurify !== 'undefined'
              ? DOMPurify
              : null;
        }
        function getMarkedParseFn() {
          if (typeof marked === 'undefined') return null;
          if (typeof marked.parse === 'function') return marked.parse.bind(marked);
          if (typeof marked === 'function') return marked;
          return null;
        }
        function renderMinutesMarkdown(text) {
          configureMinutesMarkdown();
          var src = String(text || '');
          var parseFn = getMarkedParseFn();
          var purify = getDomPurify();
          if (!parseFn || !purify) {
            return escapeHtmlMm(src).replace(/\n/g, '<br>');
          }
          try {
            var raw = parseFn(src, { async: false });
            if (raw != null && typeof raw.then === 'function') {
              return escapeHtmlMm(src).replace(/\n/g, '<br>');
            }
            return purify.sanitize(String(raw || ''), { USE_PROFILES: { html: true } });
          } catch (err) {
            return escapeHtmlMm(src).replace(/\n/g, '<br>');
          }
        }
        function refreshMinutesRendered() {
          if (!minutesRenderedEl) return;
          var raw = resultBody ? resultBody.value : '';
          if (!String(raw).trim()) {
            minutesRenderedEl.classList.add('mm-minutes-rendered-empty');
            minutesRenderedEl.innerHTML = '회의록이 없습니다. 마크다운 편집으로 내용을 입력하세요.';
            return;
          }
          minutesRenderedEl.classList.remove('mm-minutes-rendered-empty');
          minutesRenderedEl.innerHTML = renderMinutesMarkdown(raw);
        }
        function setMinutesToolbarMode(editing) {
          if (minutesCancelBtn) minutesCancelBtn.hidden = !editing;
          if (minutesEditBtn) minutesEditBtn.textContent = editing ? '편집 완료' : '마크다운 편집';
          minutesInEditMode = !!editing;
        }
        function setMinutesViewMode(showSource) {
          if (!resultBody || !minutesRenderedEl) return;
          if (!showSource) {
            refreshMinutesRendered();
            resultBody.hidden = true;
            minutesRenderedEl.hidden = false;
            setMinutesToolbarMode(false);
          } else {
            minutesRenderedEl.hidden = true;
            resultBody.hidden = false;
            setMinutesToolbarMode(true);
            try {
              resultBody.focus();
            } catch (e) {}
          }
        }
        function copyTextToClipboard(text, done) {
          var t = String(text || '');
          if (navigator.clipboard && navigator.clipboard.writeText) {
            navigator.clipboard.writeText(t).then(done).catch(function () {
              legacyCopyTextToClipboard(t, done);
            });
            return;
          }
          legacyCopyTextToClipboard(t, done);
        }
        function legacyCopyTextToClipboard(text, done) {
          var ta = document.createElement('textarea');
          ta.value = text;
          ta.setAttribute('readonly', '');
          ta.style.position = 'fixed';
          ta.style.left = '-9999px';
          document.body.appendChild(ta);
          ta.select();
          try {
            document.execCommand('copy');
          } catch (e) {}
          document.body.removeChild(ta);
          if (typeof done === 'function') done();
        }
        if (minutesCopyBtn) {
          minutesCopyBtn.addEventListener('click', function () {
            if (!resultBody) return;
            var md = String(resultBody.value || '');
            if (!md.trim()) {
              alert('복사할 회의록 내용이 없습니다.');
              return;
            }
            var btn = minutesCopyBtn;
            var prev = btn.textContent;
            copyTextToClipboard(md, function () {
              btn.textContent = '복사됨';
              window.setTimeout(function () {
                btn.textContent = prev;
              }, 1600);
            });
          });
        }
        if (transcriptCopyBtn) {
          transcriptCopyBtn.addEventListener('click', function () {
            if (!transcriptBody) return;
            var txt = String(transcriptBody.value || '');
            if (!txt.trim()) {
              alert('복사할 전사 기록이 없습니다.');
              return;
            }
            var btn = transcriptCopyBtn;
            var prev = btn.textContent;
            copyTextToClipboard(txt, function () {
              btn.textContent = '복사됨';
              window.setTimeout(function () {
                btn.textContent = prev;
              }, 1600);
            });
          });
        }
        if (sourceTextCopyBtn && mmSourceTextEl) {
          sourceTextCopyBtn.addEventListener('click', function () {
            var txt = String(mmSourceTextEl.value || '');
            if (!txt.trim()) {
              alert('복사할 회의 원문이 없습니다.');
              return;
            }
            var btn = sourceTextCopyBtn;
            var prev = btn.textContent;
            copyTextToClipboard(txt, function () {
              btn.textContent = '복사됨';
              window.setTimeout(function () {
                btn.textContent = prev;
              }, 1600);
            });
          });
        }
        if (transcriptSaveBtn) {
          transcriptSaveBtn.addEventListener('click', function () {
            if (saveResultBtn && !saveResultBtn.disabled) {
              saveResultBtn.click();
            }
          });
        }
        if (minutesEditBtn) {
          minutesEditBtn.addEventListener('click', function () {
            if (!resultBody) return;
            if (!minutesInEditMode) {
              minutesEditSnapshot = resultBody.value;
              setMinutesViewMode(true);
            } else {
              setMinutesViewMode(false);
            }
          });
        }
        if (minutesCancelBtn) {
          minutesCancelBtn.addEventListener('click', function () {
            if (resultBody) resultBody.value = minutesEditSnapshot;
            setMinutesViewMode(false);
          });
        }

        /** 음성 파일 탭일 때만 생성 결과의 전사 기록 영역 표시(텍스트 입력은 회의 원문과 중복이므로 숨김) */
        function applyTranscriptPaneVisibility() {
          var pane = document.getElementById('mmTranscriptPane');
          var audioTab = document.getElementById('mmTabAudio');
          if (!pane || !audioTab) return;
          var show = audioTab.classList.contains('is-active');
          pane.hidden = !show;
          pane.setAttribute('aria-hidden', show ? 'false' : 'true');
        }

        function setCurrentMeetingId(id) {
          currentMeetingId = id || null;
          if (saveResultBtn) saveResultBtn.disabled = !currentMeetingId;
          if (transcriptSaveBtn) transcriptSaveBtn.disabled = !currentMeetingId;
        }

        function setMeetingGenerating(on, msg) {
          if (genProgressEl) {
            genProgressEl.hidden = !on;
            genProgressEl.setAttribute('aria-busy', on ? 'true' : 'false');
            if (on) genProgressEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
          }
          if (!on) mmSetAudioPipelineVisible(false);
          if (!on) showIndeterminateProgress();
          if (genProgressMsg && on) {
            if (arguments.length >= 2) {
              genProgressMsg.textContent =
                msg === undefined || msg === null ? '' : String(msg);
            } else {
              /** 진행 시작 시 초기 HTML「처리 중…」 등이 다음 업데이트 전까지 보이지 않도록 비움 */
              genProgressMsg.textContent = '';
            }
          }
          var gText = document.getElementById('mmGenText');
          var gAudio = document.getElementById('mmGenAudio');
          if (gText) gText.disabled = !!on;
          if (gAudio) gAudio.disabled = !!on;
          if (saveResultBtn) saveResultBtn.disabled = !!on || !currentMeetingId;
          if (resultBody) resultBody.disabled = !!on;
          if (transcriptBody) transcriptBody.disabled = !!on;
          if (minutesEditBtn) minutesEditBtn.disabled = !!on;
          if (minutesCopyBtn) minutesCopyBtn.disabled = !!on;
          if (minutesCancelBtn) minutesCancelBtn.disabled = !!on;
          if (on && resultBody && minutesRenderedEl) {
            if (!resultBody.hidden) {
              refreshMinutesRendered();
              setMinutesViewMode(false);
            }
          }
        }


        /**
         * fetch ReadableStream SSE 파싱. onStreamHeaders / onUploaded 둘 중 하나로 응답 헤더 직후 1회 콜백.
         */
        function consumeSseFromFetch(response, handlers) {
          var hdr = handlers.onStreamHeaders || handlers.onUploaded;
          hdr && hdr();
          if (!response.body) {
            handlers.onError && handlers.onError(new Error('응답 스트림을 읽을 수 없습니다.'));
            return;
          }
          var reader = response.body.getReader();
          var decoder = new TextDecoder();
          var sseBuffer = '';
          var sseEvent = 'message';

          function parseSseChunk(text) {
            sseBuffer += text;
            var lines = sseBuffer.split('\n');
            sseBuffer = lines.pop();
            for (var i = 0; i < lines.length; i++) {
              var line = lines[i];
              if (line.length && line.charCodeAt(line.length - 1) === 13) line = line.slice(0, -1);
              if (line.startsWith('event: ')) {
                sseEvent = line.slice(7).trim();
              } else if (line.startsWith('data: ')) {
                try {
                  var data = JSON.parse(line.slice(6));
                  handlers.onSseEvent && handlers.onSseEvent(sseEvent, data);
                } catch (_) {}
                sseEvent = 'message';
              }
            }
          }

          function pump() {
            return reader.read().then(function (result) {
              if (result.done) {
                handlers.onEnd && handlers.onEnd();
                return;
              }
              parseSseChunk(decoder.decode(result.value, { stream: true }));
              return pump();
            });
          }
          return pump();
        }

        /** 1단계: multipart 업로드 → JSON { jobId }. onUploadProgress, onJobReady(jobId), onError */
        function postPrepareMeetingAudioJob(url, formData, callbacks) {
          var xhr = new XMLHttpRequest();
          xhr.open('POST', url);
          xhr.withCredentials = true;
          xhr.responseType = 'text';
          xhr.upload.onprogress = function (ev) {
            callbacks.onUploadProgress &&
              callbacks.onUploadProgress(ev.lengthComputable, ev.loaded, ev.total || 0);
          };
          xhr.onerror = function () {
            callbacks.onError && callbacks.onError(new Error('네트워크 오류(업로드)'));
          };
          xhr.onload = function () {
            try {
              if (xhr.status < 200 || xhr.status >= 300) {
                var jFail = null;
                try {
                  jFail = xhr.responseText ? JSON.parse(xhr.responseText) : null;
                } catch (_) {
                  jFail = null;
                }
                var em =
                  (jFail && jFail.error && String(jFail.error).slice(0, 400)) ||
                  (xhr.responseText || '').replace(/\s+/g, ' ').trim().slice(0, 200) ||
                  'HTTP ' + xhr.status;
                callbacks.onError && callbacks.onError(new Error(em));
                return;
              }
              var j = null;
              try {
                j = JSON.parse(xhr.responseText || '');
              } catch (_) {
                callbacks.onError && callbacks.onError(new Error('서버 응답 형식 오류(JSON)'));
                return;
              }
              if (!j || !j.jobId) {
                callbacks.onError && callbacks.onError(new Error('jobId가 응답에 없습니다.'));
                return;
              }
              callbacks.onJobReady && callbacks.onJobReady(typeof j.jobId === 'string' ? j.jobId : String(j.jobId));
            } catch (e) {
              callbacks.onError &&
                callbacks.onError(new Error(e && e.message ? e.message : String(e)));
            }
          };
          xhr.send(formData);
        }

        /** 2단계: GET SSE (fetch 스트림 — 단일 업로드+SSE 연결보다 브라우저/프록시에서 증분 수신 안정적) */
        function streamMeetingAudioJobSse(jobId, handlers) {
          var streamUrl =
            '/api/meeting-minutes/stream-audio/' + encodeURIComponent(jobId);
          return fetch(streamUrl, { method: 'GET', credentials: 'same-origin' })
            .then(function (response) {
              var ct = (response.headers.get('content-type') || '').toLowerCase();
              if (!response.ok) {
                return response
                  .json()
                  .catch(function () {
                    return {};
                  })
                  .then(function (jo) {
                    var msg =
                      (jo && jo.error && String(jo.error).slice(0, 400)) ||
                      'HTTP ' + response.status;
                    handlers.onError && handlers.onError(new Error(msg || 'SSE 연결 실패'));
                  });
              }
              if (ct.indexOf('text/event-stream') === -1) {
                return response.text().then(function (t) {
                  var msg = (t || '').replace(/\s+/g, ' ').trim().slice(0, 200);
                  handlers.onError &&
                    handlers.onError(new Error(msg || 'SSE가 아닙니다.'));
                });
              }
              return consumeSseFromFetch(response, handlers);
            })
            .catch(function (e) {
              handlers.onError && handlers.onError(e);
            });
        }

        /** 진행 바를 실제 퍼센트 모드로 설정 (인라인 스타일로 CSS 캐시 무관하게 동작) */
        function showDeterminedProgress(pct) {
          pct = Math.min(100, Math.max(0, Math.round(pct)));
          if (genProgressBar) {
            genProgressBar.classList.add('mm-gen-progress-bar--determined');
            genProgressBar.style.animation = 'none';
            genProgressBar.style.transform = 'none';
            genProgressBar.style.width = pct + '%';
          }
          if (genProgressPct) {
            genProgressPct.removeAttribute('hidden');
            genProgressPct.hidden = false;
            genProgressPct.textContent = pct + '%';
          }
        }

        /** 진행 바를 인디터미넌트(슬라이딩) 모드로 복귀 */
        function showIndeterminateProgress() {
          if (genProgressBar) {
            genProgressBar.classList.remove('mm-gen-progress-bar--determined');
            genProgressBar.style.animation = '';
            genProgressBar.style.transform = '';
            genProgressBar.style.width = '';
          }
          if (genProgressPct) {
            genProgressPct.hidden = true;
            genProgressPct.textContent = '';
          }
        }

        function mmPipelineStepNodes() {
          return [
            document.getElementById('mmGenStepUpload'),
            document.getElementById('mmGenStepTranscribe'),
            document.getElementById('mmGenStepTranslate'),
          ].filter(Boolean);
        }

        /** 음성 파이프라인 UI 표시 여부 — 텍스트 회의 원문 생성과 구분 */
        function mmSetAudioPipelineVisible(on) {
          if (mmAudioPipeEl) mmAudioPipeEl.hidden = !on;
          if (!on) {
            mmPipelineStepNodes().forEach(function (el) {
              el.classList.remove('is-done', 'is-active', 'is-pending');
              el.removeAttribute('aria-current');
            });
          }
          if (!on && mmGenTranslateNoteEl) mmGenTranslateNoteEl.hidden = true;
        }

        function mmUpdatePipelineStepHighlight(phaseKey) {
          var order = ['upload', 'transcribe', 'translate'];
          var ix = order.indexOf(phaseKey);
          if (ix < 0) ix = 0;
          mmPipelineStepNodes().forEach(function (el, i) {
            el.classList.remove('is-done', 'is-active', 'is-pending');
            el.removeAttribute('aria-current');
            if (i < ix) el.classList.add('is-done');
            else if (i === ix) {
              el.classList.add('is-active');
              el.setAttribute('aria-current', 'step');
            } else el.classList.add('is-pending');
          });
        }

        /** 순차 단계 배지(up/trans/translate)·막대·본문 메시지. pct가 null이면 막대 인디터미넌트. */
        function mmSetAudioGenerationPhase(phaseKey, pct, detailMsg) {
          if (phaseKey) mmAudioLastUiPhase = phaseKey;
          mmUpdatePipelineStepHighlight(phaseKey);
          var pctRounded = null;
          if (
            pct !== null &&
            pct !== undefined &&
            typeof pct === 'number' &&
            !isNaN(pct)
          ) {
            pctRounded = Math.min(100, Math.max(0, Math.round(pct)));
          }
          if (pct === null) {
            showIndeterminateProgress();
          } else if (pctRounded !== null) {
            showDeterminedProgress(pctRounded);
          }
          if (mmGenTranslateNoteEl) mmGenTranslateNoteEl.hidden = phaseKey !== 'translate';
          if (!genProgressMsg) return;
          if (detailMsg !== undefined && detailMsg !== null && detailMsg !== '') {
            genProgressMsg.textContent =
              pctRounded !== null ? pctRounded + '% · ' + detailMsg : detailMsg;
          } else if (pctRounded !== null && phaseKey === 'transcribe') {
            genProgressMsg.textContent =
              pctRounded + '% · 서버 처리 및 전사를 기다리는 중입니다…';
          }
        }

        if (typeof window !== 'undefined') {
          window.getMeetingMinutesAudioDiag = function () {
            if (!mmAudioJobDiag || !mmAudioJobDiag.active) {
              return {
                active: false,
                hintKo:
                  '진행 중인 음성 회의 생성이 없거나 이미 종료되었습니다. 「전사 및 회의록 생성」을 누른 뒤 이 함수를 실행하세요.',
              };
            }
            try {
              return JSON.parse(JSON.stringify(mmAudioJobDiag));
            } catch (e2) {
              return { active: true, cloneError: String(e2 && e2.message) };
            }
          };
        }

        function api(path, opts) {
          return fetch(path, Object.assign({ credentials: 'same-origin' }, opts || {})).then(function (r) {
            var ct = (r.headers.get('content-type') || '').toLowerCase();
            if (ct.indexOf('application/json') === -1) {
              return r.text().then(function (t) {
                var msg = (t || '').replace(/\s+/g, ' ').trim().slice(0, 200);
                throw new Error(msg || 'HTTP ' + r.status + ' ' + (r.statusText || ''));
              });
            }
            return r.json().then(function (j) {
              if (!r.ok) throw new Error(j.error || r.statusText);
              return j;
            });
          });
        }

        function loadPrompt() {
          return api('/api/meeting-minutes/prompt').then(function (d) {
            var p = d.prompt || {};
            var saved = (p.customInstructions && String(p.customInstructions).trim()) || '';
            document.getElementById('mmCustomInstr').value = saved;
          });
        }

        function loadMeetings() {
          return api('/api/meeting-minutes/meetings').then(function (d) {
            var list = d.meetings || [];
            listEl.innerHTML = '';
            if (!list.length) {
              emptyEl.hidden = false;
              return;
            }
            emptyEl.hidden = true;
            list.forEach(function (m) {
              var li = document.createElement('li');
              li.className = 'mm-meeting-item';
              var t = document.createElement('button');
              t.type = 'button';
              t.className = 'mm-meeting-item-btn';
              var title = (m.title || '제목 없음').slice(0, 60);
              var md = m.meetingDate ? String(m.meetingDate).slice(0, 10) : '';
              var mdKo = '';
              if (md && md.length >= 10) {
                var p = md.split('-');
                if (p.length === 3) mdKo = p[0] + '. ' + p[1] + '. ' + p[2] + '.';
              }
              var dt = m.createdAt ? new Date(m.createdAt).toLocaleString('ko-KR') : '';
              t.textContent = title + (mdKo ? ' · ' + mdKo : '') + (dt ? ' · ' + dt : '');
              t.addEventListener('click', function () {
                showMeeting(m.id);
              });
              var del = document.createElement('button');
              del.type = 'button';
              del.className = 'mm-meeting-del';
              del.setAttribute('aria-label', '삭제');
              del.textContent = '×';
              del.addEventListener('click', function (e) {
                e.stopPropagation();
                if (!confirm('이 회의록을 삭제할까요?')) return;
                api('/api/meeting-minutes/meetings/' + encodeURIComponent(m.id), { method: 'DELETE' }).then(loadMeetings);
              });
              li.appendChild(t);
              li.appendChild(del);
              listEl.appendChild(li);
            });
          });
        }

        function showMeeting(id) {
          api('/api/meeting-minutes/meetings/' + encodeURIComponent(id)).then(function (d) {
            var m = d.meeting;
            resultSection.hidden = false;
            setCurrentMeetingId(m.id);
            if (transcriptLabel) {
              transcriptLabel.textContent =
                m.transcriptText && String(m.transcriptText).trim() ? '전사 기록' : '회의 원문';
            }
            lastMeetingTranscriptIsAudio = !!(m.transcriptText && String(m.transcriptText).trim());
            if (transcriptBody) {
              if (lastMeetingTranscriptIsAudio) {
                transcriptBody.value = m.transcriptText != null ? String(m.transcriptText) : '';
              } else {
                transcriptBody.value = m.sourceText != null ? String(m.sourceText) : '';
              }
            }
            resultBody.value = m.generatedMinutes != null ? String(m.generatedMinutes) : '';
            setMinutesViewMode(false);
            applyTranscriptPaneVisibility();
            var title = m.title || '';
            document.getElementById('mmTitleText').value = title;
            document.getElementById('mmTitleAudio').value = title;
            var md = m.meetingDate ? String(m.meetingDate).slice(0, 10) : '';
            document.getElementById('mmDateText').value = md && md.length >= 10 ? md : '';
            document.getElementById('mmDateAudio').value = md && md.length >= 10 ? md : '';
            var src = '';
            if (m.sourceText && String(m.sourceText).trim()) src = m.sourceText;
            else if (m.transcriptText && String(m.transcriptText).trim()) src = m.transcriptText;
            document.getElementById('mmSourceText').value = src;
            if (m.chatModel) {
              var mt = document.getElementById('mmModelText');
              var ma = document.getElementById('mmModelAudio');
              if (mt) {
                for (var i = 0; i < mt.options.length; i++) {
                  if (mt.options[i].value === m.chatModel) {
                    mt.selectedIndex = i;
                    break;
                  }
                }
              }
              if (ma) {
                for (var j = 0; j < ma.options.length; j++) {
                  if (ma.options[j].value === m.chatModel) {
                    ma.selectedIndex = j;
                    break;
                  }
                }
              }
            }
            if (m.transcriptionModel) {
              var wEl = document.getElementById('mmWhisperModel');
              if (wEl) {
                for (var k = 0; k < wEl.options.length; k++) {
                  if (wEl.options[k].value === m.transcriptionModel) {
                    wEl.selectedIndex = k;
                    break;
                  }
                }
              }
            }
          });
        }

        document.getElementById('mmSavePrompt').addEventListener('click', function () {
          var body = {
            includeTitleLine: true,
            includeAttendees: true,
            includeSummary: true,
            includeActionItems: true,
            includeChecklist: false,
            customInstructions: document.getElementById('mmCustomInstr').value
          };
          api('/api/meeting-minutes/prompt', {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(body)
          }).then(function () {
            alert('저장되었습니다.');
          }).catch(function (e) {
            alert(e.message || '저장 실패');
          });
        });

        document.getElementById('mmSaveResult').addEventListener('click', function () {
          if (!currentMeetingId) return;
          setMeetingGenerating(true, '저장 중…');
          var savePayload = { generatedMinutes: resultBody.value };
          if (lastMeetingTranscriptIsAudio) {
            savePayload.transcriptText = transcriptBody ? transcriptBody.value : '';
          } else {
            savePayload.sourceText = transcriptBody ? transcriptBody.value : '';
          }
          api('/api/meeting-minutes/meetings/' + encodeURIComponent(currentMeetingId) + '/save', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(savePayload)
          })
            .then(function (d) {
              if (d.meeting) {
                if (d.meeting.generatedMinutes != null) resultBody.value = d.meeting.generatedMinutes;
                setMinutesViewMode(false);
                if (transcriptBody) {
                  if (lastMeetingTranscriptIsAudio && d.meeting.transcriptText != null) {
                    transcriptBody.value = String(d.meeting.transcriptText);
                  }
                  if (!lastMeetingTranscriptIsAudio && d.meeting.sourceText != null) {
                    transcriptBody.value = String(d.meeting.sourceText);
                    var stEl = document.getElementById('mmSourceText');
                    if (stEl) stEl.value = String(d.meeting.sourceText);
                  }
                }
              }
              loadMeetings();
              var cs = d.checklistSync;
              if (cs && cs.imported > 0) {
                alert('저장되었습니다. 업무 체크리스트에 ' + cs.imported + '건이 반영되었습니다.');
              } else if (cs && cs.extractError && !cs.disabled) {
                alert('저장되었습니다. (체크리스트 자동 연동: ' + cs.extractError + ')');
              } else {
                alert('저장되었습니다.');
              }
            })
            .catch(function (e) {
              alert(e.message || '저장 실패');
            })
            .finally(function () {
              setMeetingGenerating(false);
            });
        });

        document.getElementById('mmListRefresh').addEventListener('click', loadMeetings);

        function wireMeetingDatePicker(btnId, inputId) {
          var btn = document.getElementById(btnId);
          var inp = document.getElementById(inputId);
          if (!btn || !inp) return;
          btn.addEventListener('click', function () {
            if (typeof inp.showPicker === 'function') inp.showPicker();
            else inp.focus();
          });
        }
        wireMeetingDatePicker('mmDateTextBtn', 'mmDateText');
        wireMeetingDatePicker('mmDateAudioBtn', 'mmDateAudio');

        (function wireAudioFileTitleAutofill() {
          var audioFileEl = document.getElementById('mmAudioFile');
          var titleAudioEl = document.getElementById('mmTitleAudio');
          if (!audioFileEl || !titleAudioEl) return;
          function stripExtension(filename) {
            if (!filename) return '';
            var base = String(filename).replace(/\\/g, '/').split('/').pop() || '';
            var i = base.lastIndexOf('.');
            return i > 0 ? base.slice(0, i) : base;
          }
          audioFileEl.addEventListener('change', function () {
            var f = audioFileEl.files && audioFileEl.files[0];
            if (!f || !f.name) return;
            titleAudioEl.value = stripExtension(f.name).slice(0, 500);
          });
        })();

        document.querySelectorAll('.mm-tab').forEach(function (tab) {
          tab.addEventListener('click', function () {
            var name = tab.getAttribute('data-tab');
            document.querySelectorAll('.mm-tab').forEach(function (t) {
              t.classList.toggle('is-active', t === tab);
              t.setAttribute('aria-selected', t === tab ? 'true' : 'false');
            });
            var panelText = document.getElementById('mmPanelText');
            var panelAudio = document.getElementById('mmPanelAudio');
            panelText.hidden = name !== 'text';
            panelAudio.hidden = name !== 'audio';
            panelText.setAttribute('aria-hidden', name !== 'text' ? 'true' : 'false');
            panelAudio.setAttribute('aria-hidden', name !== 'audio' ? 'true' : 'false');
            applyTranscriptPaneVisibility();
          });
        });

        document.getElementById('mmGenText').addEventListener('click', function () {
          var title = (document.getElementById('mmTitleText').value || '').trim();
          if (!title) {
            alert('제목을 입력해 주세요.');
            return;
          }
          var sourceText = (document.getElementById('mmSourceText').value || '').trim();
          if (!sourceText) {
            alert('회의 원문을 입력해 주세요.');
            return;
          }
          setMeetingGenerating(true, '회의 원문을 보내고 있습니다…');
          api('/api/meeting-minutes/generate-text', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              title: title,
              meetingDate: document.getElementById('mmDateText').value,
              sourceText: sourceText,
              model: document.getElementById('mmModelText').value
            })
          })
            .then(function (d) {
              resultSection.hidden = false;
              setCurrentMeetingId(d.meeting && d.meeting.id);
              lastMeetingTranscriptIsAudio = false;
              if (transcriptLabel) transcriptLabel.textContent = '회의 원문';
              if (transcriptBody) transcriptBody.value = (d.meeting && d.meeting.sourceText) || '';
              resultBody.value = (d.meeting && d.meeting.generatedMinutes) || '';
              setMinutesViewMode(false);
              applyTranscriptPaneVisibility();
              loadMeetings();
              var cs = d.checklistSync;
              if (cs && cs.imported > 0) {
                alert('업무 체크리스트에 ' + cs.imported + '건이 자동 반영되었습니다. (업무 체크리스트 AI에서 확인)');
              } else if (cs && cs.extractError && !cs.disabled) {
                console.warn('체크리스트 자동 추출:', cs.extractError);
              }
            })
            .catch(function (e) {
              alert(e.message || '생성 실패');
            })
            .finally(function () {
              setMeetingGenerating(false);
            });
        });

        document.getElementById('mmGenAudio').addEventListener('click', function () {
          var fileInput = document.getElementById('mmAudioFile');
          if (!fileInput.files || !fileInput.files.length) {
            alert('음성 파일을 선택해 주세요.');
            return;
          }
          var audioTitle = (document.getElementById('mmTitleAudio').value || '').trim();
          if (!audioTitle) {
            alert('제목을 입력해 주세요.');
            return;
          }
          var audioFile = fileInput.files[0];

          mmSetAudioPipelineVisible(true);
          mmAudioLastUiPhase = 'upload';
          mmUpdatePipelineStepHighlight('upload');

          var fileSize = audioFile.size || 0;
          /** multipart 경계·헤더 등 보정 분모(multipart 업로드 total 미제공 시) */
          var mpSlop =
            fileSize > 0
              ? Math.min(262144, Math.max(6144, Math.floor(fileSize / 50) + 2048))
              : 0;
          var xhrLoadedBytes = 0;
          var xhrEstimatedTotalBytes =
            fileSize > 0 ? fileSize + mpSlop : 0;
          var xhrKnownUploadTotal = fileSize > 0;
          var uploadStartTime = Date.now();
          var uploadPhase = true;
          var transcribePrepPct = 0;
          var segmentMode = false;

          var lastSseName = '';
          var lastSseAtMs = 0;
          var lastProgressStage = null;
          var lastSegDone = 0;
          var lastSegTotal = 0;

          function mmAudioDiagRefresh() {
            var hintKo = '';
            if (uploadPhase) {
              hintKo =
                '① POST prepare-audio: XHR 업로드·파일 디스크 저장 후 jobId JSON · ② GET stream-audio: fetch SSE로 증분 수신 (단일 업로드+SSE 연결은 일부 브라우저/프록시가 본문을 버퍼링할 수 있습니다).';
            } else if (mmAudioLastUiPhase === 'translate') {
              hintKo = '번역 단계: 전사 결과를 회의록 형식으로 LLM이 작성 중입니다.';
            } else if (segmentMode) {
              hintKo =
                '전사 단계: 세그먼트 처리 중 (' +
                lastSegDone +
                ' / ' +
                lastSegTotal +
                ')';
            } else {
              hintKo =
                '전사 준비: 서버에서 설정·분할 등 — 세그먼트 init 이벤트 전일 수 있습니다.';
            }
            mmAudioJobDiag = {
              active: true,
              startedAtMs: uploadStartTime,
              elapsedMs: Date.now() - uploadStartTime,
              audioFileBytes: fileSize,
              uploadPhase: uploadPhase,
              xhrKnownTotal: xhrKnownUploadTotal,
              xhrUploadedBytes: xhrLoadedBytes,
              xhrEstimatedTotalBytes: xhrEstimatedTotalBytes,
              uiPhase: mmAudioLastUiPhase,
              transcribePrepPct: transcribePrepPct,
              segmentMode: segmentMode,
              transcribeProgress: { done: lastSegDone, total: lastSegTotal },
              lastProgressStage: lastProgressStage,
              lastSseEvent: lastSseName,
              lastSseAtMs: lastSseAtMs || null,
              hintKo: hintKo,
            };
          }

          mmAudioDiagRefresh();

          setMeetingGenerating(true);
          mmSetAudioGenerationPhase(
            'upload',
            0,
            '파일 업로드를 시작합니다(XHR)·전송률은 바이트 기준입니다.'
          );

          var sseError = null;

          function warmTranscriptionAfterPrepare() {
            window.setTimeout(function () {
              transcribePrepPct = 6;
              segmentMode = false;
              mmSetAudioGenerationPhase(
                'transcribe',
                transcribePrepPct,
                '전사: DB 및 프롬프트 로드·음성 준비 중 (이후 세그먼트별 진행률).'
              );
              mmAudioDiagRefresh();
            }, 0);
          }

          function mmMeetingAudioFatal(e) {
            uploadPhase = false;
            mmAudioJobDiag = {
              active: false,
              endedAtMs: Date.now(),
              hintKo: '요청 실패: ' + (e && e.message ? e.message : String(e)),
            };
            alert(e.message || '생성 실패');
            setMeetingGenerating(false);
          }

          var mmAudioStreamHandlers = {
            onSseEvent: function (event, data) {
              try {
                lastSseName = event;
                lastSseAtMs = Date.now();
                if (event === 'progress' && data && data.stage) lastProgressStage = data.stage;
                if (event === 'progress' && data && data.stage === 'transcribe') {
                  lastSegDone = data.done != null ? data.done : 0;
                  lastSegTotal = data.total != null ? data.total : 1;
                }
                if (event === 'accepted') {
                  transcribePrepPct = Math.max(transcribePrepPct, 22);
                  var am = data && data.message ? String(data.message) : '';
                  mmSetAudioGenerationPhase(
                    'transcribe',
                    transcribePrepPct,
                    am || '서버 접수 후 설정 로드 중입니다.'
                  );
                  return;
                }
                if (event === 'heartbeat') {
                  if (!segmentMode) {
                    transcribePrepPct = Math.min(94, transcribePrepPct + 3);
                    mmSetAudioGenerationPhase(
                      'transcribe',
                      transcribePrepPct,
                      '서버 처리 및 전사를 기다리는 중입니다…'
                    );
                  }
                  return;
                }
                if (event === 'progress') {
                  var stage = data.stage;
                  var done = data.done || 0;
                  var total = data.total || 1;
                  if (stage === 'prep') {
                    transcribePrepPct = Math.max(transcribePrepPct, 52);
                    mmSetAudioGenerationPhase(
                      'transcribe',
                      transcribePrepPct,
                      (data.message && String(data.message)) ||
                        '전사 준비(음성 구간 처리·전사 엔진 연결) 중입니다.'
                    );
                    return;
                  }
                  if (stage === 'init') {
                    segmentMode = true;
                    lastSegDone = 0;
                    lastSegTotal = total;
                    mmSetAudioGenerationPhase(
                      'transcribe',
                      0,
                      total > 1
                        ? '전사 시작: 구간별 API 호출 (0 / ' +
                          total +
                          ' 완료) — 이 단계만 100% 기준입니다.'
                        : '전사 API 호출이 시작되었습니다. 이 단계만 100% 기준입니다.'
                    );
                    return;
                  }
                  if (stage === 'transcribe') {
                    var segPct = Math.min(100, Math.round((done / total) * 100));
                    mmSetAudioGenerationPhase(
                      'transcribe',
                      segPct,
                      total > 1
                        ? '구간별 전사 중 (' + done + ' / ' + total + ' 완료)…'
                        : '전사 API 응답 처리 중…'
                    );
                    return;
                  }
                } else if (event === 'generating') {
                  mmSetAudioGenerationPhase(
                    'translate',
                    null,
                    '전사 결과를 회의록 형식(LLM 작성)으로 정리합니다.'
                  );
                } else if (event === 'done') {
                  mmSetAudioGenerationPhase(
                    'translate',
                    100,
                    '회의록 작성·저장 및 체크리스트 후처리까지 완료되었습니다.'
                  );
                  var d = data;
                  resultSection.hidden = false;
                  setCurrentMeetingId(d.meeting && d.meeting.id);
                  lastMeetingTranscriptIsAudio = true;
                  if (transcriptLabel) transcriptLabel.textContent = '전사 기록';
                  if (transcriptBody) transcriptBody.value = (d.meeting && d.meeting.transcriptText) || '';
                  resultBody.value = (d.meeting && d.meeting.generatedMinutes) || '';
                  setMinutesViewMode(false);
                  applyTranscriptPaneVisibility();
                  fileInput.value = '';
                  loadMeetings();
                  var cs = d.checklistSync;
                  if (cs && cs.imported > 0) {
                    alert(
                      '업무 체크리스트에 ' +
                        cs.imported +
                        '건이 자동 반영되었습니다. (업무 체크리스트 AI에서 확인)'
                    );
                  } else if (cs && cs.extractError && !cs.disabled) {
                    console.warn('체크리스트 자동 추출:', cs.extractError);
                  }
                } else if (event === 'error') {
                  sseError = (data && data.message) || '생성 실패';
                }
              } finally {
                mmAudioDiagRefresh();
              }
            },
            onEnd: function () {
              mmAudioJobDiag = {
                active: false,
                endedAtMs: Date.now(),
                hintKo: sseError
                  ? '오류로 종료: ' + sseError
                  : '스트림 종료(정상 완료 또는 서버 연결 종료).',
              };
              if (sseError) alert(sseError);
              setMeetingGenerating(false);
            },
            onError: mmMeetingAudioFatal,
          };

          var fd = new FormData();
          fd.append('audio', audioFile);
          fd.append('title', audioTitle);
          fd.append('meetingDate', document.getElementById('mmDateAudio').value);
          fd.append('model', document.getElementById('mmModelAudio').value);
          fd.append('whisperModel', document.getElementById('mmWhisperModel').value);

          postPrepareMeetingAudioJob('/api/meeting-minutes/prepare-audio', fd, {
            onUploadProgress: function (computable, loaded, total) {
              xhrLoadedBytes = loaded;
              var effTotal = 0;
              if (computable && total > 0) {
                effTotal = total;
              } else if (fileSize > 0) {
                effTotal = fileSize + mpSlop;
              }
              xhrEstimatedTotalBytes = effTotal;
              xhrKnownUploadTotal = !!(effTotal > 0);
              if (!uploadPhase || effTotal <= 0) {
                mmAudioDiagRefresh();
                return;
              }
              var pct = Math.min(99, Math.round((loaded * 100) / Math.max(effTotal, 1)));
              var detail =
                '업로드 중… ' +
                loaded.toLocaleString() +
                ' / 약 ' +
                effTotal.toLocaleString() +
                ' 바이트';
              mmSetAudioGenerationPhase('upload', pct, detail);
              mmAudioDiagRefresh();
            },
            onJobReady: function (jobId) {
              uploadPhase = false;
              mmSetAudioGenerationPhase(
                'upload',
                100,
                '업로드를 마쳤습니다. 전사 스트림에 연결합니다…'
              );
              mmAudioDiagRefresh();
              warmTranscriptionAfterPrepare();
              streamMeetingAudioJobSse(jobId, mmAudioStreamHandlers);
            },
            onError: mmMeetingAudioFatal,
          });
        });

        applyTranscriptPaneVisibility();
        loadPrompt().then(loadMeetings).catch(function () {});
      })();
    </script>
    <script>
      (function () {
        var panel = document.getElementById('mmPromptPanel');
        var btn = document.getElementById('mmPromptToggle');
        var body = document.getElementById('mmPromptBody');
        if (!panel || !btn || !body) return;
        function applyOpen(open) {
          if (open) {
            panel.classList.remove('mm-prompt-collapsed');
            body.hidden = false;
            btn.setAttribute('aria-expanded', 'true');
          } else {
            panel.classList.add('mm-prompt-collapsed');
            body.hidden = true;
            btn.setAttribute('aria-expanded', 'false');
          }
        }
        try {
          if (localStorage.getItem('meetingMinutesPromptOpen') === '1') {
            applyOpen(true);
          }
        } catch (e) {}
        btn.addEventListener('click', function () {
          var nowCollapsed = panel.classList.toggle('mm-prompt-collapsed');
          body.hidden = nowCollapsed;
          btn.setAttribute('aria-expanded', String(!nowCollapsed));
          try {
            localStorage.setItem('meetingMinutesPromptOpen', nowCollapsed ? '0' : '1');
          } catch (e) {}
        });
      })();
    </script>
  </body>
</html>
