diff options
| author | joonhoekim <26rote@gmail.com> | 2025-09-08 11:14:11 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-09-08 11:14:11 +0000 |
| commit | 3828852af2708d477975600cec60ff8c1802e279 (patch) | |
| tree | a0dd8cf44ac35c2787886907b6eca09741f77c13 /lib | |
| parent | 10aa3d34bc599232af07d8a643c9938be14cb5bf (diff) | |
(김준회) 협력업체관리 - 수정 기능 컨텍스트 메뉴에 추가, 업데이트 시트 항목 추가
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/vendors/table/update-vendor-sheet.tsx | 262 | ||||
| -rw-r--r-- | lib/vendors/table/vendors-table-columns.tsx | 77 | ||||
| -rw-r--r-- | lib/vendors/validations.ts | 14 |
3 files changed, 263 insertions, 90 deletions
diff --git a/lib/vendors/table/update-vendor-sheet.tsx b/lib/vendors/table/update-vendor-sheet.tsx index 08994b6a..5138d299 100644 --- a/lib/vendors/table/update-vendor-sheet.tsx +++ b/lib/vendors/table/update-vendor-sheet.tsx @@ -18,9 +18,7 @@ import { CheckCircle2, Circle as CircleIcon, User, - Building, - AlignLeft, - Calendar + Building } from "lucide-react" import { toast } from "sonner" @@ -183,6 +181,7 @@ const cashFlowRatingScaleMap: Record<string, string[]> = { export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) { const [isPending, startTransition] = React.useTransition() const [selectedAgency, setSelectedAgency] = React.useState<string>(vendor?.creditAgency || "NICE") + const { data: session } = useSession() // 폼 정의 - UpdateVendorSchema 타입을 직접 사용 const form = useForm<UpdateVendorSchema>({ @@ -192,14 +191,28 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) vendorName: vendor?.vendorName ?? "", vendorCode: vendor?.vendorCode ?? "", address: vendor?.address ?? "", + addressDetail: vendor?.addressDetail ?? "", + postalCode: vendor?.postalCode ?? "", country: vendor?.country ?? "", phone: vendor?.phone ?? "", email: vendor?.email ?? "", website: vendor?.website ?? "", creditRating: vendor?.creditRating ?? "", cashFlowRating: vendor?.cashFlowRating ?? "", - status: vendor?.status ?? "ACTIVE", + status: (vendor?.status as any) ?? "ACTIVE", vendorTypeId: vendor?.vendorTypeId ?? undefined, + isAssociationMember: (vendor as any)?.isAssociationMember || "NONE", + + // 대표자 정보 + representativeName: (vendor as any)?.representativeName ?? "", + representativeBirth: (vendor as any)?.representativeBirth ?? "", + representativeEmail: (vendor as any)?.representativeEmail ?? "", + representativePhone: (vendor as any)?.representativePhone ?? "", + representativeWorkExpirence: (vendor as any)?.representativeWorkExpirence ?? false, + corporateRegistrationNumber: vendor?.corporateRegistrationNumber ?? "", + + // 사업 정보 + businessSize: vendor?.businessSize ?? "", // 구매담당자 정보 (기본값은 비어있음) buyerName: "", @@ -217,14 +230,28 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) vendorName: vendor?.vendorName ?? "", vendorCode: vendor?.vendorCode ?? "", address: vendor?.address ?? "", + addressDetail: vendor?.addressDetail ?? "", + postalCode: vendor?.postalCode ?? "", country: vendor?.country ?? "", phone: vendor?.phone ?? "", email: vendor?.email ?? "", website: vendor?.website ?? "", creditRating: vendor?.creditRating ?? "", cashFlowRating: vendor?.cashFlowRating ?? "", - status: vendor?.status ?? "ACTIVE", + status: (vendor?.status as any) ?? "ACTIVE", vendorTypeId: vendor?.vendorTypeId ?? undefined, + isAssociationMember: (vendor as any)?.isAssociationMember || "NONE", + + // 대표자 정보 + representativeName: (vendor as any)?.representativeName ?? "", + representativeBirth: (vendor as any)?.representativeBirth ?? "", + representativeEmail: (vendor as any)?.representativeEmail ?? "", + representativePhone: (vendor as any)?.representativePhone ?? "", + representativeWorkExpirence: (vendor as any)?.representativeWorkExpirence ?? false, + corporateRegistrationNumber: vendor?.corporateRegistrationNumber ?? "", + + // 사업 정보 + businessSize: vendor?.businessSize ?? "", // 구매담당자 필드는 유지 buyerName: form.getValues("buyerName"), @@ -264,19 +291,19 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) }, [selectedAgency, form]); // 제출 핸들러 - async function onSubmit(data: UpdateVendorSchema) { + function onSubmit(data: UpdateVendorSchema) { if (!vendor) return - const { data: session } = useSession() - + if (!session?.user?.id) { toast.error("사용자 인증 정보를 찾을 수 없습니다.") return } - startTransition(async () => { + + startTransition(async () => { try { // Add status change comment if status has changed - const oldStatus = vendor.status ?? "ACTIVE" // Default to ACTIVE if undefined - const newStatus = data.status ?? "ACTIVE" // Default to ACTIVE if undefined + const oldStatus = (vendor.status as any) ?? "ACTIVE" // Default to ACTIVE if undefined + const newStatus = (data.status as any) ?? "ACTIVE" // Default to ACTIVE if undefined const statusComment = oldStatus !== newStatus @@ -289,7 +316,7 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) userId: Number(session.user.id), // Add user ID from session comment: statusComment, // Add comment for status changes ...data // 모든 데이터 전달 - 서비스 함수에서 필요한 필드만 처리 - }) + } as any) if (error) throw new Error(error) @@ -316,12 +343,8 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) {/* 업체 기본 정보 섹션 */} <div className="space-y-4"> <div className="flex items-center"> - <Building className="mr-2 h-5 w-5 text-muted-foreground" /> - <h3 className="text-sm font-medium">업체 기본 정보</h3> + <h3 className="text-sm font-medium">협력업체 정보 수정</h3> </div> - <FormDescription> - 업체가 제공한 기본 정보입니다. 필요시 수정하세요. - </FormDescription> <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> {/* vendorName */} <FormField @@ -338,7 +361,7 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) )} /> - {/* vendorCode */} + {/* vendorCode - 읽기전용 */} <FormField control={form.control} name="vendorCode" @@ -346,7 +369,11 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) <FormItem> <FormLabel>업체 코드</FormLabel> <FormControl> - <Input placeholder="예: ABC123" {...field} /> + <Input + {...field} + readOnly + className="bg-gray-50 text-gray-600 cursor-not-allowed" + /> </FormControl> <FormMessage /> </FormItem> @@ -368,6 +395,36 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) )} /> + {/* addressDetail */} + <FormField + control={form.control} + name="addressDetail" + render={({ field }) => ( + <FormItem> + <FormLabel>상세주소</FormLabel> + <FormControl> + <Input placeholder="상세주소 입력" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* postalCode */} + <FormField + control={form.control} + name="postalCode" + render={({ field }) => ( + <FormItem> + <FormLabel>우편번호</FormLabel> + <FormControl> + <Input placeholder="우편번호 입력" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + {/* country */} <FormField control={form.control} @@ -479,6 +536,37 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) }} /> + {/* 성조회 가입여부 */} + <FormField + control={form.control} + name="isAssociationMember" + render={({ field }) => ( + <FormItem> + <FormLabel>성조회 가입여부</FormLabel> + <FormControl> + <Select + value={field.value || "NONE"} + onValueChange={(value) => { + field.onChange(value === "NONE" ? "" : value); + }} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder="가입여부 선택" /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value="NONE">정보없음</SelectItem> + <SelectItem value="Y">가입</SelectItem> + <SelectItem value="N">미가입</SelectItem> + <SelectItem value="E">해당없음</SelectItem> + </SelectGroup> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> {/* 신용평가기관 선택 */} <FormField @@ -586,6 +674,142 @@ export function UpdateVendorSheet({ vendor, ...props }: UpdateVendorSheetProps) {/* 구분선 */} <Separator className="my-2" /> + {/* 대표자 정보 섹션 */} + <div className="space-y-4"> + <div className="flex items-center"> + <User className="mr-2 h-5 w-5 text-muted-foreground" /> + <h3 className="text-sm font-medium">대표자 정보</h3> + </div> + <FormDescription> + 업체 대표자의 정보를 입력하세요. + </FormDescription> + + <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> + {/* 대표자명 */} + <FormField + control={form.control} + name="representativeName" + render={({ field }) => ( + <FormItem> + <FormLabel>대표자명</FormLabel> + <FormControl> + <Input placeholder="대표자명 입력" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 대표자 생년월일 */} + <FormField + control={form.control} + name="representativeBirth" + render={({ field }) => ( + <FormItem> + <FormLabel>대표자 생년월일</FormLabel> + <FormControl> + <Input placeholder="예: 1970-01-01" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 대표자 이메일 */} + <FormField + control={form.control} + name="representativeEmail" + render={({ field }) => ( + <FormItem> + <FormLabel>대표자 이메일</FormLabel> + <FormControl> + <Input placeholder="예: ceo@company.com" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 대표자 전화번호 */} + <FormField + control={form.control} + name="representativePhone" + render={({ field }) => ( + <FormItem> + <FormLabel>대표자 전화번호</FormLabel> + <FormControl> + <Input placeholder="예: 010-1234-5678" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 법인등록번호 */} + <FormField + control={form.control} + name="corporateRegistrationNumber" + render={({ field }) => ( + <FormItem> + <FormLabel>법인등록번호</FormLabel> + <FormControl> + <Input placeholder="법인등록번호 입력" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 사업 규모 */} + <FormField + control={form.control} + name="businessSize" + render={({ field }) => ( + <FormItem> + <FormLabel>사업 규모</FormLabel> + <FormControl> + <Input placeholder="예: 중소기업, 대기업" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 대표자 경력 */} + <FormField + control={form.control} + name="representativeWorkExpirence" + render={({ field }) => ( + <FormItem> + <FormLabel>대표자 삼성중공업 근무경험 여부</FormLabel> + <FormControl> + <Select + value={field.value ? "true" : "false"} + onValueChange={(value) => field.onChange(value === "true")} + > + <SelectTrigger className="w-full"> + <SelectValue placeholder="경력 보유 여부 선택" /> + </SelectTrigger> + <SelectContent> + <SelectGroup> + <SelectItem value="false">없음</SelectItem> + <SelectItem value="true">있음</SelectItem> + </SelectGroup> + </SelectContent> + </Select> + </FormControl> + <div className="space-y-1 leading-none"> + + </div> + </FormItem> + )} + /> + </div> + </div> + + {/* 구분선 */} + <Separator className="my-2" /> + {/* 구매담당자 입력 섹션 */} <div className="space-y-4 bg-slate-50 p-4 rounded-md border border-slate-200"> <div className="flex items-center"> diff --git a/lib/vendors/table/vendors-table-columns.tsx b/lib/vendors/table/vendors-table-columns.tsx index f2de179c..738b8b5f 100644 --- a/lib/vendors/table/vendors-table-columns.tsx +++ b/lib/vendors/table/vendors-table-columns.tsx @@ -61,16 +61,6 @@ interface GetColumnsProps { userId: number; } -// 권한 체크 헬퍼 함수들 (향후 실제 권한 시스템과 연동) -const checkEditAssociationPermission = (userId: number): boolean => { - // TODO: 실제 권한 체크 로직 구현 - // 예: 특정 역할(ADMIN, VENDOR_MANAGER 등)을 가진 사용자만 수정 가능 - // const userRoles = await getUserRoles(userId); - // return userRoles.includes('VENDOR_MANAGER') || userRoles.includes('ADMIN'); - - // 개발 중에는 모든 사용자가 수정 가능 - return true; -}; @@ -118,8 +108,6 @@ export function getColumns({ setRowAction, router, userId }: GetColumnsProps): C enableHiding: false, cell: function Cell({ row }) { const [isUpdatePending, startUpdateTransition] = React.useTransition() - const isApproved = row.original.status === "PQ_APPROVED"; - const afterApproved = row.original.status === "ACTIVE"; return ( <DropdownMenu> @@ -133,13 +121,11 @@ export function getColumns({ setRowAction, router, userId }: GetColumnsProps): C </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-56"> - {(isApproved || afterApproved) && ( - <DropdownMenuItem - onSelect={() => setRowAction({ row, type: "update" })} - > - 레코드 편집 - </DropdownMenuItem> - )} + <DropdownMenuItem + onSelect={() => setRowAction({ row, type: "update" })} + > + 레코드 편집 + </DropdownMenuItem> <DropdownMenuItem onSelect={() => { @@ -420,9 +406,8 @@ export function getColumns({ setRowAction, router, userId }: GetColumnsProps): C ); } - // 성조회가입여부 처리 - 권한에 따라 드롭다운 또는 읽기전용 배지 + // 성조회가입여부 처리 - 읽기전용 배지만 표시 (편집은 update-vendor-sheet에서 처리) if (cfg.id === "isAssociationMember") { - const [isUpdating, setIsUpdating] = React.useState(false); const memberVal = row.original.isAssociationMember as string | null; const getDisplayText = (value: string | null) => { @@ -430,7 +415,7 @@ export function getColumns({ setRowAction, router, userId }: GetColumnsProps): C case "Y": return "가입"; case "N": return "미가입"; case "E": return "해당없음"; - default: return "-"; + default: return "정보없음"; } }; @@ -447,53 +432,7 @@ export function getColumns({ setRowAction, router, userId }: GetColumnsProps): C } }; - // 권한 체크: 성조회가입여부 수정 권한이 있는지 확인 - const hasEditPermission = checkEditAssociationPermission(userId); - - const handleValueChange = async (newValue: string) => { - // "NONE" 값을 null로 변환 - const actualValue = newValue === "NONE" ? null : newValue; - - setIsUpdating(true); - try { - await modifyVendor({ - id: String(row.original.id), - isAssociationMember: actualValue, - userId, - vendorName: row.original.vendorName, - comment: `성조회가입여부 변경: ${getDisplayText(memberVal)} → ${getDisplayText(actualValue)}` - } as any); - - toast.success("성조회가입여부가 업데이트되었습니다."); - } catch (error) { - toast.error("업데이트에 실패했습니다: " + getErrorMessage(error)); - } finally { - setIsUpdating(false); - } - }; - - // 권한이 있는 경우 드롭다운 표시 - if (hasEditPermission) { - return ( - <Select - value={memberVal || "NONE"} - onValueChange={handleValueChange} - disabled={isUpdating} - > - <SelectTrigger className="w-[120px] h-8"> - <SelectValue /> - </SelectTrigger> - <SelectContent> - <SelectItem value="NONE">-</SelectItem> - <SelectItem value="Y">가입</SelectItem> - <SelectItem value="N">미가입</SelectItem> - <SelectItem value="E">해당없음</SelectItem> - </SelectContent> - </Select> - ); - } - - // 권한이 없는 경우 읽기전용 배지 표시 + // 읽기전용 배지만 표시 return ( <Badge variant="outline" className={getBadgeStyle(memberVal)}> {getDisplayText(memberVal)} diff --git a/lib/vendors/validations.ts b/lib/vendors/validations.ts index 237dc846..917242d3 100644 --- a/lib/vendors/validations.ts +++ b/lib/vendors/validations.ts @@ -127,11 +127,22 @@ export const updateVendorSchema = z.object({ country: z.string().optional(), phone: z.string().optional(), email: z.string().email("유효한 이메일 주소를 입력해주세요").optional(), - website: z.string().url("유효한 URL을 입력해주세요").optional(), + website: z.string().optional(), status: z.enum(vendors.status.enumValues).optional(), vendorTypeId: z.number().optional(), isAssociationMember: z.string().optional(), // 성조회가입여부 추가 + // Representative information + representativeName: z.string().optional(), + representativeBirth: z.string().optional(), + representativeEmail: z.string().email("유효한 이메일 주소를 입력해주세요").optional(), + representativePhone: z.string().optional(), + representativeWorkExpirence: z.boolean().optional(), + corporateRegistrationNumber: z.string().optional(), + + // Business information + businessSize: z.string().optional(), + // Optional fields for buyer information buyerName: z.string().optional(), buyerDepartment: z.string().optional(), @@ -145,7 +156,6 @@ export const updateVendorSchema = z.object({ // evaluationScore: z.string().optional(), }); -export type UpdateVendorSchema = z.infer<typeof updateVendorSchema>; const contactSchema = z.object({ |
