// ─── 관리자 · 인스타 스토리(1080×1920) 생성 ─────────────────────────
// 회원 소개용 4종 변형 + PNG 로컬 저장 (unpkg UMD `html-to-image` — 브라우저에서 `import('esm.sh/...')`는 require 오류 날 수 있음)

const { useState: useSt, useRef: useRf, useCallback: useCb, useMemo: useMm, useEffect: useEf } = React;

const STORY_W = 1080;
const STORY_H = 1920;
const STORY_SCALE = 0.38;

const RAINBOW = 'linear-gradient(91deg, #161412 0%, #e23a2e 30%, #f5c419 55%, #2f6cf6 80%, #161412 100%)';

/** 풀블리드 배경 대신 배지·포인트 컬러로만 쓰여 사진 대비가 유지됩니다. */
/** 전역 `TIERS`(등급 키 배열)와 이름 충돌 방지 — 스토리 카드용 스타일만 */
const STORY_TIER_THEME = {
  diamond: {
    label: 'DIAMOND',
    badgeBg: 'linear-gradient(135deg, #9adfff, #c8b8ff, #ffc6f0)',
    badgeText: '#161412',
    accentBar: 'linear-gradient(90deg, #9adfff, #c8b8ff, #ffc6f0)',
    rim: 'rgba(200, 184, 255, 0.45)',
  },
  black: {
    label: 'BLACK',
    badgeBg: '#2a2522',
    badgeText: '#fff',
    accentBar: 'linear-gradient(90deg, #444, #888)',
    rim: 'rgba(255,255,255,0.22)',
  },
  red: {
    label: 'RED',
    badgeBg: '#e23a2e',
    badgeText: '#fff',
    accentBar: '#e23a2e',
    rim: 'rgba(226,58,46,0.5)',
  },
  blue: {
    label: 'BLUE',
    badgeBg: '#2f6cf6',
    badgeText: '#fff',
    accentBar: '#2f6cf6',
    rim: 'rgba(47,108,246,0.45)',
  },
  yellow: {
    label: 'YELLOW',
    badgeBg: '#f5c419',
    badgeText: '#161412',
    accentBar: '#f5c419',
    rim: 'rgba(245,196,25,0.55)',
  },
  white: {
    label: 'WHITE',
    badgeBg: '#fff',
    badgeText: '#161412',
    accentBar: '#d8cdb5',
    rim: 'rgba(255,255,255,0.35)',
  },
};

const Mono = (props) => (
  <span style={{ fontFamily: "'JetBrains Mono', monospace", letterSpacing: '0.04em', ...props.style }}>{props.children}</span>
);
const Serif = (props) => (
  <span style={{ fontFamily: "'Instrument Serif', serif", fontStyle: 'italic', fontWeight: 400, ...props.style }}>{props.children}</span>
);

function StoryFrame({ children, bg = '#131211', style = {}, innerRef }) {
  return (
    <div
      style={{
        width: STORY_W * STORY_SCALE,
        height: STORY_H * STORY_SCALE,
        overflow: 'hidden',
        position: 'relative',
      }}
    >
      <div
        ref={innerRef}
        style={{
          width: STORY_W,
          height: STORY_H,
          transform: `scale(${STORY_SCALE})`,
          transformOrigin: '0 0',
          background: bg,
          position: 'absolute',
          fontFamily: "'Pretendard Variable', Pretendard, sans-serif",
          color: '#f5f2ec',
          overflow: 'hidden',
          ...style,
        }}
      >
        {children}
      </div>
    </div>
  );
}

function StoryHeader({ light = true }) {
  const c = light ? 'rgba(255,255,255,0.65)' : '#94918a';
  return (
    <div
      style={{
        position: 'absolute',
        top: 80,
        left: 0,
        right: 0,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        gap: 12,
      }}
    >
      <Serif
        style={{
          fontSize: 44,
          lineHeight: 1,
          backgroundImage: RAINBOW,
          WebkitBackgroundClip: 'text',
          backgroundClip: 'text',
          color: 'transparent',
        }}
      >
        Color
      </Serif>
      <span style={{ fontSize: 26, fontWeight: 700, color: light ? '#fff' : '#161412', letterSpacing: '-0.01em' }}>소개팅</span>
      <span style={{ width: 4, height: 4, borderRadius: 999, background: c, margin: '0 6px' }} />
      <Mono style={{ fontSize: 18, color: c, textTransform: 'uppercase' }}>NEW MEMBER</Mono>
    </div>
  );
}

function StoryFooter({ light = true }) {
  const fg = light ? '#fff' : '#161412';
  const bg = light ? '#fff' : '#161412';
  const tx = light ? '#161412' : '#fff';
  return (
    <div style={{ position: 'absolute', bottom: 120, left: 0, right: 0, textAlign: 'center' }}>
      <Mono style={{ fontSize: 18, color: light ? 'rgba(255,255,255,0.55)' : '#94918a' }}>FOLLOW · APPLY · MATCH</Mono>
      <div
        style={{
          marginTop: 16,
          display: 'inline-flex',
          alignItems: 'center',
          gap: 8,
          background: bg,
          color: tx,
          padding: '16px 28px',
          borderRadius: 999,
          fontSize: 26,
          fontWeight: 700,
        }}
      >
        @color_sogaeting 팔로우 ↗
      </div>
    </div>
  );
}

function TierPill({ t, small }) {
  return (
    <span
      style={{
        display: 'inline-flex',
        alignItems: 'center',
        gap: small ? 8 : 10,
        background: t.badgeBg,
        color: t.badgeText,
        padding: small ? '8px 16px' : '10px 22px',
        borderRadius: 999,
        border: t.label === 'WHITE' ? '2px solid #d8cdb5' : '0',
        boxShadow: t.label === 'DIAMOND' ? 'inset 0 0 14px rgba(255,255,255,0.45)' : '0 2px 12px rgba(0,0,0,0.2)',
      }}
    >
      <span
        style={{
          width: small ? 8 : 10,
          height: small ? 8 : 10,
          borderRadius: 999,
          background: t.badgeText === '#fff' ? 'rgba(255,255,255,0.85)' : '#161412',
          opacity: 0.9,
        }}
      />
      <Mono style={{ fontSize: small ? 16 : 20, fontWeight: 700 }}>{t.label}</Mono>
    </span>
  );
}

/** 공통: 사진 위치/확대/맞춤 모드 — 멀리서 찍은 사진도 적절히 맞춤 */
function readPhotoView(data, baseScale = 1.14) {
  const fit = data.photoFit === 'contain' ? 'contain' : 'cover';
  const fx = Number.isFinite(Number(data.photoFocusX)) ? Number(data.photoFocusX) : 50;
  const fy = Number.isFinite(Number(data.photoFocusY)) ? Number(data.photoFocusY) : 35;
  const zoom = Number.isFinite(Number(data.photoZoom)) ? Number(data.photoZoom) : 1.0;
  // contain 모드는 오버스캔 없이 사용자 줌만, cover 모드는 블러 가장자리 완화용 베이스 오버스캔과 곱
  const finalScale = fit === 'contain' ? zoom : baseScale * zoom;
  return { fit, fx, fy, zoom, finalScale };
}

/** 공통: 사진 + 어두운 그라데이션으로 티어 배경색과 무관하게 실루엣이 보이게 */
function PhotoBackdrop({ data, opacity = 0.68, blurMul = 1.75, rim, baseScale = 1.14 }) {
  if (!data.photo) return null;
  const { fit, fx, fy, finalScale } = readPhotoView(data, baseScale);
  return (
    <div style={{ position: 'absolute', inset: 0, overflow: 'hidden', background: fit === 'contain' ? '#0a0807' : 'transparent' }}>
      <img
        src={data.photo}
        alt=""
        style={{
          width: '100%',
          height: '100%',
          objectFit: fit,
          objectPosition: `${fx}% ${fy}%`,
          filter: `blur(${data.blur * blurMul}px) saturate(1.08)`,
          transform: `scale(${finalScale})`,
          transformOrigin: `${fx}% ${fy}%`,
          opacity,
        }}
      />
      <div
        style={{
          position: 'absolute',
          inset: 0,
          background: `linear-gradient(180deg, rgba(8,7,6,0.25) 0%, rgba(10,9,8,0.5) 45%, rgba(12,11,10,0.78) 100%)`,
          boxShadow: rim ? `inset 0 0 0 3px ${rim}` : 'none',
        }}
      />
    </div>
  );
}

function VariantA({ data, innerRef }) {
  const t = STORY_TIER_THEME[data.tier] || STORY_TIER_THEME.white;
  return (
    <StoryFrame
      innerRef={innerRef}
      bg="linear-gradient(165deg, #121110 0%, #1a1816 48%, #141210 100%)"
      style={{ color: '#fff' }}
    >
      <div
        style={{
          position: 'absolute',
          top: 0,
          left: 0,
          right: 0,
          height: 8,
          background: t.accentBar,
          opacity: 0.95,
        }}
      />
      <PhotoBackdrop data={data} opacity={0.7} blurMul={1.72} rim={t.rim} />
      <StoryHeader light />
      <div style={{ position: 'absolute', top: 240, left: 0, right: 0, textAlign: 'center' }}>
        <Mono style={{ fontSize: 20, opacity: 0.75, color: 'rgba(255,255,255,0.85)' }}>NO. {data.no || '042'}</Mono>
        <div style={{ marginTop: 14, display: 'flex', justifyContent: 'center' }}>
          <TierPill t={t} />
        </div>
        <div style={{ marginTop: 22, fontSize: 62, fontWeight: 800, letterSpacing: '-0.025em', textShadow: '0 2px 24px rgba(0,0,0,0.45)' }}>
          이번에 만나볼
          <br />
          <Serif style={{ fontStyle: 'italic', fontSize: 76 }}>회원님</Serif>은
        </div>
      </div>
      <div style={{ position: 'absolute', top: 600, left: 0, right: 0, textAlign: 'center' }}>
        <div
          style={{
            fontFamily: "'Instrument Serif', serif",
            fontStyle: 'italic',
            fontSize: 360,
            lineHeight: 0.85,
            letterSpacing: '-0.04em',
            textShadow: '0 4px 40px rgba(0,0,0,0.55)',
          }}
        >
          {data.age}
        </div>
        <div style={{ marginTop: 18, fontSize: 42, fontWeight: 600, letterSpacing: '-0.02em', color: 'rgba(255,255,255,0.9)' }}>
          {data.region} · {data.job}
        </div>
      </div>
      <div
        style={{
          position: 'absolute',
          top: 1260,
          left: 72,
          right: 72,
          display: 'grid',
          gridTemplateColumns: '1fr 1fr',
          gap: 14,
        }}
      >
        <InfoCard light k="닮은꼴" v={data.lookalike} />
        <InfoCard light k="등급" v={t.label} mono />
      </div>
      <div
        style={{
          position: 'absolute',
          top: 1380,
          left: 72,
          right: 72,
          background: 'rgba(255,255,255,0.09)',
          borderRadius: 28,
          padding: '24px 28px',
          border: '1px solid rgba(255,255,255,0.12)',
        }}
      >
        <Mono style={{ fontSize: 16, opacity: 0.65 }}>IDEAL · 이상형</Mono>
        <div
          style={{
            marginTop: 10,
            fontSize: 26,
            fontWeight: 600,
            lineHeight: 1.4,
            letterSpacing: '-0.015em',
            whiteSpace: 'pre-wrap',
          }}
        >
          {data.ideal}
        </div>
        {data.adminMessage && (
          <div style={{
            marginTop: 16, paddingTop: 14,
            borderTop: '1px dashed rgba(255,255,255,0.22)',
          }}>
            <Mono style={{ fontSize: 13, opacity: 0.55 }}>NOTE · 운영자 한마디</Mono>
            <div style={{
              marginTop: 6, fontSize: 17, lineHeight: 1.5,
              letterSpacing: '-0.005em', whiteSpace: 'pre-wrap',
              color: 'rgba(255,255,255,0.88)',
            }}>
              {data.adminMessage}
            </div>
          </div>
        )}
      </div>
      <StoryFooter light />
    </StoryFrame>
  );
}

function InfoCard({ k, v, mono, light }) {
  return (
    <div
      style={{
        background: light ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.06)',
        borderRadius: 20,
        padding: '18px 22px',
        border: light ? '1px solid rgba(255,255,255,0.12)' : '1px solid rgba(0,0,0,0.06)',
      }}
    >
      <Mono style={{ fontSize: 13, opacity: 0.65 }}>{k.toUpperCase()}</Mono>
      <div
        style={{
          marginTop: 6,
          fontSize: 32,
          fontWeight: 700,
          letterSpacing: '-0.01em',
          fontFamily: mono ? "'JetBrains Mono', monospace" : 'inherit',
        }}
      >
        {v}
      </div>
    </div>
  );
}

function VariantB({ data, innerRef }) {
  const t = STORY_TIER_THEME[data.tier] || STORY_TIER_THEME.white;
  return (
    <StoryFrame innerRef={innerRef} bg="#f4efe6" style={{ color: '#161412' }}>
      {data.photo && (
        <div
          style={{
            position: 'absolute',
            top: 180,
            left: 0,
            right: 0,
            display: 'flex',
            justifyContent: 'center',
            pointerEvents: 'none',
          }}
        >
          <div
            style={{
              width: 700,
              height: 700,
              borderRadius: 999,
              overflow: 'hidden',
              boxShadow: `0 0 0 4px ${t.rim}, 0 24px 60px rgba(0,0,0,0.18)`,
              filter: 'saturate(0.95)',
              background: (data.photoFit === 'contain') ? '#f0e8d8' : 'transparent',
            }}
          >
            {(() => {
              const { fit, fx, fy, finalScale } = readPhotoView(data, 1.12);
              return (
                <img
                  src={data.photo}
                  alt=""
                  style={{
                    width: '100%',
                    height: '100%',
                    objectFit: fit,
                    objectPosition: `${fx}% ${fy}%`,
                    filter: `blur(${data.blur * 1.5}px)`,
                    transform: `scale(${finalScale})`,
                    transformOrigin: `${fx}% ${fy}%`,
                    opacity: 0.62,
                  }}
                />
              );
            })()}
          </div>
        </div>
      )}
      <div
        style={{
          position: 'absolute',
          inset: 0,
          background: 'linear-gradient(180deg, rgba(244,239,230,0.2) 0%, rgba(244,239,230,0.92) 52%, rgba(244,239,230,1) 100%)',
          pointerEvents: 'none',
        }}
      />
      <StoryHeader light={false} />
      <div style={{ position: 'absolute', top: 230, left: 72, right: 72, textAlign: 'center', zIndex: 1 }}>
        <Mono style={{ fontSize: 20, color: '#6b6359' }}>MEMBER NO. {data.no || '042'}</Mono>
        <div style={{ marginTop: 16, fontSize: 66, fontWeight: 800, letterSpacing: '-0.03em', lineHeight: 1.05 }}>
          이번 주의
          <br />
          <Serif
            style={{
              fontStyle: 'italic',
              fontSize: 86,
              backgroundImage: RAINBOW,
              WebkitBackgroundClip: 'text',
              backgroundClip: 'text',
              color: 'transparent',
            }}
          >
            회원
          </Serif>
          님
        </div>
      </div>
      <div
        style={{
          position: 'absolute',
          top: 580,
          left: 56,
          right: 56,
          background: '#fff',
          borderRadius: 34,
          padding: '48px 40px',
          boxShadow: '0 12px 40px rgba(0,0,0,0.08)',
          border: '1px solid #e8e0d4',
          zIndex: 1,
        }}
      >
        <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 24 }}>
          <TierPill t={t} />
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 14 }}>
          <BField k="나이" v={`${data.age}`} mono />
          <BField k="거주지" v={data.region} />
          <BField k="직업" v={data.job} span={2} />
          <BField k="닮은꼴" v={data.lookalike} span={2} />
        </div>
      </div>
      <div
        style={{
          position: 'absolute',
          top: 1290,
          left: 56,
          right: 56,
          background: '#161412',
          color: '#fff',
          borderRadius: 28,
          padding: '24px 28px',
          zIndex: 1,
        }}
      >
        <Mono style={{ fontSize: 15, color: 'rgba(255,255,255,0.5)' }}>IDEAL · 이상형</Mono>
        <div style={{ marginTop: 10, fontSize: 28, fontWeight: 600, lineHeight: 1.4, letterSpacing: '-0.015em', whiteSpace: 'pre-wrap' }}>
          “{data.ideal}”
        </div>
        {data.adminMessage && (
          <div style={{
            marginTop: 16, paddingTop: 14,
            borderTop: '1px dashed rgba(255,255,255,0.22)',
          }}>
            <Mono style={{ fontSize: 13, color: 'rgba(255,255,255,0.5)' }}>NOTE · 운영자 한마디</Mono>
            <div style={{
              marginTop: 6, fontSize: 17, lineHeight: 1.5,
              letterSpacing: '-0.005em', whiteSpace: 'pre-wrap',
              color: 'rgba(255,255,255,0.88)',
            }}>
              {data.adminMessage}
            </div>
          </div>
        )}
      </div>
      <StoryFooter light={false} />
    </StoryFrame>
  );
}

function BField({ k, v, mono, span }) {
  return (
    <div style={{ gridColumn: span ? `span ${span}` : undefined, borderBottom: '1px solid #ebe3d2', paddingBottom: 12 }}>
      <Mono style={{ fontSize: 15, color: '#94918a' }}>{k.toUpperCase()}</Mono>
      <div
        style={{
          marginTop: 4,
          fontSize: 34,
          fontWeight: 700,
          letterSpacing: '-0.015em',
          fontFamily: mono ? "'JetBrains Mono', monospace" : 'inherit',
        }}
      >
        {v}
      </div>
    </div>
  );
}

function VariantC({ data, innerRef }) {
  const t = STORY_TIER_THEME[data.tier] || STORY_TIER_THEME.white;
  return (
    <StoryFrame innerRef={innerRef} bg="#121110" style={{ color: '#fff' }}>
      <PhotoBackdrop data={data} opacity={0.58} blurMul={1.85} rim={t.rim} />
      <div
        style={{
          position: 'absolute',
          inset: 0,
          background: 'radial-gradient(ellipse 90% 65% at 50% 38%, rgba(18,17,16,0.15) 0%, rgba(18,17,16,0.88) 72%)',
          pointerEvents: 'none',
        }}
      />
      <StoryHeader light />
      <div style={{ position: 'absolute', top: 210, left: 0, right: 0, textAlign: 'center', zIndex: 1 }}>
        <TierPill t={t} small />
      </div>
      <div
        style={{
          position: 'absolute',
          top: 320,
          left: 0,
          right: 0,
          textAlign: 'center',
          fontFamily: "'Instrument Serif', serif",
          fontStyle: 'italic',
          fontSize: 540,
          lineHeight: 0.82,
          letterSpacing: '-0.05em',
          zIndex: 1,
          textShadow: '0 6px 48px rgba(0,0,0,0.5)',
        }}
      >
        <span
          style={{
            backgroundImage: RAINBOW,
            WebkitBackgroundClip: 'text',
            backgroundClip: 'text',
            color: 'transparent',
          }}
        >
          {data.age}
        </span>
      </div>
      <div style={{ position: 'absolute', top: 960, left: 56, right: 56, textAlign: 'center', zIndex: 1 }}>
        <div style={{ fontSize: 56, fontWeight: 800, letterSpacing: '-0.025em', lineHeight: 1.1 }}>{data.job}</div>
        <div style={{ marginTop: 10, fontSize: 26, color: 'rgba(255,255,255,0.65)' }}>
          {data.region} · 닮은꼴 {data.lookalike}
        </div>
      </div>
      <div style={{ position: 'absolute', top: 1140, left: 56, right: 56, zIndex: 1 }}>
        <div style={{ textAlign: 'center', marginBottom: 12 }}>
          <Mono style={{ fontSize: 17, color: 'rgba(255,255,255,0.5)' }}>IDEAL · 이상형</Mono>
        </div>
        <div
          style={{
            background: 'rgba(255,255,255,0.07)',
            borderRadius: 28,
            padding: '26px 32px',
            textAlign: 'center',
            border: '1px solid rgba(255,255,255,0.1)',
          }}
        >
          <div style={{ fontFamily: "'Instrument Serif', serif", fontStyle: 'italic', fontSize: 78, lineHeight: 0.65, color: '#ec4438' }}>
            "
          </div>
          <div style={{ marginTop: -4, fontSize: 28, fontWeight: 600, lineHeight: 1.4, letterSpacing: '-0.015em', whiteSpace: 'pre-wrap' }}>
            {data.ideal}
          </div>
          {data.adminMessage && (
            <div style={{
              marginTop: 16, paddingTop: 14,
              borderTop: '1px dashed rgba(255,255,255,0.22)',
              textAlign: 'left',
            }}>
              <Mono style={{ fontSize: 13, color: 'rgba(255,255,255,0.5)' }}>NOTE · 운영자 한마디</Mono>
              <div style={{
                marginTop: 6, fontSize: 17, lineHeight: 1.5,
                letterSpacing: '-0.005em', whiteSpace: 'pre-wrap',
                color: 'rgba(255,255,255,0.88)',
              }}>
                {data.adminMessage}
              </div>
            </div>
          )}
        </div>
      </div>
      <StoryFooter light />
    </StoryFrame>
  );
}

function VariantD({ data, innerRef }) {
  const t = STORY_TIER_THEME[data.tier] || STORY_TIER_THEME.white;
  return (
    <StoryFrame innerRef={innerRef} bg="#fbf8f3" style={{ color: '#161412' }}>
      {data.photo && (() => {
        const { fit, fx, fy, finalScale } = readPhotoView(data, 1.16);
        return (
          <div style={{
            position: 'absolute', top: 0, left: 0, right: 0, height: 880,
            overflow: 'hidden', pointerEvents: 'none',
            background: fit === 'contain' ? '#ebe3d2' : 'transparent',
          }}>
            <img
              src={data.photo}
              alt=""
              style={{
                width: '100%',
                height: '100%',
                objectFit: fit,
                objectPosition: `${fx}% ${fy}%`,
                filter: `blur(${data.blur * 1.45}px) saturate(0.92)`,
                transform: `scale(${finalScale})`,
                transformOrigin: `${fx}% ${fy}%`,
                opacity: 0.55,
              }}
            />
            <div
              style={{
                position: 'absolute',
                inset: 0,
                background: 'linear-gradient(180deg, rgba(251,248,243,0.05) 0%, rgba(251,248,243,0.75) 70%, rgba(251,248,243,1) 100%)',
              }}
            />
            <div
              style={{
                position: 'absolute',
                bottom: 0,
                left: 0,
                right: 0,
                height: 6,
                background: t.accentBar,
                opacity: 0.85,
              }}
            />
          </div>
        );
      })()}
      <StoryHeader light={false} />
      <div style={{ position: 'absolute', top: 230, left: 56, right: 56, textAlign: 'center', zIndex: 1 }}>
        <div style={{ display: 'flex', justifyContent: 'center', gap: 22, alignItems: 'baseline' }}>
          <Mono style={{ fontSize: 22, color: '#94918a' }}>VOL.{data.no || '042'}</Mono>
        </div>
        <div style={{ marginTop: 16, height: 2, background: '#161412', maxWidth: 200, margin: '16px auto 0' }} />
      </div>
      <div style={{ position: 'absolute', top: 360, left: 0, right: 0, display: 'flex', justifyContent: 'center', zIndex: 1 }}>
        <div
          style={{
            width: 500,
            height: 500,
            borderRadius: 999,
            background: t.badgeBg,
            color: t.badgeText,
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            border: data.tier === 'white' ? '2px solid #d8cdb5' : `3px solid ${t.rim}`,
            boxShadow: data.tier === 'diamond' ? 'inset 0 0 50px rgba(255,255,255,0.55)' : '0 16px 48px rgba(0,0,0,0.12)',
          }}
        >
          <Mono style={{ fontSize: 20, opacity: 0.65 }}>TIER</Mono>
          <div style={{ marginTop: 10, fontSize: 88, fontWeight: 900, letterSpacing: '-0.025em', lineHeight: 1 }}>{t.label}</div>
          <div
            style={{
              marginTop: 12,
              fontFamily: "'Instrument Serif', serif",
              fontStyle: 'italic',
              fontSize: 80,
              lineHeight: 1,
              letterSpacing: '-0.02em',
            }}
          >
            {data.age}
            <span style={{ fontSize: 28, opacity: 0.65, marginLeft: 4 }}>세</span>
          </div>
        </div>
      </div>
      <div style={{ position: 'absolute', top: 980, left: 72, right: 72, textAlign: 'center', zIndex: 1 }}>
        <div style={{ fontSize: 52, fontWeight: 800, letterSpacing: '-0.025em' }}>
          <Serif style={{ color: '#ec4438' }}>{data.region}</Serif> · {data.job}
        </div>
        <div style={{ marginTop: 12, fontSize: 24, color: '#6b6359' }}>
          닮은꼴 — <b style={{ color: '#161412' }}>{data.lookalike}</b>
        </div>
      </div>
      <div style={{ position: 'absolute', top: 1140, left: 56, right: 56, zIndex: 1 }}>
        <div style={{ background: '#161412', color: '#fff', borderRadius: 28, padding: '26px 30px', textAlign: 'center' }}>
          <Mono style={{ fontSize: 15, color: 'rgba(255,255,255,0.5)' }}>IDEAL TYPE</Mono>
          <div style={{ marginTop: 10, fontSize: 28, fontWeight: 600, lineHeight: 1.4, letterSpacing: '-0.015em', whiteSpace: 'pre-wrap' }}>
            {data.ideal}
          </div>
          {data.adminMessage && (
            <div style={{
              marginTop: 16, paddingTop: 14,
              borderTop: '1px dashed rgba(255,255,255,0.22)',
              textAlign: 'left',
            }}>
              <Mono style={{ fontSize: 13, color: 'rgba(255,255,255,0.5)' }}>NOTE · 운영자 한마디</Mono>
              <div style={{
                marginTop: 6, fontSize: 17, lineHeight: 1.5,
                letterSpacing: '-0.005em', whiteSpace: 'pre-wrap',
                color: 'rgba(255,255,255,0.88)',
              }}>
                {data.adminMessage}
              </div>
            </div>
          )}
        </div>
      </div>
      <StoryFooter light={false} />
    </StoryFrame>
  );
}

/**
 * 회원 검색 + 선택 → 부모 onPick(member) 호출.
 *   - 검색어로 이름·회원코드·직업·지역 필터
 *   - 결과를 박스로 드롭다운(최대 8명)
 *   - 입력 외 영역 클릭 / ESC / 항목 선택 시 닫힘
 */
function MemberSearchPicker({ members, onPick, openDetail }) {
  const [q, setQ] = useSt('');
  const [open, setOpen] = useSt(false);
  const [active, setActive] = useSt(0);
  const boxRef = useRf(null);

  const MAX_DROPDOWN = 50;
  const { list, totalMatched } = useMm(() => {
    const arr = Array.isArray(members) ? members : [];
    const needle = String(q || '').trim().toLowerCase();
    const matched = !needle
      ? arr
      : arr.filter((m) => {
          if (!m) return false;
          const fields = [
            m.name, m.memberCode, m.role, m.job, m.company, m.region,
            m.mbti, String(m.age || ''),
          ];
          return fields.some((f) => String(f || '').toLowerCase().includes(needle));
        });
    return { list: matched.slice(0, MAX_DROPDOWN), totalMatched: matched.length };
  }, [members, q]);

  useEf(() => {
    if (!open) return;
    const onDown = (e) => {
      if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  const pick = (m) => {
    onPick?.(m);
    setOpen(false);
    setQ('');
  };

  const tierLabel = (t) => {
    const map = (typeof window !== 'undefined' && window.TIER_LABEL) || {};
    return map[t] || (t || '').toUpperCase();
  };

  return (
    <div ref={boxRef} style={{ position: 'relative' }}>
      <label style={{ fontSize: 12, fontWeight: 600, color: '#34302a', display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
        <span>회원 검색 (선택 시 5개 항목 자동 입력)</span>
        <Mono style={{ fontSize: 10, color: '#948a7c' }}>{Array.isArray(members) ? `${members.length}명` : '0명'}</Mono>
      </label>
      <div
        style={{
          display: 'flex', alignItems: 'center', gap: 8,
          padding: '0 10px',
          height: 38, borderRadius: 10,
          border: '1px solid #ebe3d2',
          background: '#fff',
          boxShadow: open ? '0 0 0 3px rgba(22,20,18,0.06)' : 'none',
          transition: 'box-shadow 120ms ease',
        }}
      >
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" style={{ flexShrink: 0 }}>
          <circle cx="11" cy="11" r="7" stroke="#6b6359" strokeWidth="2" />
          <path d="M20 20l-3.5-3.5" stroke="#6b6359" strokeWidth="2" strokeLinecap="round" />
        </svg>
        <input
          value={q}
          onChange={(e) => { setQ(e.target.value); setOpen(true); setActive(0); }}
          onFocus={() => setOpen(true)}
          onKeyDown={(e) => {
            if (!open) return;
            if (e.key === 'ArrowDown') { e.preventDefault(); setActive((i) => Math.min(list.length - 1, i + 1)); }
            else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((i) => Math.max(0, i - 1)); }
            else if (e.key === 'Enter') {
              const m = list[active];
              if (m) { e.preventDefault(); pick(m); }
            }
          }}
          placeholder="이름·회원코드·직업·지역으로 검색"
          style={{
            flex: 1, minWidth: 0,
            border: 0, outline: 'none', background: 'transparent',
            fontSize: 13, color: '#161412',
          }}
        />
        {q && (
          <button
            type="button"
            onClick={() => { setQ(''); setActive(0); }}
            aria-label="검색어 지우기"
            style={{
              border: 0, background: 'transparent', cursor: 'pointer',
              fontSize: 14, color: '#948a7c', padding: 4,
            }}
          >×</button>
        )}
      </div>

      {open && (
        <div
          style={{
            position: 'absolute', top: '100%', left: 0, right: 0, marginTop: 4,
            background: '#fff', border: '1px solid #ebe3d2', borderRadius: 12,
            boxShadow: '0 12px 30px rgba(20,16,12,0.18)',
            zIndex: 30, overflow: 'hidden',
            maxHeight: 480,
            display: 'flex', flexDirection: 'column',
          }}
        >
          <div style={{ overflowY: 'auto', flex: 1 }}>
          {list.length === 0 ? (
            <div style={{ padding: 16, textAlign: 'center', fontSize: 12, color: '#948a7c' }}>
              일치하는 회원이 없습니다.
            </div>
          ) : (
            list.map((m, i) => {
              const photoUrl = (m.photos && m.photos[0] && m.photos[0].url) || null;
              const isActive = i === active;
              const sido = (m.region || '').split(' ')[0];
              return (
                <button
                  type="button"
                  key={m.id || m._id || i}
                  onClick={() => pick(m)}
                  onMouseEnter={() => setActive(i)}
                  style={{
                    width: '100%',
                    display: 'flex', alignItems: 'center', gap: 10,
                    padding: '8px 12px',
                    background: isActive ? '#f4efe6' : '#fff',
                    border: 0, borderBottom: '1px solid #f4efe6',
                    cursor: 'pointer', textAlign: 'left',
                  }}
                >
                  <div
                    style={{
                      width: 32, height: 32, borderRadius: 8, flexShrink: 0,
                      background: photoUrl ? '#eee' : '#ebe3d2',
                      overflow: 'hidden',
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      fontSize: 12, fontWeight: 700, color: '#6b6359',
                    }}
                  >
                    {photoUrl ? (
                      <img src={photoUrl} alt="" loading="lazy"
                        style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                    ) : (m.name || '?').charAt(0)}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{
                      fontSize: 13, fontWeight: 700, color: '#161412',
                      display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap',
                    }}>
                      {window.MemberNameLink
                        ? React.createElement(window.MemberNameLink, {
                            member: m,
                            openDetail,
                            stopPropagation: true,
                          })
                        : (m.name || '—')}
                      <span style={{ color: '#6b6359', fontWeight: 500, fontSize: 11 }}>
                        {m.age ? `${m.age}` : ''}{m.gender ? ` · ${m.gender === 'M' ? '남' : '여'}` : ''}
                      </span>
                      <span style={{
                        fontFamily: "'JetBrains Mono', monospace", fontSize: 9, fontWeight: 700,
                        background: '#f4efe6', color: '#6b6359',
                        padding: '1px 5px', borderRadius: 4, letterSpacing: '0.04em',
                      }}>{tierLabel(m.tier)}</span>
                    </div>
                    <div style={{
                      fontSize: 11, color: '#6b6359', marginTop: 1,
                      overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                    }}>
                      {(m.memberCode || '').toUpperCase()}{m.memberCode ? ' · ' : ''}
                      {sido}{m.role ? ` · ${m.role}` : ''}
                    </div>
                  </div>
                </button>
              );
            })
          )}
          </div>
          {totalMatched > list.length && (
            <div style={{
              padding: '8px 12px', borderTop: '1px solid #f4efe6',
              background: '#fbf8f3', fontSize: 11, color: '#6b6359',
              fontFamily: "'JetBrains Mono', monospace", textAlign: 'center',
            }}>
              상위 {list.length}명 표시 중 · 검색어를 입력해 더 좁혀보세요
              <span style={{ marginLeft: 6, color: '#948a7c' }}>(전체 {totalMatched}명 일치)</span>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function MemberForm({ data, set, members, openDetail }) {
  const onFile = (e) => {
    const f = e.target.files?.[0];
    if (!f) return;
    const reader = new FileReader();
    reader.onload = () => set('photo', reader.result);
    reader.readAsDataURL(f);
  };
  return (
    <div
      style={{
        background: '#fff',
        border: '1px solid #ebe3d2',
        borderRadius: 22,
        padding: 22,
        position: 'sticky',
        top: 16,
        fontFamily: 'Pretendard, sans-serif',
      }}
    >
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
        <span style={{ width: 8, height: 8, borderRadius: 999, background: '#ec4438' }} />
        <Mono style={{ fontSize: 11, color: '#6b6359', textTransform: 'uppercase' }}>MEMBER INFO</Mono>
      </div>
      <div style={{ display: 'grid', gap: 11 }}>
        <MemberSearchPicker
          members={members || []}
          openDetail={openDetail}
          onPick={(m) => {
            if (!m) return;
            // 자동 채움: 나이 / 거주지(시·도) / 직업(직무) / 이상형 / 등급
            const sido = (m.region || '').split(' ')[0] || m.region || '';
            set('age', m.age != null ? String(m.age) : '');
            set('region', sido);
            set('job', m.role || m.job || '');
            set('ideal', m.ideal || '');
            if (m.tier) set('tier', m.tier);
            // 회원 번호와 닮은꼴은 수동 입력 — 채우지 않음
            window.__adminNotify?.(
              '회원 정보 불러옴 ✓',
              `${m.name} (${m.memberCode || ''}) · 나이·지역·직업·이상형·등급`,
              2400,
            );
          }}
        />
        <Input label="회원 번호 (선택)" value={data.no} onChange={(v) => set('no', v)} placeholder="042" />
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
          <Input label="나이" value={data.age} onChange={(v) => set('age', v)} placeholder="29" />
          <Input label="거주지" value={data.region} onChange={(v) => set('region', v)} placeholder="서울" />
        </div>
        <Input label="직업" value={data.job} onChange={(v) => set('job', v)} placeholder="피아노 선생님" />
        <div>
          <label style={{ fontSize: 12, fontWeight: 600, color: '#34302a', display: 'block', marginBottom: 6 }}>등급</label>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4 }}>
            {Object.entries(STORY_TIER_THEME).map(([k, tv]) => (
              <button
                key={k}
                type="button"
                onClick={() => set('tier', k)}
                style={{
                  padding: '9px 4px',
                  borderRadius: 8,
                  border: data.tier === k ? '2px solid #161412' : '1px solid #ebe3d2',
                  background: tv.badgeBg,
                  color: tv.badgeText,
                  fontFamily: "'JetBrains Mono', monospace",
                  fontSize: 10,
                  fontWeight: 700,
                  letterSpacing: '0.04em',
                  cursor: 'pointer',
                  boxShadow: k === 'diamond' ? 'inset 0 0 6px rgba(255,255,255,0.5)' : 'none',
                }}
              >
                {tv.label}
              </button>
            ))}
          </div>
        </div>
        <Input label="닮은꼴" value={data.lookalike} onChange={(v) => set('lookalike', v)} placeholder="토끼" />
        <div>
          <label style={{ fontSize: 12, fontWeight: 600, color: '#34302a', display: 'block', marginBottom: 6 }}>이상형</label>
          <textarea
            value={data.ideal}
            onChange={(e) => set('ideal', e.target.value)}
            rows={3}
            placeholder="키 178 이상 연상. 두부상 좋아합니다!"
            style={{
              width: '100%',
              boxSizing: 'border-box',
              padding: '10px 12px',
              border: '1px solid #ebe3d2',
              borderRadius: 10,
              fontSize: 13,
              fontFamily: 'inherit',
              resize: 'vertical',
              minHeight: 76,
              outline: 'none',
            }}
          />
        </div>
        <div>
          <label style={{
            fontSize: 12, fontWeight: 600, color: '#34302a',
            display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 6,
          }}>
            <span>관리자 한마디</span>
            <button
              type="button"
              onClick={() => set('adminMessage', '스토리 소개팅은 신청 하시면 대기 없이 신청 가능 합니다.\n단, 사전에 신청이 된 사용자만 신청이 가능하며 회원 번호도 함께 전송 해주세요')}
              title="기본 안내 문구로 되돌리기"
              style={{
                background: 'transparent', border: 0, padding: 0,
                color: '#6b6359', fontSize: 11, fontWeight: 600,
                cursor: 'pointer', fontFamily: 'inherit',
              }}
            >기본값 복원 ↺</button>
          </label>
          <textarea
            value={data.adminMessage}
            onChange={(e) => set('adminMessage', e.target.value)}
            rows={4}
            placeholder="스토리 카드 하단에 표시될 운영자 안내 문구"
            style={{
              width: '100%',
              boxSizing: 'border-box',
              padding: '10px 12px',
              border: '1px solid #ebe3d2',
              borderRadius: 10,
              fontSize: 13,
              fontFamily: 'inherit',
              resize: 'vertical',
              minHeight: 92,
              outline: 'none',
              lineHeight: 1.55,
            }}
          />
          <div style={{
            marginTop: 4, fontSize: 10.5, color: '#948a7c', fontFamily: "'JetBrains Mono', monospace",
            letterSpacing: '0.02em',
          }}>
            {(data.adminMessage || '').length} 자 · 4종 카드 하단에 동일하게 들어갑니다
          </div>
        </div>
        <div style={{ borderTop: '1px dashed #ebe3d2', paddingTop: 12, marginTop: 2 }}>
          <label style={{ fontSize: 12, fontWeight: 600, color: '#34302a', display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
            <span>사진 (선택) · 블러</span>
            {data.photo && (
              <button
                type="button"
                onClick={() => set('photo', null)}
                style={{ background: 'transparent', border: 0, color: '#a8281d', fontSize: 11, fontWeight: 600, cursor: 'pointer', padding: 0 }}
              >
                제거 ×
              </button>
            )}
          </label>
          {!data.photo ? (
            <label
              htmlFor="adm-story-photo"
              style={{
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                gap: 8,
                padding: '12px 10px',
                border: '1.5px dashed #c6bdad',
                borderRadius: 12,
                background: '#f6f2ec',
                color: '#6b6359',
                fontSize: 12.5,
                cursor: 'pointer',
              }}
            >
              + 사진 올리기 (PNG/JPG)
              <input id="adm-story-photo" type="file" accept="image/*" onChange={onFile} style={{ display: 'none' }} />
            </label>
          ) : (
            <div>
              <div style={{ position: 'relative', borderRadius: 12, overflow: 'hidden', aspectRatio: '9/16', background: (data.photoFit === 'contain') ? '#0a0807' : '#161412', maxWidth: 220, margin: '0 auto' }}>
                {(() => {
                  const { fit, fx, fy, finalScale } = readPhotoView(data, 1.08);
                  return (
                    <img
                      src={data.photo}
                      alt=""
                      style={{
                        width: '100%',
                        height: '100%',
                        objectFit: fit,
                        objectPosition: `${fx}% ${fy}%`,
                        filter: `blur(${data.blur}px)`,
                        transform: `scale(${finalScale})`,
                        transformOrigin: `${fx}% ${fy}%`,
                      }}
                    />
                  );
                })()}
                <div
                  style={{
                    position: 'absolute',
                    bottom: 6,
                    right: 6,
                    background: 'rgba(0,0,0,0.72)',
                    color: '#fff',
                    fontSize: 10,
                    fontFamily: "'JetBrains Mono', monospace",
                    padding: '3px 8px',
                    borderRadius: 999,
                  }}
                >
                  BLUR {data.blur}px
                </div>
                <div
                  style={{
                    position: 'absolute',
                    top: 6,
                    left: 6,
                    background: 'rgba(0,0,0,0.72)',
                    color: '#fff',
                    fontSize: 9,
                    fontFamily: "'JetBrains Mono', monospace",
                    padding: '3px 7px',
                    borderRadius: 999,
                    letterSpacing: '0.04em',
                  }}
                >
                  9:16 PREVIEW
                </div>
              </div>
              <div style={{ marginTop: 10 }}>
                <label style={{ fontSize: 11, color: '#6b6359', display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
                  <span>블러 강도 (0 = 선명, 최대 = 윤곽만)</span>
                  <Mono style={{ fontSize: 11 }}>{Number(data.blur).toFixed(1)}px</Mono>
                </label>

                {/* 프리셋 — 한 번에 빠르게 */}
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(6, 1fr)', gap: 4, marginTop: 6 }}>
                  {[
                    { label: '없음', value: 0 },
                    { label: '아주 약', value: 4 },
                    { label: '약함', value: 10 },
                    { label: '중간', value: 24 },
                    { label: '강함', value: 50 },
                    { label: '매우 강', value: 90 },
                  ].map((p) => {
                    const active = Math.abs(Number(data.blur) - p.value) < 0.5;
                    return (
                      <button
                        key={p.value}
                        type="button"
                        onClick={() => set('blur', p.value)}
                        style={{
                          padding: '6px 4px', borderRadius: 8,
                          background: active ? '#161412' : '#fff',
                          color: active ? '#fff' : '#34302a',
                          border: '1px solid ' + (active ? '#161412' : '#ebe3d2'),
                          fontSize: 10.5, fontWeight: 600,
                          cursor: 'pointer',
                        }}
                      >
                        {p.label}
                        <div style={{
                          fontFamily: "'JetBrains Mono', monospace",
                          fontSize: 9, opacity: 0.7, marginTop: 1,
                        }}>{p.value}px</div>
                      </button>
                    );
                  })}
                </div>

                {/* 메인 슬라이더 — 0.5px 단위 */}
                <input
                  type="range"
                  min={0}
                  max={200}
                  step={0.5}
                  value={data.blur}
                  onChange={(e) => set('blur', Number(e.target.value))}
                  style={{ width: '100%', accentColor: '#161412', marginTop: 10 }}
                />

                {/* 정밀 조정: −/+ 버튼 + 직접 입력 */}
                <div style={{
                  display: 'grid', gridTemplateColumns: '34px 34px 1fr 34px 34px', gap: 4,
                  marginTop: 8, alignItems: 'center',
                }}>
                  <button
                    type="button"
                    onClick={() => set('blur', Math.max(0, Number(data.blur) - 1))}
                    title="-1"
                    style={{
                      height: 32, borderRadius: 7, background: '#fff', border: '1px solid #ebe3d2',
                      fontSize: 14, fontWeight: 700, cursor: 'pointer',
                    }}
                  >−1</button>
                  <button
                    type="button"
                    onClick={() => set('blur', Math.max(0, Number(data.blur) - 0.5))}
                    title="-0.5"
                    style={{
                      height: 32, borderRadius: 7, background: '#fff', border: '1px solid #ebe3d2',
                      fontSize: 11, fontWeight: 600, cursor: 'pointer',
                    }}
                  >−.5</button>
                  <input
                    type="number"
                    min={0}
                    max={200}
                    step={0.5}
                    value={Number(data.blur)}
                    onChange={(e) => {
                      const v = Math.max(0, Math.min(200, Number(e.target.value) || 0));
                      set('blur', v);
                    }}
                    style={{
                      height: 32, borderRadius: 7, border: '1px solid #ebe3d2',
                      padding: '0 10px', textAlign: 'center', fontFamily: "'JetBrains Mono', monospace",
                      fontSize: 13, color: '#161412', background: '#fff',
                    }}
                  />
                  <button
                    type="button"
                    onClick={() => set('blur', Math.min(200, Number(data.blur) + 0.5))}
                    title="+0.5"
                    style={{
                      height: 32, borderRadius: 7, background: '#fff', border: '1px solid #ebe3d2',
                      fontSize: 11, fontWeight: 600, cursor: 'pointer',
                    }}
                  >+.5</button>
                  <button
                    type="button"
                    onClick={() => set('blur', Math.min(200, Number(data.blur) + 1))}
                    title="+1"
                    style={{
                      height: 32, borderRadius: 7, background: '#fff', border: '1px solid #ebe3d2',
                      fontSize: 14, fontWeight: 700, cursor: 'pointer',
                    }}
                  >+1</button>
                </div>

                <div style={{
                  marginTop: 8, padding: '8px 10px', background: '#f6f2ec',
                  borderRadius: 8, fontSize: 10.5, color: '#6b6359', lineHeight: 1.55,
                }}>
                  💡 0~200px 범위 · 슬라이더는 0.5px 단위 · ←/→ 키로도 미세 조정 가능합니다.
                </div>
              </div>

              {/* 사진 위치 · 확대 · 맞춤 — 멀리서 찍은 사진도 자연스럽게 배치 */}
              <div style={{ marginTop: 14, paddingTop: 12, borderTop: '1px dashed #ebe3d2' }}>
                <label style={{
                  fontSize: 12, fontWeight: 600, color: '#34302a',
                  display: 'flex', justifyContent: 'space-between', marginBottom: 8,
                }}>
                  <span>사진 위치 · 확대 (멀리 찍은 사진용)</span>
                  <button
                    type="button"
                    onClick={() => {
                      set('photoFit', 'cover');
                      set('photoFocusX', 50);
                      set('photoFocusY', 35);
                      set('photoZoom', 1.0);
                    }}
                    style={{
                      background: 'transparent', border: 0, padding: 0,
                      color: '#6b6359', fontSize: 11, fontWeight: 600,
                      cursor: 'pointer', fontFamily: 'inherit',
                    }}
                  >기본값 복원 ↺</button>
                </label>

                {/* 한 번 누르면 적용되는 프리셋 — 멀리 찍은 사진 대응 */}
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 4, marginBottom: 8 }}>
                  {[
                    { label: '얼굴 (상단)', tip: '인물 사진', x: 50, y: 25, zoom: 1.0, fit: 'cover' },
                    { label: '인물 중앙', tip: '기본', x: 50, y: 35, zoom: 1.0, fit: 'cover' },
                    { label: '인물 하단', tip: '하반신', x: 50, y: 75, zoom: 1.0, fit: 'cover' },
                    { label: '얼굴 확대', tip: '멀리 찍은 사진', x: 50, y: 28, zoom: 1.6, fit: 'cover' },
                    { label: '상반신 확대', tip: '먼 전신 사진', x: 50, y: 35, zoom: 1.35, fit: 'cover' },
                    { label: '전신 노컷', tip: '여백 허용', x: 50, y: 50, zoom: 1.0, fit: 'contain' },
                  ].map((p, i) => {
                    const active =
                      (data.photoFit || 'cover') === p.fit &&
                      Math.abs(Number(data.photoFocusY ?? 50) - p.y) < 1 &&
                      Math.abs(Number(data.photoFocusX ?? 50) - p.x) < 1 &&
                      Math.abs(Number(data.photoZoom ?? 1.0) - p.zoom) < 0.04;
                    return (
                      <button
                        key={i}
                        type="button"
                        onClick={() => {
                          set('photoFit', p.fit);
                          set('photoFocusX', p.x);
                          set('photoFocusY', p.y);
                          set('photoZoom', p.zoom);
                        }}
                        style={{
                          padding: '7px 4px', borderRadius: 8,
                          background: active ? '#161412' : '#fff',
                          color: active ? '#fff' : '#34302a',
                          border: '1px solid ' + (active ? '#161412' : '#ebe3d2'),
                          fontSize: 10.5, fontWeight: 600,
                          cursor: 'pointer', lineHeight: 1.2,
                          fontFamily: 'inherit',
                        }}
                      >
                        {p.label}
                        <div style={{
                          fontSize: 9, opacity: 0.65, marginTop: 2, fontWeight: 500,
                        }}>{p.tip}</div>
                      </button>
                    );
                  })}
                </div>

                {/* 맞춤 모드 토글 */}
                <div style={{ display: 'flex', gap: 4, marginBottom: 10 }}>
                  {[
                    { value: 'cover', label: '꽉 채우기', tip: '크롭됨' },
                    { value: 'contain', label: '전체 보기', tip: '여백 생김' },
                  ].map((opt) => {
                    const active = (data.photoFit || 'cover') === opt.value;
                    return (
                      <button
                        key={opt.value}
                        type="button"
                        onClick={() => set('photoFit', opt.value)}
                        style={{
                          flex: 1,
                          padding: '8px 4px', borderRadius: 8,
                          background: active ? '#161412' : '#fff',
                          color: active ? '#fff' : '#34302a',
                          border: '1px solid ' + (active ? '#161412' : '#ebe3d2'),
                          fontSize: 11, fontWeight: 600,
                          cursor: 'pointer',
                          fontFamily: 'inherit',
                        }}
                      >
                        {opt.label}
                        <div style={{
                          fontFamily: "'JetBrains Mono', monospace",
                          fontSize: 9, opacity: 0.6, marginTop: 1,
                        }}>{opt.tip}</div>
                      </button>
                    );
                  })}
                </div>

                {/* Y 세로 위치 */}
                <div style={{ marginBottom: 8 }}>
                  <label style={{
                    fontSize: 11, color: '#6b6359',
                    display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
                  }}>
                    <span>세로 위치 (위 ↔ 아래)</span>
                    <Mono style={{ fontSize: 11 }}>{Number(data.photoFocusY ?? 50).toFixed(0)}%</Mono>
                  </label>
                  <input
                    type="range"
                    min={0}
                    max={100}
                    step={1}
                    value={Number(data.photoFocusY ?? 50)}
                    onChange={(e) => set('photoFocusY', Number(e.target.value))}
                    style={{ width: '100%', accentColor: '#161412', marginTop: 4 }}
                  />
                </div>

                {/* X 가로 위치 */}
                <div style={{ marginBottom: 8 }}>
                  <label style={{
                    fontSize: 11, color: '#6b6359',
                    display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
                  }}>
                    <span>가로 위치 (왼쪽 ↔ 오른쪽)</span>
                    <Mono style={{ fontSize: 11 }}>{Number(data.photoFocusX ?? 50).toFixed(0)}%</Mono>
                  </label>
                  <input
                    type="range"
                    min={0}
                    max={100}
                    step={1}
                    value={Number(data.photoFocusX ?? 50)}
                    onChange={(e) => set('photoFocusX', Number(e.target.value))}
                    style={{ width: '100%', accentColor: '#161412', marginTop: 4 }}
                  />
                </div>

                {/* 확대 비율 */}
                <div style={{ marginBottom: 4 }}>
                  <label style={{
                    fontSize: 11, color: '#6b6359',
                    display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
                  }}>
                    <span>확대 (작게 ↔ 크게)</span>
                    <Mono style={{ fontSize: 11 }}>×{Number(data.photoZoom ?? 1.0).toFixed(2)}</Mono>
                  </label>
                  <input
                    type="range"
                    min={0.5}
                    max={2.5}
                    step={0.05}
                    value={Number(data.photoZoom ?? 1.0)}
                    onChange={(e) => set('photoZoom', Number(e.target.value))}
                    style={{ width: '100%', accentColor: '#161412', marginTop: 4 }}
                  />
                </div>

                <div style={{
                  marginTop: 8, padding: '8px 10px', background: '#f6f2ec',
                  borderRadius: 8, fontSize: 10.5, color: '#6b6359', lineHeight: 1.55,
                }}>
                  💡 멀리서 찍은 사진은 <b>「얼굴 확대」</b> 또는 <b>「상반신 확대」</b> 프리셋을 눌러보세요.
                  전신을 그대로 보이려면 <b>「전신 노컷」</b>을 선택하면 여백이 생기는 대신 사진이 잘리지 않습니다.
                </div>
              </div>
            </div>
          )}
        </div>
      </div>
      <div style={{ marginTop: 14, padding: 11, background: '#f6f2ec', borderRadius: 10, fontSize: 11, color: '#6b6359', lineHeight: 1.55 }}>
        💡 파일명은 <b>회원번호-나이-거주지-직업-A~D.png</b> 형식입니다. 각 변형 버튼 또는 상단 <b>A~D 다운로드</b>로 1080×1920을 저장할 수 있어요. (최초 1회 라이브러리 로딩에 몇 초 걸릴 수 있어요.)
      </div>
    </div>
  );
}

function Input({ label, value, onChange, placeholder }) {
  return (
    <div>
      <label style={{ fontSize: 12, fontWeight: 600, color: '#34302a', display: 'block', marginBottom: 6 }}>{label}</label>
      <input
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        style={{
          width: '100%',
          boxSizing: 'border-box',
          padding: '9px 11px',
          border: '1px solid #ebe3d2',
          borderRadius: 10,
          fontSize: 13,
          fontFamily: 'inherit',
          outline: 'none',
        }}
      />
    </div>
  );
}

const HTML_TO_IMAGE_UMD = 'https://unpkg.com/html-to-image@1.11.11/dist/html-to-image.js';

/** UMD 전역 `window.htmlToImage` 로드 (한 번만) */
let htmlToImageLoadPromise = null;
function ensureHtmlToImage() {
  if (typeof window === 'undefined') return Promise.reject(new Error('window 없음'));
  if (window.htmlToImage && typeof window.htmlToImage.toPng === 'function') {
    return Promise.resolve(window.htmlToImage);
  }
  if (!htmlToImageLoadPromise) {
    htmlToImageLoadPromise = new Promise((resolve, reject) => {
      const s = document.createElement('script');
      s.src = HTML_TO_IMAGE_UMD;
      s.async = true;
      s.onload = () => {
        if (window.htmlToImage && typeof window.htmlToImage.toPng === 'function') {
          resolve(window.htmlToImage);
        } else {
          htmlToImageLoadPromise = null;
          reject(new Error('htmlToImage 전역 없음'));
        }
      };
      s.onerror = () => {
        htmlToImageLoadPromise = null;
        reject(new Error('html-to-image 스크립트 로드 실패'));
      };
      document.head.appendChild(s);
    });
  }
  return htmlToImageLoadPromise;
}

/** 파일명용: 경로 금지 문자 제거, 빈 값은 대시 표기 */
function sanitizeStoryFilenamePart(val) {
  const s = String(val ?? '').trim();
  if (!s) return '—';
  return s
    .replace(/[/\\:*?"<>|]/g, '')
    .replace(/\s+/g, ' ')
    .slice(0, 120);
}

/** 회원번호-나이-거주지-직업-파일타입(A~D).png */
function storyPngFilename(data, fileType) {
  const no = sanitizeStoryFilenamePart(data.no);
  const age = sanitizeStoryFilenamePart(data.age);
  const region = sanitizeStoryFilenamePart(data.region);
  const job = sanitizeStoryFilenamePart(data.job);
  const u = String(fileType || 'A').toUpperCase();
  const t = /^[A-D]$/.test(u) ? u : 'A';
  return `${no}-${age}-${region}-${job}-${t}.png`;
}

async function downloadStoryPng(innerEl, filename, options = {}) {
  const { quiet = false } = options;
  const notify = window.__adminNotify;
  if (!innerEl) {
    if (!quiet) notify?.('저장 실패', '캔버스를 찾을 수 없습니다.', 3000);
    return;
  }
  try {
    let hti;
    try {
      hti = await ensureHtmlToImage();
    } catch (e) {
      htmlToImageLoadPromise = null;
      throw e;
    }
    const dataUrl = await hti.toPng(innerEl, {
      cacheBust: true,
      pixelRatio: 1,
      width: STORY_W,
      height: STORY_H,
      style: { transform: 'none', transformOrigin: 'top left' },
    });
    const a = document.createElement('a');
    a.href = dataUrl;
    a.download = filename;
    a.click();
    if (!quiet) notify?.('저장됨', `${filename} 다운로드를 시작했습니다.`, 2400);
  } catch (err) {
    console.error(err);
    notify?.('PNG 저장 실패', err?.message || 'html-to-image 오류', 4000);
  }
}

function VariantWrap({ label, tone, children, onDownload, downloadLabel }) {
  return (
    <div>
      <div
        style={{
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          flexWrap: 'wrap',
          gap: 8,
          marginBottom: 10,
        }}
      >
        <div
          style={{
            display: 'inline-flex',
            alignItems: 'center',
            gap: 6,
            padding: '4px 12px',
            borderRadius: 999,
            background: '#161412',
            color: '#fff',
            fontFamily: "'JetBrains Mono', monospace",
            fontSize: 11,
            fontWeight: 600,
            letterSpacing: '0.04em',
          }}
        >
          <span style={{ width: 6, height: 6, borderRadius: 999, background: tone === 'dark' ? '#5ad17a' : '#f5c419' }} />
          {label}
        </div>
        <button
          type="button"
          style={{
            fontSize: 12,
            fontWeight: 600,
            padding: '9px 16px',
            borderRadius: 10,
            border: 'none',
            color: '#fff',
            background: '#e23a2e',
            cursor: 'pointer',
            boxShadow: '0 2px 10px rgba(226,58,46,0.35)',
          }}
          onClick={onDownload}
        >
          {downloadLabel || 'PNG 다운로드'}
        </button>
      </div>
      <div
        style={{
          boxShadow: '0 8px 28px rgba(0,0,0,0.12), 0 0 0 1px rgba(0,0,0,0.05)',
          borderRadius: 14,
          overflow: 'hidden',
          background: '#fff',
          display: 'inline-block',
        }}
      >
        {children}
      </div>
    </div>
  );
}

function AdminMemberStoryWorkspace({ go, members, seenNewMemberIds = [], openDetail }) {
  const [data, setData] = useSt({
    no: '042',
    age: '29',
    region: '서울',
    job: '피아노 선생님',
    tier: 'black',
    lookalike: '토끼',
    ideal: '키 178 이상 연상. 두부상 좋아합니다!',
    adminMessage:
      '스토리 소개팅은 신청 하시면 대기 없이 신청 가능 합니다.\n단, 사전에 신청이 된 사용자만 신청이 가능하며 회원 번호도 함께 전송 해주세요',
    photo: null,
    blur: 20,
  });
  const set = (k, v) => setData((d) => ({ ...d, [k]: v }));

  const refA = useRf(null);
  const refB = useRf(null);
  const refC = useRf(null);
  const refD = useRf(null);

  const saveA = useCb(() => downloadStoryPng(refA.current, storyPngFilename(data, 'A')), [data]);
  const saveB = useCb(() => downloadStoryPng(refB.current, storyPngFilename(data, 'B')), [data]);
  const saveC = useCb(() => downloadStoryPng(refC.current, storyPngFilename(data, 'C')), [data]);
  const saveD = useCb(() => downloadStoryPng(refD.current, storyPngFilename(data, 'D')), [data]);

  const saveAll = useCb(async () => {
    const list = [
      [refA, storyPngFilename(data, 'A')],
      [refB, storyPngFilename(data, 'B')],
      [refC, storyPngFilename(data, 'C')],
      [refD, storyPngFilename(data, 'D')],
    ];
    for (const [ref, name] of list) {
      await downloadStoryPng(ref.current, name, { quiet: true });
      await new Promise((r) => setTimeout(r, 450));
    }
    window.__adminNotify?.('일괄 저장', '4종 PNG 다운로드를 순서대로 시작했습니다.', 3200);
  }, [data]);

  return (
    <AdminShell go={go} active="insta-story" members={members} seenNewMemberIds={seenNewMemberIds}>
      <div className="page-head" style={{ marginBottom: 14 }}>
        <div>
          <div className="crumb">인스타 컨텐츠 생성</div>
          <h1>스토리 생성</h1>
        </div>
      </div>
      <p style={{ fontSize: 13, color: 'rgba(255,255,255,0.5)', marginBottom: 18, maxWidth: 720, lineHeight: 1.55 }}>
        인스타 스토리(9:16)용 이미지 4종입니다. 입력 후 <b style={{ color: 'rgba(255,255,255,0.85)' }}>PNG 다운로드</b>로 브라우저 다운로드 폴더에 저장하세요.
      </p>
      <div style={{ marginBottom: 18, display: 'flex', flexDirection: 'column', gap: 10 }}>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center' }}>
          <button type="button" className="btn brand" onClick={saveAll}>
            4종 한 번에 PNG 저장
          </button>
          <span className="tiny" style={{ color: 'rgba(255,255,255,0.45)' }}>
            등급 색은 배지·포인트로만 쓰이고, 배경은 어두운 톤 + 블러 사진 대비를 유지합니다.
          </span>
        </div>
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
          <span className="tiny" style={{ color: 'rgba(255,255,255,0.55)', width: '100%', marginBottom: 2 }}>
            풀해상도 1080×1920 · 변형별 개별 다운로드 (파일명: 회원번호-나이-거주지-직업-A~D.png)
          </span>
          <button type="button" className="btn brand sm" onClick={saveA}>A 다운로드</button>
          <button type="button" className="btn brand sm" onClick={saveB}>B 다운로드</button>
          <button type="button" className="btn brand sm" onClick={saveC}>C 다운로드</button>
          <button type="button" className="btn brand sm" onClick={saveD}>D 다운로드</button>
        </div>
      </div>

      <div
        style={{
          display: 'grid',
          gridTemplateColumns: 'minmax(0, 300px) minmax(0, 1fr)',
          gap: 22,
          alignItems: 'flex-start',
        }}
        className="adm-story-grid"
      >
        <MemberForm data={data} set={set} members={members} openDetail={openDetail} />
        <div>
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 14, flexWrap: 'wrap', gap: 8 }}>
            <Mono style={{ fontSize: 11, color: 'rgba(255,255,255,0.45)', textTransform: 'uppercase' }}>4 VARIANTS · 1080 × 1920</Mono>
            <Mono style={{ fontSize: 10, color: 'rgba(255,255,255,0.35)' }}>Preview @{Math.round(STORY_SCALE * 100)}%</Mono>
          </div>
          <div
            style={{
              display: 'grid',
              gridTemplateColumns: 'repeat(auto-fill, minmax(360px, 1fr))',
              gap: 20,
            }}
          >
            <VariantWrap label="A · Tier accent + photo" tone="dark" onDownload={saveA} downloadLabel="A · 1080×1920 다운로드">
              <VariantA data={data} innerRef={refA} />
            </VariantWrap>
            <VariantWrap label="B · Cream card" tone="light" onDownload={saveB} downloadLabel="B · 1080×1920 다운로드">
              <VariantB data={data} innerRef={refB} />
            </VariantWrap>
            <VariantWrap label="C · Editorial age" tone="dark" onDownload={saveC} downloadLabel="C · 1080×1920 다운로드">
              <VariantC data={data} innerRef={refC} />
            </VariantWrap>
            <VariantWrap label="D · Magazine circle" tone="light" onDownload={saveD} downloadLabel="D · 1080×1920 다운로드">
              <VariantD data={data} innerRef={refD} />
            </VariantWrap>
          </div>
        </div>
      </div>
      <style>{`
        @media (max-width: 1100px) {
          .adm-story-grid { grid-template-columns: 1fr !important; }
        }
      `}</style>
    </AdminShell>
  );
}
