o2o-negosium-original/landing/app/components/sections/hero-dataflow.tsx
Haewon Kam 7f15bc10d1 [fix] landing: 히어로 카피와 입자 밴드 겹침 — 경계를 구조로 강제
버튼 행이 입자 밴드를 파고들었다. 1280x720 에서 34px, 무리 라벨과는 7px.
1440x900 에서만 여유가 있어(45px) 안 보이던 문제다.

원인은 두 값이 서로 다른 기준으로 움직인 것:
  카피 상단 여백  19vh        화면 비율
  카피 높이       351px       고정
  밴드 시작       0.63×높이   화면 비율
카피만 px 고정이라 화면이 짧아지면 밴드가 카피를 덮는다.
0.19H + 351 > 0.63H  →  H < 798px 이면 전부 겹친다.
1280x720, 1366x768 같은 흔한 노트북 높이가 여기 들어간다.

pt-[19vh] 는 위치만 잡을 뿐 경계를 강제하지 못한다. 그래서:
- 카피 영역 높이를 밴드 시작선에 묶었다. 뷰포트와 무관하게 구조적으로 못 겹친다.
- BAND_TOP 을 export 해 캔버스와 카피가 같은 값을 쓴다. 라벨 좌표가 이미 쓰던 방식이다.
- 높이·밴드를 한 식에서 뽑는다. 따로 두면 어긋난다 — 밴드를 63vh 로 두고 높이만
  키웠더니 짧은 화면에서 변수가 실제 밴드보다 위를 가리켰다.
- min-h 바닥 780px. 밴드 위 공간에 헤더(104)+카피(351)+여유(24)=479px 가 들어가려면
  높이 760px 이 하한이라, 정렬만으로는 짧은 화면을 못 구한다. 살짝 스크롤될 뿐이다.
- pt-[104px] 를 md 이상에서도 유지. 빼면 가운데 정렬이 카피를 fixed 헤더 밑으로 민다.

검증(실측, 음수 = 밴드 밖):
  1280x620   +20 → -30,  헤더 여유 +12,  라벨 75px
  1280x720   +34 → -30,  헤더 여유 +12,  라벨 75px
  1440x900   -45 → -68,  헤더 여유 +50,  라벨 122px
  375x812          -57,  헤더 여유 +6

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 09:22:54 +09:00

103 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type * as React from "react"
import { useRef, useState } from "react"
import { ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { CTA_GAP, DISPLAY_LEAD_GAP } from "@/components/ui/section-heading"
import { Typography } from "@/components/ui/typography"
import { BAND_TOP, CLUSTER_ANCHORS, DataFlowCanvas, type DataFlowPhase } from "@/components/ui/data-flow-canvas"
/** 히어로 최소 높이. 밴드 위에 헤더·카피가 들어갈 세로 예산의 하한(계산은 section 주석 참조). */
const HERO_MIN_H = "780px"
/**
* 다크 무대 히어로 — 배경 모션이 negotium 파이프라인 자체를 그린다.
*
* 캡션이 모션과 같이 움직이는 게 핵심이다. 입자가 예뻐서 넣은 게 아니라
* 지금 화면에서 무슨 일이 일어나는지 읽히게 하려고 넣었다.
*/
export function HeroDataFlow() {
const [phase, setPhase] = useState<DataFlowPhase>("scatter")
// 순환 링이 캔버스와 같은 시계로 돌도록 진행도를 공유한다.
const progressRef = useRef(0)
return (
<section
data-stage-hero
className="stage-bg relative text-on-stage flex flex-col overflow-hidden"
/* 높이와 밴드 시작선을 같은 식에서 뽑는다. 따로 쓰면 반드시 어긋난다 —
밴드를 63vh 로 두고 높이만 키우면, 짧은 화면에서 변수가 실제 밴드보다 위를 가리킨다.
밴드 위 공간(0.63×높이)에 헤더(104) + 카피(351) + 여유(24) = 479px 가 들어가야 하므로
높이 바닥은 760px 이 하한이다. 780px 로 잡으면 짧은 화면은 살짝 스크롤될 뿐이다. */
style={
{
minHeight: `max(100vh, ${HERO_MIN_H})`,
"--band-top": `calc(${BAND_TOP} * max(100vh, ${HERO_MIN_H}))`,
} as React.CSSProperties
}
>
<DataFlowCanvas onPhaseChange={setPhase} progressRef={progressRef} className="absolute inset-0 w-full h-full" />
{/* 무대 바닥으로 떨어지는 암전 — 아래 라이트 섹션과의 경계를 부드럽게 */}
<div className="absolute inset-x-0 bottom-0 h-32 bg-linear-to-b from-transparent to-stage pointer-events-none" />
{/* 분류 단계에서만 뜨는 무리 라벨. 아이콘만으로는 "무엇이" 분류됐는지 절반만 전달된다.
좌표는 캔버스가 쓰는 값을 그대로 받아 써서 둘이 어긋날 일이 없다. */}
<div aria-hidden className="absolute inset-0 pointer-events-none z-10 hidden md:block">
{CLUSTER_ANCHORS.map((anchor) => (
<span
key={anchor.key}
className={`absolute -translate-x-1/2 -translate-y-1/2 whitespace-nowrap text-[13px] font-semibold text-on-stage-soft/80 transition-opacity duration-500 ${
phase === "cluster" ? "opacity-100" : "opacity-0"
}`}
style={{ left: `${anchor.screenX * 100}%`, top: `${anchor.labelY * 100}%` }}
>
{anchor.label}
</span>
))}
</div>
{/* 카피는 밴드 위 영역, 데이터 흐름은 하단 밴드.
예전엔 pt-[19vh] 로 위치만 잡았는데 그건 경계를 강제하지 못한다 — 카피 높이는
px 로 고정인데 밴드는 화면 비율로 내려오니, 뷰포트가 798px 보다 낮으면 버튼이
밴드를 파고들었다(1280x720 에서 34px, 라벨과는 7px). 1440x900 에서만 안 보였다.
카피 영역 높이를 밴드 시작선에 묶으면 뷰포트와 무관하게 구조적으로 겹칠 수 없다.
1.5rem 은 밴드 위 최소 숨통.
pt-[104px] 는 md 이상에서도 유지한다 — 헤더가 fixed 라 이걸 빼면 짧은 뷰포트에서
가운데 정렬이 카피를 헤더 밑으로 밀어올린다(620px 높이에서 제목 상단 8px). */}
<div className="relative z-10 w-full flex-none pt-[104px] md:h-[calc(var(--band-top)-1.5rem)] md:flex md:items-center"><div className="max-w-4xl mx-auto px-6 w-full text-center">
{/* 히어로 카피에는 진입 애니메이션을 걸지 않는다.
여기가 LCP 요소라 opacity:0 으로 시작하면 하이드레이션 전까지 첫 화면이 비고,
광고 유입처럼 JS 가 느린 환경에서 그대로 손실이 된다. 모션은 배경 캔버스가 맡는다. */}
{/* 2톤 헤드라인의 약한 쪽은 별도 회색이 아니라 같은 흰색의 투명도로 낮춘다.
네이비 위에 라벤더 회색을 얹으면 화면에 색상각이 하나 더 생겨 탁해진다. */}
<Typography variant="stageDisplay">
<span className="text-white">협력사 전원과 1:1로,</span>
<br />
<span className="text-on-stage-soft/60">동시에 협상합니다.</span>
</Typography>
{/* 히어로는 h1 이어야 해서 SectionHeading(h2 고정)을 못 쓴다. 대신 간격만은
같은 상수를 물려 최종 CTA 와 리듬이 어긋나지 않게 한다. */}
<p className={`${DISPLAY_LEAD_GAP} text-base sm:text-lg text-on-stage-soft leading-relaxed break-keep max-w-2xl mx-auto`}>
품목·목표가·마감일만 정하면 됩니다. 협상 에이전트가 협력사마다 따로 붙어 단가를 조율하고, 마감 시각에
최저 투찰가로 낙찰까지 판정합니다.
</p>
<div className={`${CTA_GAP} flex flex-col sm:flex-row gap-3 justify-center`}>
<Button href="#how-it-works" variant="stage" size="stageRound" className="group">
<span>직접 협상해 보기</span>
<ArrowRight className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
</Button>
<Button href="#contact-section" variant="stageGhost" size="stageRound">
도입 문의
</Button>
</div>
</div>
</div>
</section>
)
}