o2o-infinith-demo/src/components/Navbar.tsx
Haewon Kam 9c47df561b feat(contact): 푸터 연락처와 문의 폼 추가, 구글시트 적재·메일 알림 연동
연락처 통일
- 사이트 안에 이메일이 contact@o2o.kr · o2oteam@o2o.kr 두 갈래로 갈라져
  있던 것을 sales@o2o.kr 하나로 통일 (lib/contact.ts 단일 출처)
- 전화번호 010.2755.6463 을 처음으로 명시. discoveryContact.ts 는
  기존 import 경로를 깨지 않도록 재내보내기만 한다

푸터·문의 폼
- 푸터에 전화·이메일과 문의하기 버튼 추가 (사업자명·주소는 확인된 값이
  없어 넣지 않는다)
- ContactModal 신규: 성함·회사·전화·이메일·문의내용. 화면 구조는 기존
  DiscoveryLeadModal 을 그대로 따라 모달이 두 종류로 보이지 않게 한다
- Navbar 문의하기를 mailto 에서 모달로 교체. 메일 앱이 없는 사용자가
  mailto 에서 이탈하던 것을 막는다

수신 경로
- submitLead: Apps Script 웹앱으로 POST. 이 사이트는 서버 없는 SPA 라
  중간 서버를 두지 않았다. Apps Script 는 CORS 프리플라이트에 응답하지
  않아 Content-Type 을 text/plain 으로 보내 simple request 로 맞춘다
- scripts/apps-script/lead-intake.gs: 구글시트 행 추가와 알림 메일을
  한 실행에서 처리. 시트 트리거는 스크립트가 쓴 변경에 늦거나 누락되는
  경우가 있어 doPost 안에서 바로 보낸다
- 엔드포인트 미설정·전송 실패 시 접수된 것처럼 보이지 않게 하고 메일
  폴백을 띄운다 (기존 DiscoveryLeadModal 과 같은 규칙)

토큰 실값은 저장소에 넣지 않는다. 배포본 Apps Script 와 Vercel
환경변수(VITE_LEAD_ENDPOINT · VITE_LEAD_TOKEN)에만 둔다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 10:21:09 +09:00

85 lines
3.9 KiB
TypeScript

/**
* Navbar — 전역 상단 네비게이션.
*
* PART III 피봇:
* - "Free Report" CTA 완전 제거 (계약 기반 영업 모델로 전환).
* - Login 버튼 복원 — 계약 완료된 병원에 별도 발급된 자격 정보로 로그인 (스캐폴딩 단계).
* - 주 CTA는 `문의하기` — 2026-10-01 부터 mailto 대신 입력 폼(ContactModal)을 연다.
* 메일 앱이 없는 사용자가 mailto 에서 이탈하던 것을 막는다. 제출은 구글시트로 쌓인다.
*
* 라우팅 구성:
* - 로고 "INFINITH" → `/` (홈)
* - Product / Use Cases → `/#solution`, `/#use-cases` 앵커
* - Pricing → `/pricing?from=header` (유입 추적용 쿼리)
* - Login → `/login` (Secondary pill — 계약 병원 전용)
* - 문의하기 → ContactModal (Primary pill — 신규 리드)
*
* ⚠️ 앵커 링크를 `/#solution` 형태로 쓰는 이유:
* 단순 `#solution`은 현재 페이지 내부 해시로 해석됩니다.
* 예컨대 `/pricing`에서 `#solution`을 누르면 /pricing 페이지 내의 #solution을 찾다가 실패합니다.
* `/#solution`은 React Router가 홈으로 이동시킨 뒤 브라우저가 해시 스크롤을 처리합니다.
*/
import { useState } from 'react';
import { Link, useLocation } from 'react-router';
import { ArrowRight } from 'lucide-react';
import ContactModal from './ContactModal';
import ClinicNav, { clinicIdFromPath } from './ClinicNav';
export default function Navbar() {
const { pathname } = useLocation();
// 훅은 조기 return 위에 둔다. 아래 ClinicNav 분기가 먼저 반환되면 훅 호출 수가 달라진다.
const [contactOpen, setContactOpen] = useState(false);
// 병원 작업 공간(진단·빌드·사진 확인·확인 항목)에서는 절차 메뉴로 바꾼다.
const clinicId = clinicIdFromPath(pathname);
if (clinicId) return <ClinicNav clinicId={clinicId} />;
return (
<nav className="fixed top-0 left-0 right-0 z-50 bg-white/95 border-b border-slate-100 backdrop-blur-md">
<div className="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
{/* 로고 */}
<Link to="/" className="flex items-center gap-2">
<span className="font-serif text-3xl font-black tracking-[0.05em] bg-gradient-to-r from-[#4F1DA1] to-[#021341] bg-clip-text text-transparent">
INFINITH
</span>
</Link>
{/* 가운데 메뉴 */}
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
<a href="/#solution" className="hover:text-primary-900 transition-colors">
Product
</a>
<Link
to="/pricing?from=header"
className="hover:text-primary-900 transition-colors"
>
Pricing
</Link>
<a href="/#use-cases" className="hover:text-primary-900 transition-colors">
Use Cases
</a>
</div>
{/* 우측 CTA — Login(Secondary) + 문의하기(Primary) */}
<div className="flex items-center gap-3">
<Link
to="/login"
className="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-[#021341] bg-white border border-slate-200 rounded-full hover:bg-slate-50 hover:border-slate-300 transition-colors"
>
Login
</Link>
<button
type="button"
onClick={() => setContactOpen(true)}
className="inline-flex items-center gap-2 px-6 py-3 text-sm font-semibold text-white rounded-full transition-all shadow-sm hover:shadow-md bg-gradient-to-r from-[#4F1DA1] to-[#021341] hover:opacity-90"
>
문의하기
<ArrowRight className="w-4 h-4" />
</button>
</div>
</div>
<ContactModal open={contactOpen} onClose={() => setContactOpen(false)} source="navbar" />
</nav>
);
}