From 55e3ab02aa8f3bddc89bfca8e4d6a0a1e0dc8e17 Mon Sep 17 00:00:00 2001 From: Haewon Kam Date: Wed, 5 Aug 2026 16:22:24 +0900 Subject: [PATCH] =?UTF-8?q?[fix]=20landing:=20=EC=84=B9=EC=85=98=20?= =?UTF-8?q?=EC=84=B8=EB=A1=9C=20=EB=A6=AC=EB=93=AC=20=ED=86=B5=EC=9D=BC=20?= =?UTF-8?q?=E2=80=94=20=EA=B0=84=EA=B2=A9=EC=9D=84=20SectionHeading=20?= =?UTF-8?q?=EC=9D=B4=20=EC=86=8C=EC=9C=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 디자인 시스템이 색·활자만 소유하고 간격은 각 섹션 파일에 흩어져 있어, 같은 관계에 값이 여러 개 생겼다. 스크롤할 때 섹션마다 호흡이 달라지던 원인. eyebrow → title 12/20px → 16px title → description 16/20/32px → 20px (display 티어 32px) 머리 → 본문 48~96px 5종 → 56px / 80px(md) 컨트롤 → 본문 56/64px → 48px 리드 → CTA 56/48px → 48px - SectionHeading 이 리듬을 소유하고 HEADING_GAP·CONTROL_GAP·CTA_GAP· DISPLAY_LEAD_GAP 를 노출. 섹션 파일에서 mb-24 같은 값을 직접 쓰지 않는다. - negotiation-demo·final-cta 가 Section/SectionHeading 을 우회해 머리를 직접 조판하고 있었다 — 아이브로우 간격이 이 둘만 20px 이던 원인. 프리미티브로 환원. - SectionHeading 이 eyebrow 없이도 빈 span 을 렌더해 유령 여백이 생기던 버그 수정. - descriptionClassName 의 text-[17px] 중복 선언 제거. lead 변형이 이미 갖고 있는 모바일 축소 단계(text-[16px] sm:text-[17px])를 지우고 있었다. - reinforcement: text-balance 가 쉼표를 넘어 끊던 헤드라인을
로 명시. reinforcement 만 gap="none" 인데, 머리가 그리드 셀 안이라 마진이 상쇄되지 않아 items-center 정렬을 밀기 때문. 간격은 그리드 래퍼가 진다. 검증: tsc --noEmit 통과, 프로덕션 빌드 통과. DOM 실측으로 375/868/1440px 전 구간에서 머리→본문·eyebrow→title·컨트롤→본문 값 일치 확인. Co-Authored-By: Claude Opus 5 --- .../app/components/sections/comparison.tsx | 12 ++-- landing/app/components/sections/contact.tsx | 1 - .../app/components/sections/core-values.tsx | 3 +- landing/app/components/sections/faq.tsx | 3 +- landing/app/components/sections/final-cta.tsx | 63 ++++++++++-------- .../app/components/sections/hero-dataflow.tsx | 7 +- .../components/sections/how-it-works-demo.tsx | 9 ++- .../components/sections/negotiation-demo.tsx | 37 ++++++----- .../app/components/sections/reinforcement.tsx | 19 ++++-- .../app/components/sections/roi-simulator.tsx | 3 +- landing/app/components/ui/section-heading.tsx | 66 +++++++++++++++++-- 11 files changed, 149 insertions(+), 74 deletions(-) diff --git a/landing/app/components/sections/comparison.tsx b/landing/app/components/sections/comparison.tsx index 0ca929e..9e032dc 100644 --- a/landing/app/components/sections/comparison.tsx +++ b/landing/app/components/sections/comparison.tsx @@ -3,7 +3,7 @@ import { AnimatePresence, motion } from "motion/react" import { Clock, Database, Handshake, Scale, TrendingUp, UserRound, type LucideIcon } from "lucide-react" import { Section } from "@/components/ui/section" -import { SectionHeading } from "@/components/ui/section-heading" +import { CONTROL_GAP, SectionHeading } from "@/components/ui/section-heading" import { Typography } from "@/components/ui/typography" import { useFadeUp } from "@/lib/motion" @@ -44,18 +44,20 @@ export function Comparison() { return (
- + - {/* 모드 토글 — 이용 가이드 탭과 같은 세그먼트 문법 */} - + {/* 모드 토글 — 이용 가이드 탭과 같은 세그먼트 문법. + 아래 간격(CONTROL_GAP)은 머리 간격보다 좁다. 토글은 자기가 조종하는 + 카드들과 한 덩어리로 읽혀야지, 제목에 붙으면 무엇을 바꾸는 스위치인지 흐려진다. */} + {/* 액티브를 bg-white 로 두면 트랙(bg-fill #F0F2F8) 과 거의 같은 밝기라 멀리서 어느 쪽이 켜졌는지 안 보인다. 채움색으로 확실히 갈라준다. */}
diff --git a/landing/app/components/sections/contact.tsx b/landing/app/components/sections/contact.tsx index cebc440..254fc38 100644 --- a/landing/app/components/sections/contact.tsx +++ b/landing/app/components/sections/contact.tsx @@ -43,7 +43,6 @@ export function Contact() {
- + diff --git a/landing/app/components/sections/faq.tsx b/landing/app/components/sections/faq.tsx index a8c1fb3..a03f527 100644 --- a/landing/app/components/sections/faq.tsx +++ b/landing/app/components/sections/faq.tsx @@ -14,13 +14,12 @@ export function Faq() { return (
- + diff --git a/landing/app/components/sections/final-cta.tsx b/landing/app/components/sections/final-cta.tsx index 1991b3c..abe46ea 100644 --- a/landing/app/components/sections/final-cta.tsx +++ b/landing/app/components/sections/final-cta.tsx @@ -2,7 +2,8 @@ import { motion } from "motion/react" import { ArrowRight } from "lucide-react" import { Button } from "@/components/ui/button" -import { Typography } from "@/components/ui/typography" +import { Section } from "@/components/ui/section" +import { CTA_GAP, SectionHeading } from "@/components/ui/section-heading" import { useFadeUp } from "@/lib/motion" /** @@ -11,38 +12,44 @@ import { useFadeUp } from "@/lib/motion" * 이 섹션은 원래 디자인 시스템을 우회하고 있었다: bg-ink(무대 색이 아님), font-extrabold, * 알약 배지, blur(120px) 글로우 블롭, 다크 배경 위 라이트 토큰(text-ink-muted). * 전부 토큰으로 돌렸다. 글로우 블롭은 AI 가 만든 히어로의 대표 장식이라 걷어냈다. + * + * 섹션 껍데기와 머리도 손으로 짜던 걸 Section/SectionHeading 으로 돌렸다. + * 우회하는 동안 아이브로우 간격이 여기만 20px 이었다(다른 섹션은 12px). */ export function FinalCTA() { const fadeUp = useFadeUp() return ( -
- {/* max-w-3xl 이면 "조용히 새고 있을지 / 모릅니다" 로 끊겨 마지막 줄이 고아가 된다. */} -
- - - Start Today - + /* width="md"(max-w-4xl) — sm(max-w-3xl)이면 "조용히 새고 있을지 / 모릅니다" 로 끊겨 + 마지막 줄이 고아가 된다. */ +
+ + + 지금도 구매 예산은 +
+ 조용히 새고 있을지 모릅니다 + + } + description="협상 테이블에 오르지 못하고 그냥 넘어가던 건들이 있습니다. negotium 에이전트가 정해진 기준 안에서 그 건들을 대신 조율합니다." + descriptionClassName="max-w-xl text-on-stage-soft/75" + /> - - 지금도 구매 예산은 -
- 조용히 새고 있을지 모릅니다 -
- -

- 협상 테이블에 오르지 못하고 그냥 넘어가던 건들이 있습니다. negotium 에이전트가 정해진 기준 안에서 그 건들을 대신 - 조율합니다. -

- -
- -
-
-
-
+
+ +
+
+
) } diff --git a/landing/app/components/sections/hero-dataflow.tsx b/landing/app/components/sections/hero-dataflow.tsx index 4da983a..48c12bb 100644 --- a/landing/app/components/sections/hero-dataflow.tsx +++ b/landing/app/components/sections/hero-dataflow.tsx @@ -2,6 +2,7 @@ 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 { CLUSTER_ANCHORS, DataFlowCanvas, type DataFlowPhase } from "@/components/ui/data-flow-canvas" @@ -55,12 +56,14 @@ export function HeroDataFlow() { 동시에 협상합니다. -

+ {/* 히어로는 h1 이어야 해서 SectionHeading(h2 고정)을 못 쓴다. 대신 간격만은 + 같은 상수를 물려 최종 CTA 와 리듬이 어긋나지 않게 한다. */} +

품목·목표가·마감일만 정하면 됩니다. 협상 에이전트가 협력사마다 따로 붙어 단가를 조율하고, 마감 시각에 최저 투찰가로 낙찰까지 판정합니다.

-
+
+
) } diff --git a/landing/app/components/sections/reinforcement.tsx b/landing/app/components/sections/reinforcement.tsx index ad7660b..414f6a8 100644 --- a/landing/app/components/sections/reinforcement.tsx +++ b/landing/app/components/sections/reinforcement.tsx @@ -2,7 +2,7 @@ import { motion } from "motion/react" import { GitMerge, RefreshCw, Scale, type LucideIcon } from "lucide-react" import { Section } from "@/components/ui/section" -import { SectionHeading } from "@/components/ui/section-heading" +import { HEADING_GAP, SectionHeading } from "@/components/ui/section-heading" import { Typography } from "@/components/ui/typography" import { OrbitRing } from "@/components/ui/orbit-ring" import { useFadeUp } from "@/lib/motion" @@ -15,13 +15,25 @@ export function Reinforcement() {
{/* 순환 링은 원래 히어로에 있었는데, 루프가 의미상 속하는 자리는 여기다 — "협상할수록 좋아진다"를 말하는 섹션이고, 히어로는 이미 세로 예산이 포화였다. */} - + {/* 머리가 그리드 셀 안에 들어가 있어서 여기만 gap="none" 이다. 그리드 아이템은 + 독립 서식 문맥이라 마진이 상쇄되지 않고, 머리에 mb 를 걸면 items-center 의 + 세로 정렬을 밀어버린다. 섹션 간격은 그리드 래퍼가 대신 진다. */} +
+ 협상할수록, +
더 좋은 조건으로 + + } description={ <> 잘 깎는 요령은 담당자 머릿속에만 남고, 결과는 그날 컨디션에 흔들립니다. negotium은 지난 거래 기록과 @@ -29,7 +41,6 @@ export function Reinforcement() { 받아들여지는 선인지를 데이터로 익혀서, 결렬 없이 도달 가능한 최선을 찾아갑니다. } - descriptionClassName="mt-8" />
diff --git a/landing/app/components/sections/roi-simulator.tsx b/landing/app/components/sections/roi-simulator.tsx index 99ccb32..982c2fa 100644 --- a/landing/app/components/sections/roi-simulator.tsx +++ b/landing/app/components/sections/roi-simulator.tsx @@ -24,7 +24,6 @@ export function ROISimulator() {
@@ -38,7 +37,7 @@ export function ROISimulator() { 연간 구매 예산과 공급사 수만 입력해 보수적인 시뮬레이션 성과를 즉시 시각화해 보세요. } - descriptionClassName="mt-4 text-[17px] max-w-2xl" + descriptionClassName="max-w-2xl" />
diff --git a/landing/app/components/ui/section-heading.tsx b/landing/app/components/ui/section-heading.tsx index 36ff2b8..3f2f540 100644 --- a/landing/app/components/ui/section-heading.tsx +++ b/landing/app/components/ui/section-heading.tsx @@ -10,7 +10,45 @@ import { cn } from "@/lib/utils" * 레퍼런스에서는 영문 대문자라 조판 요소로 기능하지만, 한글은 대문자가 없어서 * 그냥 "작은 글씨"가 되고 일곱 번 반복되면 벽지가 된다. * 위계는 라벨이 아니라 헤드라인 크기(본문의 3.5~4배)와 2톤 대비로 만든다. + * + * ── 세로 리듬 ──────────────────────────────────────────────────────── + * 머리 안팎의 간격은 전부 이 파일이 소유한다. 섹션 파일에서 각자 mb-12 / mb-16 / + * mb-20 / mb-24 를 붙이던 걸 걷어낸 결과다. 같은 관계(머리 → 본문)에 값이 다섯 개 + * 있으면 스크롤할 때 섹션마다 호흡이 달라지고, 그게 "정돈이 안 됐다"는 인상의 + * 실제 원인이었다. + * + * 규칙은 하나다 — 간격이 위계를 만든다. 가까울수록 한 덩어리로 읽힌다. + * 값이 흔들리면 "어디까지가 한 덩어리인가"가 섹션마다 달라져 보인다. + * + * eyebrow → title 16px 라벨은 제목에 붙는다 + * title → description 20 / 32px 큰 활자는 더 벌린다. 64px 제목 아래 20px 은 붙어 보인다 + * 머리 → 본문 56 / 80px 섹션 안에서 가장 큰 끊김 + * + * 섹션 파일에서 이 관계들에 mb-· mt- 유틸리티를 직접 쓰지 않는다. 필요하면 아래 상수를 + * import 해서 쓰고, 새 값이 필요하다고 느껴지면 여기에 이름을 붙여서 추가한다. + * 눈대중으로 한 칸씩 조정하기 시작하면 다시 지금 상태로 돌아온다. */ + +/** 머리 → 본문. 머리를 직접 조판하는 섹션(무대 데모)도 이 상수를 쓴다. */ +export const HEADING_GAP = "mb-14 md:mb-20" +/** 탭·토글 행 → 본문. 머리 간격보다 좁아서 컨트롤이 위가 아니라 본문 쪽에 묶인다. */ +export const CONTROL_GAP = "mb-12" +/** 리드 문단 → 버튼 행 (히어로·최종 CTA). */ +export const CTA_GAP = "mt-12" +/** display 티어 제목 → 리드 문단. 히어로는 h1 이라 SectionHeading 을 못 쓰고 이 상수를 직접 쓴다. */ +export const DISPLAY_LEAD_GAP = "mt-8" + +/* + * 제목 조판 티어. 티어가 바뀌면 리드 문단까지의 간격도 같이 움직인다 — + * Typography 주석의 "크기·굵기·조임은 같이 움직인다"와 같은 이유로, 간격도 딸려간다. + */ +const SIZES = { + /** 섹션 표준 h2 (34 / 64px) */ + title: { variant: "title", descriptionGap: "mt-5" }, + /** 히어로·최종 CTA 급 디스플레이 (38~74px) */ + display: { variant: "stageDisplay", descriptionGap: DISPLAY_LEAD_GAP }, +} as const + type SectionHeadingProps = { eyebrow?: string title: React.ReactNode @@ -18,8 +56,14 @@ type SectionHeadingProps = { align?: "left" | "center" /** 다크 무대 섹션(bg="stage")에서는 "stage" 를 준다. Typography 변형이 먹색을 물고 있어서 필요하다. */ tone?: "light" | "stage" + /** 제목 조판 티어. display 는 히어로·최종 CTA 전용. */ + size?: keyof typeof SIZES + /** 머리 아래 표준 간격을 끈다. 뒤에 컨트롤 행이 붙는 섹션에서만 쓴다. */ + gap?: "heading" | "none" className?: string - /** 리드 문단 폭 제한 등 (예: "max-w-2xl") */ + /** 제목 크기 미세조정 (예: 최종 CTA 의 축소 디스플레이) */ + titleClassName?: string + /** 리드 문단 폭 제한·색 조정 (예: "max-w-2xl"). 세로 간격은 여기서 주지 않는다. */ descriptionClassName?: string } @@ -29,23 +73,31 @@ function SectionHeading({ description, align = "left", tone = "light", + size = "title", + gap = "heading", className, + titleClassName, descriptionClassName, }: SectionHeadingProps) { const onStage = tone === "stage" + const { variant, descriptionGap } = SIZES[size] return ( -
- - {eyebrow} - - +
+ {/* eyebrow 가 없을 때 빈 span 을 렌더하면 mb + 30px 행간만큼 유령 여백이 생긴다. + 지금은 모든 섹션이 eyebrow 를 넘기지만, 하나만 빼도 그 섹션만 제목이 내려앉는다. */} + {eyebrow && ( + + {eyebrow} + + )} + {title} {description && ( {description}