/* ============================================================================ * 서명전에 — TDS Top 컴포넌트 * · 페이지 최상단 영역 — 헤더/타이틀 블록을 일관되게 짜는 빌더. * 여러 슬롯(upper / 중앙 [subtitleTop+title+subtitleBottom] / right / lower)을 * 조합해서 서비스 곳곳의 진입 헤더(예: HistoryAccount 첫 줄, Result 결과 헤더, * Help/FAQ 카테고리 헤더, Onboarding 안내 헤더)를 한 줄로 짤 수 있어요. * * · 구조: * ┌──────────────────────────────────────────────┐ * │ upperGap (기본 24px) │ * ├──────────────────────────────────────────────┤ * │ upper (Asset/Lottie 등) │ * │ │ * │ ┌──────────────────────┬───────────────────┐ │ * │ │ subtitleTop │ right │ │ * │ │ title │ (Asset/Button) │ │ * │ │ subtitleBottom │ │ │ * │ └──────────────────────┴───────────────────┘ │ * │ │ * │ lower (LowerButton / LowerCTA) │ * ├──────────────────────────────────────────────┤ * │ lowerGap (기본 24px) │ * └──────────────────────────────────────────────┘ * * · 서브 (총 14): * Top.TitleParagraph — h1 텍스트 (role="heading" aria-level="1" 자동) * Top.TitleTextButton — 타이틀이 텍스트 버튼처럼 동작 (TextButton 위임) * Top.TitleSelector — 타이틀이 셀렉터 (chev-down, aria-haspopup="listbox") * Top.SubtitleParagraph — h2 부제목 (role="heading" aria-level="2" 자동) * Top.SubtitleTextButton — 부제목이 텍스트 버튼처럼 동작 * Top.SubtitleSelector — 부제목이 셀렉터 (chev-down, aria-haspopup="listbox") * Top.SubtitleBadges — 부제목 자리에 뱃지 묶음 (Badge 위임) * Top.UpperAssetContent — upper 슬롯 Asset 래퍼 (정렬 + 여백 규칙) * Top.RightAssetContent — right 슬롯 Asset 래퍼 * Top.RightButton — right 슬롯 버튼 (Button 위임, size 기본 medium) * Top.RightArrow — right 슬롯 chev-right 아이콘 단독 * Top.LowerButton — lower 작은 버튼 1개 (Button 위임, size 기본 small) * Top.LowerCTA — lower 두 버튼 (type="2-button"; left/right 버튼 슬롯) * Top.LowerCTAButton — LowerCTA 안에 들어가는 버튼 (Button 위임, size 기본 large) * * · 여백 규칙: * - upperGap / lowerGap 은 number (px) — `${n}px` 인라인. * - subtitleTop ↔ title : 6px 마진. title ↔ subtitleBottom : 6px 마진. * - upper 슬롯과 본문 사이 16px (UpperAssetContent 가 자체 margin-bottom). * - right 슬롯은 본문 텍스트 column 의 우측. 기본 vertical center, * rightVerticalAlign="end" 면 아래쪽 정렬(긴 본문에 작게 맞춤). * - lower 슬롯과 본문 사이 16px. * * · 접근성: * - TitleParagraph 는 자동으로 role="heading" + aria-level="1". * - SubtitleParagraph 는 자동으로 role="heading" + aria-level="2". * - TitleSelector / SubtitleSelector 는 자동으로 aria-haspopup="listbox". * - 호출자가 같은 prop 을 따로 주면 호출자 값이 우선(오버라이드 가능). * * · 의존: Button, TextButton, Paragraph, Badge — 미로드 시 native 폴백 + 폴백 클래스. * ========================================================================== */ // ============================================================================= // 보조 — chev SVG (셀렉터 / RightArrow) // ============================================================================= function __TopChevDown({ size = 16, color, ariaHidden = true }) { // 위에서 아래로 펼치는 셀렉터 — 16x16 viewbox return ( ); } function __TopChevRight({ size = 20, color, ariaHidden = true, ariaLabel }) { return ( ); } // ============================================================================= // 보조 — typography 힌트 (TDS t3/st2 ↔ 우리 ty-3/sub-ty-2 등) // ============================================================================= function __topTypoForTitleSize(size) { // 22 → t3 / 28 → st2 (스펙) if (size === 28) return "st2"; return "t3"; } function __topTypoForSubtitleSize(size) { // 13 → t7 / 15 → t6 / 17 → t5 (스펙) if (size === 13) return "t7"; if (size === 15) return "t6"; return "t5"; } function __topFwForSubtitleSize(size) { // 13/15 → regular / 17 → medium (스펙) if (size === 13 || size === 15) return "regular"; return "medium"; } // ============================================================================= // Root — Top // ============================================================================= function Top({ title, upperGap = 24, lowerGap = 24, upper, lower, subtitleTop, subtitleBottom, right, rightVerticalAlign = "center", className = "", style, ...rest }) { const cls = [ "tds-top", rightVerticalAlign === "end" ? "tds-top--right-end" : "tds-top--right-center", className, ] .filter(Boolean) .join(" "); // upperGap / lowerGap 은 number → `${n}px`. 음수/NaN 은 0 으로. const safeNum = function (v, fallback) { if (typeof v !== "number" || !isFinite(v) || v < 0) return fallback; return v; }; const padTop = safeNum(upperGap, 24); const padBottom = safeNum(lowerGap, 24); const mergedStyle = Object.assign( { paddingTop: `${padTop}px`, paddingBottom: `${padBottom}px`, }, style || {} ); return (
{upper ?
{upper}
: null} {/* 본문 — 좌측 텍스트 컬럼 + 우측 슬롯 */} {(title || subtitleTop || subtitleBottom || right) ? (
{subtitleTop ?
{subtitleTop}
: null} {title ?
{title}
: null} {subtitleBottom ?
{subtitleBottom}
: null}
{right ?
{right}
: null}
) : null} {lower ?
{lower}
: null}
); } // ============================================================================= // Top.TitleParagraph // ============================================================================= function TopTitleParagraph({ size = 22, color, typography, fontWeight = "bold", className = "", children, // 접근성 자동 부여 — 호출자가 명시하면 그쪽 우선 role, "aria-level": ariaLevel, ...rest }) { // size 검증 — 22 / 28 만 if (size !== 22 && size !== 28) { if (window.console && console.warn) { console.warn(`[Top.TitleParagraph] size 는 22 | 28 만 가능. 받은 값: ${size}. 22 폴백.`); } size = 22; } const tg = typography || __topTypoForTitleSize(size); // 색은 인라인으로 주입(Paragraph 는 color prop 지원). 기본 grey-800. const effectiveColor = color || "var(--tds-grey-800)"; const cls = ["tds-top__title-paragraph", className].filter(Boolean).join(" "); // Paragraph 컴포넌트 위임 — 미로드 시

폴백 if (typeof window.Paragraph === "function") { return ( {children} ); } return (

{children}

); } // ============================================================================= // Top.TitleTextButton — 타이틀이 텍스트 버튼처럼 동작 // ============================================================================= function TopTitleTextButton({ size = "xlarge", variant, color, className = "", children, ...rest }) { const cls = ["tds-top__title-text-button", className].filter(Boolean).join(" "); const effectiveColor = color || "var(--tds-grey-800)"; if (typeof window.TextButton === "function") { return ( {children} ); } // 폴백 — TextButton 미로드 시 native button return ( ); } // ============================================================================= // Top.TitleSelector — 타이틀이 셀렉터 (chev-down + aria-haspopup="listbox") // ============================================================================= function TopTitleSelector({ color, typography, fontWeight = "bold", className = "", children, // 접근성 자동 role, "aria-level": ariaLevel, "aria-haspopup": ariaHaspopup, onClick, ...rest }) { // 타이틀 셀렉터는 spec 상 size 별 토큰 — typography 만 있고 size 가 없는 형태. // 우리는 ty-3 (=22 라인) 을 기본으로 두고, 호출자가 typography 로 덮어쓸 수 있게. const tg = typography || "t3"; const effectiveColor = color || "var(--tds-grey-800)"; const cls = ["tds-top__title-selector", className].filter(Boolean).join(" "); return ( ); } // ============================================================================= // Top.SubtitleParagraph // ============================================================================= function TopSubtitleParagraph({ size = 17, color, typography, fontWeight, className = "", children, role, "aria-level": ariaLevel, ...rest }) { if (size !== 13 && size !== 15 && size !== 17) { if (window.console && console.warn) { console.warn(`[Top.SubtitleParagraph] size 는 13 | 15 | 17 만 가능. 받은 값: ${size}. 17 폴백.`); } size = 17; } const tg = typography || __topTypoForSubtitleSize(size); const fw = fontWeight || __topFwForSubtitleSize(size); const effectiveColor = color || "var(--tds-grey-700)"; const cls = ["tds-top__subtitle-paragraph", className].filter(Boolean).join(" "); if (typeof window.Paragraph === "function") { return ( {children} ); } return (

{children}

); } // ============================================================================= // Top.SubtitleTextButton // ============================================================================= function TopSubtitleTextButton({ size = "medium", variant = "arrow", color, className = "", children, ...rest }) { const cls = ["tds-top__subtitle-text-button", className].filter(Boolean).join(" "); const effectiveColor = color || "var(--tds-grey-700)"; if (typeof window.TextButton === "function") { return ( {children} ); } return ( ); } // ============================================================================= // Top.SubtitleSelector — 부제목이 셀렉터 (chev-down + aria-haspopup="listbox") // ============================================================================= function TopSubtitleSelector({ size = 17, color, typography, fontWeight, type = "arrow", // "arrow" 면 chev-down 표시, 그 외(none 등) 면 표시 안 함 className = "", children, "aria-haspopup": ariaHaspopup, onClick, ...rest }) { if (size !== 13 && size !== 15 && size !== 17) { if (window.console && console.warn) { console.warn(`[Top.SubtitleSelector] size 는 13 | 15 | 17 만 가능. 받은 값: ${size}. 17 폴백.`); } size = 17; } const tg = typography || __topTypoForSubtitleSize(size); const fw = fontWeight || __topFwForSubtitleSize(size); const effectiveColor = color || "var(--tds-grey-700)"; // chev 크기 — size 와 1:1 (16/14/12) const chevPx = size === 13 ? 14 : size === 15 ? 16 : 18; const cls = ["tds-top__subtitle-selector", className].filter(Boolean).join(" "); return ( ); } // ============================================================================= // Top.SubtitleBadges // ============================================================================= function TopSubtitleBadges({ badges, className = "", ...rest }) { if (!Array.isArray(badges)) { if (window.console && console.warn) { console.warn("[Top.SubtitleBadges] badges 는 배열이 필요해요."); } return null; } const cls = ["tds-top__subtitle-badges", className].filter(Boolean).join(" "); return (
{badges.map(function (b, i) { if (!b || typeof b !== "object") return null; // 별칭 정규화 — TDS spec 의 type/style 도 받음 const color = b.color || b.type || "blue"; const variant = b.variant || b.style || "weak"; const text = b.text; const key = b.key != null ? b.key : `top-badge-${i}`; if (typeof window.Badge === "function") { return ( {text} ); } return ( {text} ); })}
); } // ============================================================================= // Top.UpperAssetContent — upper 슬롯 Asset 래퍼 (여백 규칙) // ============================================================================= function TopUpperAssetContent({ content, className = "", ...rest }) { const cls = ["tds-top__upper-asset", className].filter(Boolean).join(" "); return (
{content}
); } // ============================================================================= // Top.RightAssetContent — right 슬롯 Asset 래퍼 // ============================================================================= function TopRightAssetContent({ content, className = "", ...rest }) { const cls = ["tds-top__right-asset", className].filter(Boolean).join(" "); return (
{content}
); } // ============================================================================= // Top.RightButton — Button 위임 (size 기본 medium) // ============================================================================= function TopRightButton({ size = "medium", color = "primary", variant, className = "", children, ...rest }) { const cls = ["tds-top__right-button", className].filter(Boolean).join(" "); if (typeof window.Button === "function") { return ( {children} ); } return ( ); } // ============================================================================= // Top.RightArrow — chev-right 아이콘 단독 (Icon 위임 자리에 inline SVG) // ============================================================================= function TopRightArrow({ color = "var(--tds-grey-600)", size = 20, name, // 호환용 — 무시되지만 받아둠 className = "", ...rest }) { const cls = ["tds-top__right-arrow", className].filter(Boolean).join(" "); return ( <__TopChevRight size={size} color={color} /> ); } // ============================================================================= // Top.LowerButton — 작은 보조 버튼 1개 (size 기본 small) // ============================================================================= function TopLowerButton({ size = "small", color = "primary", variant = "weak", display = "inline", className = "", children, ...rest }) { const cls = ["tds-top__lower-button", className].filter(Boolean).join(" "); if (typeof window.Button === "function") { return ( {children} ); } return ( ); } // ============================================================================= // Top.LowerCTA — 두 버튼 가로 배치 (type="2-button") // ============================================================================= function TopLowerCTA({ type = "2-button", leftButton, rightButton, className = "", ...rest }) { if (type !== "2-button") { if (window.console && console.warn) { console.warn(`[Top.LowerCTA] type 은 "2-button" 만 지원해요. 받은 값: ${type}.`); } } const cls = ["tds-top__lower-cta", `tds-top__lower-cta--${type}`, className] .filter(Boolean) .join(" "); return (
{leftButton}
{rightButton}
); } // ============================================================================= // Top.LowerCTAButton — LowerCTA 안 버튼 (size 기본 large) // ============================================================================= function TopLowerCTAButton({ size = "large", color = "primary", variant, display = "block", className = "", children, ...rest }) { const cls = ["tds-top__lower-cta-button", className].filter(Boolean).join(" "); if (typeof window.Button === "function") { return ( {children} ); } return ( ); } // ============================================================================= // 서브 매달기 // ============================================================================= Top.TitleParagraph = TopTitleParagraph; Top.TitleTextButton = TopTitleTextButton; Top.TitleSelector = TopTitleSelector; Top.SubtitleParagraph = TopSubtitleParagraph; Top.SubtitleTextButton = TopSubtitleTextButton; Top.SubtitleSelector = TopSubtitleSelector; Top.SubtitleBadges = TopSubtitleBadges; Top.UpperAssetContent = TopUpperAssetContent; Top.RightAssetContent = TopRightAssetContent; Top.RightButton = TopRightButton; Top.RightArrow = TopRightArrow; Top.LowerButton = TopLowerButton; Top.LowerCTA = TopLowerCTA; Top.LowerCTAButton = TopLowerCTAButton; window.Top = Top; Object.assign(window, { Top });