refactor(design): 요청문과 설명문을 텍스트 역할 토큰으로 가른다
"한 장씩 봐 주세요" 같은 요청문이 읽고 넘어가는 설명문과 같은 무게로 놓여 있어 지나치기 쉬웠다. 역할별 토큰과 클래스를 두고 화면에 적용한다. .ui-ask 요청문. 사람이 무엇을 해야 하는지 말하는 문장 (17px·600·인디고) .ui-ask-block 문단으로 서는 요청문. 왼쪽 규칙선으로 설명과 구분한다 .ui-body 설명 본문 (16px·slate-600) .ui-note 보조·출처 (14px·slate-400) 크기는 기존 화면을 세어 맞췄다. text-base 167회·text-sm 58회가 지배적이라 body 를 16px, note 를 14px 로 두면 치환해도 크기가 변하지 않고 역할과 색만 정리된다. 한글 줄바꿈: word-break keep-all 로 어절을 지키고, 긴 URL·영문 토큰만 overflow-wrap anywhere 로 자른다. text-wrap 으로 마지막 줄에 한 어절만 남는 것을 줄이고, 62ch 로 줄 길이를 묶는다. 제목에도 keep-all 과 balance 를 건다. 다크 섹션은 .on-dark 로 값을 뒤집고, 그 안의 흰 카드는 .on-light 로 되돌린다. 이걸 두지 않으면 흰 글씨가 흰 배경에 얹혀 보이지 않는다(디자인 시스템 기록된 실수). 사진 확인 화면의 썸네일 줄이 누를 수 있는 것으로 안 읽혀 같이 고친다. 72px 카드, 호버 반응, 순번 배지, 현재 위치 표시, 안 정한 것 아래 띠, 범례, 이전·다음 이동을 둔다. 상태가 안 보이는 것이 크기보다 큰 문제였다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
be1ea72fcc
commit
14c4b82155
@ -41,7 +41,7 @@ export default function AbmrLoop({ axes }: AbmrLoopProps) {
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="relative overflow-hidden bg-[#0A1128] rounded-2xl px-8 py-7 flex flex-col items-center text-left"
|
||||
className="on-dark relative overflow-hidden bg-[#0A1128] rounded-2xl px-8 py-7 flex flex-col items-center text-left"
|
||||
>
|
||||
{/* Background glow */}
|
||||
<div className="absolute top-[45%] left-1/2 -translate-x-1/2 -translate-y-1/2 w-[420px] h-[420px] bg-accent/20 rounded-full blur-[80px] pointer-events-none" />
|
||||
@ -65,7 +65,7 @@ export default function AbmrLoop({ axes }: AbmrLoopProps) {
|
||||
</svg>
|
||||
|
||||
{/* Center orb */}
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[2] w-[120px] h-[120px] md:w-[148px] md:h-[148px] rounded-full bg-[#0A1128]/90 border border-white/5 shadow-[0_0_60px_rgba(167,139,250,0.12)] flex flex-col items-center justify-center gap-0.5">
|
||||
<div className="on-dark absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[2] w-[120px] h-[120px] md:w-[148px] md:h-[148px] rounded-full bg-[#0A1128]/90 border border-white/5 shadow-[0_0_60px_rgba(167,139,250,0.12)] flex flex-col items-center justify-center gap-0.5">
|
||||
<span className="text-2xl md:text-3xl font-bold bg-gradient-to-r from-[#fff3eb] via-[#e4cfff] to-[#f5f9ff] bg-clip-text text-transparent">
|
||||
ABMR
|
||||
</span>
|
||||
@ -77,7 +77,7 @@ export default function AbmrLoop({ axes }: AbmrLoopProps) {
|
||||
{/* Nodes */}
|
||||
{NODES.map((node) => (
|
||||
<div key={node.letter} className={`absolute ${node.pos} z-[3] flex flex-col items-center gap-[7px]`}>
|
||||
<div className="w-12 h-12 md:w-[58px] md:h-[58px] rounded-full border border-purple-500/30 bg-[#0A1128]/80 shadow-[0_0_20px_rgba(168,85,247,0.15)] flex items-center justify-center">
|
||||
<div className="on-dark w-12 h-12 md:w-[58px] md:h-[58px] rounded-full border border-purple-500/30 bg-[#0A1128]/80 shadow-[0_0_20px_rgba(168,85,247,0.15)] flex items-center justify-center">
|
||||
<span className="text-xl md:text-[25px] font-bold text-purple-300">{node.letter}</span>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-purple-100">{node.label}</span>
|
||||
|
||||
@ -16,14 +16,14 @@ function ItemRow({ it }: { it: ItemScoreV2; key?: string }) {
|
||||
return (
|
||||
<div className="py-4 grid md:grid-cols-[200px_1fr_120px] gap-3 items-start">
|
||||
<div>
|
||||
<span className="text-sm font-semibold text-slate-400">{it.item.id}</span>
|
||||
<span className="ui-note font-semibold">{it.item.id}</span>
|
||||
<p className="text-base font-bold text-[#0A1128]">{it.item.name}</p>
|
||||
<p className="text-sm text-slate-400 mt-0.5">{it.item.weight}점 · {METHOD_LABEL[it.item.method]}</p>
|
||||
<p className="ui-note mt-0.5">{it.item.weight}점 · {METHOD_LABEL[it.item.method]}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-base text-slate-700">{it.item.question}</p>
|
||||
{it.evidence && <p className="text-sm text-slate-500 mt-1">{it.evidence}</p>}
|
||||
<p className="text-sm text-slate-400 mt-1">
|
||||
<p className="ui-body">{it.item.question}</p>
|
||||
{it.evidence && <p className="ui-note mt-1">{it.evidence}</p>}
|
||||
<p className="ui-note mt-1">
|
||||
{BASIS_LABEL[it.basis]}
|
||||
{it.signals.length > 0 && ` · ${it.signals.map((s) => `${s.criterionId}=${s.level === 'unverified' ? '미검증' : s.level}`).join(' · ')}`}
|
||||
</p>
|
||||
@ -51,7 +51,7 @@ function AxisCard({ ax }: { ax: AxisScoreV2 }) {
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-slate-400">검증 {ax.verifiedCount}/{ax.items.length} · {ax.earned} / {ax.possible}점</p>
|
||||
<p className="ui-note">검증 {ax.verifiedCount}/{ax.items.length} · {ax.earned} / {ax.possible}점</p>
|
||||
{ax.cappedBy.length > 0 && (
|
||||
<p className="text-sm font-semibold text-[#C084CF]">게이트 상한 {Math.min(...ax.cappedBy.map((g) => g.gate.cap))} (원점수 {ax.rawScore})</p>
|
||||
)}
|
||||
@ -60,7 +60,7 @@ function AxisCard({ ax }: { ax: AxisScoreV2 }) {
|
||||
<ScoreRing score={ax.score} size={96} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-base text-slate-600 mb-3">{ax.axis.description}</p>
|
||||
<p className="ui-body mb-3">{ax.axis.description}</p>
|
||||
<div className="divide-y divide-slate-100">
|
||||
{ax.items.map((it) => <ItemRow key={it.item.id} it={it} />)}
|
||||
</div>
|
||||
@ -81,8 +81,8 @@ export function AeoGeoV2Panel({ rubric, score }: { rubric: RubricV2; score: Over
|
||||
<ScoreRing score={ax.score} size={120} label={ax.axis.nameEn} />
|
||||
<div>
|
||||
<p className="font-serif text-2xl font-bold text-[#1D0024]">{ax.axis.code} · {ax.axis.name}</p>
|
||||
<p className="text-base text-slate-600 mt-1">{ax.axis.question}</p>
|
||||
<p className="text-sm text-slate-400 mt-2">
|
||||
<p className="ui-body mt-1">{ax.axis.question}</p>
|
||||
<p className="ui-note mt-2">
|
||||
등급 <span className="font-bold text-[#0A1128]">{ax.grade}</span> · 검증 {ax.verifiedCount}/{ax.items.length}
|
||||
{ax.cappedBy.length > 0 && ` · 게이트 상한 적용 (원점수 ${ax.rawScore})`}
|
||||
</p>
|
||||
@ -95,7 +95,7 @@ export function AeoGeoV2Panel({ rubric, score }: { rubric: RubricV2; score: Over
|
||||
<p className="text-base font-bold text-[#0A1128] mb-2">게이트 (점수가 아니라 상한)</p>
|
||||
<ul className="grid md:grid-cols-2 gap-2">
|
||||
{score.gates.map((g) => (
|
||||
<li key={g.gate.id} className="text-base text-slate-700">
|
||||
<li key={g.gate.id} className="ui-body">
|
||||
<span className={`font-bold ${g.passed ? 'text-[#6C5CE7]' : 'text-[#C084CF]'}`}>{g.gate.id} {g.gate.name} · {g.passed ? '통과' : `실패 (${g.failedBy.join(', ')}) → 상한 ${g.gate.cap}`}</span>
|
||||
<span className="text-slate-500"> {g.gate.reason}</span>
|
||||
</li>
|
||||
|
||||
@ -38,7 +38,7 @@ export default function AiAnswerMock({ clinicName, criteriaCount }: AiAnswerMock
|
||||
<circle cx="9" cy="9" r="5.5" stroke="#64748b" strokeWidth="1.5" />
|
||||
<path d="M13.5 13.5 17 17" stroke="#64748b" strokeWidth="1.5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<span className="text-base font-medium text-slate-700 break-keep">
|
||||
<span className="ui-body font-medium">
|
||||
강남에서 안전하게 가슴성형 상담받을 수 있는 병원 추천해줘
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -97,8 +97,12 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) {
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="text-sm font-bold tracking-[0.24em] text-accent mb-4">SUPPORTERS BUILD · 병원 확인 항목</div>
|
||||
<h2 className="font-serif text-3xl md:text-[40px] font-bold leading-tight text-primary-900 mb-3 break-keep">서포터즈 사이트 미리보기와 병원이 확인해 주실 것</h2>
|
||||
<p className="text-[17px] leading-[1.7] text-slate-500 mb-10 break-keep">
|
||||
병원 홈페이지 공개 자료만으로 만든 초안입니다. 아래 항목은 자료에 없어 병원만 답할 수 있는 값이며, 입력 전까지는 사이트에 "확인 대기"로 표시됩니다. 의학 검토와 게시 승인은 병원이 확정합니다.
|
||||
<p className="ui-body mb-4">
|
||||
병원 홈페이지 공개 자료만으로 만든 초안입니다.{' '}
|
||||
입력 전까지는 사이트에 "확인 대기"로 표시되고, 의학 검토와 게시 승인은 병원이 확정합니다.
|
||||
</p>
|
||||
<p className="ui-ask ui-ask-block mb-10">
|
||||
아래 항목은 공개 자료에 없어 병원만 답할 수 있는 값입니다. 채워 주세요.
|
||||
</p>
|
||||
|
||||
{/* 빌드 상태 */}
|
||||
@ -108,9 +112,9 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) {
|
||||
{build && (
|
||||
<div className="grid md:grid-cols-[1fr_auto] gap-6 items-start">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500 mb-1">상태</p>
|
||||
<p className="text-xl font-bold text-primary-900">{STATUS_LABEL[build.status] ?? build.status}{build.phase ? <span className="text-sm font-medium text-slate-500 ml-2">단계 {build.phase}</span> : null}</p>
|
||||
<p className="text-sm text-slate-400 mt-1">갱신 {build.updated_at.slice(0, 16).replace('T', ' ')} · 글 {posts.length}편{typeof build.report?.totalCostUsd === 'number' ? ` · 생성 비용 $${build.report.totalCostUsd}` : ''}</p>
|
||||
<p className="ui-note mb-1">상태</p>
|
||||
<p className="text-xl font-bold text-primary-900">{STATUS_LABEL[build.status] ?? build.status}{build.phase ? <span className="ui-note font-medium ml-2">단계 {build.phase}</span> : null}</p>
|
||||
<p className="ui-note mt-1">갱신 {build.updated_at.slice(0, 16).replace('T', ' ')} · 글 {posts.length}편{typeof build.report?.totalCostUsd === 'number' ? ` · 생성 비용 $${build.report.totalCostUsd}` : ''}</p>
|
||||
</div>
|
||||
{build.preview_url && (
|
||||
<a href={build.preview_url} target="_blank" rel="noreferrer" className="inline-flex items-center justify-center rounded-full bg-primary-900 text-white font-semibold px-6 py-3 text-base">미리보기 열기 (noindex)</a>
|
||||
@ -135,7 +139,7 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) {
|
||||
<h3 className="font-bold text-lg text-primary-900 break-keep">{f.title}</h3>
|
||||
<span className={`shrink-0 text-xs font-bold px-2.5 py-1 rounded-full ${cur ? 'bg-[#E9F7EF] text-[#2E7D4F]' : 'bg-[#FFF4E5] text-[#B26A00]'}`}>{cur ? `입력됨 ${String(current(f.key)?.created_at ?? '').slice(0, 10)}` : '확인 대기'}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mb-4 break-keep">{f.hint}</p>
|
||||
<p className="ui-body mb-4">{f.hint}</p>
|
||||
<div className="grid sm:grid-cols-2 gap-3">
|
||||
{f.inputs.map((inp) => {
|
||||
const value = draft[inp.name] ?? (cur ? String(Array.isArray(cur[inp.name]) ? (cur[inp.name] as string[]).join(', ') : cur[inp.name] ?? '') : '');
|
||||
@ -175,7 +179,7 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) {
|
||||
{build?.report?.channels && (
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-[0_8px_32px_rgba(31,38,135,0.05)]">
|
||||
<h3 className="font-bold text-lg text-primary-900 mb-1">발견한 채널</h3>
|
||||
<p className="text-sm text-slate-500 mb-3 break-keep">홈페이지·웹검색·레지스트리에서 찾아 병원 것인지 확인한 채널입니다. "확인 필요"는 병원이 맞는지 알려 주셔야 사이트에 연결합니다.</p>
|
||||
<p className="ui-note mb-3">홈페이지·웹검색·레지스트리에서 찾아 병원 것인지 확인한 채널입니다. "확인 필요"는 병원이 맞는지 알려 주셔야 사이트에 연결합니다.</p>
|
||||
<ul className="text-sm flex flex-col gap-2">
|
||||
{(['youtube', 'instagram', 'facebook', 'naverBlog', 'tiktok', 'kakao', 'gangnamunni', 'naverPlace', 'babitalk'] as const).map((p) => {
|
||||
const ok = build.report?.channels?.confirmed[p] ?? [];
|
||||
@ -195,15 +199,15 @@ export function ClinicInputsPanel({ clinicId }: { clinicId: string }) {
|
||||
)}
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-[0_8px_32px_rgba(31,38,135,0.05)]">
|
||||
<h3 className="font-bold text-lg text-primary-900 mb-1">의료진 ID</h3>
|
||||
<p className="text-sm text-slate-500 mb-3 break-keep">담당 원장·검토자 입력에 쓰는 ID 입니다. 홈페이지 의료진 페이지에서 읽었습니다.</p>
|
||||
<p className="ui-note mb-3">담당 원장·검토자 입력에 쓰는 ID 입니다. 홈페이지 의료진 페이지에서 읽었습니다.</p>
|
||||
{physicians.length ? (
|
||||
<ul className="text-sm text-slate-700 flex flex-col gap-1">{physicians.map((p) => <li key={p.id}><span className="font-mono text-accent">{p.id}</span> · {p.name} {p.title}</li>)}</ul>
|
||||
) : <p className="text-sm text-slate-400">빌드 후 표시됩니다.</p>}
|
||||
) : <p className="ui-note">빌드 후 표시됩니다.</p>}
|
||||
</div>
|
||||
<div className="bg-white border border-slate-200 rounded-2xl p-6 shadow-[0_8px_32px_rgba(31,38,135,0.05)]">
|
||||
<h3 className="font-bold text-lg text-primary-900 mb-1">2. 글별 의학 검토 · 게시 승인</h3>
|
||||
<p className="text-sm text-slate-500 mb-4 break-keep">검토가 끝난 글만 승인할 수 있고, 승인된 글만 색인 허용 빌드에 실립니다. 승인자는 의료기관 개설자(대표원장) 이름과 직함으로 적습니다.</p>
|
||||
{posts.length === 0 && <p className="text-sm text-slate-400">빌드 후 글 목록이 표시됩니다.</p>}
|
||||
<p className="ui-note mb-4">검토가 끝난 글만 승인할 수 있고, 승인된 글만 색인 허용 빌드에 실립니다. 승인자는 의료기관 개설자(대표원장) 이름과 직함으로 적습니다.</p>
|
||||
{posts.length === 0 && <p className="ui-note">빌드 후 글 목록이 표시됩니다.</p>}
|
||||
<div className="flex flex-col gap-4">
|
||||
{posts.map((p) => {
|
||||
const rv = current('post_review', p.id)?.value as { reviewer?: string; reviewedAt?: string } | undefined;
|
||||
|
||||
@ -12,7 +12,7 @@ interface DiscoveryCtaProps {
|
||||
|
||||
export default function DiscoveryCta({ criteriaCount, onStart }: DiscoveryCtaProps) {
|
||||
return (
|
||||
<section className="bg-[#0A1128] px-6 py-24">
|
||||
<section className="on-dark bg-[#0A1128] px-6 py-24">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
@ -30,7 +30,7 @@ export default function DiscoveryCta({ criteriaCount, onStart }: DiscoveryCtaPro
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStart}
|
||||
className="inline-flex items-center h-[52px] px-[30px] bg-white rounded-full text-base font-bold text-primary-900 hover:bg-slate-100 transition-colors"
|
||||
className="on-light inline-flex items-center h-[52px] px-[30px] bg-white rounded-full text-base font-bold text-primary-900 hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
URL 입력으로 시작하기
|
||||
</button>
|
||||
|
||||
@ -93,7 +93,7 @@ export default function DiscoveryLeadModal({ open, onClose }: DiscoveryLeadModal
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center px-6 bg-[#0A1128]/60 backdrop-blur-sm"
|
||||
className="on-dark fixed inset-0 z-50 flex items-center justify-center px-6 bg-[#0A1128]/60 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
data-no-print
|
||||
>
|
||||
@ -102,7 +102,7 @@ export default function DiscoveryLeadModal({ open, onClose }: DiscoveryLeadModal
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 24, scale: 0.98 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="relative w-full max-w-md bg-white rounded-2xl shadow-2xl p-8"
|
||||
className="on-light relative w-full max-w-md bg-white rounded-2xl shadow-2xl p-8"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@ -134,7 +134,7 @@ export default function DiscoveryLeadModal({ open, onClose }: DiscoveryLeadModal
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-primary-900 mb-2">신청이 접수되었습니다</h3>
|
||||
<p className="text-sm leading-relaxed text-slate-600 mb-6">
|
||||
<p className="ui-body mb-6">
|
||||
입력하신 URL을 실측 진단한 뒤, 남겨주신 연락처로 리포트를 안내드립니다.
|
||||
<br />
|
||||
문의: {DISCOVERY_CONTACT_EMAIL}
|
||||
@ -150,7 +150,7 @@ export default function DiscoveryLeadModal({ open, onClose }: DiscoveryLeadModal
|
||||
) : (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<h3 className="text-xl font-bold text-primary-900 mb-1.5">URL 입력으로 시작하기</h3>
|
||||
<p className="text-sm leading-relaxed text-slate-600 mb-6">
|
||||
<p className="ui-body mb-6">
|
||||
병원 홈페이지 URL만 입력하면 AI 검색 인용 가능성을 실측 진단합니다. 진단은
|
||||
무료입니다.
|
||||
</p>
|
||||
@ -175,7 +175,7 @@ export default function DiscoveryLeadModal({ open, onClose }: DiscoveryLeadModal
|
||||
value={clinicName}
|
||||
onChange={(e) => setClinicName(e.target.value)}
|
||||
placeholder="뷰성형외과"
|
||||
className="w-full h-12 px-4 mb-4 rounded-xl border border-slate-200 bg-white text-[15px] text-primary-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[#6C5CE7]/40"
|
||||
className="on-light w-full h-12 px-4 mb-4 rounded-xl border border-slate-200 bg-white text-[15px] text-primary-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[#6C5CE7]/40"
|
||||
/>
|
||||
|
||||
<label className="block text-xs font-semibold text-slate-500 mb-1.5">
|
||||
@ -186,7 +186,7 @@ export default function DiscoveryLeadModal({ open, onClose }: DiscoveryLeadModal
|
||||
value={contact}
|
||||
onChange={(e) => setContact(e.target.value)}
|
||||
placeholder="marketing@clinic.com"
|
||||
className="w-full h-12 px-4 mb-4 rounded-xl border border-slate-200 bg-white text-[15px] text-primary-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[#6C5CE7]/40"
|
||||
className="on-light w-full h-12 px-4 mb-4 rounded-xl border border-slate-200 bg-white text-[15px] text-primary-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-[#6C5CE7]/40"
|
||||
/>
|
||||
|
||||
{fieldError && (
|
||||
|
||||
@ -116,7 +116,7 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="bg-white py-20 px-6">
|
||||
<section className="on-light bg-white py-20 px-6">
|
||||
<div className="max-w-7xl mx-auto"><p className="text-slate-500">사진을 불러오는 중입니다.</p></div>
|
||||
</section>
|
||||
);
|
||||
@ -124,10 +124,10 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
|
||||
if (!items.length || !previewUrl) {
|
||||
return (
|
||||
<section className="bg-white py-20 px-6 [word-break:keep-all]">
|
||||
<div className="max-w-3xl mx-auto bg-white border border-slate-200 rounded-2xl p-8 shadow-[3px_4px_12px_rgba(0,0,0,0.06)]">
|
||||
<section className="on-light bg-white py-20 px-6 [word-break:keep-all]">
|
||||
<div className="on-light max-w-3xl mx-auto bg-white border border-slate-200 rounded-2xl p-8 shadow-[3px_4px_12px_rgba(0,0,0,0.06)]">
|
||||
<h2 className="font-bold text-xl text-primary-900 mb-2">아직 확인하실 사진이 없습니다</h2>
|
||||
<p className="text-[17px] leading-[1.75] text-slate-600">
|
||||
<p className="ui-body">
|
||||
{loadError
|
||||
? `자료를 불러오지 못했습니다. ${loadError}`
|
||||
: !previewUrl
|
||||
@ -140,19 +140,22 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="bg-white py-16 md:py-20 px-6 [word-break:keep-all]">
|
||||
<section className="on-light bg-white py-16 md:py-20 px-6 [word-break:keep-all]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* 진행 */}
|
||||
<div className="flex flex-wrap items-end justify-between gap-4 mb-3">
|
||||
<div>
|
||||
<h2 className="font-serif text-3xl md:text-4xl font-bold text-[#0A1128]">Photo Check</h2>
|
||||
<p className="text-[17px] leading-[1.75] text-slate-600 mt-2 max-w-[62ch]">
|
||||
홈페이지에서 모은 사진 {items.length}장입니다. 서포터즈 사이트에 실어도 괜찮은 사진인지 한 장씩 봐 주세요.
|
||||
{build?.report?.images?.used ? ` 이 중 ${build.report.images.used}장이 지금 사이트에 실립니다.` : ''}
|
||||
<p className="ui-ask ui-ask-block mt-3">
|
||||
홈페이지에서 모은 사진 {items.length}장입니다.{' '}
|
||||
서포터즈 사이트에 실어도 괜찮은 사진인지 한 장씩 봐 주세요.
|
||||
</p>
|
||||
{build?.report?.images?.used ? (
|
||||
<p className="ui-body mt-2">이 중 {build.report.images.used}장이 지금 사이트에 실립니다.</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<p className="text-sm text-slate-500">확인</p>
|
||||
<p className="ui-note">확인</p>
|
||||
<p className="text-2xl font-bold text-[#1D0024]">{decidedCount}<span className="text-slate-400 text-lg"> / {items.length}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
@ -174,7 +177,7 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.22 }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden"
|
||||
>
|
||||
<div className="bg-slate-50 flex items-center justify-center p-4">
|
||||
<img
|
||||
@ -189,11 +192,11 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
<RoleBadge role={current.role} />
|
||||
<span className="text-xs font-semibold px-2.5 py-1 rounded-full bg-slate-100 text-slate-600">{CATEGORY_LABEL[current.category]}</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-[#1D0024] mb-1.5">이 사진을 사이트에 써도 될까요?</h3>
|
||||
<p className="text-[15px] leading-[1.7] text-slate-600 mb-1">
|
||||
<h3 className="ui-ask text-lg mb-1.5">이 사진을 사이트에 써도 될까요?</h3>
|
||||
<p className="ui-body mb-1">
|
||||
{current.alt || '사진에 붙은 설명이 없습니다.'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">
|
||||
<p className="ui-note">
|
||||
출처 {current.pages[0] ?? '홈페이지'}{current.pages.length > 1 ? ` 외 ${current.pages.length - 1}곳` : ''}
|
||||
{' · '}
|
||||
<a href={current.sourceUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 underline underline-offset-4 text-slate-500">
|
||||
@ -224,14 +227,25 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
>
|
||||
<CrossFilled size={16} /> 빼 주세요
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCursor((c) => Math.max(c - 1, 0))}
|
||||
disabled={cursor === 0}
|
||||
className="rounded-full px-5 py-3 text-base font-semibold text-slate-500 disabled:opacity-40"
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
<div className="flex items-center gap-1 ml-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCursor((c) => Math.max(c - 1, 0))}
|
||||
disabled={cursor === 0}
|
||||
className="rounded-full px-4 py-3 text-base font-semibold text-slate-600 hover:bg-slate-100 disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
>
|
||||
이전
|
||||
</button>
|
||||
<span className="text-sm text-slate-400 tabular-nums px-1">{cursor + 1} / {items.length}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setCursor((c) => Math.min(c + 1, items.length - 1))}
|
||||
disabled={cursor === items.length - 1}
|
||||
className="rounded-full px-4 py-3 text-base font-semibold text-slate-600 hover:bg-slate-100 disabled:opacity-40 disabled:hover:bg-transparent"
|
||||
>
|
||||
다음
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -239,49 +253,101 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
|
||||
{/* 전체 훑기 */}
|
||||
<div className="mt-6">
|
||||
<p className="text-sm text-slate-500 mb-2">전체 {items.length}장 · 눌러서 이동</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex items-baseline justify-between gap-3 mb-3">
|
||||
<h3 className="text-base font-bold text-[#1D0024]">다른 사진으로 이동</h3>
|
||||
<p className="ui-note">
|
||||
{items.length - decidedCount > 0
|
||||
? <>아직 <strong className="text-[#1D0024]">{items.length - decidedCount}장</strong>이 남았습니다</>
|
||||
: '모두 확인하셨습니다'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{items.map((i, idx) => {
|
||||
const d = decisions[i.id];
|
||||
const isCur = idx === cursor;
|
||||
return (
|
||||
<button
|
||||
key={i.id}
|
||||
type="button"
|
||||
onClick={() => setCursor(idx)}
|
||||
aria-label={`${idx + 1}번째 사진`}
|
||||
className={`relative w-14 h-14 rounded-lg overflow-hidden border-2 transition-colors ${
|
||||
idx === cursor ? 'border-[#6C5CE7]' : d === 'exclude' ? 'border-[#D4889A]' : d === 'use' ? 'border-[#9B8AD4]' : 'border-slate-200'
|
||||
}`}
|
||||
aria-label={`${idx + 1}번째 사진으로 이동${d ? (d === 'use' ? ', 사용으로 정함' : ', 빼기로 정함') : ', 아직 안 정함'}`}
|
||||
aria-current={isCur ? 'true' : undefined}
|
||||
title={i.alt || `${idx + 1}번째 사진`}
|
||||
className={`group relative w-[72px] h-[72px] rounded-xl overflow-hidden bg-slate-100 cursor-pointer
|
||||
transition-all duration-150 outline-none
|
||||
hover:-translate-y-0.5 hover:shadow-[3px_5px_14px_rgba(0,0,0,0.14)]
|
||||
focus-visible:ring-2 focus-visible:ring-[#6C5CE7] focus-visible:ring-offset-2
|
||||
${isCur
|
||||
? 'ring-2 ring-[#6C5CE7] ring-offset-2 shadow-[3px_4px_12px_rgba(108,92,231,0.28)]'
|
||||
: 'ring-1 ring-slate-200 hover:ring-[#9B8AD4]'}`}
|
||||
>
|
||||
<img src={`${previewUrl}${i.src}`} alt="" className="w-full h-full object-cover" loading="lazy" />
|
||||
{d === 'exclude' && <span className="absolute inset-0 bg-[#FFF0F0]/70 flex items-center justify-center text-[#7C3A4B]"><CrossFilled size={14} /></span>}
|
||||
{d === 'use' && <span className="absolute bottom-0 right-0 bg-[#F3F0FF] text-[#4A3A7C] p-0.5 rounded-tl"><CheckFilled size={10} /></span>}
|
||||
<img
|
||||
src={`${previewUrl}${i.src}`}
|
||||
alt=""
|
||||
loading="lazy"
|
||||
className={`w-full h-full object-cover transition-transform duration-200 group-hover:scale-[1.06]
|
||||
${d === 'exclude' ? 'grayscale opacity-45' : ''}`}
|
||||
/>
|
||||
|
||||
{/* 순번. 어디까지 봤는지 세기 쉽게 한다 */}
|
||||
<span className="on-dark absolute top-1 left-1 min-w-[18px] h-[18px] px-1 rounded-md bg-[#0A1128]/65 text-white text-[11px] font-semibold leading-[18px] text-center">
|
||||
{idx + 1}
|
||||
</span>
|
||||
|
||||
{/* 정한 것 */}
|
||||
{d === 'exclude' && (
|
||||
<span className="absolute inset-0 flex items-center justify-center">
|
||||
<span className="w-6 h-6 rounded-full bg-[#FFF0F0] border border-[#F5D5DC] text-[#7C3A4B] flex items-center justify-center">
|
||||
<CrossFilled size={12} />
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{d === 'use' && (
|
||||
<span className="absolute bottom-1 right-1 w-5 h-5 rounded-full bg-[#F3F0FF] border border-[#D5CDF5] text-[#4A3A7C] flex items-center justify-center">
|
||||
<CheckFilled size={11} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 아직 안 정한 것은 아래에 띠를 둬서 남은 일이 보이게 한다 */}
|
||||
{!d && <span className="absolute inset-x-0 bottom-0 h-1 bg-[#D4A872]" />}
|
||||
|
||||
{/* 현재 보고 있는 것 */}
|
||||
{isCur && (
|
||||
<span className="absolute inset-x-0 bottom-0 bg-[#6C5CE7] text-white text-[10px] font-bold text-center py-0.5">
|
||||
보는 중
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="ui-note flex flex-wrap items-center gap-x-4 gap-y-1 mt-3">
|
||||
<span className="inline-flex items-center gap-1.5"><span className="w-3 h-1 rounded-full bg-[#D4A872]" /> 아직 안 정함</span>
|
||||
<span className="inline-flex items-center gap-1.5"><span className="w-3.5 h-3.5 rounded-full bg-[#F3F0FF] border border-[#D5CDF5]" /> 사용</span>
|
||||
<span className="inline-flex items-center gap-1.5"><span className="w-3.5 h-3.5 rounded-full bg-[#FFF0F0] border border-[#F5D5DC]" /> 빼기</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 판단 기준 + 제출 */}
|
||||
<div className="flex flex-col gap-5 lg:sticky lg:top-24">
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6">
|
||||
<div className="flex items-center gap-2 mb-3 text-[#7C5C3A]">
|
||||
<WarningFilled size={16} />
|
||||
<h3 className="text-base font-bold">이런 사진이면 빼 주세요</h3>
|
||||
</div>
|
||||
<ul className="flex flex-col gap-2">
|
||||
{CHECKPOINTS.map((c) => (
|
||||
<li key={c} className="text-[15px] leading-[1.65] text-[#1D0024] flex gap-2">
|
||||
<li key={c} className="ui-body !text-[#1D0024] flex gap-2">
|
||||
<span className="mt-[9px] w-1.5 h-1.5 rounded-full bg-[#D4A872] shrink-0" />
|
||||
<span>{c}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="text-sm text-slate-500 mt-4">판단이 서지 않으면 빼는 쪽으로 봐 주세요. 나중에 다시 넣을 수 있습니다.</p>
|
||||
<p className="ui-ask text-[15px] mt-4">판단이 서지 않으면 빼는 쪽으로 봐 주세요.{' '}<span className="ui-note">나중에 다시 넣을 수 있습니다.</span></p>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6">
|
||||
<h3 className="text-base font-bold text-[#1D0024] mb-3">확인 결과</h3>
|
||||
<dl className="flex flex-col gap-1.5 text-[15px]">
|
||||
<div className="flex justify-between"><dt className="text-slate-500">사용</dt><dd className="font-bold text-[#4A3A7C]">{decidedCount - excluded.length}장</dd></div>
|
||||
@ -313,7 +379,7 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
)}
|
||||
{saveError && <p className="text-sm text-[#7C3A4B] mt-3">보내지 못했습니다. {saveError}</p>}
|
||||
{!done && decidedCount > 0 && !saved && (
|
||||
<p className="text-sm text-slate-500 mt-3">지금 보내고 나중에 이어서 보셔도 됩니다.</p>
|
||||
<p className="ui-note mt-3">지금 보내고 나중에 이어서 보셔도 됩니다.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -322,7 +388,7 @@ export function ImageReviewPanel({ clinicId }: { clinicId: string }) {
|
||||
href={previewUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-white border border-slate-200 text-[#021341] font-semibold px-6 py-3 text-base"
|
||||
className="on-light inline-flex items-center justify-center gap-2 rounded-full bg-white border border-slate-200 text-[#021341] font-semibold px-6 py-3 text-base"
|
||||
>
|
||||
<EyeFilled size={15} /> 사이트 초안 열기
|
||||
</a>
|
||||
|
||||
@ -89,7 +89,7 @@ export default function SampleReport({ overall, axes, result }: SampleReportProp
|
||||
<div className={`px-3.5 py-[5px] border rounded-full ${GRADE_BADGE_STYLE[ax.grade]}`}>
|
||||
<span className="text-sm font-bold">{ax.code} 등급 {ax.grade}</span>
|
||||
</div>
|
||||
<span className="text-sm text-slate-500">{ax.name}</span>
|
||||
<span className="ui-note">{ax.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -161,7 +161,7 @@ export default function SampleReport({ overall, axes, result }: SampleReportProp
|
||||
<span className="text-[13px] font-bold">{finding.label}</span>
|
||||
</div>
|
||||
<div className="text-[15.5px] font-semibold text-primary-900 break-keep">{finding.title}</div>
|
||||
<p className="text-sm leading-[1.7] text-slate-500 break-keep">{finding.detail}</p>
|
||||
<p className="ui-note leading-[1.7]">{finding.detail}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -28,7 +28,7 @@ const STATS = [
|
||||
|
||||
export default function WhyNow() {
|
||||
return (
|
||||
<section id="why-now" className="bg-[#0A1128] px-6 py-22">
|
||||
<section id="why-now" className="on-dark bg-[#0A1128] px-6 py-22">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
|
||||
@ -22,7 +22,7 @@ export function SectionWrapper({
|
||||
id={id}
|
||||
className={`
|
||||
${dark
|
||||
? 'bg-[#0A1128] text-white relative overflow-hidden'
|
||||
? 'on-dark bg-[#0A1128] text-white relative overflow-hidden'
|
||||
: 'bg-white'
|
||||
}
|
||||
py-16 md:py-20 px-6
|
||||
|
||||
@ -20,6 +20,22 @@
|
||||
|
||||
--color-accent: #6C5CE7; /* Purple accent */
|
||||
|
||||
/* 흰·밝은 배경 위 강조 텍스트. haewon이 말하는 "인디고" */
|
||||
--color-indigo: #1D0024;
|
||||
|
||||
/* 텍스트 역할 토큰.
|
||||
ask = 요청문. 사람에게 무엇을 해달라고 하는 문장 (읽고 끝나는 설명과 구분한다)
|
||||
body = 설명 본문
|
||||
note = 보조 설명·출처 */
|
||||
--color-text-ask: #1D0024;
|
||||
--color-text-body: #475569;
|
||||
--color-text-note: #94A3B8;
|
||||
--color-ask-rule: #D5CDF5;
|
||||
|
||||
--text-ask: 1.0625rem; /* 17px. 본문(16px)보다 한 단 크게 */
|
||||
--text-body: 1rem; /* 16px. 기존 text-base 와 같다 */
|
||||
--text-note: 0.875rem; /* 14px. 기존 text-sm 과 같다 */
|
||||
|
||||
/* INFINITH Semantic Status Colors — pastel palette derived from brand gradient */
|
||||
/* Critical/Negative: warm rose (peach-toned) */
|
||||
--color-status-critical-bg: #FFF0F0;
|
||||
@ -158,4 +174,75 @@
|
||||
|
||||
.animation-delay-14000 {
|
||||
animation-delay: 14s;
|
||||
}
|
||||
}
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────
|
||||
텍스트 역할 클래스
|
||||
|
||||
요청문(.ui-ask)과 설명문(.ui-body)을 눈에 띄게 가른다. 요청문은 사람이
|
||||
무엇을 해야 하는지 말하는 문장이라, 읽고 넘어가는 설명과 같은 무게로 두면
|
||||
지나친다. 색·굵기·크기를 함께 올리고 왼쪽에 규칙선을 둔다.
|
||||
|
||||
한글 줄바꿈:
|
||||
word-break: keep-all → 단어(어절) 중간에서 자르지 않는다
|
||||
overflow-wrap: anywhere → 긴 URL·영문 토큰은 예외적으로 자른다
|
||||
text-wrap: pretty → 마지막 줄에 한 어절만 남는 것을 줄인다
|
||||
max-width 62ch → 한 줄이 길어 눈이 되돌아오는 것을 막는다
|
||||
────────────────────────────────────────────────────────────── */
|
||||
.ui-ask,
|
||||
.ui-body,
|
||||
.ui-note {
|
||||
word-break: keep-all;
|
||||
overflow-wrap: anywhere;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
.ui-ask {
|
||||
font-size: var(--text-ask);
|
||||
line-height: 1.75;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-ask);
|
||||
max-width: 62ch;
|
||||
}
|
||||
|
||||
/* 문단으로 서는 요청문. 왼쪽 규칙선으로 설명 본문과 구분한다 */
|
||||
.ui-ask-block {
|
||||
border-left: 3px solid var(--color-ask-rule);
|
||||
padding-left: 0.875rem;
|
||||
}
|
||||
|
||||
.ui-body {
|
||||
font-size: var(--text-body);
|
||||
line-height: 1.75;
|
||||
color: var(--color-text-body);
|
||||
max-width: 62ch;
|
||||
}
|
||||
|
||||
.ui-note {
|
||||
font-size: var(--text-note);
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-note);
|
||||
}
|
||||
|
||||
/* 다크 섹션 위에서는 같은 역할을 밝은 값으로 뒤집는다 */
|
||||
.on-dark .ui-ask { color: #FFFFFF; }
|
||||
.on-dark .ui-ask-block { border-left-color: rgba(213, 205, 245, 0.45); }
|
||||
.on-dark .ui-body { color: rgba(255, 255, 255, 0.72); }
|
||||
.on-dark .ui-note { color: rgba(255, 255, 255, 0.45); }
|
||||
|
||||
/* 다크 섹션 안의 흰 카드는 다시 라이트 값으로 되돌린다.
|
||||
이걸 두지 않으면 흰 글씨가 흰 배경에 얹혀 보이지 않는다. 흰 카드에 .on-light 를 붙인다. */
|
||||
.on-dark .on-light .ui-ask,
|
||||
.on-light .ui-ask { color: var(--color-text-ask); }
|
||||
.on-dark .on-light .ui-ask-block,
|
||||
.on-light .ui-ask-block { border-left-color: var(--color-ask-rule); }
|
||||
.on-dark .on-light .ui-body,
|
||||
.on-light .ui-body { color: var(--color-text-body); }
|
||||
.on-dark .on-light .ui-note,
|
||||
.on-light .ui-note { color: var(--color-text-note); }
|
||||
|
||||
/* 제목도 어절 단위로 끊는다. 한글 제목이 글자 중간에서 잘리면 읽기 나쁘다 */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
word-break: keep-all;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@ -192,7 +192,7 @@ export default function ClinicProfilePage() {
|
||||
<div className="pt-20 min-h-screen bg-slate-50">
|
||||
|
||||
{/* ── Hero / Clinic Header ── */}
|
||||
<div className="bg-[#0A1128] pt-10 pb-16 px-6 relative overflow-hidden">
|
||||
<div className="on-dark bg-[#0A1128] pt-10 pb-16 px-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full bg-[#7B2D8E]/10 blur-[120px]" />
|
||||
<div className="absolute bottom-0 left-0 w-[300px] h-[300px] rounded-full bg-purple-500/5 blur-[100px]" />
|
||||
|
||||
@ -253,7 +253,7 @@ export default function ClinicProfilePage() {
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
>
|
||||
<h2 className="font-serif text-lg font-bold text-primary-900 mb-4 flex items-center gap-2">
|
||||
<Building2 size={18} className="text-[#6C5CE7]" />
|
||||
@ -304,7 +304,7 @@ export default function ClinicProfilePage() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
>
|
||||
<h2 className="font-serif text-lg font-bold text-primary-900 mb-4 flex items-center gap-2">
|
||||
<Star size={18} className="text-[#6C5CE7]" />
|
||||
@ -348,7 +348,7 @@ export default function ClinicProfilePage() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
>
|
||||
<h2 className="font-serif text-lg font-bold text-primary-900 mb-4 flex items-center gap-2">
|
||||
<Stethoscope size={18} className="text-[#6C5CE7]" />
|
||||
@ -396,7 +396,7 @@ export default function ClinicProfilePage() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
>
|
||||
<h2 className="font-serif text-lg font-bold text-primary-900 mb-4 flex items-center gap-2">
|
||||
<Heart size={18} className="text-[#6C5CE7]" />
|
||||
@ -428,7 +428,7 @@ export default function ClinicProfilePage() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
>
|
||||
<h2 className="font-serif text-lg font-bold text-primary-900 mb-4 flex items-center gap-2">
|
||||
<ShieldCheck size={18} className="text-[#6C5CE7]" />
|
||||
@ -464,7 +464,7 @@ export default function ClinicProfilePage() {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6"
|
||||
>
|
||||
<h2 className="font-serif text-lg font-bold text-primary-900 mb-4 flex items-center gap-2">
|
||||
<TrendingUp size={18} className="text-[#6C5CE7]" />
|
||||
|
||||
@ -237,7 +237,7 @@ export default function DataValidationPage() {
|
||||
return (
|
||||
<div className="pt-20 min-h-screen">
|
||||
{/* ── Header ── */}
|
||||
<div className="bg-[#0A1128] py-14 px-6 relative overflow-hidden">
|
||||
<div className="on-dark bg-[#0A1128] py-14 px-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full bg-[#6C5CE7]/10 blur-[120px]" />
|
||||
<div className="absolute bottom-0 left-0 w-[300px] h-[300px] rounded-full bg-purple-500/5 blur-[100px]" />
|
||||
<div className="max-w-6xl mx-auto relative">
|
||||
@ -259,7 +259,7 @@ export default function DataValidationPage() {
|
||||
<h2 className="font-serif text-2xl font-bold text-primary-900 mb-2">수집 프로세스</h2>
|
||||
<p className="text-slate-500 text-sm mb-6">URL 하나로 시작하는 멀티소스 데이터 수집 파이프라인</p>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 md:p-8">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 md:p-8">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-4 md:gap-0">
|
||||
{CRAWL_STEPS.map((step, i) => {
|
||||
const Icon = step.icon;
|
||||
@ -309,7 +309,7 @@ export default function DataValidationPage() {
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden"
|
||||
>
|
||||
{/* Card Header */}
|
||||
<div className="p-5 pb-4 flex items-center justify-between">
|
||||
@ -409,7 +409,7 @@ export default function DataValidationPage() {
|
||||
</motion.div>
|
||||
|
||||
{/* Alternatives Table */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden">
|
||||
<div className="grid grid-cols-[1fr_1fr_auto_auto] gap-x-4 px-5 py-3 bg-slate-50 border-b border-slate-100 text-xs font-semibold text-slate-400 uppercase tracking-wider">
|
||||
<span>차단된 데이터</span>
|
||||
<span>대안 솔루션</span>
|
||||
@ -443,7 +443,7 @@ export default function DataValidationPage() {
|
||||
<h2 className="font-serif text-2xl font-bold text-primary-900 mb-2">전체 데이터 커버리지</h2>
|
||||
<p className="text-slate-500 text-sm mb-6">3가지 수집 방법 조합으로 100% 분석 데이터 확보</p>
|
||||
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 md:p-8 space-y-5">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 md:p-8 space-y-5">
|
||||
{COVERAGE.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.method}
|
||||
@ -548,7 +548,7 @@ export default function DataValidationPage() {
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ delay: i * 0.08 }}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-5 text-center"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-5 text-center"
|
||||
>
|
||||
<p className="text-xs text-slate-400 mb-1">{item.name}</p>
|
||||
<p className="text-xl font-bold text-primary-900 mb-0.5">{item.cost}</p>
|
||||
|
||||
@ -97,7 +97,7 @@ export default function DistributionPage() {
|
||||
return (
|
||||
<div className="pt-20 min-h-screen">
|
||||
{/* Header */}
|
||||
<div className="bg-[#0A1128] py-14 px-6 relative overflow-hidden">
|
||||
<div className="on-dark bg-[#0A1128] py-14 px-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full bg-[#6C5CE7]/10 blur-[120px]" />
|
||||
<div className="max-w-5xl mx-auto relative">
|
||||
<p className="text-xs font-semibold text-purple-300 tracking-widest uppercase mb-3">Content Distribution</p>
|
||||
|
||||
@ -10,7 +10,7 @@ export default function ImageReviewPage() {
|
||||
if (!id) return null;
|
||||
return (
|
||||
<div className="[word-break:keep-all]">
|
||||
<section className="bg-[#0A1128] text-white relative overflow-hidden pt-28 pb-14 md:pt-36 md:pb-16 px-6">
|
||||
<section className="on-dark bg-[#0A1128] text-white relative overflow-hidden pt-28 pb-14 md:pt-36 md:pb-16 px-6">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(108,92,231,0.18),transparent_60%)]" />
|
||||
<div className="relative max-w-7xl mx-auto">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-white/10 border border-white/10 px-4 py-1.5 mb-6">
|
||||
@ -19,10 +19,11 @@ export default function ImageReviewPage() {
|
||||
<h1 className="font-serif text-4xl md:text-6xl font-bold tracking-[-0.02em] mb-3">
|
||||
<span className="bg-gradient-to-r from-purple-300 to-blue-300 bg-clip-text text-transparent">사진을 확인해 주세요</span>
|
||||
</h1>
|
||||
<p className="text-white/70 text-base max-w-[62ch] leading-[1.75]">
|
||||
병원 홈페이지에 올라와 있는 사진을 그대로 가져왔습니다. 그중에는 사이트에 싣기 어려운 사진이 섞여 있을 수 있어,
|
||||
병원에서 한 장씩 봐 주셔야 합니다.
|
||||
<p className="ui-body">
|
||||
병원 홈페이지에 올라와 있는 사진을 그대로 가져왔습니다.{' '}
|
||||
그중에는 사이트에 싣기 어려운 사진이 섞여 있을 수 있습니다.
|
||||
</p>
|
||||
<p className="ui-ask ui-ask-block mt-4">병원에서 한 장씩 봐 주셔야 합니다.</p>
|
||||
<p className="text-purple-200 text-sm mt-4">
|
||||
병원 ID <span className="font-mono text-white">{id}</span>
|
||||
{' · '}
|
||||
|
||||
@ -143,7 +143,7 @@ export default function PerformancePage() {
|
||||
return (
|
||||
<div className="pt-20 min-h-screen">
|
||||
{/* Header */}
|
||||
<div className="bg-[#0A1128] py-14 px-6 relative overflow-hidden">
|
||||
<div className="on-dark bg-[#0A1128] py-14 px-6 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 w-[500px] h-[500px] rounded-full bg-[#6C5CE7]/10 blur-[120px]" />
|
||||
<div className="absolute bottom-0 left-0 w-[300px] h-[300px] rounded-full bg-purple-500/5 blur-[100px]" />
|
||||
<div className="max-w-6xl mx-auto relative">
|
||||
@ -177,7 +177,7 @@ export default function PerformancePage() {
|
||||
{OVERVIEW_STATS.map((stat, i) => (
|
||||
<motion.div
|
||||
key={stat.label}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-4"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-4"
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.05 }}
|
||||
@ -190,7 +190,7 @@ export default function PerformancePage() {
|
||||
</div>
|
||||
|
||||
{/* ═══ Section 1: Marketing Funnel ═══ */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 className="font-serif font-bold text-xl text-[#0A1128]">마케팅 퍼널</h3>
|
||||
@ -260,7 +260,7 @@ export default function PerformancePage() {
|
||||
</div>
|
||||
|
||||
{/* ═══ Section 2: Channel Trend (Stacked Bar) ═══ */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 className="font-serif font-bold text-xl text-[#0A1128]">채널별 주간 트렌드</h3>
|
||||
@ -298,7 +298,7 @@ export default function PerformancePage() {
|
||||
animate={{ scaleY: 1 }}
|
||||
transition={{ duration: 0.5, delay: wi * 0.1 }}
|
||||
>
|
||||
<div className="absolute -top-8 left-1/2 -translate-x-1/2 hidden group-hover:block bg-[#0A1128] text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10">
|
||||
<div className="on-dark absolute -top-8 left-1/2 -translate-x-1/2 hidden group-hover:block bg-[#0A1128] text-white text-xs px-2 py-1 rounded whitespace-nowrap z-10">
|
||||
{ch.label}: {val}K
|
||||
</div>
|
||||
</motion.div>
|
||||
@ -321,7 +321,7 @@ export default function PerformancePage() {
|
||||
</div>
|
||||
|
||||
{/* ═══ Section 3: Day × Time Heatmap ═══ */}
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-6 mb-10">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h3 className="font-serif font-bold text-xl text-[#0A1128]">최적 게시 시간</h3>
|
||||
@ -387,7 +387,7 @@ export default function PerformancePage() {
|
||||
return (
|
||||
<motion.div
|
||||
key={ch.id}
|
||||
className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-5"
|
||||
className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] p-5"
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: i * 0.08 }}
|
||||
@ -421,8 +421,8 @@ export default function PerformancePage() {
|
||||
|
||||
{/* Top Content */}
|
||||
<h3 className="font-serif font-bold text-xl text-[#0A1128] mb-4">인기 콘텐츠 TOP 5</h3>
|
||||
<div className="bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden mb-10">
|
||||
<div className="grid grid-cols-[1fr_100px_80px_80px_60px_70px_70px] gap-2 px-5 py-3 bg-[#0A1128] text-white text-xs font-medium">
|
||||
<div className="on-light bg-white rounded-2xl border border-slate-100 shadow-[3px_4px_12px_rgba(0,0,0,0.06)] overflow-hidden mb-10">
|
||||
<div className="on-dark grid grid-cols-[1fr_100px_80px_80px_60px_70px_70px] gap-2 px-5 py-3 bg-[#0A1128] text-white text-xs font-medium">
|
||||
<span>콘텐츠</span>
|
||||
<span>채널</span>
|
||||
<span className="text-right">조회수</span>
|
||||
|
||||
@ -194,7 +194,7 @@ const TierCard: React.FC<TierCardProps> = ({ tier, billing, onSelect }) => {
|
||||
{/* 이름 + 태그라인 */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-2xl font-serif font-bold text-primary-900 mb-2">{tier.name}</h3>
|
||||
<p className="text-sm text-slate-500 leading-relaxed break-keep">{tier.tagline}</p>
|
||||
<p className="ui-note">{tier.tagline}</p>
|
||||
</div>
|
||||
|
||||
{/* 가격 */}
|
||||
@ -203,7 +203,7 @@ const TierCard: React.FC<TierCardProps> = ({ tier, billing, onSelect }) => {
|
||||
<span className="text-4xl font-bold text-primary-900 tracking-tight">
|
||||
{formatKRW(price)}
|
||||
</span>
|
||||
<span className="text-slate-500 text-sm">/월</span>
|
||||
<span className="ui-note">/월</span>
|
||||
</div>
|
||||
{isAnnual && (
|
||||
<p className="text-xs text-accent font-semibold mt-2">
|
||||
|
||||
@ -10,7 +10,7 @@ export default function SupportersBuildPage() {
|
||||
if (!id) return null;
|
||||
return (
|
||||
<div className="[word-break:keep-all]">
|
||||
<section className="bg-[#0A1128] text-white relative overflow-hidden pt-28 pb-14 md:pt-36 md:pb-16 px-6">
|
||||
<section className="on-dark bg-[#0A1128] text-white relative overflow-hidden pt-28 pb-14 md:pt-36 md:pb-16 px-6">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,rgba(108,92,231,0.18),transparent_60%)]" />
|
||||
<div className="relative max-w-7xl mx-auto">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-white/10 border border-white/10 px-4 py-1.5 mb-6">
|
||||
@ -20,8 +20,8 @@ export default function SupportersBuildPage() {
|
||||
<span className="bg-gradient-to-r from-purple-300 to-blue-300 bg-clip-text text-transparent">Supporters Preview</span>
|
||||
</h1>
|
||||
<p className="text-purple-200 text-base">병원 ID <span className="font-mono text-white">{id}</span></p>
|
||||
<p className="text-white/60 text-sm mt-4">
|
||||
홈페이지 공개 자료만으로 만든 서포터즈 사이트 초안입니다. 아래에서 미리보기를 열고, 병원만 답할 수 있는 항목을 입력해 주세요.
|
||||
<p className="ui-ask ui-ask-block mt-4">
|
||||
아래에서 미리보기를 열고, 병원만 답할 수 있는 항목을 입력해 주세요.
|
||||
{' '}<Link to="/discovery" className="underline underline-offset-4 text-purple-300">AI Discovery 소개</Link>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user