110 lines
4.6 KiB
TypeScript
110 lines
4.6 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { CheckCircle2 } from 'lucide-react'
|
|
import { toast } from 'sonner'
|
|
import { useMeQuery, useSaveExtraInfoMutation, getApiErrorMessage } from '@/apis'
|
|
import type { SessionField } from '@/apis/auth/auth.type'
|
|
import { useChatStore } from '@/features/chat/stores/useChatStore'
|
|
import { useChatInitStore } from '@/features/chat/stores/useChatInitStore'
|
|
|
|
// 협상 타결(Summary) 직후 부가정보 입력 폼. 필드 정의(session_fields)는 회사 설정(/me)에서 온다.
|
|
// 저장하면 sessions.custom 에 기록되고 negodata 견적상세에 표시된다. 정의가 없으면 렌더하지 않는다.
|
|
export function ExtraInfoForm() {
|
|
const sessionId = useChatStore((s) => s.sessionId)
|
|
const { data: user } = useMeQuery()
|
|
const fields: SessionField[] = user?.sessionFields ?? []
|
|
const save = useSaveExtraInfoMutation()
|
|
const existing = useChatInitStore((s) => s.custom) // 기존 입력값(재진입 프리필)
|
|
|
|
const [values, setValues] = useState<Record<string, unknown>>({})
|
|
const [inited, setInited] = useState(false)
|
|
const [saved, setSaved] = useState(false)
|
|
|
|
// fields(회사 설정)와 기존값(sessions.custom)이 준비된 첫 시점에 프리필 — 이후 사용자 편집은 보존.
|
|
useEffect(() => {
|
|
if (inited || fields.length === 0) return
|
|
const init: Record<string, unknown> = {}
|
|
for (const f of fields) init[f.key] = existing?.[f.key] ?? (f.type === 'boolean' ? false : '')
|
|
setValues(init)
|
|
setInited(true)
|
|
}, [inited, fields, existing])
|
|
|
|
if (fields.length === 0) return null
|
|
|
|
const set = (key: string, value: unknown) => setValues((v) => ({ ...v, [key]: value }))
|
|
|
|
const handleSave = () => {
|
|
const custom: Record<string, unknown> = {}
|
|
for (const f of fields) {
|
|
const v = values[f.key]
|
|
if (f.type === 'boolean') custom[f.key] = !!v
|
|
else if (v !== '' && v != null) custom[f.key] = f.type === 'number' ? Number(v) : v
|
|
}
|
|
save.mutate(
|
|
{ sessionId, request: { custom } },
|
|
{
|
|
onSuccess: () => {
|
|
setSaved(true)
|
|
toast.success('부가정보가 저장되었습니다.')
|
|
},
|
|
onError: (error) => toast.error(getApiErrorMessage(error, '부가정보 저장에 실패했습니다.')),
|
|
},
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="w-full rounded-2xl border border-border bg-white p-5 shadow-sm">
|
|
<div className="mb-3 flex items-center gap-2">
|
|
<CheckCircle2 className="size-5 text-brand-600" />
|
|
<h3 className="text-sm font-bold text-neutral-90">부가정보 입력</h3>
|
|
</div>
|
|
<p className="text-sm leading-relaxed text-neutral-70 break-keep">
|
|
협상이 완료되었습니다. 아래 정보를 입력해 주세요. (저장 후에도 세션 목록에서 수정할 수 있습니다.)
|
|
</p>
|
|
|
|
<div className="mt-4 space-y-4">
|
|
{fields.map((f) => (
|
|
<div key={f.key} className="space-y-1.5">
|
|
<label className="block text-sm font-semibold text-neutral-80">{f.label}</label>
|
|
{f.type === 'boolean' ? (
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={!!values[f.key]}
|
|
onClick={() => set(f.key, !values[f.key])}
|
|
disabled={saved}
|
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors disabled:opacity-50 ${
|
|
values[f.key] ? 'bg-brand-600' : 'bg-neutral-30'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`inline-block size-5 transform rounded-full bg-white shadow transition-transform ${
|
|
values[f.key] ? 'translate-x-5' : 'translate-x-0.5'
|
|
}`}
|
|
/>
|
|
</button>
|
|
) : (
|
|
<input
|
|
type={f.type === 'number' ? 'number' : 'text'}
|
|
value={String(values[f.key] ?? '')}
|
|
onChange={(e) => set(f.key, e.target.value)}
|
|
disabled={saved}
|
|
className="h-11 w-full rounded-xl border border-border bg-white px-3 text-sm outline-none focus:border-brand-600 focus:ring-1 focus:ring-brand-600 disabled:bg-neutral-10"
|
|
placeholder={f.label}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={handleSave}
|
|
disabled={save.isPending || saved}
|
|
className="mt-4 h-11 w-full rounded-xl bg-brand-600 text-sm font-bold text-white transition-colors hover:bg-brand-700 disabled:opacity-50"
|
|
>
|
|
{saved ? '저장 완료' : save.isPending ? '저장 중…' : '부가정보 저장'}
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|