// src/features/gen-html-multiple/infrastructure/ui.ts
function getGenHtmlMultipleUiHtml() {
  return `<!doctype html>
<html lang="ko">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>ju-hong-e /gen-html-multiple</title>
    <style>
      :root {
        --bg: #f4efe7;
        --panel: rgba(255, 252, 247, 0.92);
        --line: rgba(33, 37, 41, 0.12);
        --text: #1e252b;
        --muted: #56616c;
        --accent: #0f766e;
        --accent-strong: #115e59;
        --danger: #b42318;
        --warning: #b54708;
        --done: #027a48;
        --shadow: 0 24px 60px rgba(32, 38, 44, 0.08);
      }

      * {
        box-sizing: border-box;
      }

      body {
        margin: 0;
        font-family: "SF Pro Display", "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;
        color: var(--text);
        background:
          radial-gradient(circle at top left, rgba(15, 118, 110, 0.14), transparent 32%),
          radial-gradient(circle at top right, rgba(180, 71, 8, 0.12), transparent 28%),
          linear-gradient(180deg, #f7f1e6 0%, #f2ece2 52%, #ebe3d8 100%);
      }

      a {
        color: inherit;
      }

      .page {
        min-height: 100vh;
        padding: 28px;
      }

      .hero {
        display: flex;
        flex-wrap: wrap;
        gap: 16px;
        justify-content: space-between;
        align-items: flex-start;
        margin-bottom: 20px;
      }

      .hero__title {
        margin: 0;
        font-size: clamp(28px, 4vw, 46px);
        line-height: 1.02;
        letter-spacing: -0.04em;
      }

      .hero__subtitle {
        margin: 10px 0 0;
        max-width: 780px;
        color: var(--muted);
        font-size: 15px;
        line-height: 1.6;
      }

      .hero__status {
        min-width: 260px;
        padding: 18px 20px;
        border: 1px solid var(--line);
        border-radius: 22px;
        background: var(--panel);
        box-shadow: var(--shadow);
      }

      .status-badge {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 8px 12px;
        border-radius: 999px;
        font-size: 13px;
        font-weight: 700;
        letter-spacing: 0.01em;
        background: rgba(15, 118, 110, 0.1);
        color: var(--accent-strong);
      }

      .status-badge.is-running {
        background: rgba(180, 71, 8, 0.12);
        color: var(--warning);
      }

      .status-badge.is-completed {
        background: rgba(2, 122, 72, 0.12);
        color: var(--done);
      }

      .status-badge.is-closed {
        background: rgba(100, 116, 139, 0.14);
        color: #475569;
      }

      .shutdown-banner {
        margin: 0 0 12px;
        padding: 12px 16px;
        border-radius: 10px;
        background: rgba(180, 35, 24, 0.08);
        border: 1px solid rgba(180, 35, 24, 0.25);
        color: #b42318;
        font-size: 13px;
        font-weight: 600;
        line-height: 1.5;
      }

      .hero__status dl {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 10px 14px;
        margin: 16px 0 0;
      }

      .hero__status dt {
        color: var(--muted);
        font-size: 12px;
      }

      .hero__status dd {
        margin: 4px 0 0;
        font-size: 16px;
        font-weight: 700;
      }

      .layout {
        display: grid;
        gap: 18px;
        grid-template-columns: minmax(320px, 0.92fr) minmax(380px, 1.08fr);
      }

      .panel {
        border: 1px solid var(--line);
        border-radius: 28px;
        background: var(--panel);
        box-shadow: var(--shadow);
      }

      .panel__header {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        justify-content: space-between;
        gap: 12px;
        padding: 22px 22px 14px;
      }

      .panel__title {
        margin: 0;
        font-size: 18px;
        font-weight: 800;
        letter-spacing: -0.03em;
      }

      .panel__body {
        padding: 0 22px 22px;
      }

      .panel__actions {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
      }

      .run-controls {
        display: inline-flex;
        align-items: center;
        gap: 10px;
        padding-left: 12px;
        border-left: 1px solid rgba(30, 37, 43, 0.14);
      }

      .concurrency-control {
        display: inline-flex;
        align-items: center;
        gap: 8px;
        padding: 6px 6px 6px 12px;
        border-radius: 18px;
        background: rgba(30, 37, 43, 0.07);
      }

      .concurrency-control__label {
        font-size: 12px;
        font-weight: 700;
        color: var(--muted);
        white-space: nowrap;
        padding-right: 2px;
      }

      .concurrency-control button {
        min-width: 38px;
        padding: 8px 10px;
      }

      .concurrency-control__value {
        min-width: 34px;
        text-align: center;
        font-weight: 900;
        color: var(--accent-strong);
      }

      button {
        border: 0;
        border-radius: 16px;
        padding: 11px 15px;
        font: inherit;
        font-weight: 700;
        color: white;
        background: var(--accent);
        cursor: pointer;
        transition: transform 120ms ease, background 120ms ease, opacity 120ms ease;
      }

      button:hover {
        background: var(--accent-strong);
        transform: translateY(-1px);
      }

      button:disabled {
        cursor: not-allowed;
        opacity: 0.5;
        transform: none;
      }

      .button-secondary {
        color: var(--text);
        background: rgba(30, 37, 43, 0.08);
      }

      .button-secondary:hover {
        background: rgba(30, 37, 43, 0.14);
      }

      .button-danger {
        background: var(--danger);
      }

      .button-danger:hover {
        background: #912018;
      }

      .component-list {
        display: grid;
        gap: 16px;
      }

      .component-card {
        border: 1px solid rgba(30, 37, 43, 0.09);
        border-radius: 24px;
        background: rgba(255, 255, 255, 0.66);
        padding: 16px;
      }

      .component-card__header {
        display: flex;
        justify-content: space-between;
        gap: 10px;
        align-items: center;
        margin-bottom: 14px;
      }

      .component-card__title {
        margin: 0;
        font-size: 14px;
        font-weight: 800;
        color: var(--muted);
        text-transform: uppercase;
        letter-spacing: 0.08em;
      }

      .field-label {
        display: block;
        margin-bottom: 8px;
        font-size: 13px;
        font-weight: 700;
        color: var(--muted);
      }

      .field {
        width: 100%;
        border: 1px solid rgba(30, 37, 43, 0.12);
        border-radius: 14px;
        background: rgba(255, 255, 255, 0.96);
        padding: 12px 13px;
        font: inherit;
        color: var(--text);
      }

      .field:focus {
        outline: 2px solid rgba(15, 118, 110, 0.16);
        border-color: rgba(15, 118, 110, 0.42);
      }

      .field-hint {
        margin: 4px 0 10px;
        font-size: 12px;
        line-height: 1.5;
        color: var(--muted);
      }

      .reference-list {
        display: grid;
        gap: 12px;
      }

      .reference-row {
        display: grid;
        gap: 10px;
        grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) auto;
        align-items: end;
        border-radius: 18px;
        padding: 12px;
        background: rgba(244, 239, 231, 0.88);
      }

      .reference-row__remove {
        min-width: 56px;
      }

      .runtime-list {
        display: grid;
        gap: 14px;
      }

      .runtime-card {
        border: 1px solid rgba(30, 37, 43, 0.1);
        border-radius: 22px;
        background: rgba(255, 255, 255, 0.72);
        overflow: hidden;
      }

      .runtime-card__header {
        display: flex;
        flex-wrap: wrap;
        justify-content: space-between;
        gap: 10px;
        align-items: center;
        padding: 14px 16px 10px;
        border-bottom: 1px solid rgba(30, 37, 43, 0.08);
      }

      .runtime-card__name {
        margin: 0;
        font-size: 17px;
        font-weight: 800;
        letter-spacing: -0.03em;
      }

      .runtime-card__meta {
        display: flex;
        flex-wrap: wrap;
        gap: 8px;
        font-size: 12px;
        color: var(--muted);
      }

      .runtime-card__status {
        padding: 6px 10px;
        border-radius: 999px;
        font-size: 12px;
        font-weight: 800;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        background: rgba(86, 97, 108, 0.12);
        color: var(--muted);
      }

      .runtime-card__status[data-status="running"] {
        background: rgba(180, 71, 8, 0.14);
        color: var(--warning);
      }

      .runtime-card__status[data-status="refreshing"] {
        background: rgba(15, 118, 110, 0.14);
        color: var(--accent-strong);
      }

      .runtime-card__status[data-status="completed"] {
        background: rgba(2, 122, 72, 0.12);
        color: var(--done);
      }

      .runtime-card__status[data-status="failed"] {
        background: rgba(180, 35, 24, 0.14);
        color: var(--danger);
      }

      .runtime-card__status[data-status="cancelled"] {
        background: rgba(86, 97, 108, 0.16);
        color: var(--muted);
      }

      .runtime-card__actions {
        display: flex;
        align-items: center;
        gap: 8px;
      }

      .runtime-card__cancel {
        padding: 6px 12px;
        border-radius: 999px;
        border: 1px solid var(--danger);
        background: transparent;
        color: var(--danger);
        font-size: 12px;
        font-weight: 800;
        cursor: pointer;
      }

      .runtime-card__cancel:hover {
        background: rgba(180, 35, 24, 0.1);
      }

      .runtime-card__cancel:disabled {
        opacity: 0.5;
        cursor: not-allowed;
      }

      .runtime-card__body {
        padding: 14px 16px 16px;
      }

      .runtime-card__links {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        margin-bottom: 10px;
      }

      .runtime-card__link {
        font-size: 13px;
        font-weight: 700;
        color: var(--accent-strong);
        text-decoration: none;
      }

      .runtime-card__link:hover {
        text-decoration: underline;
      }

      .runtime-card__files {
        margin-bottom: 10px;
        padding: 10px 12px;
        border-radius: 12px;
        background: rgba(2, 122, 72, 0.06);
        border: 1px solid rgba(2, 122, 72, 0.18);
      }

      .runtime-card__files-title {
        font-size: 12px;
        font-weight: 700;
        color: var(--accent-strong);
        margin-bottom: 6px;
      }

      .runtime-card__file-list {
        margin: 0;
        padding-left: 18px;
        display: grid;
        gap: 3px;
      }

      .runtime-card__file {
        font-family: "SF Mono", "JetBrains Mono", monospace;
        font-size: 12px;
        line-height: 1.5;
        color: var(--text);
        word-break: break-all;
      }

      .runtime-card__log {
        margin: 0;
        min-height: 180px;
        max-height: 360px;
        overflow: auto;
        padding: 14px;
        border-radius: 16px;
        background: #132128;
        color: #c2f7ea;
        font-family: "SF Mono", "JetBrains Mono", monospace;
        font-size: 12px;
        line-height: 1.55;
        white-space: pre-wrap;
      }

      .empty {
        padding: 24px;
        border: 1px dashed rgba(30, 37, 43, 0.16);
        border-radius: 20px;
        color: var(--muted);
        text-align: center;
      }

      .notice {
        margin-top: 14px;
        padding: 12px 14px;
        border-radius: 16px;
        background: rgba(15, 118, 110, 0.09);
        color: var(--accent-strong);
        font-size: 13px;
        line-height: 1.5;
      }

      @media (max-width: 1024px) {
        .layout {
          grid-template-columns: 1fr;
        }
      }

      @media (max-width: 720px) {
        .page {
          padding: 18px;
        }

        .reference-row {
          grid-template-columns: 1fr;
        }

        .panel__header,
        .panel__body {
          padding-left: 16px;
          padding-right: 16px;
        }
      }
    </style>
  </head>
  <body>
    <div class="page">
      <section class="hero">
        <div>
          <h1 class="hero__title">/gen-html-multiple</h1>
          <p class="hero__subtitle">
            \uCEF4\uD3EC\uB10C\uD2B8\uBCC4\uB85C \uC5EC\uB7EC Figma \uB9C1\uD06C\uC640 \uC124\uBA85\uC744 \uBB36\uC5B4 \uB3D9\uC2DC \uC0DD\uC131\uD569\uB2C8\uB2E4. artifact \uC0DD\uC131\uC740 \uC9C0\uC815\uD55C \uAC1C\uC218\uB9CC\uD07C \uBCD1\uB82C\uB85C \uC9C4\uD589\uD558\uACE0,
            shared Storybook preview \uBC18\uC601\uC740 \uCDA9\uB3CC\uC744 \uD53C\uD558\uAE30 \uC704\uD574 \uC9C1\uB82C \uCC98\uB9AC\uD569\uB2C8\uB2E4.
          </p>
        </div>
        <div class="hero__status">
          <div id="overall-status-badge" class="status-badge">\uB300\uAE30 \uC911</div>
          <dl>
            <div>
              <dt>\uC804\uCCB4 \uC0C1\uD0DC</dt>
              <dd id="overall-status-text">idle</dd>
            </div>
            <div>
              <dt>\uBC30\uCE58 \uC218</dt>
              <dd id="batch-count-text">0</dd>
            </div>
            <div>
              <dt>\uB3D9\uC2DC \uC2E4\uD589</dt>
              <dd id="concurrency-text">1</dd>
            </div>
            <div>
              <dt>\uB300\uAE30/\uC2E4\uD589</dt>
              <dd id="queue-running-text">0 / 0</dd>
            </div>
            <div>
              <dt>\uC131\uACF5</dt>
              <dd id="success-count-text">0</dd>
            </div>
            <div>
              <dt>\uC2E4\uD328</dt>
              <dd id="failure-count-text">0</dd>
            </div>
            <div>
              <dt>\uC7AC\uC2DC\uC791 \uD544\uC694</dt>
              <dd id="restart-required-text">false</dd>
            </div>
          </dl>
          <div class="notice">
            \uC774 \uD654\uBA74\uC744 \uC0AC\uC6A9\uD558\uB294 \uB3D9\uC548 \uD130\uBBF8\uB110 \uC138\uC158\uC740 \uC720\uC9C0\uB418\uC5B4\uC57C \uD569\uB2C8\uB2E4. \uC0DD\uC131\uC774 \uB05D\uB098\uBA74 \uD558\uB2E8 \uC885\uB8CC \uBC84\uD2BC\uC73C\uB85C \uC11C\uBC84\uB97C \uB2EB\uC744 \uC218 \uC788\uC2B5\uB2C8\uB2E4.
          </div>
        </div>
      </section>

      <section class="layout">
        <article class="panel">
          <div class="panel__header">
            <h2 class="panel__title">\uC785\uB825 \uD3FC</h2>
            <div class="panel__actions">
              <button id="add-component-button" type="button" class="button-secondary" title="\uC785\uB825 \uD3FC\uC5D0 \uCEF4\uD3EC\uB10C\uD2B8 \uCE74\uB4DC\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4">+ \uCEF4\uD3EC\uB10C\uD2B8 \uCD94\uAC00</button>
              <div class="run-controls">
                <div class="concurrency-control" aria-label="\uB3D9\uC2DC \uC2E4\uD589 \uAC1C\uC218" title="\uD55C \uBC88\uC5D0 \uB3D9\uC2DC\uC5D0 \uC0DD\uC131\uD560 \uCEF4\uD3EC\uB10C\uD2B8 \uC218">
                  <span class="concurrency-control__label">\uB3D9\uC2DC \uC2E4\uD589 \uC218</span>
                  <button id="decrease-concurrency-button" type="button" class="button-secondary">-</button>
                  <span id="concurrency-value" class="concurrency-control__value">1</span>
                  <button id="increase-concurrency-button" type="button" class="button-secondary">+</button>
                </div>
                <button id="submit-button" type="button">\uC804\uCCB4 \uC77C\uAD04 \uC0DD\uC131</button>
              </div>
            </div>
          </div>
          <div class="panel__body">
            <div id="component-list" class="component-list"></div>
          </div>
        </article>

        <article class="panel">
          <div class="panel__header">
            <h2 class="panel__title">\uC9C4\uD589 \uD604\uD669</h2>
            <div class="panel__actions">
              <a id="storybook-link" class="runtime-card__link" href="#" target="_blank" rel="noreferrer">Storybook \uC5F4\uAE30</a>
              <button id="shutdown-button" type="button" class="button-danger" title="\uB300\uAE30 \uC911\uC778 \uC791\uC5C5\uC744 \uBAA8\uB450 \uC911\uB2E8\uD558\uACE0 gen-html-multiple \uC11C\uBC84\uB97C \uB2EB\uC2B5\uB2C8\uB2E4.">\uC804\uCCB4 \uC885\uB8CC (\uC11C\uBC84 \uB2EB\uAE30)</button>
            </div>
          </div>
          <div class="panel__body">
            <div id="shutdown-banner" class="shutdown-banner" hidden>\uC11C\uBC84\uAC00 \uC885\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC9C4\uD589 \uC911\uC774\uB358 \uC791\uC5C5\uC740 \uBAA8\uB450 \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC774 \uD0ED\uC740 \uB2EB\uC544\uB3C4 \uB429\uB2C8\uB2E4.</div>
            <div id="runtime-list" class="runtime-list">
              <div class="empty">\uC544\uC9C1 \uC2E4\uD589\uB41C \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</div>
            </div>
          </div>
        </article>
      </section>
    </div>

    <template id="component-card-template">
      <section class="component-card" data-component-card>
        <div class="component-card__header">
          <h3 class="component-card__title">Component</h3>
          <button type="button" class="button-secondary" data-remove-component>\uC0AD\uC81C</button>
        </div>
        <label class="field-label">
          \uCEF4\uD3EC\uB10C\uD2B8\uBA85
          <input class="field" type="text" data-component-name placeholder="\uC608: email-send-modal" />
        </label>
        <div class="field-label">Figma \uCC38\uC870 \uBAA9\uB85D (\uC0C1\uD0DC\uBCC4 \uB9C1\uD06C)</div>
        <p class="field-hint">\uAC19\uC740 \uCEF4\uD3EC\uB10C\uD2B8\uC758 \uC5EC\uB7EC \uC0C1\uD0DC(\uAE30\uBCF8 / \uC5D0\uB7EC / hover / \uC785\uB825\uAC12 \uB4F1)\uB97C Figma \uB9C1\uD06C\uB85C \uD568\uAED8 \uCD94\uAC00\uD558\uBA74, AI\uAC00 \uC0C1\uD0DC \uAC04 \uCC28\uC774\uB97C \uBD84\uC11D\uD574 \uC0C1\uD0DC \uC804\uD658\xB7\uC774\uBCA4\uD2B8 \uD578\uB4E4\uB7EC\uAE4C\uC9C0 \uC790\uB3D9\uC73C\uB85C \uC791\uC131\uD569\uB2C8\uB2E4. \uB2E8\uC77C \uC0C1\uD0DC\uBA74 \uB9C1\uD06C 1\uAC1C\uB9CC \uC785\uB825\uD558\uC138\uC694.</p>
        <div class="reference-list" data-reference-list></div>
        <div class="panel__actions" style="margin-top: 12px;">
          <button type="button" class="button-secondary" data-add-reference title="\uAC19\uC740 \uCEF4\uD3EC\uB10C\uD2B8\uC758 \uB2E4\uB978 \uC0C1\uD0DC\uB97C \uAC00\uB9AC\uD0A4\uB294 Figma \uB9C1\uD06C\uB97C \uCD94\uAC00\uD569\uB2C8\uB2E4">+ \uC0C1\uD0DC(\uB9C1\uD06C) \uCD94\uAC00</button>
        </div>
      </section>
    </template>

    <template id="reference-row-template">
      <div class="reference-row" data-reference-row>
        <label class="field-label" style="margin-bottom: 0;">
          Figma \uB9C1\uD06C
          <input class="field" type="text" data-figma-address placeholder="https://www.figma.com/design/..." />
        </label>
        <label class="field-label" style="margin-bottom: 0;">
          \uC124\uBA85
          <input class="field" type="text" data-reference-description placeholder="\uC608: \uAE30\uBCF8 \uC0C1\uD0DC / \uC5D0\uB7EC \uC0C1\uD0DC / \uCF54\uBA58\uD2B8" />
        </label>
        <button type="button" class="button-secondary reference-row__remove" data-remove-reference>\uC0AD\uC81C</button>
      </div>
    </template>

    <script>
      const componentList = document.getElementById('component-list');
      const runtimeList = document.getElementById('runtime-list');
      const submitButton = document.getElementById('submit-button');
      const shutdownButton = document.getElementById('shutdown-button');
      const addComponentButton = document.getElementById('add-component-button');
      const decreaseConcurrencyButton = document.getElementById('decrease-concurrency-button');
      const increaseConcurrencyButton = document.getElementById('increase-concurrency-button');
      const concurrencyValue = document.getElementById('concurrency-value');
      const storybookLink = document.getElementById('storybook-link');
      const componentCardTemplate = document.getElementById('component-card-template');
      const referenceRowTemplate = document.getElementById('reference-row-template');
      const statusBadge = document.getElementById('overall-status-badge');
      const statusText = document.getElementById('overall-status-text');
      const batchCountText = document.getElementById('batch-count-text');
      const concurrencyText = document.getElementById('concurrency-text');
      const queueRunningText = document.getElementById('queue-running-text');
      const successCountText = document.getElementById('success-count-text');
      const failureCountText = document.getElementById('failure-count-text');
      const restartRequiredText = document.getElementById('restart-required-text');
      const shutdownBanner = document.getElementById('shutdown-banner');
      let currentState = null;
      let desiredConcurrency = 1;
      let shutdownRequested = false;
      let statePollingTimerId = null;

      function clampConcurrency(value) {
        return Math.max(1, Math.min(6, Math.floor(Number(value) || 1)));
      }

      function escapeHtml(value) {
        return String(value || '')
          .replaceAll('&', '&amp;')
          .replaceAll('<', '&lt;')
          .replaceAll('>', '&gt;')
          .replaceAll('"', '&quot;')
          .replaceAll("'", '&#39;');
      }

      function escapeAttribute(value) {
        return escapeHtml(value);
      }

      function updateConcurrencyView() {
        concurrencyValue.textContent = String(desiredConcurrency);
        concurrencyText.textContent = String(desiredConcurrency);
        decreaseConcurrencyButton.disabled = desiredConcurrency <= 1;
        increaseConcurrencyButton.disabled = desiredConcurrency >= 6;
      }

      function createReferenceRow() {
        return referenceRowTemplate.content.firstElementChild.cloneNode(true);
      }

      function addReferenceRow(referenceList, value) {
        const row = createReferenceRow();
        const addressInput = row.querySelector('[data-figma-address]');
        const descriptionInput = row.querySelector('[data-reference-description]');
        const removeButton = row.querySelector('[data-remove-reference]');

        addressInput.value = value?.figmaAddress || '';
        descriptionInput.value = value?.description || '';
        removeButton.addEventListener('click', () => {
          row.remove();
        });

        referenceList.appendChild(row);
      }

      function addComponentCard(value) {
        const card = componentCardTemplate.content.firstElementChild.cloneNode(true);
        const title = card.querySelector('.component-card__title');
        const nameInput = card.querySelector('[data-component-name]');
        const removeButton = card.querySelector('[data-remove-component]');
        const addReferenceButton = card.querySelector('[data-add-reference]');
        const referenceList = card.querySelector('[data-reference-list]');

        nameInput.value = value?.componentName || '';
        title.textContent = value?.componentName || 'Component';
        nameInput.addEventListener('input', () => {
          title.textContent = nameInput.value.trim() || 'Component';
        });
        removeButton.addEventListener('click', () => {
          card.remove();
        });
        addReferenceButton.addEventListener('click', () => {
          addReferenceRow(referenceList);
        });

        const references = Array.isArray(value?.references) && value.references.length > 0
          ? value.references
          : [{ figmaAddress: '', description: '' }];

        references.forEach((reference) => addReferenceRow(referenceList, reference));
        componentList.appendChild(card);
      }

      function collectComponents() {
        return Array.from(componentList.querySelectorAll('[data-component-card]'))
          .map((card) => {
            const componentName = card.querySelector('[data-component-name]').value.trim();
            const references = Array.from(card.querySelectorAll('[data-reference-row]'))
              .map((row) => ({
                description: row.querySelector('[data-reference-description]').value.trim(),
                figmaAddress: row.querySelector('[data-figma-address]').value.trim()
              }))
              .filter((reference) => reference.figmaAddress);

            return { componentName, references };
          })
          .filter((component) => component.componentName || component.references.length > 0);
      }

      function renderStatusBadge(status) {
        statusBadge.className = 'status-badge';
        if (status === 'running') {
          statusBadge.classList.add('is-running');
          statusBadge.textContent = '\uC2E4\uD589 \uC911';
          return;
        }
        if (status === 'completed') {
          statusBadge.classList.add('is-completed');
          statusBadge.textContent = '\uC804\uCCB4 \uC644\uB8CC';
          return;
        }
        if (status === 'closed') {
          statusBadge.classList.add('is-closed');
          statusBadge.textContent = '\uC885\uB8CC\uB428';
          return;
        }
        statusBadge.textContent = '\uB300\uAE30 \uC911';
      }

      function renderRuntimeState(state) {
        if (!state || !Array.isArray(state.components) || state.components.length === 0) {
          runtimeList.innerHTML = '<div class="empty">\uC544\uC9C1 \uC2E4\uD589\uB41C \uCEF4\uD3EC\uB10C\uD2B8\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</div>';
          return;
        }

        const logScrollPositions = new Map(
          Array.from(runtimeList.querySelectorAll('[data-runtime-card]')).map((card) => {
            const componentId = card.getAttribute('data-component-id') || '';
            const logElement = card.querySelector('.runtime-card__log');

            return [componentId, logElement ? logElement.scrollTop : 0];
          })
        );

        runtimeList.innerHTML = state.components.map((component) => {
          const links = [
            component.previewUrl ? '<a class="runtime-card__link" target="_blank" rel="noreferrer" href="' + escapeAttribute(component.previewUrl) + '">Preview</a>' : '',
            component.historyPath ? '<span class="runtime-card__meta">history: ' + escapeHtml(component.historyPath) + '</span>' : '',
            component.errorReportPath ? '<span class="runtime-card__meta">error report: ' + escapeHtml(component.errorReportPath) + '</span>' : '',
            component.outputPath ? '<span class="runtime-card__meta">output: ' + escapeHtml(component.outputPath) + '</span>' : '',
            component.restartRequired ? '<span class="runtime-card__meta">restart required</span>' : ''
          ].filter(Boolean).join('');
          // \uC0DD\uC131\uB41C \uD30C\uC77C\uC744 \uC5D0\uB514\uD130\uC5D0\uC11C \uBC14\uB85C \uCC3E\uC744 \uC218 \uC788\uB3C4\uB85D \uC791\uC5C5 \uB8E8\uD2B8 \uAE30\uC900 \uC804\uCCB4 \uACBD\uB85C\uB85C \uD45C\uC2DC\uD55C\uB2E4.
          const generatedFiles = Array.isArray(component.generatedFiles) ? component.generatedFiles : [];
          const fileItems = generatedFiles.map(function (file) {
            const fullPath = component.outputPath ? component.outputPath + '/' + file : file;
            return '<li class="runtime-card__file">' + escapeHtml(fullPath) + '</li>';
          }).join('');
          const filesBlock = fileItems
            ? '<div class="runtime-card__files"><div class="runtime-card__files-title">\uC0DD\uC131 \uD30C\uC77C (' + generatedFiles.length + ')</div><ul class="runtime-card__file-list">' + fileItems + '</ul></div>'
            : '';
          const meta = [
            component.queuedAt ? 'queued ' + component.queuedAt : '',
            component.startedAt ? 'started ' + component.startedAt : '',
            component.refreshStartedAt ? 'refresh ' + component.refreshStartedAt : '',
            component.finishedAt ? 'finished ' + component.finishedAt : '',
            component.errorMessage ? 'error ' + component.errorMessage : ''
          ].filter(Boolean).join(' \xB7 ');

          // \uB300\uAE30/\uC2E4\uD589/\uBC18\uC601 \uC911\uC778 \uC791\uC5C5\uB9CC \uAC1C\uBCC4 \uCDE8\uC18C(ESC\uC640 \uB3D9\uC77C) \uAC00\uB2A5\uD558\uB2E4.
          const isCancellable = component.status === 'queued' || component.status === 'running' || component.status === 'refreshing';
          const cancelButton = isCancellable
            ? '<button type="button" class="runtime-card__cancel" data-cancel-component-id="' + escapeAttribute(component.id) + '">\uCDE8\uC18C</button>'
            : '';

          return '<section class="runtime-card" data-runtime-card data-component-id="' + escapeAttribute(component.id) + '">'
            + '<div class="runtime-card__header">'
            + '<div>'
            + '<h3 class="runtime-card__name">' + escapeHtml(component.componentName || 'Unnamed') + '</h3>'
            + '<div class="runtime-card__meta">' + escapeHtml(meta || '\uB300\uAE30 \uC911') + '</div>'
            + '</div>'
            + '<div class="runtime-card__actions">'
            + cancelButton
            + '<div class="runtime-card__status" data-status="' + escapeAttribute(component.status) + '">' + escapeHtml(component.status) + '</div>'
            + '</div>'
            + '</div>'
            + '<div class="runtime-card__body">'
            + (links ? '<div class="runtime-card__links">' + links + '</div>' : '')
            + filesBlock
            + '<pre class="runtime-card__log">' + escapeHtml(component.log || '(\uB85C\uADF8 \uC5C6\uC74C)') + '</pre>'
            + '</div>'
            + '</section>';
        }).join('');

        runtimeList.querySelectorAll('[data-runtime-card]').forEach((card) => {
          const componentId = card.getAttribute('data-component-id') || '';
          const logElement = card.querySelector('.runtime-card__log');
          const previousScrollTop = logScrollPositions.get(componentId);

          if (logElement && typeof previousScrollTop === 'number') {
            logElement.scrollTop = previousScrollTop;
          }
        });
      }

      function renderState(state) {
        currentState = state;
        if (state.overallStatus === 'running') {
          desiredConcurrency = clampConcurrency(state.desiredConcurrency || desiredConcurrency);
        }
        renderStatusBadge(state.overallStatus);
        statusText.textContent = state.overallStatus;
        batchCountText.textContent = String(state.summary.batchCount || 0);
        queueRunningText.textContent = String(state.queueCount || 0) + ' / ' + String(state.runningCount || 0);
        successCountText.textContent = String(state.summary.completedComponents || 0);
        failureCountText.textContent = String(state.summary.failedComponents || 0);
        restartRequiredText.textContent = String(Boolean(state.restartRequired));
        submitButton.disabled = state.overallStatus === 'running';
        // \uC885\uB8CC\uB294 \uBC30\uCE58 \uC2E4\uD589 \uC911\uC5D0\uB3C4 \uC5B8\uC81C\uB4E0 \uAC00\uB2A5\uD558\uB3C4\uB85D \uBE44\uD65C\uC131\uD654\uD558\uC9C0 \uC54A\uB294\uB2E4. (\uD074\uB9AD \uC2DC shutdownServer\uAC00 \uC9C1\uC811 \uBE44\uD65C\uC131\uD654)
        storybookLink.href = state.storybookBaseUrl || '#';
        storybookLink.textContent = state.storybookBaseUrl ? 'Storybook \uC5F4\uAE30' : 'Storybook \uC900\uBE44 \uC911';
        updateConcurrencyView();
        renderRuntimeState(state);
      }

      // \uC804\uCCB4 \uC885\uB8CC \uC131\uACF5 \uC2DC: \uC11C\uBC84/\uD3F4\uB9C1\uC774 \uBA48\uCDB0 \uB354\uB294 \uC0C1\uD0DC\uB97C \uBC1B\uC544\uC62C \uC218 \uC5C6\uC73C\uBBC0\uB85C,
      // \uB9C8\uC9C0\uB9C9 \uC2A4\uB0C5\uC0F7\uC744 \uC885\uB8CC \uC0C1\uD0DC\uB85C \uD55C \uBC88 \uB2E4\uC2DC \uADF8\uB9B0\uB2E4. (running/queued/refreshing \u2192 cancelled \uD45C\uAE30)
      function renderShutdownState() {
        const base = currentState;
        const components = base && Array.isArray(base.components)
          ? base.components.map((component) => {
              const isActive = component.status === 'queued' || component.status === 'running' || component.status === 'refreshing';
              if (!isActive) {
                return component;
              }
              return Object.assign({}, component, {
                status: 'cancelled',
                errorMessage: component.errorMessage || '\uC804\uCCB4 \uC885\uB8CC\uB85C \uC911\uB2E8\uB418\uC5C8\uC2B5\uB2C8\uB2E4.'
              });
            })
          : [];

        // \uC6B0\uCE21 \uC0C1\uB2E8 \uC804\uCCB4 \uC0C1\uD0DC \uBC43\uC9C0/\uD14D\uC2A4\uD2B8\uC640 \uD050 \uCE74\uC6B4\uD2B8\uB97C \uC885\uB8CC \uAE30\uC900\uC73C\uB85C \uAC31\uC2E0\uD55C\uB2E4.
        renderStatusBadge('closed');
        statusText.textContent = '\uC885\uB8CC\uB428';
        queueRunningText.textContent = '0 / 0';

        renderRuntimeState({ components });
        shutdownBanner.hidden = false;

        // \uC885\uB8CC \uD6C4\uC5D0\uB294 \uB354 \uC870\uC791\uD560 \uC218 \uC5C6\uB3C4\uB85D \uC785\uB825/\uC2E4\uD589 \uCEE8\uD2B8\uB864\uC744 \uBAA8\uB450 \uC7A0\uADFC\uB2E4.
        submitButton.disabled = true;
        addComponentButton.disabled = true;
        decreaseConcurrencyButton.disabled = true;
        increaseConcurrencyButton.disabled = true;
        shutdownButton.disabled = true;
      }

      async function loadState() {
        if (shutdownRequested) {
          return;
        }

        const response = await fetch('/api/state', { cache: 'no-store' });
        if (!response.ok) {
          throw new Error('state \uC694\uCCAD \uC2E4\uD328');
        }

        renderState(await response.json());
      }

      function stopStatePolling() {
        shutdownRequested = true;

        if (statePollingTimerId !== null) {
          window.clearInterval(statePollingTimerId);
          statePollingTimerId = null;
        }
      }

      function startStatePolling() {
        if (statePollingTimerId !== null) {
          return;
        }

        statePollingTimerId = window.setInterval(() => {
          loadState().catch(() => {});
        }, 1000);
      }

      async function postJson(url, payload) {
        const response = await fetch(url, {
          method: 'POST',
          headers: { 'content-type': 'application/json' },
          body: JSON.stringify(payload || {})
        });
        const body = await response.json();

        if (!response.ok) {
          throw new Error(body.message || '\uC694\uCCAD\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4.');
        }

        return body;
      }

      async function setConcurrency(nextValue) {
        desiredConcurrency = clampConcurrency(nextValue);
        updateConcurrencyView();

        if (currentState?.overallStatus === 'running') {
          renderState(await postJson('/api/concurrency', { concurrency: desiredConcurrency }));
        }
      }

      async function submitBatch() {
        const components = collectComponents();

        if (components.length === 0) {
          window.alert('\uCD5C\uC18C 1\uAC1C\uC758 \uCEF4\uD3EC\uB10C\uD2B8 \uC785\uB825\uC774 \uD544\uC694\uD569\uB2C8\uB2E4.');
          return;
        }

        renderState(await postJson('/api/batch', { components, concurrency: desiredConcurrency }));
      }

      async function cancelComponent(componentId) {
        if (!componentId) {
          return;
        }

        const button = runtimeList.querySelector('[data-cancel-component-id="' + componentId + '"]');
        if (button) {
          button.disabled = true;
          button.textContent = '\uCDE8\uC18C \uC911\u2026';
        }

        try {
          renderState(await postJson('/api/cancel', { id: componentId }));
        } catch (error) {
          if (button) {
            button.disabled = false;
            button.textContent = '\uCDE8\uC18C';
          }
          throw error;
        }
      }

      async function shutdownServer() {
        if (!window.confirm('\uB300\uAE30 \uC911\uC778 \uC791\uC5C5\uC744 \uBAA8\uB450 \uC911\uB2E8\uD558\uACE0 gen-html-multiple \uC11C\uBC84\uB97C \uB2EB\uC2B5\uB2C8\uB2E4. \uC9C4\uD589\uD560\uAE4C\uC694? (\uAC1C\uBCC4 \uC791\uC5C5\uB9CC \uBA48\uCD94\uB824\uBA74 \uAC01 \uC791\uC5C5\uC758 "\uCDE8\uC18C" \uBC84\uD2BC\uC744 \uC0AC\uC6A9\uD558\uC138\uC694)')) {
          return;
        }

        shutdownButton.disabled = true;
        stopStatePolling();

        try {
          await postJson('/api/shutdown');
          // \uD3F4\uB9C1\uC774 \uBA48\uCDB0 \uB354\uB294 \uAC31\uC2E0\uB418\uC9C0 \uC54A\uC73C\uBBC0\uB85C, \uB9C8\uC9C0\uB9C9 \uC2A4\uB0C5\uC0F7\uC744 \uC885\uB8CC \uC0C1\uD0DC\uB85C \uB2E4\uC2DC \uADF8\uB9B0\uB2E4.
          renderShutdownState();
        } catch (error) {
          shutdownRequested = false;
          startStatePolling();
          loadState().catch(() => {});
          throw error;
        }
      }

      addComponentButton.addEventListener('click', () => addComponentCard());
      decreaseConcurrencyButton.addEventListener('click', () => {
        setConcurrency(desiredConcurrency - 1).catch((error) => window.alert(error instanceof Error ? error.message : String(error)));
      });
      increaseConcurrencyButton.addEventListener('click', () => {
        setConcurrency(desiredConcurrency + 1).catch((error) => window.alert(error instanceof Error ? error.message : String(error)));
      });
      submitButton.addEventListener('click', () => {
        submitBatch().catch((error) => window.alert(error instanceof Error ? error.message : String(error)));
      });
      shutdownButton.addEventListener('click', () => {
        shutdownServer().catch((error) => window.alert(error instanceof Error ? error.message : String(error)));
      });
      // \uC9C4\uD589 \uD604\uD669 \uCE74\uB4DC\uB294 polling\uB9C8\uB2E4 \uB2E4\uC2DC \uADF8\uB824\uC9C0\uBBC0\uB85C \uCEE8\uD14C\uC774\uB108\uC5D0 \uC704\uC784\uD574 \uAC1C\uBCC4 "\uCDE8\uC18C" \uD074\uB9AD\uC744 \uCC98\uB9AC\uD55C\uB2E4.
      runtimeList.addEventListener('click', (event) => {
        const target = event.target;
        const button = target && target.closest ? target.closest('[data-cancel-component-id]') : null;

        if (!button) {
          return;
        }

        cancelComponent(button.getAttribute('data-cancel-component-id')).catch((error) =>
          window.alert(error instanceof Error ? error.message : String(error))
        );
      });
      addComponentCard();
      updateConcurrencyView();
      loadState().catch(() => {});
      startStatePolling();
    </script>
  </body>
</html>`;
}

export { getGenHtmlMultipleUiHtml };
