o2o-negosium-original/negodata/front/src/features/partners/components/PartnerFormSheet.tsx
2026-07-06 17:27:59 +09:00

221 lines
7.6 KiB
TypeScript

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Trash2 } from 'lucide-react';
import type { ReqCreateSupplier as SupplierCreate } from '@/api/generated/model/reqCreateSupplier';
import type { ReqUpdateSupplier as SupplierUpdate } from '@/api/generated/model/reqUpdateSupplier';
import { showToast } from '@/lib/notify';
import { Typography } from '@/components/ui/typography';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Sheet } from '@/components/ui/sheet';
import { type Partner } from '../types';
const schema = z.object({
name: z.string().trim().min(1, '회사명/협력사명을 작성해 주십시오.'),
code: z.string().trim().min(1, '협력사코드를 입력해 주십시오.'),
managerName: z.string().trim().min(1, '담당자명을 입력해 주십시오.'),
managerEmail: z.string().trim().email('정확한 담당자 이메일 형식을 점검해 주십시오.'),
managerPhone: z.string().trim().min(1, '담당자 연락처를 입력해 주십시오.'),
totalRevenue: z.string().trim().optional(), // 총매출액(원)
});
type FormValues = z.infer<typeof schema>;
type PartnerFormSheetProps = {
open: boolean;
mode: 'create' | 'edit';
partner: Partner | null; // edit 모드일 때 초기값 출처
onCreate: (data: SupplierCreate) => Promise<void>;
onUpdate: (supplierId: string, data: SupplierUpdate) => Promise<void>;
onDelete: (supplierId: string, name: string) => void;
onClose: () => void;
};
// mode/partner로 폼 초기값 산출. create는 기본값, edit는 partner에서.
function buildDefaults(mode: 'create' | 'edit', partner: Partner | null): FormValues {
if (mode === 'edit' && partner) {
return {
name: partner.name ?? '',
code: partner.code || '',
managerName: partner.manager_name || '',
managerEmail: partner.manager_email || '',
managerPhone: partner.manager_contact_number || '',
totalRevenue: partner.total_revenue != null ? String(partner.total_revenue) : '',
};
}
return {
name: '',
code: `PART-CORP-${Math.floor(100 + Math.random() * 900)}`,
managerName: '',
managerEmail: '',
managerPhone: '010-',
totalRevenue: '',
};
}
const inputClass = 'text-foreground text-xs';
export function PartnerFormSheet({
open,
mode,
partner,
onCreate,
onUpdate,
onDelete,
onClose,
}: PartnerFormSheetProps) {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormValues>({
resolver: zodResolver(schema),
defaultValues: buildDefaults(mode, partner),
});
const onValid = async (v: FormValues) => {
const common = {
name: v.name,
code: v.code,
manager_name: v.managerName,
manager_email: v.managerEmail,
manager_contact_number: v.managerPhone,
total_revenue: v.totalRevenue?.trim() ? Number(v.totalRevenue.replace(/[^0-9]/g, '')) : undefined,
};
if (mode === 'create') {
try {
await onCreate(common as SupplierCreate);
showToast('신규 협력사가 등록되었습니다.', 'success');
onClose();
} catch (err) {
showToast(err instanceof Error ? err.message : '협력사 등록 실패', 'error');
}
} else if (partner) {
try {
await onUpdate(partner.supplier_id, common as SupplierUpdate);
showToast('협력사 정보가 최신화되었습니다.', 'success');
onClose();
} catch (err) {
showToast(err instanceof Error ? err.message : '협력사 수정 실패', 'error');
}
}
};
return (
<Sheet
open={open}
title={mode === 'create' ? '신규 협력사 등록' : '협력사 정보 관리'}
onClose={onClose}
>
<form onSubmit={handleSubmit(onValid)} className="mt-6 space-y-4 text-xs font-mono">
{/* Partner Name */}
<div className="space-y-1">
<Typography as="label" variant="label">협력사명</Typography>
<Input
id="form-partner-name"
type="text"
{...register('name')}
className={inputClass}
placeholder="예: (주)대현정밀부품"
/>
{errors.name && <p className="text-[10px] text-rose-500">{errors.name.message}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
{/* Code */}
<div className="space-y-1">
<Typography as="label" variant="label">협력사코드</Typography>
<Input
id="form-partner-code"
type="text"
{...register('code')}
className={inputClass}
placeholder="PART-XXXX"
/>
{errors.code && <p className="text-[10px] text-rose-500">{errors.code.message}</p>}
</div>
{/* 총매출액 */}
<div className="space-y-1">
<Typography as="label" variant="label">총매출액 (원, 선택)</Typography>
<Input
id="form-partner-revenue"
type="number"
min={0}
{...register('totalRevenue')}
className={inputClass}
placeholder="예: 5000000000"
/>
</div>
</div>
{/* Manager Name */}
<div className="space-y-1">
<Typography as="label" variant="label">담당자명</Typography>
<Input
id="form-partner-manager"
type="text"
{...register('managerName')}
className={inputClass}
placeholder="김민우 책임과장"
/>
{errors.managerName && <p className="text-[10px] text-rose-500">{errors.managerName.message}</p>}
</div>
{/* Manager Email */}
<div className="space-y-1">
<Typography as="label" variant="label">담당자 이메일</Typography>
<Input
id="form-partner-email"
type="email"
{...register('managerEmail')}
className={inputClass}
placeholder="manager@company.co.kr"
/>
{errors.managerEmail && <p className="text-[10px] text-rose-500">{errors.managerEmail.message}</p>}
</div>
{/* Manager Phone */}
<div className="space-y-1">
<Typography as="label" variant="label">담당자 연락처</Typography>
<Input
id="form-partner-phone"
type="text"
{...register('managerPhone')}
className={inputClass}
placeholder="010-XXXX-XXXX"
/>
{errors.managerPhone && <p className="text-[10px] text-rose-500">{errors.managerPhone.message}</p>}
</div>
{/* Buttons wrapper */}
<div className="pt-4 flex items-center gap-2 border-t border-border mt-8 justify-between">
{mode === 'edit' && partner && (
<Button
type="button"
variant="destructive"
size="sm"
onClick={() => {
onDelete(partner.supplier_id, partner.name);
onClose();
}}
>
<Trash2 />
삭제
</Button>
)}
<div className="flex items-center gap-2 flex-1 justify-end">
<Button type="button" variant="outline" size="sm" onClick={onClose}>
취소
</Button>
<Button type="submit" size="sm" disabled={isSubmitting}>
{mode === 'create' ? '신규 파트너 등록' : '변경사항 저장'}
</Button>
</div>
</div>
</form>
</Sheet>
);
}