File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { useEffect, useState } from 'react'
import { HELP } from './helpContent'
/**
* 버튼·영역 옆에 붙는 물음표. 누르면 그 기능의 설명서 모달이 열린다.
*
* 설명 문구는 helpContent.ts 한 곳에만 둔다 - 화면 곳곳에 흩어 두면 화면을 고쳤을 때
* 어떤 설명이 낡았는지 찾을 수 없게 된다.
*
* 캡처 이미지는 public/help 아래 실제 화면이다. 파일이 아직 없거나 깨져도 설명은
* 그대로 읽히도록 이미지 로드 실패는 조용히 감춘다.
*/
interface Props {
/** helpContent.ts의 키. 없는 키면 아무것도 그리지 않는다(잘못된 키로 화면이 깨지지 않게). */
topic: string
}
export default function HelpButton({ topic }: Props) {
const [open, setOpen] = useState(false)
const help = HELP[topic]
useEffect(() => {
if (!open) {
return
}
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
setOpen(false)
}
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [open])
if (!help) {
return null
}
return (
<>
<button
type="button"
aria-label={`${help.title} 도움말`}
title={`${help.title} 도움말`}
onClick={(e) => {
// 카드/표 줄 안에 놓이는 경우가 있어 상위의 클릭(기관 열기 등)까지 번지면 안 된다.
e.stopPropagation()
setOpen(true)
}}
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-gray-300 text-[10px] leading-none text-gray-500 hover:border-blue-400 hover:text-blue-600"
>
?
</button>
{open && (
<div
role="dialog"
aria-label={`${help.title} 설명서`}
onClick={() => setOpen(false)}
className="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/40 p-6"
>
<div
onClick={(e) => e.stopPropagation()}
className="w-full max-w-4xl rounded-lg bg-white shadow-xl"
>
<div className="flex items-start justify-between gap-3 border-b border-gray-200 px-5 py-4">
<div>
<h2 className="text-base font-semibold">{help.title}</h2>
<p className="mt-1 text-sm text-gray-600">{help.summary}</p>
</div>
<button
type="button"
aria-label="닫기"
onClick={() => setOpen(false)}
className="shrink-0 rounded-md border border-gray-300 px-2 py-1 text-xs text-gray-600 hover:bg-gray-50"
>
닫기
</button>
</div>
<div className="space-y-5 px-5 py-4">
{help.images?.map((image) => (
<figure key={image.src} className="space-y-1.5">
<img
src={image.src}
alt={image.alt}
onError={(e) => {
;(e.currentTarget.parentElement as HTMLElement).style.display = 'none'
}}
// 작은 캡처(경고창 등)가 모달 폭에 맞춰 늘어나면 흐려진다.
// 원본보다 크게 키우지 않고 가운데 정렬만 한다.
className="mx-auto max-h-[26rem] w-auto max-w-full rounded-md border border-gray-200"
/>
<figcaption className="text-xs text-gray-500">{image.caption}</figcaption>
</figure>
))}
{help.steps && (
<div>
<h3 className="text-sm font-semibold">사용 방법</h3>
<ol className="mt-2 space-y-1.5">
{help.steps.map((step, i) => (
<li key={step} className="flex gap-2 text-sm text-gray-700">
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-gray-100 text-xs font-medium text-gray-600">
{i + 1}
</span>
<span>{step}</span>
</li>
))}
</ol>
</div>
)}
{help.cautions && (
<div>
<h3 className="text-sm font-semibold text-amber-700">알아둘 점</h3>
<ul className="mt-2 space-y-1.5 rounded-md bg-amber-50 p-3">
{help.cautions.map((caution) => (
<li key={caution} className="flex gap-2 text-sm text-amber-900">
<span aria-hidden="true">·</span>
<span>{caution}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
</div>
)}
</>
)
}