diff options
Diffstat (limited to 'lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx')
| -rw-r--r-- | lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx | 680 |
1 files changed, 424 insertions, 256 deletions
diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx index 2a668ca8..972af75d 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-create-dialog.tsx @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ - 'use client'; - /* IMPORT */ import { Button } from '@/components/ui/button'; import { @@ -11,11 +9,12 @@ import { CardHeader, CardTitle, } from '@/components/ui/card'; -import { createRegEvalCriteriaWithDetails } from '../service'; +import { createRegEvalCriteriaFixed, createRegEvalCriteriaVariable } from '../service'; import { Dialog, DialogContent, DialogDescription, + DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'; @@ -46,11 +45,11 @@ import { Textarea } from '@/components/ui/textarea'; import { toast } from 'sonner'; import { useForm, useFieldArray } from 'react-hook-form'; import { useEffect, useTransition } from 'react'; +import { useSession } from 'next-auth/react'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; // ---------------------------------------------------------------------------------------------------- - /* TYPES */ const regEvalCriteriaFormSchema = z.object({ category: z.string().min(1, '평가부문은 필수 항목입니다.'), @@ -59,18 +58,24 @@ const regEvalCriteriaFormSchema = z.object({ classification: z.string().min(1, '구분은 필수 항목입니다.'), range: z.string().nullable().optional(), remarks: z.string().nullable().optional(), + scoreType: z.enum(['fixed', 'variable']).default('fixed'), + variableScoreMin: z.coerce.number().nullable().optional(), + variableScoreMax: z.coerce.number().nullable().optional(), + variableScoreUnit: z.string().nullable().optional(), criteriaDetails: z.array( z.object({ id: z.number().optional(), - detail: z.string().min(1, '평가내용은 필수 항목입니다.'), + detail: z.string().optional(), scoreEquipShip: z.coerce.number().nullable().optional(), scoreEquipMarine: z.coerce.number().nullable().optional(), scoreBulkShip: z.coerce.number().nullable().optional(), scoreBulkMarine: z.coerce.number().nullable().optional(), }) - ).min(1, '최소 1개의 평가 내용이 필요합니다.'), + ).optional(), }); + type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>; + interface CriteriaDetailFormProps { index: number form: any @@ -83,9 +88,8 @@ interface RegEvalCriteriaFormSheetProps { onOpenChange: (open: boolean) => void, onSuccess: () => void, }; - // ---------------------------------------------------------------------------------------------------- - +/* CRITERIA DETAIL FORM COPONENT */ /* CRITERIA DETAIL FORM COPONENT */ function CriteriaDetailForm({ index, @@ -99,7 +103,7 @@ function CriteriaDetailForm({ <Card> <CardHeader> <div className="flex items-center justify-between"> - <CardTitle className="text-lg">Detail Item - {index + 1}</CardTitle> + <CardTitle className="text-lg">평가 옵션 {index + 1}</CardTitle> {canRemove && ( <Button type="button" @@ -127,91 +131,11 @@ function CriteriaDetailForm({ name={`criteriaDetails.${index}.detail`} render={({ field }) => ( <FormItem> - <FormLabel>평가내용</FormLabel> + <FormLabel>평가 옵션 내용</FormLabel> <FormControl> <Textarea - placeholder="평가내용을 입력하세요." - {...field} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreEquipShip`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/기자재/조선</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/기자재/조선" + placeholder="평가 옵션 내용을 입력하세요. (예: 우수, 보통, 미흡)" {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreEquipMarine`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/기자재/해양</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/기자재/해양" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreBulkShip`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/벌크/조선</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/벌크/조선" - {...field} - value={field.value ?? 0} - disabled={disabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.scoreBulkMarine`} - render={({ field }) => ( - <FormItem> - <FormLabel>배점/벌크/해양</FormLabel> - <FormControl> - <Input - type="number" - step="0.1" - placeholder="배점/벌크/해양" - {...field} - value={field.value ?? 0} disabled={disabled} /> </FormControl> @@ -219,11 +143,92 @@ function CriteriaDetailForm({ </FormItem> )} /> + <div className="grid grid-cols-4 gap-4"> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreEquipShip`} + render={({ field }) => ( + <FormItem> + <FormLabel>기자재-조선</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreEquipMarine`} + render={({ field }) => ( + <FormItem> + <FormLabel>기자재-해양</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreBulkShip`} + render={({ field }) => ( + <FormItem> + <FormLabel>벌크-조선</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name={`criteriaDetails.${index}.scoreBulkMarine`} + render={({ field }) => ( + <FormItem> + <FormLabel>벌크-해양</FormLabel> + <FormControl> + <Input + type="number" + step="0.01" + placeholder="0.00" + {...field} + value={field.value ?? ''} + disabled={disabled} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> </CardContent> </Card> ) } - /* CRITERIA FORM SHEET COPONENT */ function RegEvalCriteriaCreateDialog({ open, @@ -231,6 +236,7 @@ function RegEvalCriteriaCreateDialog({ onSuccess, }: RegEvalCriteriaFormSheetProps) { const [isPending, startTransition] = useTransition(); + const { data: session } = useSession(); const form = useForm<RegEvalCriteriaFormData>({ resolver: zodResolver(regEvalCriteriaFormSchema), defaultValues: { @@ -240,6 +246,10 @@ function RegEvalCriteriaCreateDialog({ classification: '', range: '', remarks: '', + scoreType: 'fixed', + variableScoreMin: null, + variableScoreMax: null, + variableScoreUnit: '', criteriaDetails: [ { id: undefined, @@ -252,12 +262,11 @@ function RegEvalCriteriaCreateDialog({ ], }, }); - const { fields, append, remove } = useFieldArray({ control: form.control, name: 'criteriaDetails', }); - + const scoreType = form.watch('scoreType'); useEffect(() => { if (open) { form.reset({ @@ -267,6 +276,10 @@ function RegEvalCriteriaCreateDialog({ classification: '', range: '', remarks: '', + scoreType: 'fixed', + variableScoreMin: null, + variableScoreMax: null, + variableScoreUnit: '', criteriaDetails: [ { id: undefined, @@ -280,32 +293,92 @@ function RegEvalCriteriaCreateDialog({ }) } }, [open, form]); - - const onSubmit = async (data: RegEvalCriteriaFormData) => { + const onSubmit = async (data: any) => { startTransition(async () => { try { - const criteriaData = { - category: data.category, - category2: data.category2, - item: data.item, - classification: data.classification, - range: data.range, - remarks: data.remarks, - }; - const detailList = data.criteriaDetails.map((detailItem) => ({ - id: detailItem.id, - detail: detailItem.detail, - scoreEquipShip: detailItem.scoreEquipShip != null - ? String(detailItem.scoreEquipShip) : null, - scoreEquipMarine: detailItem.scoreEquipMarine != null - ? String(detailItem.scoreEquipMarine) : null, - scoreBulkShip: detailItem.scoreBulkShip != null - ? String(detailItem.scoreBulkShip) : null, - scoreBulkMarine: detailItem.scoreBulkMarine != null - ? String(detailItem.scoreBulkMarine) : null, - })); + const userId = session?.user?.id ? Number(session.user.id) : 1; + + if (data.scoreType === 'fixed') { + // 고정 점수 검증 + if (!data.criteriaDetails || data.criteriaDetails.length === 0) { + toast.error('고정 점수 유형에서는 최소 1개의 평가내용이 필요합니다.'); + return; + } + + // 평가내용이 비어있는지 확인 + const hasEmptyDetail = data.criteriaDetails.some((detail: NonNullable<RegEvalCriteriaFormData['criteriaDetails']>[0]) => + !detail.detail || (detail.detail && detail.detail.trim() === '') + ); + if (hasEmptyDetail) { + toast.error('평가내용을 입력해주세요.'); + return; + } + + const baseCriteriaData = { + category: data.category, + category2: data.category2, + item: data.item, + classification: data.classification, + range: data.range || null, + remarks: data.remarks || null, + scoreType: data.scoreType, + variableScoreMin: null, // 고정 점수에서는 null + variableScoreMax: null, // 고정 점수에서는 null + variableScoreUnit: null, // 고정 점수에서는 null + createdBy: userId, + updatedBy: userId, + }; + + const detailList = data.criteriaDetails.map((detailItem: NonNullable<RegEvalCriteriaFormData['criteriaDetails']>[0]) => ({ + detail: detailItem.detail?.trim() || '', + scoreEquipShip: detailItem.scoreEquipShip != null ? String(detailItem.scoreEquipShip) : null, + scoreEquipMarine: detailItem.scoreEquipMarine != null ? String(detailItem.scoreEquipMarine) : null, + scoreBulkShip: detailItem.scoreBulkShip != null ? String(detailItem.scoreBulkShip) : null, + scoreBulkMarine: detailItem.scoreBulkMarine != null ? String(detailItem.scoreBulkMarine) : null, + })); + + await createRegEvalCriteriaFixed(baseCriteriaData, detailList); - await createRegEvalCriteriaWithDetails(criteriaData, detailList); + } else if (data.scoreType === 'variable') { + // 변동 점수 검증 + if (data.variableScoreMin == null || data.variableScoreMin === '') { + toast.error('변동 점수 유형에서는 최소 점수가 필수입니다.'); + return; + } + if (data.variableScoreMax == null || data.variableScoreMax === '') { + toast.error('변동 점수 유형에서는 최대 점수가 필수입니다.'); + return; + } + if (!data.variableScoreUnit || data.variableScoreUnit.trim() === '') { + toast.error('변동 점수 유형에서는 단위가 필수입니다.'); + return; + } + if (Number(data.variableScoreMin) >= Number(data.variableScoreMax)) { + toast.error('최소 점수는 최대 점수보다 작아야 합니다.'); + return; + } + + const variableCriteriaData = { + category: data.category, + category2: data.category2, + item: data.item, + classification: data.classification, + range: data.range || null, + remarks: data.remarks || null, + scoreType: data.scoreType, + variableScoreMin: String(data.variableScoreMin), + variableScoreMax: String(data.variableScoreMax), + variableScoreUnit: data.variableScoreUnit.trim(), + createdBy: userId, + updatedBy: userId, + }; + + await createRegEvalCriteriaVariable(variableCriteriaData); + } else { + toast.error('올바른 점수 유형을 선택해주세요.'); + return; + } + toast.success('평가 기준표가 생성되었습니다.'); onSuccess(); onOpenChange(false); @@ -317,15 +390,13 @@ function RegEvalCriteriaCreateDialog({ } }) } - if (!open) { return null; } - return ( <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="w-1/3 max-w-[50vw] max-h-[90vh] overflow-y-auto"> - <DialogHeader className="mb-4"> + <DialogContent className="max-w-4xl max-h-[90vh] flex flex-col"> + <DialogHeader className="flex-shrink-0"> <DialogTitle className="font-bold"> 새 협력업체 평가 기준표 생성 </DialogTitle> @@ -334,33 +405,112 @@ function RegEvalCriteriaCreateDialog({ </DialogDescription> </DialogHeader> <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> - <ScrollArea className="overflow-y-auto"> + <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0"> + <ScrollArea className="flex-1 pr-4 overflow-y-auto"> <div className="space-y-6"> <Card className="w-full"> <CardHeader> - <CardTitle>Criterion Info</CardTitle> + <CardTitle>기준 정보</CardTitle> </CardHeader> <CardContent className="space-y-4"> + <div className="grid grid-cols-2 gap-4"> + <FormField + control={form.control} + name="category" + render={({ field }) => ( + <FormItem> + <FormLabel>평가부문</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value || ''}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + {REG_EVAL_CRITERIA_CATEGORY.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="category2" + render={({ field }) => ( + <FormItem> + <FormLabel>점수구분</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value || ''}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + {REG_EVAL_CRITERIA_CATEGORY2.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="item" + render={({ field }) => ( + <FormItem> + <FormLabel>항목</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value || ''}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + {REG_EVAL_CRITERIA_ITEM.map((option) => ( + <SelectItem key={option.value} value={option.value}> + {option.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="classification" + render={({ field }) => ( + <FormItem> + <FormLabel>구분</FormLabel> + <FormControl> + <Input placeholder="구분을 입력하세요." {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> <FormField control={form.control} - name="category" + name="range" render={({ field }) => ( <FormItem> - <FormLabel>평가부문</FormLabel> + <FormLabel>평가명</FormLabel> <FormControl> - <Select onValueChange={field.onChange} value={field.value || ''}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - {REG_EVAL_CRITERIA_CATEGORY.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> + <Input + placeholder="평가명을 입력하세요." {...field} + value={field.value ?? ''} + /> </FormControl> <FormMessage /> </FormItem> @@ -368,23 +518,16 @@ function RegEvalCriteriaCreateDialog({ /> <FormField control={form.control} - name="category2" + name="remarks" render={({ field }) => ( <FormItem> - <FormLabel>점수구분</FormLabel> + <FormLabel>비고</FormLabel> <FormControl> - <Select onValueChange={field.onChange} value={field.value || ''}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - {REG_EVAL_CRITERIA_CATEGORY2.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> + <Textarea + placeholder="비고를 입력하세요." + {...field} + value={field.value ?? ''} + /> </FormControl> <FormMessage /> </FormItem> @@ -392,21 +535,18 @@ function RegEvalCriteriaCreateDialog({ /> <FormField control={form.control} - name="item" + name="scoreType" render={({ field }) => ( <FormItem> - <FormLabel>항목</FormLabel> + <FormLabel>점수 유형</FormLabel> <FormControl> - <Select onValueChange={field.onChange} value={field.value || ''}> + <Select onValueChange={field.onChange} value={field.value}> <SelectTrigger> - <SelectValue placeholder="선택" /> + <SelectValue placeholder="점수 유형 선택" /> </SelectTrigger> <SelectContent> - {REG_EVAL_CRITERIA_ITEM.map((option) => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} + <SelectItem value="fixed">고정</SelectItem> + <SelectItem value="variable">변동</SelectItem> </SelectContent> </Select> </FormControl> @@ -414,123 +554,151 @@ function RegEvalCriteriaCreateDialog({ </FormItem> )} /> - <FormField - control={form.control} - name="classification" - render={({ field }) => ( - <FormItem> - <FormLabel>구분</FormLabel> - <FormControl> - <Input placeholder="구분을 입력하세요." {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="range" - render={({ field }) => ( - <FormItem> - <FormLabel>범위</FormLabel> - <FormControl> - <Input - placeholder="범위를 입력하세요." {...field} - value={field.value ?? ''} + {scoreType === 'variable' && ( + <div className="grid grid-cols-3 gap-4"> + <FormField + control={form.control} + name="variableScoreMin" + render={({ field }) => ( + <FormItem> + <FormLabel>최소점수</FormLabel> + <FormControl> + <Input + type="number" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="remarks" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="비고를 입력하세요." - {...field} - value={field.value ?? ''} + <FormField + control={form.control} + name="variableScoreMax" + render={({ field }) => ( + <FormItem> + <FormLabel>최대점수</FormLabel> + <FormControl> + <Input + type="number" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="variableScoreUnit" + render={({ field }) => ( + <FormItem> + <FormLabel>점수단위</FormLabel> + <FormControl> + <Input + placeholder="예: 감점, 가점" + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - </Card> - <Card> - <CardHeader> - <div className="flex items-center justify-between"> - <div> - <CardTitle>Evaluation Criteria Item</CardTitle> - <CardDescription> - Set Evaluation Criteria Item. - </CardDescription> </div> - <Button - type="button" - variant="outline" - size="sm" - className="ml-4" - onClick={() => - append({ - id: undefined, - detail: '', - scoreEquipShip: null, - scoreEquipMarine: null, - scoreBulkShip: null, - scoreBulkMarine: null, - }) - } - disabled={isPending} - > - <Plus className="w-4 h-4 mr-2" /> - New Item - </Button> - </div> - </CardHeader> - <CardContent> - <div className="space-y-4"> - {fields.map((field, index) => ( - <CriteriaDetailForm - key={field.id} - index={index} - form={form} - onRemove={() => remove(index)} - canRemove={fields.length > 1} - disabled={isPending} - /> - ))} - </div> + )} </CardContent> </Card> + + {scoreType === 'fixed' && ( + <Card> + <CardHeader> + <div className="flex items-center justify-between"> + <div> + <CardTitle>평가 옵션 설정</CardTitle> + <CardDescription> + 평가 옵션을 설정합니다. + </CardDescription> + </div> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => + append({ + id: undefined, + detail: '', + scoreEquipShip: null, + scoreEquipMarine: null, + scoreBulkShip: null, + scoreBulkMarine: null, + }) + } + disabled={isPending} + > + <Plus className="w-4 h-4 mr-2" /> + 항목 추가 + </Button> + </div> + </CardHeader> + <CardContent> + <div className="space-y-4"> + {fields.map((field, index) => ( + <CriteriaDetailForm + key={field.id} + index={index} + form={form} + onRemove={() => remove(index)} + canRemove={fields.length > 1} + disabled={isPending} + /> + ))} + </div> + </CardContent> + </Card> + )} + + {scoreType === 'variable' && ( + <Card> + <CardHeader> + <CardTitle>변동 점수 설정</CardTitle> + <CardDescription> + 변동 점수 유형에서는 개별 평가 옵션을 설정할 수 없습니다. + 최소/최대 점수와 단위를 설정하여 점수 범위를 정의하세요. + </CardDescription> + </CardHeader> + </Card> + )} </div> </ScrollArea> - <div className="flex justify-end gap-2 pt-4 border-t"> + + <DialogFooter className="flex-shrink-0 mt-4 pt-4 border-t"> <Button type="button" variant="outline" onClick={() => onOpenChange(false)} disabled={isPending} > - Cancel + 취소 </Button> <Button type="submit" disabled={isPending}> - {isPending ? 'Saving...' : 'Create'} + {isPending ? '저장 중...' : '생성'} </Button> - </div> + </DialogFooter> </form> </Form> </DialogContent> </Dialog> ) } - // ---------------------------------------------------------------------------------------------------- - /* EXPORT */ export default RegEvalCriteriaCreateDialog;
\ No newline at end of file |
