summaryrefslogtreecommitdiff
path: root/lib/b-rfq/initial
diff options
context:
space:
mode:
Diffstat (limited to 'lib/b-rfq/initial')
-rw-r--r--lib/b-rfq/initial/add-initial-rfq-dialog.tsx584
-rw-r--r--lib/b-rfq/initial/delete-initial-rfq-dialog.tsx149
-rw-r--r--lib/b-rfq/initial/initial-rfq-detail-columns.tsx446
-rw-r--r--lib/b-rfq/initial/initial-rfq-detail-table.tsx267
-rw-r--r--lib/b-rfq/initial/initial-rfq-detail-toolbar-actions.tsx287
-rw-r--r--lib/b-rfq/initial/short-list-confirm-dialog.tsx269
-rw-r--r--lib/b-rfq/initial/update-initial-rfq-sheet.tsx496
7 files changed, 0 insertions, 2498 deletions
diff --git a/lib/b-rfq/initial/add-initial-rfq-dialog.tsx b/lib/b-rfq/initial/add-initial-rfq-dialog.tsx
deleted file mode 100644
index 58a091ac..00000000
--- a/lib/b-rfq/initial/add-initial-rfq-dialog.tsx
+++ /dev/null
@@ -1,584 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { z } from "zod"
-import { Plus, Check, ChevronsUpDown, Search, Building, CalendarIcon } from "lucide-react"
-import { toast } from "sonner"
-
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "@/components/ui/command"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover"
-import { Button } from "@/components/ui/button"
-import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
-import { Checkbox } from "@/components/ui/checkbox"
-import { cn, formatDate } from "@/lib/utils"
-import { addInitialRfqRecord, getIncotermsForSelection, getVendorsForSelection } from "../service"
-import { Calendar } from "@/components/ui/calendar"
-import { InitialRfqDetailView } from "@/db/schema"
-
-// Initial RFQ 추가 폼 스키마
-const addInitialRfqSchema = z.object({
- vendorId: z.number({
- required_error: "벤더를 선택해주세요.",
- }),
- initialRfqStatus: z.enum(["DRAFT", "Init. RFQ Sent", "S/L Decline", "Init. RFQ Answered"], {
- required_error: "초기 RFQ 상태를 선택해주세요.",
- }).default("DRAFT"),
- dueDate: z.date({
- required_error: "마감일을 선택해주세요.",
- }),
- validDate: z.date().optional(),
- incotermsCode: z.string().optional(),
- gtc: z.string().optional(),
- gtcValidDate: z.string().optional(),
- classification: z.string().optional(),
- sparepart: z.string().optional(),
- shortList: z.boolean().default(false),
- returnYn: z.boolean().default(false),
- cpRequestYn: z.boolean().default(false),
- prjectGtcYn: z.boolean().default(false),
- returnRevision: z.number().default(0),
-})
-
-export type AddInitialRfqFormData = z.infer<typeof addInitialRfqSchema>
-
-interface Vendor {
- id: number
- vendorName: string
- vendorCode: string
- country: string
- taxId: string
- status: string
-}
-
-interface Incoterm {
- id: number
- code: string
- description: string
-}
-
-interface AddInitialRfqDialogProps {
- rfqId: number
- onSuccess?: () => void
- defaultValues?: InitialRfqDetailView // 선택된 항목의 기본값
-}
-
-export function AddInitialRfqDialog({ rfqId, onSuccess, defaultValues }: AddInitialRfqDialogProps) {
- const [open, setOpen] = React.useState(false)
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [vendors, setVendors] = React.useState<Vendor[]>([])
- const [vendorsLoading, setVendorsLoading] = React.useState(false)
- const [vendorSearchOpen, setVendorSearchOpen] = React.useState(false)
- const [incoterms, setIncoterms] = React.useState<Incoterm[]>([])
- const [incotermsLoading, setIncotermsLoading] = React.useState(false)
- const [incotermsSearchOpen, setIncotermsSearchOpen] = React.useState(false)
-
- // 기본값 설정 (선택된 항목이 있으면 해당 값 사용, 없으면 일반 기본값)
- const getDefaultFormValues = React.useCallback((): Partial<AddInitialRfqFormData> => {
- if (defaultValues) {
- return {
- vendorId: defaultValues.vendorId,
- initialRfqStatus: "DRAFT", // 새로 추가할 때는 항상 DRAFT로 시작
- dueDate: defaultValues.dueDate || new Date(),
- validDate: defaultValues.validDate,
- incotermsCode: defaultValues.incotermsCode || "",
- classification: defaultValues.classification || "",
- sparepart: defaultValues.sparepart || "",
- shortList: false, // 새로 추가할 때는 기본적으로 false
- returnYn: false,
- cpRequestYn: defaultValues.cpRequestYn || false,
- prjectGtcYn: defaultValues.prjectGtcYn || false,
- returnRevision: 0,
- }
- }
-
- return {
- initialRfqStatus: "DRAFT",
- shortList: false,
- returnYn: false,
- cpRequestYn: false,
- prjectGtcYn: false,
- returnRevision: 0,
- }
- }, [defaultValues])
-
- const form = useForm<AddInitialRfqFormData>({
- resolver: zodResolver(addInitialRfqSchema),
- defaultValues: getDefaultFormValues(),
- })
-
- // 벤더 목록 로드
- const loadVendors = React.useCallback(async () => {
- setVendorsLoading(true)
- try {
- const vendorList = await getVendorsForSelection()
- setVendors(vendorList)
- } catch (error) {
- console.error("Failed to load vendors:", error)
- toast.error("벤더 목록을 불러오는데 실패했습니다.")
- } finally {
- setVendorsLoading(false)
- }
- }, [])
-
- // Incoterms 목록 로드
- const loadIncoterms = React.useCallback(async () => {
- setIncotermsLoading(true)
- try {
- const incotermsList = await getIncotermsForSelection()
- setIncoterms(incotermsList)
- } catch (error) {
- console.error("Failed to load incoterms:", error)
- toast.error("Incoterms 목록을 불러오는데 실패했습니다.")
- } finally {
- setIncotermsLoading(false)
- }
- }, [])
-
- // 다이얼로그 열릴 때 실행
- React.useEffect(() => {
- if (open) {
- // 폼을 기본값으로 리셋
- form.reset(getDefaultFormValues())
-
- // 데이터 로드
- if (vendors.length === 0) {
- loadVendors()
- }
- if (incoterms.length === 0) {
- loadIncoterms()
- }
- }
- }, [open, vendors.length, incoterms.length, loadVendors, loadIncoterms, form, getDefaultFormValues])
-
- // 다이얼로그 닫기 핸들러
- const handleOpenChange = (newOpen: boolean) => {
- if (!newOpen && !isSubmitting) {
- form.reset(getDefaultFormValues())
- }
- setOpen(newOpen)
- }
-
- // 폼 제출
- const onSubmit = async (data: AddInitialRfqFormData) => {
- setIsSubmitting(true)
-
- try {
- const result = await addInitialRfqRecord({
- ...data,
- rfqId,
- })
-
- if (result.success) {
- toast.success(result.message || "초기 RFQ가 성공적으로 추가되었습니다.")
- form.reset(getDefaultFormValues())
- handleOpenChange(false)
- onSuccess?.()
- } else {
- toast.error(result.message || "초기 RFQ 추가에 실패했습니다.")
- }
-
- } catch (error) {
- console.error("Submit error:", error)
- toast.error("초기 RFQ 추가 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- }
- }
-
- // 선택된 벤더 정보
- const selectedVendor = vendors.find(vendor => vendor.id === form.watch("vendorId"))
- const selectedIncoterm = incoterms.find(incoterm => incoterm.code === form.watch("incotermsCode"))
-
- // 기본값이 있을 때 버튼 텍스트 변경
- const buttonText = defaultValues ? "유사 항목 추가" : "초기 RFQ 추가"
- const dialogTitle = defaultValues ? "유사 초기 RFQ 추가" : "초기 RFQ 추가"
- const dialogDescription = defaultValues
- ? "선택된 항목을 기본값으로 하여 새로운 초기 RFQ를 추가합니다."
- : "새로운 벤더를 대상으로 하는 초기 RFQ를 추가합니다."
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogTrigger asChild>
- <Button variant="outline" size="sm" className="gap-2">
- <Plus className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">{buttonText}</span>
- </Button>
- </DialogTrigger>
-
- <DialogContent className="sm:max-w-[600px] max-h-[80vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>{dialogTitle}</DialogTitle>
- <DialogDescription>
- {dialogDescription}
- {defaultValues && (
- <div className="mt-2 p-2 bg-muted rounded-md text-sm">
- <strong>기본값 출처:</strong> {defaultValues.vendorName} ({defaultValues.vendorCode})
- </div>
- )}
- </DialogDescription>
- </DialogHeader>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
- {/* 벤더 선택 */}
- <FormField
- control={form.control}
- name="vendorId"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>벤더 선택 *</FormLabel>
- <Popover open={vendorSearchOpen} onOpenChange={setVendorSearchOpen}>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- aria-expanded={vendorSearchOpen}
- className="justify-between"
- disabled={vendorsLoading}
- >
- {selectedVendor ? (
- <div className="flex items-center gap-2">
- <Building className="h-4 w-4" />
- <span className="truncate">
- {selectedVendor.vendorName} ({selectedVendor.vendorCode})
- </span>
- </div>
- ) : (
- <span className="text-muted-foreground">
- {vendorsLoading ? "로딩 중..." : "벤더를 선택하세요"}
- </span>
- )}
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0" align="start">
- <Command>
- <CommandInput
- placeholder="벤더명 또는 코드로 검색..."
- className="h-9"
- />
- <CommandList>
- <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
- <CommandGroup>
- {vendors.map((vendor) => (
- <CommandItem
- key={vendor.id}
- value={`${vendor.vendorName} ${vendor.vendorCode}`}
- onSelect={() => {
- field.onChange(vendor.id)
- setVendorSearchOpen(false)
- }}
- >
- <div className="flex items-center gap-2 w-full">
- <Building className="h-4 w-4" />
- <div className="flex-1 min-w-0">
- <div className="font-medium truncate">
- {vendor.vendorName}
- </div>
- <div className="text-sm text-muted-foreground">
- {vendor.vendorCode} • {vendor.country} • {vendor.taxId}
- </div>
- </div>
- <Check
- className={cn(
- "ml-auto h-4 w-4",
- vendor.id === field.value ? "opacity-100" : "opacity-0"
- )}
- />
- </div>
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 날짜 필드들 */}
- <div className="grid grid-cols-2 gap-4">
- <FormField
- control={form.control}
- name="dueDate"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>견적 마감일 *</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- className={cn(
- "w-full pl-3 text-left font-normal",
- !field.value && "text-muted-foreground"
- )}
- >
- {field.value ? (
- formatDate(field.value, "KR")
- ) : (
- <span>견적 마감일을 선택하세요</span>
- )}
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={field.value}
- onSelect={field.onChange}
- disabled={(date) =>
- date < new Date() || date < new Date("1900-01-01")
- }
- initialFocus
- />
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="validDate"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>견적 유효일</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- className={cn(
- "w-full pl-3 text-left font-normal",
- !field.value && "text-muted-foreground"
- )}
- >
- {field.value ? (
- formatDate(field.value, "KR")
- ) : (
- <span>견적 유효일을 선택하세요</span>
- )}
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={field.value}
- onSelect={field.onChange}
- disabled={(date) =>
- date < new Date() || date < new Date("1900-01-01")
- }
- initialFocus
- />
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- {/* Incoterms 선택 */}
- <FormField
- control={form.control}
- name="incotermsCode"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>Incoterms</FormLabel>
- <Popover open={incotermsSearchOpen} onOpenChange={setIncotermsSearchOpen}>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- aria-expanded={incotermsSearchOpen}
- className="justify-between"
- disabled={incotermsLoading}
- >
- {selectedIncoterm ? (
- <div className="flex items-center gap-2">
- <span className="truncate">
- {selectedIncoterm.code} - {selectedIncoterm.description}
- </span>
- </div>
- ) : (
- <span className="text-muted-foreground">
- {incotermsLoading ? "로딩 중..." : "인코텀즈를 선택하세요"}
- </span>
- )}
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0" align="start">
- <Command>
- <CommandInput
- placeholder="코드 또는 내용으로 검색..."
- className="h-9"
- />
- <CommandList>
- <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
- <CommandGroup>
- {incoterms.map((incoterm) => (
- <CommandItem
- key={incoterm.id}
- value={`${incoterm.code} ${incoterm.description}`}
- onSelect={() => {
- field.onChange(incoterm.code)
- setIncotermsSearchOpen(false)
- }}
- >
- <div className="flex items-center gap-2 w-full">
- <div className="flex-1 min-w-0">
- <div className="font-medium truncate">
- {incoterm.code} - {incoterm.description}
- </div>
- </div>
- <Check
- className={cn(
- "ml-auto h-4 w-4",
- incoterm.code === field.value ? "opacity-100" : "opacity-0"
- )}
- />
- </div>
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 옵션 체크박스 */}
- <div className="grid grid-cols-2 gap-4">
- <FormField
- control={form.control}
- name="cpRequestYn"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none ml-2">
- <FormLabel>CP 요청</FormLabel>
- </div>
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="prjectGtcYn"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none ml-2">
- <FormLabel>Project용 GTC 사용</FormLabel>
- </div>
- </FormItem>
- )}
- />
- </div>
-
- {/* 분류 정보 */}
- <div className="grid grid-cols-2 gap-4">
- <FormField
- control={form.control}
- name="classification"
- render={({ field }) => (
- <FormItem>
- <FormLabel>선급</FormLabel>
- <FormControl>
- <Input placeholder="선급" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="sparepart"
- render={({ field }) => (
- <FormItem>
- <FormLabel>Spare part</FormLabel>
- <FormControl>
- <Input placeholder="O1, O2" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => handleOpenChange(false)}
- disabled={isSubmitting}
- >
- 취소
- </Button>
- <Button type="submit" disabled={isSubmitting}>
- {isSubmitting ? "추가 중..." : "추가"}
- </Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/initial/delete-initial-rfq-dialog.tsx b/lib/b-rfq/initial/delete-initial-rfq-dialog.tsx
deleted file mode 100644
index b5a231b7..00000000
--- a/lib/b-rfq/initial/delete-initial-rfq-dialog.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Row } from "@tanstack/react-table"
-import { Loader, Trash } from "lucide-react"
-import { toast } from "sonner"
-
-import { useMediaQuery } from "@/hooks/use-media-query"
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from "@/components/ui/dialog"
-import {
- Drawer,
- DrawerClose,
- DrawerContent,
- DrawerDescription,
- DrawerFooter,
- DrawerHeader,
- DrawerTitle,
- DrawerTrigger,
-} from "@/components/ui/drawer"
-
-import { InitialRfqDetailView } from "@/db/schema"
-import { removeInitialRfqs } from "../service"
-
-interface DeleteInitialRfqDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- initialRfqs: Row<InitialRfqDetailView>["original"][]
- showTrigger?: boolean
- onSuccess?: () => void
-}
-
-export function DeleteInitialRfqDialog({
- initialRfqs,
- showTrigger = true,
- onSuccess,
- ...props
-}: DeleteInitialRfqDialogProps) {
- const [isDeletePending, startDeleteTransition] = React.useTransition()
- const isDesktop = useMediaQuery("(min-width: 640px)")
-
- function onDelete() {
- startDeleteTransition(async () => {
- const { error } = await removeInitialRfqs({
- ids: initialRfqs.map((rfq) => rfq.id),
- })
-
- if (error) {
- toast.error(error)
- return
- }
-
- props.onOpenChange?.(false)
- toast.success("초기 RFQ가 삭제되었습니다")
- onSuccess?.()
- })
- }
-
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({initialRfqs.length})
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
- <DialogDescription>
- 이 작업은 되돌릴 수 없습니다. 선택한{" "}
- <span className="font-medium">{initialRfqs.length}개</span>의
- 초기 RFQ{initialRfqs.length === 1 ? "를" : "들을"} 영구적으로 삭제합니다.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="gap-2 sm:space-x-0">
- <DialogClose asChild>
- <Button variant="outline">취소</Button>
- </DialogClose>
- <Button
- aria-label="Delete selected rows"
- variant="destructive"
- onClick={onDelete}
- disabled={isDeletePending}
- >
- {isDeletePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- 삭제
- </Button>
- </DialogFooter>
- </DialogContent>
- </Dialog>
- )
- }
-
- return (
- <Drawer {...props}>
- {showTrigger ? (
- <DrawerTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({initialRfqs.length})
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
- <DrawerDescription>
- 이 작업은 되돌릴 수 없습니다. 선택한{" "}
- <span className="font-medium">{initialRfqs.length}개</span>의
- 초기 RFQ{initialRfqs.length === 1 ? "를" : "들을"} 영구적으로 삭제합니다.
- </DrawerDescription>
- </DrawerHeader>
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">취소</Button>
- </DrawerClose>
- <Button
- aria-label="Delete selected rows"
- variant="destructive"
- onClick={onDelete}
- disabled={isDeletePending}
- >
- {isDeletePending && (
- <Loader className="mr-2 size-4 animate-spin" aria-hidden="true" />
- )}
- 삭제
- </Button>
- </DrawerFooter>
- </DrawerContent>
- </Drawer>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/initial/initial-rfq-detail-columns.tsx b/lib/b-rfq/initial/initial-rfq-detail-columns.tsx
deleted file mode 100644
index 2d9c3a68..00000000
--- a/lib/b-rfq/initial/initial-rfq-detail-columns.tsx
+++ /dev/null
@@ -1,446 +0,0 @@
-// initial-rfq-detail-columns.tsx
-"use client"
-
-import * as React from "react"
-import { type ColumnDef } from "@tanstack/react-table"
-import { type Row } from "@tanstack/react-table"
-import {
- Ellipsis, Building, Eye, Edit, Trash,
- MessageSquare, Settings, CheckCircle2, XCircle
-} from "lucide-react"
-
-import { formatDate } from "@/lib/utils"
-import { Badge } from "@/components/ui/badge"
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
- DropdownMenu, DropdownMenuContent, DropdownMenuItem,
- DropdownMenuSeparator, DropdownMenuTrigger, DropdownMenuShortcut
-} from "@/components/ui/dropdown-menu"
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
-import { InitialRfqDetailView } from "@/db/schema"
-
-
-// RowAction 타입 정의
-export interface DataTableRowAction<TData> {
- row: Row<TData>
- type: "update" | "delete"
-}
-
-interface GetInitialRfqDetailColumnsProps {
- onSelectDetail?: (detail: any) => void
- setRowAction?: React.Dispatch<React.SetStateAction<DataTableRowAction<InitialRfqDetailView> | null>>
-}
-
-export function getInitialRfqDetailColumns({
- onSelectDetail,
- setRowAction
-}: GetInitialRfqDetailColumnsProps = {}): ColumnDef<InitialRfqDetailView>[] {
-
- return [
- /** ───────────── 체크박스 ───────────── */
- {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- className="translate-y-0.5"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(value) => row.toggleSelected(!!value)}
- aria-label="Select row"
- className="translate-y-0.5"
- />
- ),
- size: 40,
- enableSorting: false,
- enableHiding: false,
- },
-
- /** ───────────── RFQ 정보 ───────────── */
- {
- accessorKey: "initialRfqStatus",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="RFQ 상태" />
- ),
- cell: ({ row }) => {
- const status = row.getValue("initialRfqStatus") as string
- const getInitialStatusColor = (status: string) => {
- switch (status) {
- case "DRAFT": return "outline"
- case "Init. RFQ Sent": return "default"
- case "Init. RFQ Answered": return "success"
- case "S/L Decline": return "destructive"
- default: return "secondary"
- }
- }
- return (
- <Badge variant={getInitialStatusColor(status) as any}>
- {status}
- </Badge>
- )
- },
- size: 120
- },
- {
- accessorKey: "rfqCode",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="RFQ No." />
- ),
- cell: ({ row }) => (
- <div className="text-sm">
- {row.getValue("rfqCode") as string}
- </div>
- ),
- size: 120,
- },
- {
- accessorKey: "rfqRevision",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="RFQ 리비전" />
- ),
- cell: ({ row }) => (
- <div className="text-sm">
- Rev. {row.getValue("rfqRevision") as number}
- </div>
- ),
- size: 120,
- },
-
- /** ───────────── 벤더 정보 ───────────── */
- {
- id: "vendorInfo",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="벤더 정보" />
- ),
- cell: ({ row }) => {
- const vendorName = row.original.vendorName as string
- const vendorCode = row.original.vendorCode as string
- const vendorType = row.original.vendorCategory as string
- const vendorCountry = row.original.vendorCountry === "KR" ? "D":"F"
- const businessSize = row.original.vendorBusinessSize as string
-
- return (
- <div className="space-y-1">
- <div className="flex items-center gap-2">
- <Building className="h-4 w-4 text-muted-foreground" />
- <div className="font-medium">{vendorName}</div>
- </div>
- <div className="text-sm text-muted-foreground">
- {vendorCode} • {vendorType} • {vendorCountry}
- </div>
- {businessSize && (
- <Badge variant="outline" className="text-xs">
- {businessSize}
- </Badge>
- )}
- </div>
- )
- },
- size: 200,
- },
-
- {
- accessorKey: "cpRequestYn",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="CP" />
- ),
- cell: ({ row }) => {
- const cpRequest = row.getValue("cpRequestYn") as boolean
- return cpRequest ? (
- <Badge variant="outline" className="text-xs">
- Yes
- </Badge>
- ) : (
- <span className="text-muted-foreground text-xs">-</span>
- )
- },
- size: 60,
- },
- {
- accessorKey: "prjectGtcYn",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Project GTC" />
- ),
- cell: ({ row }) => {
- const projectGtc = row.getValue("prjectGtcYn") as boolean
- return projectGtc ? (
- <Badge variant="outline" className="text-xs">
- Yes
- </Badge>
- ) : (
- <span className="text-muted-foreground text-xs">-</span>
- )
- },
- size: 100,
- },
- {
- accessorKey: "gtcYn",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="GTC" />
- ),
- cell: ({ row }) => {
- const gtc = row.getValue("gtcYn") as boolean
- return gtc ? (
- <Badge variant="outline" className="text-xs">
- Yes
- </Badge>
- ) : (
- <span className="text-muted-foreground text-xs">-</span>
- )
- },
- size: 60,
- },
- {
- accessorKey: "gtcValidDate",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="GTC 유효일" />
- ),
- cell: ({ row }) => {
- const gtcValidDate = row.getValue("gtcValidDate") as string
- return gtcValidDate ? (
- <div className="text-sm">
- {gtcValidDate}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- )
- },
- size: 100,
- },
-
- {
- accessorKey: "classification",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="선급" />
- ),
- cell: ({ row }) => {
- const classification = row.getValue("classification") as string
- return classification ? (
- <div className="text-sm font-medium max-w-[120px] truncate" title={classification}>
- {classification}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- )
- },
- size: 120,
- },
-
- {
- accessorKey: "sparepart",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Spare Part" />
- ),
- cell: ({ row }) => {
- const sparepart = row.getValue("sparepart") as string
- return sparepart ? (
- <Badge variant="outline" className="text-xs">
- {sparepart}
- </Badge>
- ) : (
- <span className="text-muted-foreground">-</span>
- )
- },
- size: 100,
- },
-
- {
- id: "incoterms",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Incoterms" />
- ),
- cell: ({ row }) => {
- const code = row.original.incotermsCode as string
- const description = row.original.incotermsDescription as string
-
- return code ? (
- <div className="space-y-1">
- <Badge variant="outline">{code}</Badge>
- {description && (
- <div className="text-xs text-muted-foreground max-w-[150px] truncate" title={description}>
- {description}
- </div>
- )}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- )
- },
- size: 120,
- },
-
- /** ───────────── 날짜 정보 ───────────── */
- {
- accessorKey: "validDate",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="유효일" />
- ),
- cell: ({ row }) => {
- const validDate = row.getValue("validDate") as Date
- return validDate ? (
- <div className="text-sm">
- {formatDate(validDate, "KR")}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- )
- },
- size: 100,
- },
- {
- accessorKey: "dueDate",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="마감일" />
- ),
- cell: ({ row }) => {
- const dueDate = row.getValue("dueDate") as Date
- const isOverdue = dueDate && new Date(dueDate) < new Date()
-
- return dueDate ? (
- <div className={`${isOverdue ? 'text-red-600' : ''}`}>
- <div className="font-medium">{formatDate(dueDate, "KR")}</div>
- {isOverdue && (
- <div className="text-xs text-red-600">지연</div>
- )}
- </div>
- ) : (
- <span className="text-muted-foreground">-</span>
- )
- },
- size: 120,
- },
- {
- accessorKey: "returnYn",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="RFQ 회신여부" />
- ),
- cell: ({ row }) => {
- const returnFlag = row.getValue("returnYn") as boolean
- return returnFlag ? (
- <Badge variant="outline" className="text-xs">
- Yes
- </Badge>
- ) : (
- <span className="text-muted-foreground text-xs">-</span>
- )
- },
- size: 70,
- },
- {
- accessorKey: "returnRevision",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="회신 리비전" />
- ),
- cell: ({ row }) => {
- const revision = row.getValue("returnRevision") as number
- return revision > 0 ? (
- <Badge variant="outline">
- Rev. {revision}
- </Badge>
- ) : (
- <span className="text-muted-foreground">-</span>
- )
- },
- size: 80,
- },
-
- {
- accessorKey: "shortList",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="Short List" />
- ),
- cell: ({ row }) => {
- const shortList = row.getValue("shortList") as boolean
- return shortList ? (
- <Badge variant="secondary" className="text-xs">
- <CheckCircle2 className="h-3 w-3 mr-1" />
- Yes
- </Badge>
- ) : (
- <span className="text-muted-foreground text-xs">-</span>
- )
- },
- size: 90,
- },
-
- /** ───────────── 등록/수정 정보 ───────────── */
- {
- accessorKey: "createdAt",
- header: ({ column }) => (
- <DataTableColumnHeaderSimple column={column} title="등록일" />
- ),
- cell: ({ row }) => {
- const created = row.getValue("createdAt") as Date
- const updated = row.original.updatedAt as Date
-
- return (
- <div className="space-y-1">
- <div className="text-sm">{formatDate(created, "KR")}</div>
- {updated && new Date(updated) > new Date(created) && (
- <div className="text-xs text-blue-600">
- 수정: {formatDate(updated, "KR")}
- </div>
- )}
- </div>
- )
- },
- size: 120,
- },
-
- /** ───────────── 액션 ───────────── */
- {
- id: "actions",
- enableHiding: false,
- cell: function Cell({ row }) {
- return (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- aria-label="Open menu"
- variant="ghost"
- className="flex size-8 p-0 data-[state=open]:bg-muted"
- >
- <Ellipsis className="size-4" aria-hidden="true" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-48">
- <DropdownMenuItem>
- <MessageSquare className="mr-2 h-4 w-4" />
- 벤더 응답 보기
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- {setRowAction && (
- <>
- <DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "update" })}
- >
- <Edit className="mr-2 h-4 w-4" />
- 수정
- </DropdownMenuItem>
- <DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "delete" })}
- >
- <Trash className="mr-2 h-4 w-4" />
- 삭제
- <DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
- </DropdownMenuItem>
- </>
- )}
-
- </DropdownMenuContent>
- </DropdownMenu>
- )
- },
- size: 40,
- },
- ]
-} \ No newline at end of file
diff --git a/lib/b-rfq/initial/initial-rfq-detail-table.tsx b/lib/b-rfq/initial/initial-rfq-detail-table.tsx
deleted file mode 100644
index 5ea6b0bf..00000000
--- a/lib/b-rfq/initial/initial-rfq-detail-table.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type DataTableAdvancedFilterField, type DataTableFilterField } from "@/types/table"
-import { useDataTable } from "@/hooks/use-data-table"
-import { DataTable } from "@/components/data-table/data-table"
-import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
-import { getInitialRfqDetail } from "../service" // 앞서 만든 서버 액션
-import {
- getInitialRfqDetailColumns,
- type DataTableRowAction
-} from "./initial-rfq-detail-columns"
-import { InitialRfqDetailTableToolbarActions } from "./initial-rfq-detail-toolbar-actions"
-import { DeleteInitialRfqDialog } from "./delete-initial-rfq-dialog"
-import { UpdateInitialRfqSheet } from "./update-initial-rfq-sheet"
-import { InitialRfqDetailView } from "@/db/schema"
-
-interface InitialRfqDetailTableProps {
- promises: Promise<Awaited<ReturnType<typeof getInitialRfqDetail>>>
- rfqId?: number
-}
-
-export function InitialRfqDetailTable({ promises, rfqId }: InitialRfqDetailTableProps) {
- const { data, pageCount } = React.use(promises)
-
- // 선택된 상세 정보
- const [selectedDetail, setSelectedDetail] = React.useState<any>(null)
-
- // Row action 상태 (update/delete)
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<InitialRfqDetailView> | null>(null)
-
- const columns = React.useMemo(
- () => getInitialRfqDetailColumns({
- onSelectDetail: setSelectedDetail,
- setRowAction: setRowAction
- }),
- []
- )
-
- /**
- * 필터 필드 정의
- */
- const filterFields: DataTableFilterField<any>[] = [
- {
- id: "rfqCode",
- label: "RFQ 코드",
- placeholder: "RFQ 코드로 검색...",
- },
- {
- id: "vendorName",
- label: "벤더명",
- placeholder: "벤더명으로 검색...",
- },
- {
- id: "rfqStatus",
- label: "RFQ 상태",
- options: [
- { label: "Draft", value: "DRAFT", count: 0 },
- { label: "문서 접수", value: "Doc. Received", count: 0 },
- { label: "담당자 배정", value: "PIC Assigned", count: 0 },
- { label: "문서 확정", value: "Doc. Confirmed", count: 0 },
- { label: "초기 RFQ 발송", value: "Init. RFQ Sent", count: 0 },
- { label: "초기 RFQ 응답", value: "Init. RFQ Answered", count: 0 },
- { label: "TBE 시작", value: "TBE started", count: 0 },
- { label: "TBE 완료", value: "TBE finished", count: 0 },
- { label: "최종 RFQ 발송", value: "Final RFQ Sent", count: 0 },
- { label: "견적 접수", value: "Quotation Received", count: 0 },
- { label: "벤더 선정", value: "Vendor Selected", count: 0 },
- ],
- },
- {
- id: "initialRfqStatus",
- label: "초기 RFQ 상태",
- options: [
- { label: "초안", value: "DRAFT", count: 0 },
- { label: "발송", value: "Init. RFQ Sent", count: 0 },
- { label: "응답", value: "Init. RFQ Answered", count: 0 },
- { label: "거절", value: "S/L Decline", count: 0 },
- ],
- },
- {
- id: "vendorCountry",
- label: "벤더 국가",
- options: [
- { label: "한국", value: "KR", count: 0 },
- { label: "중국", value: "CN", count: 0 },
- { label: "일본", value: "JP", count: 0 },
- { label: "미국", value: "US", count: 0 },
- { label: "독일", value: "DE", count: 0 },
- ],
- },
- ]
-
- /**
- * 고급 필터 필드
- */
- const advancedFilterFields: DataTableAdvancedFilterField<any>[] = [
- {
- id: "rfqCode",
- label: "RFQ 코드",
- type: "text",
- },
- {
- id: "vendorName",
- label: "벤더명",
- type: "text",
- },
- {
- id: "vendorCode",
- label: "벤더 코드",
- type: "text",
- },
- {
- id: "vendorCountry",
- label: "벤더 국가",
- type: "multi-select",
- options: [
- { label: "한국", value: "KR" },
- { label: "중국", value: "CN" },
- { label: "일본", value: "JP" },
- { label: "미국", value: "US" },
- { label: "독일", value: "DE" },
- ],
- },
- {
- id: "rfqStatus",
- label: "RFQ 상태",
- type: "multi-select",
- options: [
- { label: "Draft", value: "DRAFT" },
- { label: "문서 접수", value: "Doc. Received" },
- { label: "담당자 배정", value: "PIC Assigned" },
- { label: "문서 확정", value: "Doc. Confirmed" },
- { label: "초기 RFQ 발송", value: "Init. RFQ Sent" },
- { label: "초기 RFQ 응답", value: "Init. RFQ Answered" },
- { label: "TBE 시작", value: "TBE started" },
- { label: "TBE 완료", value: "TBE finished" },
- { label: "최종 RFQ 발송", value: "Final RFQ Sent" },
- { label: "견적 접수", value: "Quotation Received" },
- { label: "벤더 선정", value: "Vendor Selected" },
- ],
- },
- {
- id: "initialRfqStatus",
- label: "초기 RFQ 상태",
- type: "multi-select",
- options: [
- { label: "초안", value: "DRAFT" },
- { label: "발송", value: "Init. RFQ Sent" },
- { label: "응답", value: "Init. RFQ Answered" },
- { label: "거절", value: "S/L Decline" },
- ],
- },
- {
- id: "vendorBusinessSize",
- label: "벤더 규모",
- type: "multi-select",
- options: [
- { label: "대기업", value: "LARGE" },
- { label: "중기업", value: "MEDIUM" },
- { label: "소기업", value: "SMALL" },
- { label: "스타트업", value: "STARTUP" },
- ],
- },
- {
- id: "incotermsCode",
- label: "Incoterms",
- type: "text",
- },
- {
- id: "dueDate",
- label: "마감일",
- type: "date",
- },
- {
- id: "validDate",
- label: "유효일",
- type: "date",
- },
- {
- id: "shortList",
- label: "Short List",
- type: "boolean",
- },
- {
- id: "returnYn",
- label: "Return 여부",
- type: "boolean",
- },
- {
- id: "cpRequestYn",
- label: "CP Request 여부",
- type: "boolean",
- },
- {
- id: "prjectGtcYn",
- label: "Project GTC 여부",
- type: "boolean",
- },
- {
- id: "classification",
- label: "분류",
- type: "text",
- },
- {
- id: "sparepart",
- label: "예비부품",
- type: "text",
- },
- {
- id: "createdAt",
- label: "등록일",
- type: "date",
- },
- ]
-
- const { table } = useDataTable({
- data,
- columns,
- pageCount,
- filterFields,
- enableAdvancedFilter: true,
- initialState: {
- sorting: [{ id: "createdAt", desc: true }],
- columnPinning: { right: ["actions"] },
- },
- getRowId: (originalRow) => originalRow.initialRfqId ? originalRow.initialRfqId.toString():"1",
- shallow: false,
- clearOnDefault: true,
- })
-
- return (
- <div className="space-y-6">
- {/* 메인 테이블 */}
- <div className="h-full w-full">
- <DataTable table={table} className="h-full">
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- shallow={false}
- >
- <InitialRfqDetailTableToolbarActions table={table} rfqId={rfqId} />
- </DataTableAdvancedToolbar>
- </DataTable>
- </div>
-
- {/* Update Sheet */}
- <UpdateInitialRfqSheet
- open={rowAction?.type === "update"}
- onOpenChange={() => setRowAction(null)}
- initialRfq={rowAction?.type === "update" ? rowAction.row.original : null}
- />
-
- {/* Delete Dialog */}
- <DeleteInitialRfqDialog
- open={rowAction?.type === "delete"}
- onOpenChange={() => setRowAction(null)}
- initialRfqs={rowAction?.type === "delete" ? [rowAction.row.original] : []}
- showTrigger={false}
- onSuccess={() => {
- setRowAction(null)
- // 테이블 리프레시는 revalidatePath로 자동 처리됨
- }}
- />
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/initial/initial-rfq-detail-toolbar-actions.tsx b/lib/b-rfq/initial/initial-rfq-detail-toolbar-actions.tsx
deleted file mode 100644
index c26bda28..00000000
--- a/lib/b-rfq/initial/initial-rfq-detail-toolbar-actions.tsx
+++ /dev/null
@@ -1,287 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import { useRouter } from "next/navigation"
-import { toast } from "sonner"
-import { Button } from "@/components/ui/button"
-import {
- Download,
- Mail,
- RefreshCw,
- Settings,
- Trash2,
- FileText,
- CheckCircle2,
- Loader
-} from "lucide-react"
-import { AddInitialRfqDialog } from "./add-initial-rfq-dialog"
-import { DeleteInitialRfqDialog } from "./delete-initial-rfq-dialog"
-import { ShortListConfirmDialog } from "./short-list-confirm-dialog"
-import { InitialRfqDetailView } from "@/db/schema"
-import { sendBulkInitialRfqEmails } from "../service"
-
-interface InitialRfqDetailTableToolbarActionsProps {
- table: Table<InitialRfqDetailView>
- rfqId?: number
- onRefresh?: () => void // 데이터 새로고침 콜백
-}
-
-export function InitialRfqDetailTableToolbarActions({
- table,
- rfqId,
- onRefresh
-}: InitialRfqDetailTableToolbarActionsProps) {
- const router = useRouter()
-
- // 선택된 행들 가져오기
- const selectedRows = table.getFilteredSelectedRowModel().rows
- const selectedDetails = selectedRows.map((row) => row.original)
- const selectedCount = selectedRows.length
-
- // 상태 관리
- const [showDeleteDialog, setShowDeleteDialog] = React.useState(false)
- const [showShortListDialog, setShowShortListDialog] = React.useState(false)
- const [isEmailSending, setIsEmailSending] = React.useState(false)
-
- // 전체 벤더 리스트 가져오기 (ShortList 확정용)
- const allVendors = table.getRowModel().rows.map(row => row.original)
-
-const handleBulkEmail = async () => {
- if (selectedCount === 0) return
-
- setIsEmailSending(true)
-
- try {
- const initialRfqIds = selectedDetails
- .map(detail => detail.initialRfqId)
- .filter((id): id is number => id !== null);
-
- if (initialRfqIds.length === 0) {
- toast.error("유효한 RFQ ID가 없습니다.")
- return
- }
-
- const result = await sendBulkInitialRfqEmails({
- initialRfqIds,
- language: "en" // 기본 영어, 필요시 사용자 설정으로 변경
- })
-
- if (result.success) {
- toast.success(result.message)
-
- // 에러가 있다면 별도 알림
- if (result.errors && result.errors.length > 0) {
- setTimeout(() => {
- toast.warning(`일부 오류 발생: ${result.errors?.join(', ')}`)
- }, 1000)
- }
-
- // 선택 해제
- table.toggleAllRowsSelected(false)
-
- // 데이터 새로고침
- if (onRefresh) {
- onRefresh()
- }
- } else {
- toast.error(result.message || "RFQ 발송에 실패했습니다.")
- }
-
- } catch (error) {
- console.error("Email sending error:", error)
- toast.error("RFQ 발송 중 오류가 발생했습니다.")
- } finally {
- setIsEmailSending(false)
- }
- }
-
- const handleBulkDelete = () => {
- // DRAFT가 아닌 상태의 RFQ 확인
- const nonDraftRfqs = selectedDetails.filter(
- detail => detail.initialRfqStatus !== "DRAFT"
- )
-
- if (nonDraftRfqs.length > 0) {
- const statusMessages = {
- "Init. RFQ Sent": "이미 발송된",
- "S/L Decline": "Short List 거절 처리된",
- "Init. RFQ Answered": "답변 완료된"
- }
-
- const nonDraftStatuses = [...new Set(nonDraftRfqs.map(rfq => rfq.initialRfqStatus))]
- const statusText = nonDraftStatuses
- .map(status => statusMessages[status as keyof typeof statusMessages] || status)
- .join(", ")
-
- toast.error(
- `${statusText} RFQ는 삭제할 수 없습니다. DRAFT 상태의 RFQ만 삭제 가능합니다.`
- )
- return
- }
-
- setShowDeleteDialog(true)
- }
-
- // S/L 확정 버튼 클릭
- const handleSlConfirm = () => {
- if (!rfqId || allVendors.length === 0) {
- toast.error("S/L 확정할 벤더가 없습니다.")
- return
- }
-
- // 진행 가능한 상태 확인
- const validVendors = allVendors.filter(vendor =>
- vendor.initialRfqStatus === "Init. RFQ Answered" ||
- vendor.initialRfqStatus === "Init. RFQ Sent"
- )
-
- if (validVendors.length === 0) {
- toast.error("S/L 확정이 가능한 벤더가 없습니다. (RFQ 발송 또는 응답 완료된 벤더만 가능)")
- return
- }
-
- setShowShortListDialog(true)
- }
-
- // 초기 RFQ 추가 성공 시 처리
- const handleAddSuccess = () => {
- // 선택 해제
- table.toggleAllRowsSelected(false)
-
- // 데이터 새로고침
- if (onRefresh) {
- onRefresh()
- } else {
- // fallback으로 페이지 새로고침
- setTimeout(() => {
- window.location.reload()
- }, 1000)
- }
- }
-
- // 삭제 성공 시 처리
- const handleDeleteSuccess = () => {
- // 선택 해제
- table.toggleAllRowsSelected(false)
- setShowDeleteDialog(false)
-
- // 데이터 새로고침
- if (onRefresh) {
- onRefresh()
- }
- }
-
- // Short List 확정 성공 시 처리
- const handleShortListSuccess = () => {
- // 선택 해제
- table.toggleAllRowsSelected(false)
- setShowShortListDialog(false)
-
- // 데이터 새로고침
- if (onRefresh) {
- onRefresh()
- }
-
- // 최종 RFQ 페이지로 이동
- if (rfqId) {
- toast.success("Short List가 확정되었습니다. 최종 RFQ 페이지로 이동합니다.")
- setTimeout(() => {
- router.push(`/evcp/b-rfq/${rfqId}`)
- }, 1500)
- }
- }
-
- // 선택된 항목 중 첫 번째를 기본값으로 사용
- const defaultValues = selectedCount > 0 ? selectedDetails[0] : undefined
-
- const canDelete = selectedDetails.every(detail => detail.initialRfqStatus === "DRAFT")
- const draftCount = selectedDetails.filter(detail => detail.initialRfqStatus === "DRAFT").length
-
- // S/L 확정 가능한 벤더 수
- const validForShortList = allVendors.filter(vendor =>
- vendor.initialRfqStatus === "Init. RFQ Answered" ||
- vendor.initialRfqStatus === "Init. RFQ Sent"
- ).length
-
- return (
- <>
- <div className="flex items-center gap-2">
- {/** 선택된 항목이 있을 때만 표시되는 액션들 */}
- {selectedCount > 0 && (
- <>
- <Button
- variant="outline"
- size="sm"
- onClick={handleBulkEmail}
- className="h-8"
- disabled={isEmailSending}
- >
- {isEmailSending ? <Loader className="mr-2 h-4 w-4 animate-spin" /> : <Mail className="mr-2 h-4 w-4" />}
- RFQ 발송 ({selectedCount})
- </Button>
-
- <Button
- variant="outline"
- size="sm"
- onClick={handleBulkDelete}
- className="h-8 text-red-600 hover:text-red-700"
- disabled={!canDelete || selectedCount === 0}
- title={!canDelete ? "DRAFT 상태의 RFQ만 삭제할 수 있습니다" : ""}
- >
- <Trash2 className="mr-2 h-4 w-4" />
- 삭제 ({draftCount}/{selectedCount})
- </Button>
- </>
- )}
-
- {/* S/L 확정 버튼 */}
- {rfqId && (
- <Button
- variant="default"
- size="sm"
- onClick={handleSlConfirm}
- className="h-8"
- disabled={validForShortList === 0}
- title={validForShortList === 0 ? "S/L 확정이 가능한 벤더가 없습니다" : `${validForShortList}개 벤더 중 Short List 선택`}
- >
- <CheckCircle2 className="mr-2 h-4 w-4" />
- S/L 확정 ({validForShortList})
- </Button>
- )}
-
- {/* 초기 RFQ 추가 버튼 */}
- {rfqId && (
- <AddInitialRfqDialog
- rfqId={rfqId}
- onSuccess={handleAddSuccess}
- defaultValues={defaultValues}
- />
- )}
- </div>
-
- {/* 삭제 다이얼로그 */}
- <DeleteInitialRfqDialog
- open={showDeleteDialog}
- onOpenChange={setShowDeleteDialog}
- initialRfqs={selectedDetails}
- showTrigger={false}
- onSuccess={handleDeleteSuccess}
- />
-
- {/* Short List 확정 다이얼로그 */}
- {rfqId && (
- <ShortListConfirmDialog
- open={showShortListDialog}
- onOpenChange={setShowShortListDialog}
- rfqId={rfqId}
- vendors={allVendors.filter(vendor =>
- vendor.initialRfqStatus === "Init. RFQ Answered" ||
- vendor.initialRfqStatus === "Init. RFQ Sent"
- )}
- onSuccess={handleShortListSuccess}
- />
- )}
- </>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/initial/short-list-confirm-dialog.tsx b/lib/b-rfq/initial/short-list-confirm-dialog.tsx
deleted file mode 100644
index 92c62dc0..00000000
--- a/lib/b-rfq/initial/short-list-confirm-dialog.tsx
+++ /dev/null
@@ -1,269 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { useForm } from "react-hook-form"
-import { toast } from "sonner"
-import { z } from "zod"
-import { Loader2, Building, CheckCircle2, XCircle } from "lucide-react"
-
-import { Button } from "@/components/ui/button"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import { Badge } from "@/components/ui/badge"
-import { Separator } from "@/components/ui/separator"
-import { ScrollArea } from "@/components/ui/scroll-area"
-
-import { shortListConfirm } from "../service"
-import { InitialRfqDetailView } from "@/db/schema"
-
-const shortListSchema = z.object({
- selectedVendorIds: z.array(z.number()).min(1, "최소 1개 이상의 벤더를 선택해야 합니다."),
-})
-
-type ShortListFormData = z.infer<typeof shortListSchema>
-
-interface ShortListConfirmDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- rfqId: number
- vendors: InitialRfqDetailView[]
- onSuccess?: () => void
-}
-
-export function ShortListConfirmDialog({
- open,
- onOpenChange,
- rfqId,
- vendors,
- onSuccess
-}: ShortListConfirmDialogProps) {
- const [isLoading, setIsLoading] = React.useState(false)
-
- const form = useForm<ShortListFormData>({
- resolver: zodResolver(shortListSchema),
- defaultValues: {
- selectedVendorIds: vendors
- .filter(vendor => vendor.shortList === true)
- .map(vendor => vendor.vendorId)
- .filter(Boolean) as number[]
- },
- })
-
- const watchedSelectedIds = form.watch("selectedVendorIds")
-
- // 선택된/탈락된 벤더 계산
- const selectedVendors = vendors.filter(vendor =>
- vendor.vendorId && watchedSelectedIds.includes(vendor.vendorId)
- )
- const rejectedVendors = vendors.filter(vendor =>
- vendor.vendorId && !watchedSelectedIds.includes(vendor.vendorId)
- )
-
- async function onSubmit(data: ShortListFormData) {
- if (!rfqId) return
-
- setIsLoading(true)
-
- try {
- const result = await shortListConfirm({
- rfqId,
- selectedVendorIds: data.selectedVendorIds,
- rejectedVendorIds: vendors
- .filter(v => v.vendorId && !data.selectedVendorIds.includes(v.vendorId))
- .map(v => v.vendorId!)
- })
-
- if (result.success) {
- toast.success(result.message)
- onOpenChange(false)
- form.reset()
- onSuccess?.()
- } else {
- toast.error(result.message || "Short List 확정에 실패했습니다.")
- }
- } catch (error) {
- console.error("Short List confirm error:", error)
- toast.error("Short List 확정 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- }
- }
-
- const handleVendorToggle = (vendorId: number, checked: boolean) => {
- const currentSelected = form.getValues("selectedVendorIds")
-
- if (checked) {
- form.setValue("selectedVendorIds", [...currentSelected, vendorId])
- } else {
- form.setValue("selectedVendorIds", currentSelected.filter(id => id !== vendorId))
- }
- }
-
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl max-h-[80vh]">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <CheckCircle2 className="h-5 w-5 text-green-600" />
- Short List 확정
- </DialogTitle>
- <DialogDescription>
- 최종 RFQ로 진행할 벤더를 선택해주세요. 선택되지 않은 벤더에게는 자동으로 Letter of Regret이 발송됩니다.
- </DialogDescription>
- </DialogHeader>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
- <FormField
- control={form.control}
- name="selectedVendorIds"
- render={() => (
- <FormItem>
- <FormLabel className="text-base font-semibold">
- 벤더 선택 ({vendors.length}개 업체)
- </FormLabel>
- <FormControl>
- <ScrollArea className="h-[400px] border rounded-md p-4">
- <div className="space-y-4">
- {vendors.map((vendor) => {
- const isSelected = vendor.vendorId && watchedSelectedIds.includes(vendor.vendorId)
-
- return (
- <div
- key={vendor.vendorId}
- className={`flex items-start space-x-3 p-3 rounded-lg border transition-colors ${
- isSelected
- ? 'border-green-200 bg-green-50'
- : 'border-red-100 bg-red-50'
- }`}
- >
- <Checkbox
- checked={isSelected}
- onCheckedChange={(checked) =>
- vendor.vendorId && handleVendorToggle(vendor.vendorId, !!checked)
- }
- className="mt-1"
- />
- <div className="flex-1 space-y-2">
- <div className="flex items-center gap-2">
- <Building className="h-4 w-4 text-muted-foreground" />
- <span className="font-medium">{vendor.vendorName}</span>
- {isSelected ? (
- <Badge variant="secondary" className="bg-green-100 text-green-800">
- 선택됨
- </Badge>
- ) : (
- <Badge variant="secondary" className="bg-red-100 text-red-800">
- 탈락
- </Badge>
- )}
- </div>
- <div className="text-sm text-muted-foreground">
- <span className="font-mono">{vendor.vendorCode}</span>
- {vendor.vendorCountry && (
- <>
- <span className="mx-2">•</span>
- <span>{vendor.vendorCountry === "KR" ? "국내" : "해외"}</span>
- </>
- )}
- {vendor.vendorCategory && (
- <>
- <span className="mx-2">•</span>
- <span>{vendor.vendorCategory}</span>
- </>
- )}
- {vendor.vendorBusinessSize && (
- <>
- <span className="mx-2">•</span>
- <span>{vendor.vendorBusinessSize}</span>
- </>
- )}
- </div>
- <div className="text-xs text-muted-foreground">
- RFQ 상태: <Badge variant="outline" className="text-xs">
- {vendor.initialRfqStatus}
- </Badge>
- </div>
- </div>
- </div>
- )
- })}
- </div>
- </ScrollArea>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 요약 정보 */}
- <div className="grid grid-cols-2 gap-4 p-4 bg-muted/50 rounded-lg">
- <div className="space-y-2">
- <div className="flex items-center gap-2 text-green-700">
- <CheckCircle2 className="h-4 w-4" />
- <span className="font-medium">선택된 벤더</span>
- </div>
- <div className="text-2xl font-bold text-green-700">
- {selectedVendors.length}개 업체
- </div>
- {selectedVendors.length > 0 && (
- <div className="text-sm text-muted-foreground">
- {selectedVendors.map(v => v.vendorName).join(", ")}
- </div>
- )}
- </div>
- <div className="space-y-2">
- <div className="flex items-center gap-2 text-red-700">
- <XCircle className="h-4 w-4" />
- <span className="font-medium">탈락 벤더</span>
- </div>
- <div className="text-2xl font-bold text-red-700">
- {rejectedVendors.length}개 업체
- </div>
- {rejectedVendors.length > 0 && (
- <div className="text-sm text-muted-foreground">
- Letter of Regret 발송 예정
- </div>
- )}
- </div>
- </div>
-
- <DialogFooter>
- <Button
- type="button"
- variant="outline"
- onClick={() => onOpenChange(false)}
- disabled={isLoading}
- >
- 취소
- </Button>
- <Button
- type="submit"
- disabled={isLoading || selectedVendors.length === 0}
- >
- {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- Short List 확정
- </Button>
- </DialogFooter>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/b-rfq/initial/update-initial-rfq-sheet.tsx b/lib/b-rfq/initial/update-initial-rfq-sheet.tsx
deleted file mode 100644
index a19b5172..00000000
--- a/lib/b-rfq/initial/update-initial-rfq-sheet.tsx
+++ /dev/null
@@ -1,496 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { CalendarIcon, Loader, ChevronsUpDown, Check } from "lucide-react"
-import { useForm } from "react-hook-form"
-import { toast } from "sonner"
-import { format } from "date-fns"
-import { ko } from "date-fns/locale"
-
-import { cn } from "@/lib/utils"
-import { Button } from "@/components/ui/button"
-import { Calendar } from "@/components/ui/calendar"
-import { Checkbox } from "@/components/ui/checkbox"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
- } from "@/components/ui/command"
-import { Input } from "@/components/ui/input"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import { UpdateInitialRfqSchema, updateInitialRfqSchema } from "../validations"
-import { getIncotermsForSelection, modifyInitialRfq } from "../service"
-import { InitialRfqDetailView } from "@/db/schema"
-
-interface UpdateInitialRfqSheetProps
- extends React.ComponentPropsWithRef<typeof Sheet> {
- initialRfq: InitialRfqDetailView | null
-}
-
-interface Incoterm {
- id: number
- code: string
- description: string
-}
-
-export function UpdateInitialRfqSheet({ initialRfq, ...props }: UpdateInitialRfqSheetProps) {
- const [isUpdatePending, startUpdateTransition] = React.useTransition()
- const [incoterms, setIncoterms] = React.useState<Incoterm[]>([])
- const [incotermsLoading, setIncotermsLoading] = React.useState(false)
- const [incotermsSearchOpen, setIncotermsSearchOpen] = React.useState(false)
-
- const loadIncoterms = React.useCallback(async () => {
- setIncotermsLoading(true)
- try {
- const incotermsList = await getIncotermsForSelection()
- setIncoterms(incotermsList)
- } catch (error) {
- console.error("Failed to load incoterms:", error)
- toast.error("Incoterms 목록을 불러오는데 실패했습니다.")
- } finally {
- setIncotermsLoading(false)
- }
- }, [])
-
- React.useEffect(() => {
- if (incoterms.length === 0) {
- loadIncoterms()
- }
- }, [incoterms.length, loadIncoterms])
-
- const form = useForm<UpdateInitialRfqSchema>({
- resolver: zodResolver(updateInitialRfqSchema),
- defaultValues: {
- initialRfqStatus: initialRfq?.initialRfqStatus ?? "DRAFT",
- dueDate: initialRfq?.dueDate ?? new Date(),
- validDate: initialRfq?.validDate ?? undefined,
- incotermsCode: initialRfq?.incotermsCode ?? "",
- classification: initialRfq?.classification ?? "",
- sparepart: initialRfq?.sparepart ?? "",
- rfqRevision: initialRfq?.rfqRevision ?? 0,
- shortList: initialRfq?.shortList ?? false,
- returnYn: initialRfq?.returnYn ?? false,
- cpRequestYn: initialRfq?.cpRequestYn ?? false,
- prjectGtcYn: initialRfq?.prjectGtcYn ?? false,
- },
- })
-
- // initialRfq가 변경될 때 폼 값을 업데이트
- React.useEffect(() => {
- if (initialRfq) {
- form.reset({
- initialRfqStatus: initialRfq.initialRfqStatus ?? "DRAFT",
- dueDate: initialRfq.dueDate,
- validDate: initialRfq.validDate,
- incotermsCode: initialRfq.incotermsCode ?? "",
- classification: initialRfq.classification ?? "",
- sparepart: initialRfq.sparepart ?? "",
- shortList: initialRfq.shortList ?? false,
- returnYn: initialRfq.returnYn ?? false,
- rfqRevision: initialRfq.rfqRevision ?? 0,
- cpRequestYn: initialRfq.cpRequestYn ?? false,
- prjectGtcYn: initialRfq.prjectGtcYn ?? false,
- })
- }
- }, [initialRfq, form])
-
- function onSubmit(input: UpdateInitialRfqSchema) {
- startUpdateTransition(async () => {
- if (!initialRfq || !initialRfq.initialRfqId) {
- toast.error("유효하지 않은 RFQ입니다.")
- return
- }
-
- const { error } = await modifyInitialRfq({
- id: initialRfq.initialRfqId,
- ...input,
- })
-
- if (error) {
- toast.error(error)
- return
- }
-
- form.reset()
- props.onOpenChange?.(false)
- toast.success("초기 RFQ가 수정되었습니다")
- })
- }
-
- const selectedIncoterm = incoterms.find(incoterm => incoterm.code === form.watch("incotermsCode"))
-
- return (
- <Sheet {...props}>
- <SheetContent className="flex flex-col h-full sm:max-w-md">
- {/* 고정 헤더 */}
- <SheetHeader className="flex-shrink-0 text-left pb-6">
- <SheetTitle>초기 RFQ 수정</SheetTitle>
- <SheetDescription>
- 초기 RFQ 정보를 수정하고 변경사항을 저장하세요
- </SheetDescription>
- </SheetHeader>
-
- {/* 스크롤 가능한 폼 영역 */}
- <div className="flex-1 overflow-y-auto">
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col gap-4 pr-2"
- >
- {/* RFQ 리비전 */}
- <FormField
- control={form.control}
- name="rfqRevision"
- render={({ field }) => (
- <FormItem>
- <FormLabel>RFQ 리비전</FormLabel>
- <FormControl>
- <Input
- type="number"
- min="0"
- placeholder="0"
- {...field}
- onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 마감일 */}
- <FormField
- control={form.control}
- name="dueDate"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>마감일 *</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant={"outline"}
- className={cn(
- "w-full pl-3 text-left font-normal",
- !field.value && "text-muted-foreground"
- )}
- >
- {field.value ? (
- format(field.value, "PPP", { locale: ko })
- ) : (
- <span>날짜를 선택하세요</span>
- )}
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={field.value}
- onSelect={field.onChange}
- disabled={(date) =>
- date < new Date("1900-01-01")
- }
- initialFocus
- />
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 유효일 */}
- <FormField
- control={form.control}
- name="validDate"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>유효일</FormLabel>
- <Popover>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant={"outline"}
- className={cn(
- "w-full pl-3 text-left font-normal",
- !field.value && "text-muted-foreground"
- )}
- >
- {field.value ? (
- format(field.value, "PPP", { locale: ko })
- ) : (
- <span>날짜를 선택하세요</span>
- )}
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-auto p-0" align="start">
- <Calendar
- mode="single"
- selected={field.value}
- onSelect={field.onChange}
- disabled={(date) =>
- date < new Date("1900-01-01")
- }
- initialFocus
- />
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* Incoterms 코드 */}
- <FormField
- control={form.control}
- name="incotermsCode"
- render={({ field }) => (
- <FormItem className="flex flex-col">
- <FormLabel>Incoterms</FormLabel>
- <Popover open={incotermsSearchOpen} onOpenChange={setIncotermsSearchOpen}>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- aria-expanded={incotermsSearchOpen}
- className="justify-between"
- disabled={incotermsLoading}
- >
- {selectedIncoterm ? (
- <div className="flex items-center gap-2">
- <span className="truncate">
- {selectedIncoterm.code} - {selectedIncoterm.description}
- </span>
- </div>
- ) : (
- <span className="text-muted-foreground">
- {incotermsLoading ? "로딩 중..." : "인코텀즈를 선택하세요"}
- </span>
- )}
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
- </Button>
- </FormControl>
- </PopoverTrigger>
- <PopoverContent className="w-full p-0" align="start">
- <Command>
- <CommandInput
- placeholder="코드 또는 내용으로 검색..."
- className="h-9"
- />
- <CommandList>
- <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
- <CommandGroup>
- {incoterms.map((incoterm) => (
- <CommandItem
- key={incoterm.id}
- value={`${incoterm.code} ${incoterm.description}`}
- onSelect={() => {
- field.onChange(incoterm.code)
- setIncotermsSearchOpen(false)
- }}
- >
- <div className="flex items-center gap-2 w-full">
- <div className="flex-1 min-w-0">
- <div className="font-medium truncate">
- {incoterm.code} - {incoterm.description}
- </div>
- </div>
- <Check
- className={cn(
- "ml-auto h-4 w-4",
- incoterm.code === field.value ? "opacity-100" : "opacity-0"
- )}
- />
- </div>
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
- {/* 체크박스 옵션들 */}
- <div className="space-y-3">
- <FormField
- control={form.control}
- name="shortList"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none ml-2">
- <FormLabel>Short List</FormLabel>
- </div>
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="returnYn"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none ml-2">
- <FormLabel>회신 여부</FormLabel>
- </div>
- </FormItem>
- )}
- />
-
- {/* 선급 */}
- <FormField
- control={form.control}
- name="classification"
- render={({ field }) => (
- <FormItem>
- <FormLabel>선급</FormLabel>
- <FormControl>
- <Input
- placeholder="선급"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 예비부품 */}
- <FormField
- control={form.control}
- name="sparepart"
- render={({ field }) => (
- <FormItem>
- <FormLabel>예비부품</FormLabel>
- <FormControl>
- <Input
- placeholder="O1, O2"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
-
-
-
- <FormField
- control={form.control}
- name="cpRequestYn"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none ml-2">
- <FormLabel>CP 요청</FormLabel>
- </div>
- </FormItem>
- )}
- />
-
- <FormField
- control={form.control}
- name="prjectGtcYn"
- render={({ field }) => (
- <FormItem className="flex flex-row items-start space-x-3 space-y-0">
- <FormControl>
- <Checkbox
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- <div className="space-y-1 leading-none ml-2">
- <FormLabel>프로젝트 GTC</FormLabel>
- </div>
- </FormItem>
- )}
- />
- </div>
-
- {/* 하단 여백 */}
- <div className="h-4" />
- </form>
- </Form>
- </div>
-
- {/* 고정 푸터 */}
- <SheetFooter className="flex-shrink-0 gap-2 pt-6 sm:space-x-0">
- <SheetClose asChild>
- <Button type="button" variant="outline">
- 취소
- </Button>
- </SheetClose>
- <Button
- onClick={form.handleSubmit(onSubmit)}
- disabled={isUpdatePending}
- >
- {isUpdatePending && (
- <Loader
- className="mr-2 size-4 animate-spin"
- aria-hidden="true"
- />
- )}
- 저장
- </Button>
- </SheetFooter>
- </SheetContent>
- </Sheet>
- )
-} \ No newline at end of file