o2o-clinicad-frontend/src/components/brand/BrandConsistencyMap.tsx

136 lines
6.0 KiB
TypeScript

import { useState } from "react";
import AlertCircleIcon from "@/assets/icons/alert-circle.svg?react";
import ChevronRightIcon from "@/assets/report/chevron-right.svg?react";
import CheckCircleIcon from "@/assets/report/check-circle.svg?react";
import XCircleIcon from "@/assets/report/x-circle.svg?react";
import type { BrandInconsistency } from "@/types/brandConsistency";
export type BrandConsistencyMapProps = {
inconsistencies: BrandInconsistency[];
className?: string;
/** false면 상단 제목·설명을 숨김 (플랜 브랜딩 가이드 탭 등) */
showHeading?: boolean;
};
export function BrandConsistencyMap({
inconsistencies,
className = "",
showHeading = true,
}: BrandConsistencyMapProps) {
const [expanded, setExpanded] = useState<number | null>(0);
if (inconsistencies.length === 0) return null;
return (
<div
className={`${showHeading ? "mt-8 animate-fade-in-up animation-delay-200 " : ""}${className}`.trim()}
>
{showHeading ? (
<>
<h3 className="font-serif headline-20 text-navy-900 mb-2 break-keep">Brand Consistency Map</h3>
<p className="body-14 text-neutral-60 mb-5 break-keep"> </p>
</>
) : null}
<div className="space-y-3">
{inconsistencies.map((item, i) => {
const wrongCount = item.values.filter((v) => !v.isCorrect).length;
const isOpen = expanded === i;
return (
<div
key={item.field}
className="rounded-2xl border border-neutral-20 bg-white card-shadow overflow-hidden"
>
<button
type="button"
onClick={() => setExpanded(isOpen ? null : i)}
className="w-full flex items-center justify-between gap-3 p-5 text-left hover:bg-neutral-10/80 transition-colors cursor-pointer"
>
<div className="flex items-center gap-3 min-w-0">
<div className="w-8 h-8 rounded-lg bg-navy-900 flex items-center justify-center text-white label-12-semibold shrink-0">
{wrongCount}
</div>
<div className="min-w-0">
<p className="title-14 text-navy-900 break-keep">{item.field}</p>
<p className="label-12 text-neutral-60 break-keep">{wrongCount} </p>
</div>
</div>
<ChevronRightIcon
width={16}
height={16}
className={`text-neutral-60 shrink-0 transition-transform ${isOpen ? "rotate-90" : ""}`}
aria-hidden
/>
</button>
{isOpen ? (
<div className="px-5 pb-5 border-t border-neutral-20">
<div className="grid gap-2 mt-4 mb-4">
{item.values.map((v) => (
<div
key={v.channel}
className={`flex flex-wrap items-center justify-between gap-2 py-3 px-3 rounded-lg body-14 sm:flex-nowrap ${
v.isCorrect
? "bg-[var(--color-status-good-bg)]/60"
: "bg-[var(--color-status-critical-bg)]/60"
}`}
>
<span className="font-medium text-neutral-80 shrink-0 min-w-[100px] break-keep">
{v.channel}
</span>
<span
className={`flex-1 text-right min-w-0 break-keep ${
v.isCorrect
? "text-[var(--color-status-good-text)]"
: "text-[var(--color-status-critical-text)]"
}`}
>
{v.value}
</span>
<span className="ml-auto sm:ml-3 shrink-0">
{v.isCorrect ? (
<CheckCircleIcon
width={15}
height={15}
className="text-[var(--color-status-good-dot)]"
aria-hidden
/>
) : (
<XCircleIcon
width={15}
height={15}
className="text-[var(--color-status-critical-dot)]"
aria-hidden
/>
)}
</span>
</div>
))}
</div>
<div className="rounded-xl bg-[var(--color-status-warning-bg)] border border-[var(--color-status-warning-border)] p-4 mb-3">
<p className="label-12-semibold text-[var(--color-status-warning-text)] uppercase tracking-wide mb-1 break-keep">
<AlertCircleIcon className="inline mr-1 align-text-bottom shrink-0" width={12} height={12} aria-hidden />
Impact
</p>
<p className="body-14 text-[var(--color-status-warning-text)] break-keep">{item.impact}</p>
</div>
<div className="rounded-xl bg-[var(--color-status-good-bg)] border border-[var(--color-status-good-border)] p-4">
<p className="label-12-semibold text-[var(--color-status-good-text)] uppercase tracking-wide mb-1 break-keep">
<CheckCircleIcon className="inline mr-1 align-text-bottom shrink-0" width={12} height={12} aria-hidden />
Recommendation
</p>
<p className="body-14 text-[var(--color-status-good-text)] break-keep">{item.recommendation}</p>
</div>
</div>
) : null}
</div>
);
})}
</div>
</div>
);
}