diff options
Diffstat (limited to 'lib/evaluation-criteria/table')
6 files changed, 1283 insertions, 998 deletions
diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx index 88c8107b..d48e097b 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-columns.tsx @@ -85,6 +85,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Category', type: 'select', }, + size: 50, }, { accessorKey: 'category2', @@ -106,6 +107,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Score Category', type: 'select', }, + size: 50, }, { accessorKey: 'item', @@ -127,6 +129,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Item', type: 'select', }, + size: 50, }, { accessorKey: 'classification', @@ -144,6 +147,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Classification', type: 'text', }, + size: 100, }, { accessorKey: 'range', @@ -280,6 +284,7 @@ function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<RegEvalCriteri excelHeader: 'Remarks', type: 'text', }, + size: 300, }; // [5] HIDDEN ID COLUMNS 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 diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx index 60ca173b..cfcf6e26 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-details-sheet.tsx @@ -2,20 +2,20 @@ /* IMPORT */ import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; + import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { useEffect, useState } from 'react'; -import { Loader2, X } from 'lucide-react'; +import { Loader2 } from 'lucide-react'; import { REG_EVAL_CRITERIA_CATEGORY, REG_EVAL_CRITERIA_CATEGORY2, REG_EVAL_CRITERIA_ITEM, - type RegEvalCriteriaView, type RegEvalCriteriaDetails, + type RegEvalCriteria, } from '@/db/schema'; import { getRegEvalCriteriaDetails } from '../service'; // 서버 액션 import @@ -23,7 +23,7 @@ import { getRegEvalCriteriaDetails } from '../service'; // 서버 액션 import /* TYPES */ interface RegEvalCriteriaDetailsSheetProps { - criteriaViewData: RegEvalCriteriaView; + criteriaViewData: RegEvalCriteria; open: boolean; onOpenChange: (open: boolean) => void; } @@ -125,7 +125,7 @@ export function RegEvalCriteriaDetailsSheet({ </div> <div> - <p className="text-sm font-medium text-muted-foreground">평가명 (범위)</p> + <p className="text-sm font-medium text-muted-foreground">평가명</p> <p className="text-sm mt-1 font-medium">{criteriaViewData.range || '-'}</p> </div> @@ -140,78 +140,118 @@ export function RegEvalCriteriaDetailsSheet({ <Separator /> - {/* 평가 옵션 및 점수 카드 */} - <Card> - <CardHeader> - <CardTitle className="text-lg">평가 옵션 및 점수</CardTitle> - <p className="text-sm text-muted-foreground"> - 각 평가 옵션에 따른 점수를 확인할 수 있습니다. - </p> - </CardHeader> - <CardContent> - {loading ? ( - <div className="flex justify-center items-center py-8"> - <Loader2 className="h-4 w-4 animate-spin mr-2" /> - <span className="text-sm text-muted-foreground">로딩 중...</span> - </div> - ) : error ? ( - <div className="flex justify-center items-center py-8"> - <div className="text-sm text-destructive">{error}</div> + {/* 점수 정보 카드 - 점수 유형에 따라 다른 UI 표시 */} + {criteriaViewData.scoreType === 'variable' ? ( + /* 변동점수 정보 카드 */ + <Card> + <CardHeader> + <CardTitle className="text-lg">변동점수 설정</CardTitle> + <p className="text-sm text-muted-foreground"> + 이 평가 기준은 변동점수 유형으로 설정되어 있습니다. + </p> + </CardHeader> + <CardContent className="space-y-4"> + <div className="grid grid-cols-3 gap-4"> + <div> + <p className="text-sm font-medium text-muted-foreground">최소점수</p> + <Badge variant="outline" className="mt-1"> + {criteriaViewData.variableScoreMin || '-'} + </Badge> + </div> + <div> + <p className="text-sm font-medium text-muted-foreground">최대점수</p> + <Badge variant="outline" className="mt-1 "> + {criteriaViewData.variableScoreMax || '-'} + </Badge> + </div> + <div> + <p className="text-sm font-medium text-muted-foreground">점수단위</p> + <Badge variant="outline" className="mt-1"> + {criteriaViewData.variableScoreUnit || '-'} + </Badge> + </div> </div> - ) : details.length === 0 ? ( - <div className="flex justify-center items-center py-8"> - <div className="text-sm text-muted-foreground">등록된 평가 옵션이 없습니다.</div> + <div className="p-4 bg-muted rounded-lg"> + <p className="text-sm text-muted-foreground"> + 변동점수 유형에서는 평가 시 설정된 점수 범위 내에서 점수를 부여합니다. + </p> </div> - ) : ( - <div className="border rounded-lg"> - <Table> - <TableHeader> - <TableRow> - <TableHead className="w-12">#</TableHead> - <TableHead className="min-w-[200px]">평가 옵션</TableHead> - <TableHead className="text-center w-24">기자재-조선</TableHead> - <TableHead className="text-center w-24">기자재-해양</TableHead> - <TableHead className="text-center w-24">벌크-조선</TableHead> - <TableHead className="text-center w-24">벌크-해양</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {details.map((detail, index) => ( - <TableRow key={detail.id}> - <TableCell className="font-medium"> - {(detail.orderIndex ?? index) + 1} - </TableCell> - <TableCell className="font-medium"> - {detail.detail} - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreEquipShip)} - </Badge> - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreEquipMarine)} - </Badge> - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreBulkShip)} - </Badge> - </TableCell> - <TableCell className="text-center"> - <Badge variant="outline" className="font-mono"> - {formatScore(detail.scoreBulkMarine)} - </Badge> - </TableCell> + </CardContent> + </Card> + ) : ( + /* 고정점수 - 평가 옵션 및 점수 카드 */ + <Card> + <CardHeader> + <CardTitle className="text-lg">평가 옵션 및 점수</CardTitle> + <p className="text-sm text-muted-foreground"> + 각 평가 옵션에 따른 점수를 확인할 수 있습니다. + </p> + </CardHeader> + <CardContent> + {loading ? ( + <div className="flex justify-center items-center py-8"> + <Loader2 className="h-4 w-4 animate-spin mr-2" /> + <span className="text-sm text-muted-foreground">로딩 중...</span> + </div> + ) : error ? ( + <div className="flex justify-center items-center py-8"> + <div className="text-sm text-destructive">{error}</div> + </div> + ) : details.length === 0 ? ( + <div className="flex justify-center items-center py-8"> + <div className="text-sm text-muted-foreground">등록된 평가 옵션이 없습니다.</div> + </div> + ) : ( + <div className="border rounded-lg"> + <Table> + <TableHeader> + <TableRow> + <TableHead className="w-12">#</TableHead> + <TableHead className="min-w-[200px]">평가 옵션</TableHead> + <TableHead className="text-center w-24">기자재-조선</TableHead> + <TableHead className="text-center w-24">기자재-해양</TableHead> + <TableHead className="text-center w-24">벌크-조선</TableHead> + <TableHead className="text-center w-24">벌크-해양</TableHead> </TableRow> - ))} - </TableBody> - </Table> - </div> - )} - </CardContent> - </Card> + </TableHeader> + <TableBody> + {details.map((detail, index) => ( + <TableRow key={detail.id}> + <TableCell className="font-medium"> + {(detail.orderIndex ?? index) + 1} + </TableCell> + <TableCell className="font-medium"> + {detail.detail} + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreEquipShip)} + </Badge> + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreEquipMarine)} + </Badge> + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreBulkShip)} + </Badge> + </TableCell> + <TableCell className="text-center"> + <Badge variant="outline" className="font-mono"> + {formatScore(detail.scoreBulkMarine)} + </Badge> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </div> + )} + </CardContent> + </Card> + )} </div> </ScrollArea> diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx index 3362d810..8f4c4413 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-form-sheet.tsx @@ -1,586 +1,586 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ +// /* eslint-disable @typescript-eslint/no-explicit-any */ -'use client'; +// 'use client'; -/* IMPORT */ -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { - createRegEvalCriteriaWithDetails, - getRegEvalCriteriaWithDetails, - modifyRegEvalCriteriaWithDetails, -} from '../service'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { Plus, Trash2 } from 'lucide-react'; -import { - REG_EVAL_CRITERIA_CATEGORY, - REG_EVAL_CRITERIA_CATEGORY2, - REG_EVAL_CRITERIA_ITEM, - type RegEvalCriteriaDetails, - type RegEvalCriteriaView, -} from '@/db/schema'; -import { ScrollArea } from '@/components/ui/scroll-area'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select'; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, -} from '@/components/ui/sheet'; -import { Textarea } from '@/components/ui/textarea'; -import { toast } from 'sonner'; -import { useForm, useFieldArray } from 'react-hook-form'; -import { useEffect, useTransition } from 'react'; -import { z } from 'zod'; -import { zodResolver } from '@hookform/resolvers/zod'; +// /* IMPORT */ +// import { Button } from '@/components/ui/button'; +// import { +// Card, +// CardContent, +// CardDescription, +// CardHeader, +// CardTitle, +// } from '@/components/ui/card'; +// import { +// createRegEvalCriteriaWithDetails, +// getRegEvalCriteriaWithDetails, +// modifyRegEvalCriteriaWithDetails, +// } from '../service'; +// import { +// Form, +// FormControl, +// FormField, +// FormItem, +// FormLabel, +// FormMessage, +// } from '@/components/ui/form'; +// import { Input } from '@/components/ui/input'; +// import { Plus, Trash2 } from 'lucide-react'; +// import { +// REG_EVAL_CRITERIA_CATEGORY, +// REG_EVAL_CRITERIA_CATEGORY2, +// REG_EVAL_CRITERIA_ITEM, +// type RegEvalCriteriaDetails, +// type RegEvalCriteriaView, +// } from '@/db/schema'; +// import { ScrollArea } from '@/components/ui/scroll-area'; +// import { +// Select, +// SelectContent, +// SelectItem, +// SelectTrigger, +// SelectValue +// } from '@/components/ui/select'; +// import { +// Sheet, +// SheetContent, +// SheetDescription, +// SheetHeader, +// SheetTitle, +// } from '@/components/ui/sheet'; +// import { Textarea } from '@/components/ui/textarea'; +// import { toast } from 'sonner'; +// import { useForm, useFieldArray } from 'react-hook-form'; +// import { useEffect, useTransition } from 'react'; +// import { z } from 'zod'; +// import { zodResolver } from '@hookform/resolvers/zod'; -// ---------------------------------------------------------------------------------------------------- +// // ---------------------------------------------------------------------------------------------------- -/* TYPES */ -const regEvalCriteriaFormSchema = z.object({ - category: z.string().min(1, '평가부문은 필수 항목입니다.'), - category2: z.string().min(1, '점수구분은 필수 항목입니다.'), - item: z.string().min(1, '항목은 필수 항목입니다.'), - classification: z.string().min(1, '구분은 필수 항목입니다.'), - range: z.string().nullable().optional(), - remarks: z.string().nullable().optional(), - criteriaDetails: z.array( - z.object({ - id: z.number().optional(), - detail: z.string().min(1, '평가내용은 필수 항목입니다.'), - 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개의 평가 내용이 필요합니다.'), -}); -type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>; -interface CriteriaDetailFormProps { - index: number - form: any - onRemove: () => void - canRemove: boolean - disabled?: boolean -} -interface RegEvalCriteriaFormSheetProps { - open: boolean, - onOpenChange: (open: boolean) => void, - criteriaViewData: RegEvalCriteriaView | null, - onSuccess: () => void, -}; +// /* TYPES */ +// const regEvalCriteriaFormSchema = z.object({ +// category: z.string().min(1, '평가부문은 필수 항목입니다.'), +// category2: z.string().min(1, '점수구분은 필수 항목입니다.'), +// item: z.string().min(1, '항목은 필수 항목입니다.'), +// classification: z.string().min(1, '구분은 필수 항목입니다.'), +// range: z.string().nullable().optional(), +// remarks: z.string().nullable().optional(), +// criteriaDetails: z.array( +// z.object({ +// id: z.number().optional(), +// detail: z.string().min(1, '평가내용은 필수 항목입니다.'), +// 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개의 평가 내용이 필요합니다.'), +// }); +// type RegEvalCriteriaFormData = z.infer<typeof regEvalCriteriaFormSchema>; +// interface CriteriaDetailFormProps { +// index: number +// form: any +// onRemove: () => void +// canRemove: boolean +// disabled?: boolean +// } +// interface RegEvalCriteriaFormSheetProps { +// open: boolean, +// onOpenChange: (open: boolean) => void, +// criteriaViewData: RegEvalCriteriaView | null, +// onSuccess: () => void, +// }; -// ---------------------------------------------------------------------------------------------------- +// // ---------------------------------------------------------------------------------------------------- -/* CRITERIA DETAIL FORM COPONENT */ -function CriteriaDetailForm({ - index, - form, - onRemove, - canRemove, - disabled = false, -}: CriteriaDetailFormProps) { +// /* CRITERIA DETAIL FORM COPONENT */ +// function CriteriaDetailForm({ +// index, +// form, +// onRemove, +// canRemove, +// disabled = false, +// }: CriteriaDetailFormProps) { - return ( - <Card> - <CardHeader> - <div className="flex items-center justify-between"> - <CardTitle className="text-lg">Detail Item - {index + 1}</CardTitle> - {canRemove && ( - <Button - type="button" - variant="ghost" - size="sm" - onClick={onRemove} - className="text-destructive hover:text-destructive" - disabled={disabled} - > - <Trash2 className="w-4 h-4" /> - </Button> - )} - </div> - </CardHeader> - <CardContent className="space-y-4"> - <FormField - control={form.control} - name={`criteriaDetails.${index}.id`} - render={({ field }) => ( - <Input type="hidden" {...field} /> - )} - /> - <FormField - control={form.control} - name={`criteriaDetails.${index}.detail`} - render={({ field }) => ( - <FormItem> - <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="배점/기자재/조선" - {...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> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - </Card> - ) -} +// return ( +// <Card> +// <CardHeader> +// <div className="flex items-center justify-between"> +// <CardTitle className="text-lg">Detail Item - {index + 1}</CardTitle> +// {canRemove && ( +// <Button +// type="button" +// variant="ghost" +// size="sm" +// onClick={onRemove} +// className="text-destructive hover:text-destructive" +// disabled={disabled} +// > +// <Trash2 className="w-4 h-4" /> +// </Button> +// )} +// </div> +// </CardHeader> +// <CardContent className="space-y-4"> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.id`} +// render={({ field }) => ( +// <Input type="hidden" {...field} /> +// )} +// /> +// <FormField +// control={form.control} +// name={`criteriaDetails.${index}.detail`} +// render={({ field }) => ( +// <FormItem> +// <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="배점/기자재/조선" +// {...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> +// <FormMessage /> +// </FormItem> +// )} +// /> +// </CardContent> +// </Card> +// ) +// } -/* CRITERIA FORM SHEET COPONENT */ -function RegEvalCriteriaFormSheet({ - open, - onOpenChange, - criteriaViewData, - onSuccess, -}: RegEvalCriteriaFormSheetProps) { - const [isPending, startTransition] = useTransition(); - const isUpdateMode = !!criteriaViewData; +// /* CRITERIA FORM SHEET COPONENT */ +// function RegEvalCriteriaFormSheet({ +// open, +// onOpenChange, +// criteriaViewData, +// onSuccess, +// }: RegEvalCriteriaFormSheetProps) { +// const [isPending, startTransition] = useTransition(); +// const isUpdateMode = !!criteriaViewData; - const form = useForm<RegEvalCriteriaFormData>({ - resolver: zodResolver(regEvalCriteriaFormSchema), - defaultValues: { - category: '', - category2: '', - item: '', - classification: '', - range: '', - remarks: '', - criteriaDetails: [ - { - id: undefined, - detail: '', - scoreEquipShip: null, - scoreEquipMarine: null, - scoreBulkShip: null, - scoreBulkMarine: null, - }, - ], - }, - }); +// const form = useForm<RegEvalCriteriaFormData>({ +// resolver: zodResolver(regEvalCriteriaFormSchema), +// defaultValues: { +// category: '', +// category2: '', +// item: '', +// classification: '', +// range: '', +// remarks: '', +// criteriaDetails: [ +// { +// id: undefined, +// detail: '', +// scoreEquipShip: null, +// scoreEquipMarine: null, +// scoreBulkShip: null, +// scoreBulkMarine: null, +// }, +// ], +// }, +// }); - const { fields, append, remove } = useFieldArray({ - control: form.control, - name: 'criteriaDetails', - }); +// const { fields, append, remove } = useFieldArray({ +// control: form.control, +// name: 'criteriaDetails', +// }); - useEffect(() => { - if (open && isUpdateMode && criteriaViewData) { - startTransition(async () => { - try { - const targetData = await getRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!); - if (targetData) { - form.reset({ - category: targetData.category, - category2: targetData.category2, - item: targetData.item, - classification: targetData.classification, - range: targetData.range, - remarks: targetData.remarks, - criteriaDetails: targetData.criteriaDetails?.map((detailItem: RegEvalCriteriaDetails) => ({ - id: detailItem.id, - detail: detailItem.detail, - scoreEquipShip: detailItem.scoreEquipShip !== null - ? Number(detailItem.scoreEquipShip) : null, - scoreEquipMarine: detailItem.scoreEquipMarine !== null - ? Number(detailItem.scoreEquipMarine) : null, - scoreBulkShip: detailItem.scoreBulkShip !== null - ? Number(detailItem.scoreBulkShip) : null, - scoreBulkMarine: detailItem.scoreBulkMarine !== null - ? Number(detailItem.scoreBulkMarine) : null, - })) || [], - }) - } - } catch (error) { - console.error('Error in Loading Regular Evaluation Criteria for Updating:', error) - toast.error(error instanceof Error ? error.message : '편집할 데이터를 불러오는 데 실패했습니다.') - } - }) - } else if (open && !isUpdateMode) { - form.reset({ - category: '', - category2: '', - item: '', - classification: '', - range: '', - remarks: '', - criteriaDetails: [ - { - id: undefined, - detail: '', - scoreEquipShip: null, - scoreEquipMarine: null, - scoreBulkShip: null, - scoreBulkMarine: null, - }, - ], - }) - } - }, [open, isUpdateMode, criteriaViewData, form]); +// useEffect(() => { +// if (open && isUpdateMode && criteriaViewData) { +// startTransition(async () => { +// try { +// const targetData = await getRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!); +// if (targetData) { +// form.reset({ +// category: targetData.category, +// category2: targetData.category2, +// item: targetData.item, +// classification: targetData.classification, +// range: targetData.range, +// remarks: targetData.remarks, +// criteriaDetails: targetData.criteriaDetails?.map((detailItem: RegEvalCriteriaDetails) => ({ +// id: detailItem.id, +// detail: detailItem.detail, +// scoreEquipShip: detailItem.scoreEquipShip !== null +// ? Number(detailItem.scoreEquipShip) : null, +// scoreEquipMarine: detailItem.scoreEquipMarine !== null +// ? Number(detailItem.scoreEquipMarine) : null, +// scoreBulkShip: detailItem.scoreBulkShip !== null +// ? Number(detailItem.scoreBulkShip) : null, +// scoreBulkMarine: detailItem.scoreBulkMarine !== null +// ? Number(detailItem.scoreBulkMarine) : null, +// })) || [], +// }) +// } +// } catch (error) { +// console.error('Error in Loading Regular Evaluation Criteria for Updating:', error) +// toast.error(error instanceof Error ? error.message : '편집할 데이터를 불러오는 데 실패했습니다.') +// } +// }) +// } else if (open && !isUpdateMode) { +// form.reset({ +// category: '', +// category2: '', +// item: '', +// classification: '', +// range: '', +// remarks: '', +// criteriaDetails: [ +// { +// id: undefined, +// detail: '', +// scoreEquipShip: null, +// scoreEquipMarine: null, +// scoreBulkShip: null, +// scoreBulkMarine: null, +// }, +// ], +// }) +// } +// }, [open, isUpdateMode, criteriaViewData, form]); - const onSubmit = async (data: RegEvalCriteriaFormData) => { - 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 onSubmit = async (data: RegEvalCriteriaFormData) => { +// 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, +// })); - if (isUpdateMode && criteriaViewData) { - await modifyRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!, criteriaData, detailList); - toast.success('평가 기준표가 수정되었습니다.'); - } else { - await createRegEvalCriteriaWithDetails(criteriaData, detailList); - toast.success('평가 기준표가 생성되었습니다.'); - } - onSuccess(); - onOpenChange(false); - } catch (error) { - console.error('Error in Saving Regular Evaluation Criteria:', error); - toast.error( - error instanceof Error ? error.message : '평가 기준표 저장 중 오류가 발생했습니다.' - ); - } - }) - } +// if (isUpdateMode && criteriaViewData) { +// await modifyRegEvalCriteriaWithDetails(criteriaViewData.criteriaId!, criteriaData, detailList); +// toast.success('평가 기준표가 수정되었습니다.'); +// } else { +// await createRegEvalCriteriaWithDetails(criteriaData, detailList); +// toast.success('평가 기준표가 생성되었습니다.'); +// } +// onSuccess(); +// onOpenChange(false); +// } catch (error) { +// console.error('Error in Saving Regular Evaluation Criteria:', error); +// toast.error( +// error instanceof Error ? error.message : '평가 기준표 저장 중 오류가 발생했습니다.' +// ); +// } +// }) +// } - if (!open) { - return null; - } +// if (!open) { +// return null; +// } - return ( - <Sheet open={open} onOpenChange={onOpenChange}> - <SheetContent className="w-[900px] sm:max-w-[900px] overflow-y-auto"> - <SheetHeader className="mb-4"> - <SheetTitle className="font-bold"> - {isUpdateMode ? '협력업체 평가 기준표 수정' : '새 협력업체 평가 기준표 생성'} - </SheetTitle> - <SheetDescription> - {isUpdateMode ? '협력업체 평가 기준표의 정보를 수정합니다.' : '새로운 협력업체 평가 기준표를 생성합니다.'} - </SheetDescription> - </SheetHeader> - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> - <ScrollArea className="h-[calc(100vh-200px)] pr-4"> - <div className="space-y-6"> - <Card> - <CardHeader> - <CardTitle>Criterion Info</CardTitle> - </CardHeader> - <CardContent className="space-y-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> - )} - /> - <FormField - control={form.control} - name="range" - render={({ field }) => ( - <FormItem> - <FormLabel>범위</FormLabel> - <FormControl> - <Input - placeholder="범위를 입력하세요." {...field} - value={field.value ?? ''} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - <FormField - control={form.control} - name="remarks" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="비고를 입력하세요." - {...field} - value={field.value ?? ''} - /> - </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> - </div> - </ScrollArea> - <div className="flex justify-end gap-2 pt-4 border-t"> - <Button - type="button" - variant="outline" - onClick={() => onOpenChange(false)} - disabled={isPending} - > - Cancel - </Button> - <Button type="submit" disabled={isPending}> - {isPending - ? 'Saving...' - : isUpdateMode - ? 'Modify' - : 'Create'} - </Button> - </div> - </form> - </Form> - </SheetContent> - </Sheet> - ) -} +// return ( +// <Sheet open={open} onOpenChange={onOpenChange}> +// <SheetContent className="w-[900px] sm:max-w-[900px] overflow-y-auto"> +// <SheetHeader className="mb-4"> +// <SheetTitle className="font-bold"> +// {isUpdateMode ? '협력업체 평가 기준표 수정' : '새 협력업체 평가 기준표 생성'} +// </SheetTitle> +// <SheetDescription> +// {isUpdateMode ? '협력업체 평가 기준표의 정보를 수정합니다.' : '새로운 협력업체 평가 기준표를 생성합니다.'} +// </SheetDescription> +// </SheetHeader> +// <Form {...form}> +// <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> +// <ScrollArea className="h-[calc(100vh-200px)] pr-4"> +// <div className="space-y-6"> +// <Card> +// <CardHeader> +// <CardTitle>Criterion Info</CardTitle> +// </CardHeader> +// <CardContent className="space-y-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> +// )} +// /> +// <FormField +// control={form.control} +// name="range" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>범위</FormLabel> +// <FormControl> +// <Input +// placeholder="범위를 입력하세요." {...field} +// value={field.value ?? ''} +// /> +// </FormControl> +// <FormMessage /> +// </FormItem> +// )} +// /> +// <FormField +// control={form.control} +// name="remarks" +// render={({ field }) => ( +// <FormItem> +// <FormLabel>비고</FormLabel> +// <FormControl> +// <Textarea +// placeholder="비고를 입력하세요." +// {...field} +// value={field.value ?? ''} +// /> +// </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> +// </div> +// </ScrollArea> +// <div className="flex justify-end gap-2 pt-4 border-t"> +// <Button +// type="button" +// variant="outline" +// onClick={() => onOpenChange(false)} +// disabled={isPending} +// > +// Cancel +// </Button> +// <Button type="submit" disabled={isPending}> +// {isPending +// ? 'Saving...' +// : isUpdateMode +// ? 'Modify' +// : 'Create'} +// </Button> +// </div> +// </form> +// </Form> +// </SheetContent> +// </Sheet> +// ) +// } -// ---------------------------------------------------------------------------------------------------- +// // ---------------------------------------------------------------------------------------------------- -/* EXPORT */ -export default RegEvalCriteriaFormSheet;
\ No newline at end of file +// /* EXPORT */ +// export default RegEvalCriteriaFormSheet;
\ No newline at end of file diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx index f066fa92..7594f07f 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-table-toolbar-actions.tsx @@ -14,11 +14,9 @@ import { } from '@/components/ui/alert-dialog'; import { Button } from '@/components/ui/button'; import { Download, Plus, Trash2, Upload } from 'lucide-react'; -import { exportRegEvalCriteriaToExcel, exportRegEvalCriteriaTemplate } from '../excel/reg-eval-criteria-excel-export'; -import { importRegEvalCriteriaExcel } from '../excel/reg-eval-criteria-excel-import'; -import { removeRegEvalCriteria } from '../service'; +import { removeRegEvalCriteria, generateRegEvalCriteriaTemplate, generateRegEvalCriteriaExcel, importRegEvalCriteriaExcel } from '../service'; import { toast } from 'sonner'; -import { type RegEvalCriteriaView } from '@/db/schema'; +import { type RegEvalCriteria } from '@/db/schema'; import { type Table } from '@tanstack/react-table'; import { ChangeEvent, useMemo, useRef, useState } from 'react'; @@ -26,7 +24,7 @@ import { ChangeEvent, useMemo, useRef, useState } from 'react'; /* TYPES */ interface RegEvalCriteriaTableToolbarActionsProps { - table: Table<RegEvalCriteriaView>, + table: Table<RegEvalCriteria>, onCreateCriteria: () => void, onRefresh: () => void, } @@ -40,7 +38,7 @@ function RegEvalCriteriaTableToolbarActions(props: RegEvalCriteriaTableToolbarAc const selectedRows = table.getFilteredSelectedRowModel().rows; const hasSelection = selectedRows.length > 0; const selectedIds = useMemo(() => { - return [...new Set(selectedRows.map(row => row.original.criteriaId))]; + return [...new Set(selectedRows.map(row => row.original.id))]; }, [selectedRows]); const fileInputRef = useRef<HTMLInputElement>(null); @@ -117,14 +115,20 @@ function RegEvalCriteriaTableToolbarActions(props: RegEvalCriteriaTableToolbarAc } }; - // Excel Export + // Excel Export (DB에서 1:n 관계 포함하여 전체 데이터 내보내기) const handleExport = async () => { try { - await exportRegEvalCriteriaToExcel(table, { - filename: 'Regular_Evaluation_Criteria', - excludeColumns: ['select', 'actions'], + const buffer = await generateRegEvalCriteriaExcel(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); - toast.success('Excel 파일이 다운로드되었습니다.'); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = "평가_기준_전체.xlsx"; + link.click(); + URL.revokeObjectURL(url); + toast.success('Excel 파일이 다운로드되었습니다. (모든 평가내용 포함)'); } catch (error) { console.error('Error in Exporting to Excel: ', error); toast.error('Excel 내보내기 중 오류가 발생했습니다.'); @@ -134,7 +138,16 @@ function RegEvalCriteriaTableToolbarActions(props: RegEvalCriteriaTableToolbarAc // Excel Template Download const handleTemplateDownload = async () => { try { - await exportRegEvalCriteriaTemplate(); + const buffer = await generateRegEvalCriteriaTemplate(); + const blob = new Blob([buffer], { + type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = "평가_기준_템플릿.xlsx"; + link.click(); + URL.revokeObjectURL(url); toast.success('템플릿 파일이 다운로드되었습니다.'); } catch (error) { console.error('Error in Template Download: ', error); diff --git a/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx b/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx index bbf4f36d..f48fdf2e 100644 --- a/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx +++ b/lib/evaluation-criteria/table/reg-eval-criteria-update-sheet.tsx @@ -13,7 +13,8 @@ import { } from '@/components/ui/card'; import { getRegEvalCriteriaWithDetails, - modifyRegEvalCriteriaWithDetails, + modifyRegEvalCriteriaFixed, + modifyRegEvalCriteriaVariable, } from '../service'; import { Form, @@ -64,7 +65,6 @@ 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(), @@ -72,13 +72,13 @@ const regEvalCriteriaFormSchema = z.object({ 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>; @@ -341,38 +341,86 @@ function RegEvalCriteriaUpdateSheet({ }, [open, criteriaData, form]); const onSubmit = async (data: RegEvalCriteriaFormData) => { - startTransition(async () => { try { - const criteriaDataToUpdate = { - category: data.category, - category2: data.category2, - item: data.item, - classification: data.classification, - range: data.range, - remarks: data.remarks, - scoreType: data.scoreType, - variableScoreMin: data.variableScoreMin != null - ? String(data.variableScoreMin) : null, - variableScoreMax: data.variableScoreMax != null - ? String(data.variableScoreMax) : null, - variableScoreUnit: data.variableScoreUnit, - }; - - 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, - })); - - await modifyRegEvalCriteriaWithDetails(criteriaData.id, criteriaDataToUpdate, detailList); + 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 criteriaDataToUpdate = { + 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 + }; + + const detailList = data.criteriaDetails.map((detailItem: NonNullable<RegEvalCriteriaFormData['criteriaDetails']>[0]) => ({ + id: detailItem.id, + 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 modifyRegEvalCriteriaFixed(criteriaData.id, criteriaDataToUpdate, detailList); + + } else if (data.scoreType === 'variable') { + // 변동 점수 검증 + if (data.variableScoreMin == null) { + toast.error('변동 점수 유형에서는 최소 점수가 필수입니다.'); + return; + } + if (data.variableScoreMax == null) { + toast.error('변동 점수 유형에서는 최대 점수가 필수입니다.'); + return; + } + if (!data.variableScoreUnit || data.variableScoreUnit.trim() === '') { + toast.error('변동 점수 유형에서는 단위가 필수입니다.'); + return; + } + if (Number(data.variableScoreMin) >= Number(data.variableScoreMax)) { + toast.error('최소 점수는 최대 점수보다 작아야 합니다.'); + return; + } + + const criteriaDataToUpdate = { + 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(), + }; + + await modifyRegEvalCriteriaVariable(criteriaData.id, criteriaDataToUpdate); + } else { + toast.error('올바른 점수 유형을 선택해주세요.'); + return; + } + toast.success('평가 기준이 수정되었습니다.'); onSuccess(); onOpenChange(false); @@ -490,30 +538,6 @@ function RegEvalCriteriaUpdateSheet({ /> <FormField control={form.control} - name="scoreType" - render={({ field }) => ( - <FormItem> - <FormLabel>점수유형</FormLabel> - <FormControl> - <Select onValueChange={field.onChange} value={field.value}> - <SelectTrigger> - <SelectValue placeholder="선택" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="fixed">고정점수</SelectItem> - <SelectItem value="variable">변동점수</SelectItem> - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - - <div className="grid grid-cols-2 gap-4"> - <FormField - control={form.control} name="classification" render={({ field }) => ( <FormItem> @@ -525,6 +549,7 @@ function RegEvalCriteriaUpdateSheet({ </FormItem> )} /> + </div> <FormField control={form.control} name="range" @@ -541,16 +566,55 @@ function RegEvalCriteriaUpdateSheet({ </FormItem> )} /> - </div> - {/* 변동점수 설정 */} + + + <FormField + control={form.control} + name="remarks" + render={({ field }) => ( + <FormItem> + <FormLabel>비고</FormLabel> + <FormControl> + <Textarea + placeholder="비고를 입력하세요." + {...field} + value={field.value ?? ''} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="scoreType" + render={({ field }) => ( + <FormItem> + <FormLabel>점수유형</FormLabel> + <FormControl> + <Select onValueChange={field.onChange} value={field.value}> + <SelectTrigger> + <SelectValue placeholder="선택" /> + </SelectTrigger> + <SelectContent> + <SelectItem value="fixed">고정점수</SelectItem> + <SelectItem value="variable">변동점수</SelectItem> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + {/* 변동점수 설정 */} {scoreType === 'variable' && ( <> <Separator /> <div className="space-y-4"> <h4 className="font-medium">변동점수 설정</h4> <div className="grid grid-cols-3 gap-4"> - <FormField + <FormField control={form.control} name="variableScoreMin" render={({ field }) => ( @@ -559,8 +623,9 @@ function RegEvalCriteriaUpdateSheet({ <FormControl> <Input type="number" - step="0.01" - placeholder="0.00" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" {...field} value={field.value ?? ''} /> @@ -578,8 +643,9 @@ function RegEvalCriteriaUpdateSheet({ <FormControl> <Input type="number" - step="0.01" - placeholder="0.00" + min="0" + step="1" + placeholder="점수 입력 (0 이상)" {...field} value={field.value ?? ''} /> @@ -596,7 +662,7 @@ function RegEvalCriteriaUpdateSheet({ <FormLabel>점수단위</FormLabel> <FormControl> <Input - placeholder="예: 점, %" + placeholder="예: 감점, 가점" {...field} value={field.value ?? ''} /> @@ -609,27 +675,9 @@ function RegEvalCriteriaUpdateSheet({ </div> </> )} - - <FormField - control={form.control} - name="remarks" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="비고를 입력하세요." - {...field} - value={field.value ?? ''} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> </CardContent> </Card> - + {scoreType === 'fixed' && ( <Card> <CardHeader> <div className="flex items-center justify-between"> @@ -638,8 +686,7 @@ function RegEvalCriteriaUpdateSheet({ <CardDescription> {scoreType === 'fixed' ? '각 평가 옵션별 점수를 설정하세요.' - : '평가 옵션을 설정하세요. (점수는 변동점수 설정을 따릅니다.)' - } + : '평가 옵션을 설정하세요. (점수는 변동점수 설정을 따릅니다.)'} </CardDescription> </div> <Button @@ -680,6 +727,18 @@ function RegEvalCriteriaUpdateSheet({ </div> </CardContent> </Card> + )} + {scoreType === 'variable' && ( + <Card> + <CardHeader> + <CardTitle>변동 점수 설정</CardTitle> + <CardDescription> + 변동 점수 유형에서는 개별 평가 옵션을 설정할 수 없습니다. + 최소/최대 점수와 단위를 설정하여 점수 범위를 정의하세요. + </CardDescription> + </CardHeader> + </Card> + )} </div> </div> |
