diff --git a/docker-compose.yml b/docker-compose.yml index d3d5cb4..d429772 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ # negosium 서버: http://localhost:9300/docs # negosium 프론트: http://localhost:3300 # negodata 서버: http://localhost:9400/docs +# 솔루션 랜딩: http://localhost:3100 # agent 서버: http://localhost:9500/docs # anchoring 배치: 포트 없음 — 상주 스케줄러(격주 토 00:00 KST), docker logs anchoring 으로 확인 # @@ -60,6 +61,17 @@ services: - /app/node_modules restart: unless-stopped + # 솔루션 랜딩페이지 (react-router dev 서버. 배포는 `npm run build` 산출물 build/client 정적 서빙). + landing: + build: ./landing + container_name: negosium-landing + ports: + - "3100:3100" + volumes: + - ./landing:/app + - /app/node_modules + restart: unless-stopped + # 협상 에이전트 (negosium_db 공유, learning 스키마 사용). agent: build: ./agent diff --git a/landing/.gitignore b/landing/.gitignore new file mode 100644 index 0000000..27d1340 --- /dev/null +++ b/landing/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +.react-router diff --git a/landing/Dockerfile b/landing/Dockerfile new file mode 100644 index 0000000..37ade9d --- /dev/null +++ b/landing/Dockerfile @@ -0,0 +1,15 @@ +# front 와 동일 구성 — Debian slim(glibc) 로 alpine(musl)+esbuild 의 ETXTBSY 회피. +FROM node:22-slim + +WORKDIR /app + +# 의존성 먼저 설치 (레이어 캐시) +COPY package.json package-lock.json ./ +RUN npm install + +COPY . . + +EXPOSE 3100 + +# react-router dev — --host 0.0.0.0 로 컨테이너 외부 노출 (package.json scripts.dev) +CMD ["npm", "run", "dev"] diff --git a/landing/app/app.css b/landing/app/app.css new file mode 100644 index 0000000..e83ae98 --- /dev/null +++ b/landing/app/app.css @@ -0,0 +1,73 @@ +@import "tailwindcss"; + +/* Pretendard 가변폰트 self-host (public/fonts). + format 은 표준 'woff2' 고정 — 'woff2-variations' 는 윈도우 브라우저가 무시함. */ +@font-face { + font-family: 'Pretendard Variable'; + font-weight: 45 920; + font-style: normal; + font-display: swap; + src: url('/fonts/PretendardVariable.woff2') format('woff2'); +} + +/* + * 랜딩 디자인 토큰 (Toss 스타일 라이트 팔레트). + * 색은 전부 여기서만 정의하고, 컴포넌트는 토큰 클래스(text-ink, bg-primary …)만 쓴다. + */ +@theme { + --font-sans: 'Pretendard Variable', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Apple SD Gothic Neo", "Malgun Gothic", sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + + /* 브랜드 */ + --color-primary: #3182F6; + --color-primary-deep: #1B64DA; /* hover */ + --color-primary-soft: #E8F3FF; /* 연한 파랑 배경 — 봇 말풍선, 배지 */ + + /* 텍스트. ink 는 다크 섹션에선 배경색으로도 쓴다 */ + --color-ink: #191F28; + --color-ink-soft: #4E5968; /* 보조 본문 */ + --color-ink-muted: #8B95A1; /* 캡션 */ + --color-ink-faint: #B0B8C1; /* 플레이스홀더·각주 */ + + /* 면·선 */ + --color-surface: #F9FAFB; /* 회색 섹션 배경 */ + --color-surface-neu: #F2F4F7; /* 뉴모피즘 히어로 전용 배경 */ + --color-fill: #F2F4F6; /* 회색 채움 — 탭 트랙, 보조 버튼 */ + --color-fill-hover: #EAECEF; + --color-line: #F2F4F6; /* 섹션 구분선 */ + --color-line-strong: #E5E8EB; /* 카드 테두리 */ + + --color-positive: #0B8F57; /* 낙찰·성공 강조 */ +} + +html { + scroll-behavior: smooth; + background-color: #ffffff; + color: var(--color-ink); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + margin: 0; +} + +/* 스크롤은 되지만 스크롤바는 숨김 (협상 콘솔 채팅 영역) */ +.scrollbar-none::-webkit-scrollbar { + display: none; +} +.scrollbar-none { + -ms-overflow-style: none; + scrollbar-width: none; +} + +/* 은은한 원형 글로우 장식 (히어로 배경) */ +.mesh-bg { + position: absolute; + width: 1000px; + height: 1000px; + border-radius: 50%; + background: radial-gradient(circle, rgba(49, 130, 246, 0.08) 0%, rgba(255, 255, 255, 0) 70%); + filter: blur(100px); + pointer-events: none; +} diff --git a/landing/app/components/sections/contact.tsx b/landing/app/components/sections/contact.tsx new file mode 100644 index 0000000..2fcc467 --- /dev/null +++ b/landing/app/components/sections/contact.tsx @@ -0,0 +1,213 @@ +import { useState } from "react" +import { AnimatePresence, motion } from "motion/react" +import { Building, CheckCircle2, Loader2, Mail, Phone, Send } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Input, Textarea } from "@/components/ui/input" +import { Section } from "@/components/ui/section" +import { SectionHeading } from "@/components/ui/section-heading" +import { Typography } from "@/components/ui/typography" +import { EASE_OUT_EXPO } from "@/lib/motion" +import type { LucideIcon } from "lucide-react" + +const EMPTY_FORM = { + companyName: '', + contactName: '', + email: '', + phone: '', + message: '', +} + +/** 도입 문의 폼. 백엔드 미연결 — 제출은 데모 처리(1.2초 후 성공 화면). */ +export function Contact() { + const [formData, setFormData] = useState(EMPTY_FORM) + const [status, setStatus] = useState<'idle' | 'submitting' | 'success'>('idle') + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + if (!formData.companyName || !formData.contactName || !formData.email || !formData.phone) { + return + } + + setStatus('submitting') + setTimeout(() => setStatus('success'), 1200) + } + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target + setFormData((prev) => ({ ...prev, [name]: value })) + } + + const submitting = status === 'submitting' + + return ( +
+
+
+ + } + > + + +
+ + {status !== 'success' ? ( + +
+ + + + + + + +
+ +
+ + + + + + + +
+ + +