// 쇼핑쇼츠 > 스레드 카드 소메뉴
//
// 흐름: 후보 + 이미지 입력 → 선정(제품×페르소나×후킹) → 글 생성 → 미리보기 → 발행
//
// 📌 사진과 글은 분리된다:
//   캐러셀 = 실사 사진(영상 프레임·쿠팡 상품 이미지). 글자를 얹지 않는다.
//   이야기 = 본문 1개 + 답글 체인. 마지막 답글에 제휴 링크.
//
// 설계 배경과 주의사항은 리포 루트 `쇼핑-스레드-파이프라인.md` 참조.

function ShoppingThreadManualPanel() {
  const { useState: useS } = React;
  const [candidatesText, setCandidatesText] = useS('');
  const [imageUrlsText, setImageUrlsText] = useS('');
  const [affiliateUrl, setAffiliateUrl] = useS('');
  const [partCount, setPartCount] = useS(5);
  const [plan, setPlan] = useS(null);
  const [busy, setBusy] = useS('');
  const [message, setMessage] = useS('');
  const [publishResult, setPublishResult] = useS('');
  const [videoUrlsText, setVideoUrlsText] = useS('');
  const [frameStatus, setFrameStatus] = useS('');

  // 한 줄에 하나씩: 제품명 | 특징 | 시즌메모
  function parseCandidates() {
    return candidatesText
      .split('\n')
      .map((line) => line.trim())
      .filter(Boolean)
      .map((line, index) => {
        const [title, productHint, seasonNote] = line.split('|').map((part) => (part || '').trim());
        return { id: `c${index + 1}`, title, productHint, seasonNote };
      })
      .filter((item) => item.title);
  }

  function parseImageUrls() {
    return imageUrlsText
      .split('\n')
      .map((line) => line.trim())
      .filter((line) => line.startsWith('https://'));
  }

  // 소스 영상 → VPS가 자막 없는 구간에서 프레임 추출 → 사진 칸에 자동 입력
  async function handleExtractFrames() {
    const sourceUrls = videoUrlsText
      .split('\n')
      .map((line) => line.trim())
      .filter((line) => line.startsWith('https://'));
    if (sourceUrls.length === 0) {
      setFrameStatus('소스 영상 링크(https)를 넣어주세요.');
      return;
    }
    setBusy('frames');
    setFrameStatus('추출 요청 중…');
    try {
      const res = await fetch('/api/shopping/frames', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sourceUrls }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`);

      // VPS 워커가 폴링 주기(기본 60초)로 가져가므로 넉넉히 기다린다
      const jobId = data.jobId;
      const deadline = Date.now() + 10 * 60 * 1000;
      while (Date.now() < deadline) {
        await new Promise((resolve) => setTimeout(resolve, 8000));
        const poll = await fetch(`/api/shopping/frames?jobId=${encodeURIComponent(jobId)}`, { cache: 'no-store' });
        const state = await poll.json().catch(() => null);
        if (!poll.ok || !state) continue;
        if (state.status === 'failed') throw new Error(state.error || '프레임 추출 실패');
        if ((state.frameUrls || []).length > 0) {
          setImageUrlsText((prev) => [prev.trim(), state.frameUrls.join('\n')].filter(Boolean).join('\n'));
          setFrameStatus(`프레임 ${state.frameUrls.length}장 추가됨`);
          return;
        }
        setFrameStatus(`추출 중… (${state.stage || state.status})`);
      }
      setFrameStatus('시간 초과 — VPS 워커 로그를 확인해 주세요.');
    } catch (error) {
      setFrameStatus(`실패: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setBusy('');
    }
  }

  async function handlePlan() {
    const candidates = parseCandidates();
    if (candidates.length === 0) {
      setMessage('후보를 한 줄에 하나씩 입력해 주세요.');
      return;
    }
    setBusy('plan');
    setMessage('');
    setPublishResult('');
    try {
      const res = await fetch('/api/shopping/thread-plan', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          candidates,
          affiliateUrl,
          partCount,
          manualImageUrls: parseImageUrls(),
        }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`);
      setPlan(data);
      setMessage(`선정 완료 — 후보 ${candidates.length}개 중 1개 선택`);
    } catch (error) {
      setMessage(`실패: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setBusy('');
    }
  }

  async function handlePublish(dryRun) {
    if (!plan) return;
    setBusy('publish');
    setPublishResult('');
    try {
      const res = await fetch('/api/shopping/threads-publish', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ postId: plan.postId, dryRun }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`);
      if (dryRun) {
        setPublishResult(
          `발행 전 확인 OK — 사진 ${(data.imageUrls || []).length}장(${data.mediaMode}), 글 ${data.totalPosts}개(본문+답글+링크)`,
        );
      } else {
        const errs = data.replyErrors || [];
        setPublishResult(
          `발행 완료 — ${data.publishedId} · 사진 ${data.imageCount}장 · 답글 ${data.replyCount}개` +
            (errs.length ? ` ⚠️ ${errs.join(' / ')}` : ''),
        );
      }
    } catch (error) {
      setPublishResult(`실패: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setBusy('');
    }
  }

  const images = (plan && plan.images) || [];
  const warnings = (plan && plan.imageWarnings) || [];

  return (
    <div className="card" style={{ marginBottom: 20 }}>
      <div className="card-head">
        <span className="card-title"><Icon name="grid" size={14} /> 스레드 카드 만들기</span>
        <span className="badge">사진 캐러셀 + 답글 체인</span>
      </div>

      <div className="thread-grid">
        <div className="thread-input">
          <label className="thread-label" htmlFor="thread-candidates">
            <strong>후보 제품</strong>
            <span>한 줄에 하나씩 · <code>제품명 | 특징 | 시즌메모</code></span>
          </label>
          <textarea
            id="thread-candidates"
            rows={5}
            value={candidatesText}
            placeholder={'에어컨 실외기 냉각 분무기 | 실외기에 분무해 온도 낮춤 | 폭염\n자동 신발커버 디스펜서 | 발 넣으면 비닐커버 씌워짐 | 장마철 현관'}
            onChange={(event) => setCandidatesText(event.target.value)}
          />

          <label className="thread-label" htmlFor="thread-videos">
            <strong>소스 영상 링크</strong>
            <span>한 줄에 하나씩 · 자막 없는 구간에서 사진을 뽑아옵니다</span>
          </label>
          <textarea
            id="thread-videos"
            rows={3}
            value={videoUrlsText}
            placeholder={'https://www.douyin.com/video/...\nhttps://www.tiktok.com/@.../video/...'}
            onChange={(event) => setVideoUrlsText(event.target.value)}
          />
          <button type="button" className="btn outline" disabled={busy !== ''} onClick={handleExtractFrames}>
            {busy === 'frames' ? '프레임 추출 중… (최대 10분)' : '영상에서 사진 뽑기'}
          </button>
          {frameStatus ? <p className="thread-message">{frameStatus}</p> : null}

          <label className="thread-label" htmlFor="thread-images">
            <strong>사진 URL</strong>
            <span>한 줄에 하나씩 · https만 · 비워두면 쿠팡 상품 이미지로 채웁니다</span>
          </label>
          <textarea
            id="thread-images"
            rows={4}
            value={imageUrlsText}
            placeholder={'https://.../product-1.jpg\nhttps://.../frame-01.jpg'}
            onChange={(event) => setImageUrlsText(event.target.value)}
          />

          <label className="thread-label" htmlFor="thread-affiliate">
            <strong>쿠팡 파트너스 링크</strong>
            <span>마지막 답글에 들어갑니다 (본문 아님)</span>
          </label>
          <input
            id="thread-affiliate"
            type="text"
            value={affiliateUrl}
            placeholder="https://link.coupang.com/a/..."
            onChange={(event) => setAffiliateUrl(event.target.value)}
          />

          <label className="thread-label" htmlFor="thread-partcount">
            <strong>글 개수 (4~8)</strong>
            <span>본문 1개 + 답글 나머지</span>
          </label>
          <input
            id="thread-partcount"
            type="number"
            min={4}
            max={8}
            value={partCount}
            onChange={(event) => setPartCount(Number(event.target.value))}
          />

          <button type="button" className="btn primary" style={{ marginTop: 12 }} disabled={busy !== ''} onClick={handlePlan}>
            {busy === 'plan' ? '선정 및 생성 중…' : '후보 선정 + 글 생성'}
          </button>
          {message ? <p className="thread-message">{message}</p> : null}
        </div>

        <div className="thread-result">
          {!plan ? (
            <p className="thread-empty">후보를 입력하면 선정 결과와 글이 여기에 표시됩니다.</p>
          ) : (
            <>
              <div className="thread-selection">
                <h3>선정 결과</h3>
                <p>
                  <strong>{plan.persona || '페르소나 미정'}</strong> · {plan.primaryHook || '후킹 미정'} ·{' '}
                  {plan.selection.winner.total}점 ({plan.selection.winner.reachTier})
                </p>
                <p className="thread-risk">⚠️ {plan.selection.winner.risk}</p>
                <p className="thread-note">
                  실측 반영 {plan.selection.pastResultCount}건
                  {plan.selection.pastResultCount === 0 ? ' — 아직 데이터 없음. 회차가 쌓이면 정확해집니다.' : ''}
                </p>
              </div>

              {warnings.length ? (
                <div className="thread-warn">
                  {warnings.map((warning) => <p key={warning}>⚠️ {warning}</p>)}
                </div>
              ) : null}

              <div className="thread-section-label">사진 {images.length}장 (글자 없는 실사)</div>
              {images.length ? (
                <div className="thread-photos">
                  {images.map((image, index) => (
                    <figure key={image.url + index}>
                      <img src={image.url} alt={`사진 ${index + 1}`} loading="lazy" />
                      <figcaption>{image.source}</figcaption>
                    </figure>
                  ))}
                </div>
              ) : (
                <p className="thread-empty" style={{ padding: '20px 12px' }}>
                  사진이 없습니다. URL을 직접 넣어주세요 — 최소 2장이어야 캐러셀이 됩니다.
                </p>
              )}

              <div className="thread-section-label">글 {1 + (plan.threadParts || []).length}개 + 링크 답글</div>
              <ol className="thread-parts">
                <li><span className="thread-part-tag">본문</span><pre>{plan.postText}</pre></li>
                {(plan.threadParts || []).map((part, index) => (
                  <li key={index}>
                    <span className="thread-part-tag">답글 {index + 1}</span>
                    <pre>{part}</pre>
                  </li>
                ))}
                <li><span className="thread-part-tag">링크</span><pre>{plan.linkReplyText}</pre></li>
              </ol>

              <div className="thread-actions">
                <button type="button" className="btn outline" disabled={busy !== ''} onClick={() => handlePublish(true)}>
                  발행 전 확인
                </button>
                <button type="button" className="btn primary" disabled={busy !== ''} onClick={() => handlePublish(false)}>
                  {busy === 'publish' ? '발행 중…' : '스레드에 발행'}
                </button>
              </div>
              {publishResult ? <p className="thread-message">{publishResult}</p> : null}
            </>
          )}
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// 무인 발행 후보 풀
//
// 크론(하루 3회)이 이 풀에서만 제품을 꺼낸다. 풀이 비면 아무것도 올리지 않는다 —
// 없는 제품을 지어내서 올리는 것보다 안 올리는 게 낫다는 판단이다.
//
// 쿠팡: 제품명만 넣으면 검색 → 딥링크 → 상품 이미지까지 자동.
// 토스: 제휴 API가 없어서 링크를 직접 넣어야 한다.
// ─────────────────────────────────────────────────────────────
function ShoppingThreadAutoPanel() {
  const { useState: useS, useEffect: useE } = React;
  const [provider, setProvider] = useS('coupang');
  const [title, setTitle] = useS('');
  const [hint, setHint] = useS('');
  const [season, setSeason] = useS('');
  const [link, setLink] = useS('');
  const [imagesText, setImagesText] = useS('');
  const [evidenceText, setEvidenceText] = useS('');
  const [videoText, setVideoText] = useS('');
  const [rows, setRows] = useS([]);
  const [counts, setCounts] = useS({});
  const [coupangApi, setCoupangApi] = useS(null);
  const [providers, setProviders] = useS([]);
  const [busy, setBusy] = useS('');
  const [message, setMessage] = useS('');

  async function loadPool() {
    try {
      const res = await fetch('/api/shopping/candidates');
      const data = await res.json().catch(() => null);
      if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`);
      setRows(data.candidates || []);
      setCounts(data.counts || {});
      setCoupangApi(Boolean(data.coupangApi));
      setProviders(data.providers || []);
    } catch (error) {
      setMessage(`후보 목록을 불러오지 못했습니다: ${error.message}`);
    }
  }

  useE(() => { loadPool(); }, []);

  function parseImages() {
    return imagesText
      .split('\n')
      .map((line) => line.trim())
      .filter((line) => line.startsWith('https://'));
  }

  // 한 계정에서 여러 제휴사를 함께 돌린다. 제휴사별 계정 분리는 도달만 쪼갠다.
  // 링크를 직접 넣어야 하는 상황인가 —
  // 자동 생성이 없는 제휴사는 항상, 쿠팡은 파트너스 API(최종승인 후 발급)가 없을 때.
  const current = providers.find((item) => item.id === provider);
  const providerLabel = (current && current.label) || provider;
  const linkRequired = current ? (!current.autoLink || coupangApi === false) : coupangApi === false;

  async function handleAdd() {
    if (!title.trim()) { setMessage('제품명을 넣어주세요.'); return; }
    if (linkRequired && !link.trim()) {
      setMessage(
        current && current.autoLink
          ? '쿠팡 파트너스 API가 아직 없어서(최종승인 전) 링크를 직접 넣어야 합니다.'
          : `${providerLabel}은(는) 제휴 링크를 직접 넣어야 합니다.`,
      );
      return;
    }
    const images = parseImages();
    if (linkRequired && images.length < 2) {
      setMessage('API가 없으면 사진도 자동으로 못 가져옵니다 — 이미지 URL을 2장 이상 넣어주세요.');
      return;
    }
    setBusy('add');
    setMessage('');
    try {
      const res = await fetch('/api/shopping/candidates', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          affiliateProvider: provider,
          affiliateUrl: link.trim(),
          manualImageUrls: images,
          evidence: evidenceText.split('\n').map((l) => l.trim()).filter(Boolean),
          items: [{
            title: title.trim(),
            productHint: hint.trim() || null,
            seasonNote: season.trim() || null,
          }],
        }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`);
      setMessage(`"${title.trim()}" 를 풀에 넣었습니다.`);
      setTitle(''); setHint(''); setSeason(''); setLink('');
      setImagesText(''); setVideoText(''); setEvidenceText('');
      await loadPool();
    } catch (error) {
      setMessage(`실패: ${error.message}`);
    } finally {
      setBusy('');
    }
  }

  // API가 없을 때 사진을 얻는 주 경로 — 소스 영상에서 자막 없는 구간을 캡처한다
  async function handleExtract() {
    const sourceUrls = videoText.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('https://'));
    if (sourceUrls.length === 0) { setMessage('소스 영상 링크(https)를 넣어주세요.'); return; }
    setBusy('frames');
    setMessage('프레임 추출 요청 중…');
    try {
      const res = await fetch('/api/shopping/frames', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sourceUrls, productHint: title.trim() || null }),
      });
      const data = await res.json().catch(() => null);
      if (!res.ok) throw new Error((data && data.error) || `HTTP ${res.status}`);
      const jobId = data.jobId;
      const deadline = Date.now() + 10 * 60 * 1000;
      while (Date.now() < deadline) {
        await new Promise((r) => setTimeout(r, 8000));
        const poll = await fetch(`/api/shopping/frames?jobId=${encodeURIComponent(jobId)}`, { cache: 'no-store' });
        const state = await poll.json().catch(() => null);
        if (!poll.ok || !state) continue;
        if (state.status === 'failed') throw new Error(state.error || '프레임 추출 실패');
        if ((state.frameUrls || []).length > 0) {
          setImagesText((prev) => [prev.trim(), state.frameUrls.join('\n')].filter(Boolean).join('\n'));
          setMessage(`프레임 ${state.frameUrls.length}장을 사진 칸에 넣었습니다.`);
          return;
        }
        setMessage(`추출 중… (${state.stage || state.status})`);
      }
      setMessage('시간 초과 — VPS 워커 로그를 확인해 주세요.');
    } catch (error) {
      setMessage(`실패: ${error.message}`);
    } finally {
      setBusy('');
    }
  }

  async function setStatus(id, status) {
    try {
      await fetch('/api/shopping/candidates', {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ id, status }),
      });
      await loadPool();
    } catch (error) {
      setMessage(`상태 변경 실패: ${error.message}`);
    }
  }

  async function handleDryRun() {
    setBusy('dry');
    setMessage('');
    try {
      const res = await fetch('/api/cron/publish-shopping-threads?dryRun=1', { method: 'POST' });
      const data = await res.json().catch(() => null);
      if (!res.ok) throw new Error((data && data.error) || (data && data.detail) || `HTTP ${res.status}`);
      if (data.action === 'blocked') {
        setMessage(`게이트에 걸렸습니다 — ${(data.blockers || []).join(' / ')}`);
      } else if (data.action === 'skipped' && data.score) {
        setMessage(
          `통과 — "${data.title}" ${data.score}점 (${data.reachTier}) · ${data.persona} / ${data.hook}` +
            ((data.warnings || []).length ? ` · 주의: ${data.warnings.join(' / ')}` : ''),
        );
      } else {
        setMessage(data.detail || '결과 없음');
      }
      await loadPool();
    } catch (error) {
      setMessage(`실패: ${error.message}`);
    } finally {
      setBusy('');
    }
  }

  const statusLabel = {
    pending: '대기', used: '발행됨', blocked: '게이트', failed: '실패', skipped: '제외',
  };

  return (
    <div className="card" style={{ marginBottom: 20 }}>
      <div className="card-head">
        <span className="card-title"><Icon name="zap" size={14} /> 무인 발행 후보 풀</span>
        <span className="badge">
          대기 {counts.pending || 0} · 발행 {counts.used || 0} · 걸림 {(counts.blocked || 0) + (counts.failed || 0)}
        </span>
      </div>

      {coupangApi === false ? (
        <div className="thread-warn" style={{ margin: '0 0 14px' }}>
          <p>
            ⚠️ 쿠팡 파트너스 API가 아직 없습니다 — 최종승인(누적 판매 15만원) 이후에 열립니다.
            그때까지는 <strong>파트너스 사이트에서 만든 상품 링크와 이미지 URL을 직접</strong> 넣어주세요.
            링크만 채워지면 선정·글쓰기·발행은 그대로 무인으로 돕니다.
          </p>
        </div>
      ) : null}

      <div className="thread-grid">
        <div className="thread-input">
          <label className="thread-label" htmlFor="pool-provider">
            <strong>제휴처</strong>
            <span>한 계정에 다 올립니다 · {coupangApi ? '쿠팡만 링크·사진 자동' : '지금은 전부 링크 직접 입력'}</span>
          </label>
          <select id="pool-provider" value={provider} onChange={(e) => setProvider(e.target.value)}>
            {(providers.length ? providers : [{ id: 'coupang', label: '쿠팡 파트너스' }]).map((item) => (
              <option key={item.id} value={item.id}>{item.label}</option>
            ))}
          </select>

          <label className="thread-label" htmlFor="pool-title">
            <strong>제품명</strong>
            <span>선정·검색에 쓰이는 이름</span>
          </label>
          <input id="pool-title" type="text" value={title} placeholder="에어컨 실외기 냉각 분무기"
            onChange={(e) => setTitle(e.target.value)} />

          <label className="thread-label" htmlFor="pool-hint">
            <strong>특징</strong>
            <span>무엇이 다른 물건인가 — 글의 재료가 됩니다</span>
          </label>
          <input id="pool-hint" type="text" value={hint} placeholder="실외기에 분무해 온도를 낮춤"
            onChange={(e) => setHint(e.target.value)} />

          <label className="thread-label" htmlFor="pool-season">
            <strong>시즌·이슈 메모</strong>
            <span>선택 · 시의성 점수에 반영됩니다</span>
          </label>
          <input id="pool-season" type="text" value={season} placeholder="폭염"
            onChange={(e) => setSeason(e.target.value)} />

          <label className="thread-label" htmlFor="pool-evidence">
            <strong>근거 메모</strong>
            <span>상품 페이지에서 확인한 사실만 · 한 줄에 하나씩</span>
          </label>
          <textarea id="pool-evidence" rows={3} value={evidenceText}
            placeholder={'리뷰 3,912개 평점 4.7\n정가 27,900원 → 9,900원 (64% 할인)\n72매 10팩 총 720매'}
            onChange={(e) => setEvidenceText(e.target.value)} />
          <p className="thread-note" style={{ margin: '-4px 0 8px' }}>
            여기에 사실을 넣어야 <strong>구체성·사회적 증거·권위</strong> 후킹이 열립니다.
            비워두면 엔진이 수치·후기 언급을 전부 막습니다 (날조 방지).
          </p>

          <label className="thread-label" htmlFor="pool-link">
            <strong>제휴 링크 {linkRequired ? '(필수)' : '(선택 — 비우면 자동 생성)'}</strong>
            <span>파트너스 → 링크 생성에서 만든 단축 링크</span>
          </label>
          <input id="pool-link" type="text" value={link}
            placeholder={
              provider === 'toss' ? 'https://toss.im/_m/...'
                : provider === 'naver' ? 'https://naver.me/...'
                : provider === 'zigzag' ? 'https://s.zigzag.kr/...'
                : 'https://link.coupang.com/a/...'
            }
            onChange={(e) => setLink(e.target.value)} />

          <label className="thread-label" htmlFor="pool-images">
            <strong>사진 URL {linkRequired ? '(2장 이상)' : '(선택)'}</strong>
            <span>한 줄에 하나씩 · 글자 없는 실사만</span>
          </label>
          <textarea id="pool-images" rows={4} value={imagesText}
            placeholder={'https://.../product-1.jpg\nhttps://.../frame-01.jpg'}
            onChange={(e) => setImagesText(e.target.value)} />

          <label className="thread-label" htmlFor="pool-video">
            <strong>소스 영상에서 뽑기</strong>
            <span>자막 없는 구간을 캡처해 위 사진 칸에 넣습니다</span>
          </label>
          <textarea id="pool-video" rows={2} value={videoText}
            placeholder={'https://www.douyin.com/video/...'}
            onChange={(e) => setVideoText(e.target.value)} />
          <button type="button" className="btn outline" disabled={busy !== ''} onClick={handleExtract}>
            {busy === 'frames' ? '추출 중… (최대 10분)' : '영상에서 사진 뽑기'}
          </button>

          <div className="thread-actions" style={{ marginTop: 12 }}>
            <button type="button" className="btn primary" disabled={busy !== ''} onClick={handleAdd}>
              {busy === 'add' ? '추가 중…' : '풀에 넣기'}
            </button>
            <button type="button" className="btn outline" disabled={busy !== ''} onClick={handleDryRun}>
              {busy === 'dry' ? '점검 중…' : '발행 없이 점검'}
            </button>
          </div>
          {message ? <p className="thread-message">{message}</p> : null}

          <p className="thread-note" style={{ marginTop: 12 }}>
            크론은 하루 3회(한국시간 11시·18시·21시) 한 건씩 올립니다.
            실제 발행은 <code>SHOPPING_THREAD_AUTOPUBLISH=on</code> 을 켜야 시작됩니다.
          </p>
        </div>

        <div className="thread-result">
          {rows.length === 0 ? (
            <p className="thread-empty">풀이 비어 있습니다. 제품을 넣으면 크론이 하나씩 집어갑니다.</p>
          ) : (
            <ol className="thread-parts">
              {rows.map((row) => (
                <li key={row.id}>
                  <span className="thread-part-tag">{statusLabel[row.status] || row.status}</span>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <strong>{row.title}</strong>
                    <div style={{ fontSize: 12, opacity: 0.7 }}>
                      {(providers.find((x) => x.id === row.affiliate_provider) || {}).label || row.affiliate_provider}
                      {row.affiliate_url ? ' · 링크 있음' : ' · 링크 자동'}
                      {` · 사진 ${(row.manual_image_urls || []).length + (row.frame_urls || []).length}장`}
                      {row.attempts ? ` · 시도 ${row.attempts}회` : ''}
                    </div>
                    {row.last_error ? <p className="thread-risk">⚠️ {row.last_error}</p> : null}
                  </div>
                  {row.status === 'pending' ? (
                    <button type="button" className="btn outline" onClick={() => setStatus(row.id, 'skipped')}>제외</button>
                  ) : (row.status === 'blocked' || row.status === 'failed') ? (
                    <button type="button" className="btn outline" onClick={() => setStatus(row.id, 'pending')}>다시</button>
                  ) : null}
                </li>
              ))}
            </ol>
          )}
        </div>
      </div>
    </div>
  );
}

// Shopping.jsx 가 부르는 진입점 — 무인 라인이 위, 수동 라인이 아래.
function ShoppingThreadsPanel() {
  return (
    <>
      <ShoppingThreadAutoPanel />
      <ShoppingThreadManualPanel />
    </>
  );
}
