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 { createContext, useContext, useEffect, useState, type ReactNode } from 'react'
import { getCodes, type CodeMap } from '../api/client'
/**
* 화면 선택지를 서버에서 한 번 받아 앱 전체에 나눠준다.
*
* 화면마다 따로 부르지 않는 이유는 코드가 자주 안 바뀌고, 선택지를 기다리느라 폼이
* 깜빡이는 것을 피하기 위해서다. 로그인 화면에서는 필요 없으므로 인증 이후에 감싼다.
*/
const CodeContext = createContext<CodeMap | null>(null)
type LoadState = 'loading' | 'ready' | 'error'
/**
* @param codes 넘기면 서버를 부르지 않고 그 값을 그대로 쓴다. 테스트에서 화면 하나만
* 떼어 그릴 때 쓰라고 열어둔 자리다.
*/
export function CodeProvider({ children, codes: given }: { children: ReactNode; codes?: CodeMap }) {
const [codes, setCodes] = useState<CodeMap | null>(given ?? null)
const [state, setState] = useState<LoadState>(given ? 'ready' : 'loading')
const [message, setMessage] = useState('')
async function load() {
setState('loading')
try {
setCodes(await getCodes())
setState('ready')
} catch (e) {
setMessage((e as Error).message)
setState('error')
}
}
useEffect(() => {
if (!given) {
void load()
}
}, [given])
if (state === 'loading') {
return <div className="p-8 text-sm text-gray-500">불러오는 중…</div>
}
// 선택지를 못 받은 채로 화면을 그리면 빈 드롭다운이 뜨고, 거기서 저장하면 값이
// 지워진다. 그리지 않고 오류를 보여주는 편이 안전하다.
if (state === 'error' || !codes) {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-3 p-8 text-sm">
<p className="text-red-600">선택지 정보를 불러오지 못했습니다. {message}</p>
<button
type="button"
onClick={() => void load()}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm"
>
다시 시도
</button>
</div>
)
}
return <CodeContext.Provider value={codes}>{children}</CodeContext.Provider>
}
/** Provider 밖에서 부르면 개발 중에 바로 알아채도록 예외를 던진다. */
export function useCodeMap(): CodeMap {
const codes = useContext(CodeContext)
if (!codes) {
throw new Error('CodeProvider 안에서만 쓸 수 있습니다.')
}
return codes
}