summaryrefslogtreecommitdiff
path: root/lib/pcr/table
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pcr/table')
-rw-r--r--lib/pcr/table/approve-reject-pcr-dialog.tsx231
-rw-r--r--lib/pcr/table/create-pcr-dialog.tsx642
-rw-r--r--lib/pcr/table/detail-table/create-pcr-pr-dialog.tsx598
-rw-r--r--lib/pcr/table/detail-table/pcr-detail-column.tsx333
-rw-r--r--lib/pcr/table/detail-table/pcr-detail-table.tsx121
-rw-r--r--lib/pcr/table/detail-table/pcr-detail-toolbar-action.tsx79
-rw-r--r--lib/pcr/table/edit-pcr-sheet.tsx237
-rw-r--r--lib/pcr/table/pcr-table-column.tsx416
-rw-r--r--lib/pcr/table/pcr-table-toolbar-actions.tsx120
-rw-r--r--lib/pcr/table/pcr-table.tsx396
10 files changed, 3173 insertions, 0 deletions
diff --git a/lib/pcr/table/approve-reject-pcr-dialog.tsx b/lib/pcr/table/approve-reject-pcr-dialog.tsx
new file mode 100644
index 00000000..065a30fa
--- /dev/null
+++ b/lib/pcr/table/approve-reject-pcr-dialog.tsx
@@ -0,0 +1,231 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+import { CheckCircle, XCircle, Loader2 } from "lucide-react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Textarea } from "@/components/ui/textarea"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import * as z from "zod"
+import { approvePcrAction, rejectPcrAction } from "@/lib/pcr/actions"
+import { PcrPoData } from "@/lib/pcr/types"
+
+// 승인 다이얼로그 스키마
+const approveSchema = z.object({
+ reason: z.string().optional(),
+})
+
+// 거절 다이얼로그 스키마
+const rejectSchema = z.object({
+ reason: z.string().min(1, "거절 사유를 입력해주세요."),
+})
+
+type ApproveFormValues = z.infer<typeof approveSchema>
+type RejectFormValues = z.infer<typeof rejectSchema>
+
+interface ApproveRejectPcrDialogProps {
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ pcrData?: PcrPoData | null
+ actionType: 'approve' | 'reject'
+ onSuccess?: () => void
+}
+
+export function ApproveRejectPcrDialog({
+ open,
+ onOpenChange,
+ pcrData,
+ actionType,
+ onSuccess,
+}: ApproveRejectPcrDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [internalOpen, setInternalOpen] = React.useState(false)
+
+ const dialogOpen = open !== undefined ? open : internalOpen
+ const setDialogOpen = onOpenChange || setInternalOpen
+
+ const isApprove = actionType === 'approve'
+
+ // 승인 폼
+ const approveForm = useForm<ApproveFormValues>({
+ resolver: zodResolver(approveSchema),
+ defaultValues: {
+ reason: "",
+ },
+ })
+
+ // 거절 폼
+ const rejectForm = useForm<RejectFormValues>({
+ resolver: zodResolver(rejectSchema),
+ defaultValues: {
+ reason: "",
+ },
+ })
+
+ const currentForm = isApprove ? approveForm : rejectForm
+
+ const handleSubmit = async (data: ApproveFormValues | RejectFormValues) => {
+ if (!pcrData) return
+
+ try {
+ setIsLoading(true)
+
+ const reason = 'reason' in data ? data.reason : undefined
+ const result = isApprove
+ ? await approvePcrAction(pcrData.id, reason)
+ : await rejectPcrAction(pcrData.id, reason || '')
+
+ if (result.success) {
+ toast.success(result.message)
+ currentForm.reset()
+ setDialogOpen(false)
+ onSuccess?.()
+ } else {
+ toast.error(result.error || `${isApprove ? '승인' : '거절'}에 실패했습니다.`)
+ }
+ } catch (error) {
+ console.error(`PCR ${isApprove ? '승인' : '거절'} 오류:`, error)
+ toast.error(`${isApprove ? '승인' : '거절'} 중 오류가 발생했습니다.`)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ if (!pcrData) return null
+
+ return (
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <DialogContent className="max-w-md">
+ <DialogHeader>
+ <div className="flex items-center gap-3">
+ {isApprove ? (
+ <CheckCircle className="size-6 text-green-600" />
+ ) : (
+ <XCircle className="size-6 text-red-600" />
+ )}
+ <DialogTitle>
+ PCR {isApprove ? '승인' : '거절'} 확인
+ </DialogTitle>
+ </div>
+ <DialogDescription>
+ 다음 PCR을 {isApprove ? '승인' : '거절'}하시겠습니까?
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* PCR 정보 표시 */}
+ <div className="space-y-3 p-4 bg-gray-50 rounded-lg">
+ <div className="grid grid-cols-2 gap-2 text-sm">
+ <div>
+ <span className="font-medium text-gray-600">PO/계약번호:</span>
+ <p className="font-mono text-gray-900">{pcrData.poContractNumber}</p>
+ </div>
+ <div>
+ <span className="font-medium text-gray-600">프로젝트:</span>
+ <p className="text-gray-900">{pcrData.project || '-'}</p>
+ </div>
+ <div>
+ <span className="font-medium text-gray-600">변경 구분:</span>
+ <p className="text-gray-900">{pcrData.changeType}</p>
+ </div>
+ <div>
+ <span className="font-medium text-gray-600">요청일자:</span>
+ <p className="text-gray-900">
+ {pcrData.pcrRequestDate.toLocaleDateString('ko-KR')}
+ </p>
+ </div>
+ </div>
+ {pcrData.details && (
+ <div>
+ <span className="font-medium text-gray-600 text-sm">상세:</span>
+ <p className="text-gray-900 text-sm mt-1">{pcrData.details}</p>
+ </div>
+ )}
+ </div>
+
+ {/* 승인/거절 사유 입력 폼 */}
+ <Form {...currentForm}>
+ <form onSubmit={currentForm.handleSubmit(handleSubmit)} className="space-y-4">
+ <FormField
+ control={currentForm.control}
+ name="reason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ {isApprove ? '승인 사유 (선택)' : '거절 사유 (필수)'}
+ </FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder={
+ isApprove
+ ? "승인 사유를 입력하세요 (선택사항)"
+ : "거절 사유를 입력하세요"
+ }
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 버튼들 */}
+ <div className="flex justify-end gap-3 pt-4">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setDialogOpen(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ variant={isApprove ? "default" : "destructive"}
+ disabled={isLoading}
+ >
+ {isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ {isApprove ? '승인 중...' : '거절 중...'}
+ </>
+ ) : (
+ <>
+ {isApprove ? (
+ <>
+ <CheckCircle className="mr-2 h-4 w-4" />
+ 승인
+ </>
+ ) : (
+ <>
+ <XCircle className="mr-2 h-4 w-4" />
+ 거절
+ </>
+ )}
+ </>
+ )}
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/lib/pcr/table/create-pcr-dialog.tsx b/lib/pcr/table/create-pcr-dialog.tsx
new file mode 100644
index 00000000..cddb20d3
--- /dev/null
+++ b/lib/pcr/table/create-pcr-dialog.tsx
@@ -0,0 +1,642 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+import { CalendarIcon, Loader2, Plus, Check, ChevronsUpDown } from "lucide-react"
+import { Calendar } from "@/components/ui/calendar"
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
+import { format } from "date-fns"
+import { ko } from "date-fns/locale"
+
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command"
+import { zodResolver } from "@hookform/resolvers/zod"
+import { useForm } from "react-hook-form"
+import * as z from "zod"
+import { createPcrPo, getVendorsForPcr } from "@/lib/pcr/service"
+import { PCR_CHANGE_TYPES } from "@/lib/pcr/types"
+import { cn } from "@/lib/utils"
+
+// PCR 생성 스키마
+const createPcrSchema = z.object({
+ changeType: z.string().optional(),
+ details: z.string().optional(),
+ project: z.string().optional(),
+ pcrRequestDate: z.date({
+ required_error: "PCR 요청일자를 선택해주세요.",
+ }),
+ poContractNumber: z.string().min(1, "PO/계약번호를 입력해주세요."),
+ revItemNumber: z.string().optional(),
+ purchaseContractManager: z.string().optional(),
+ pcrCreator: z.string().optional(),
+ poContractAmountBefore: z.number().optional(),
+ poContractAmountAfter: z.number().optional(),
+ contractCurrency: z.string().optional(),
+ pcrReason: z.string().optional(),
+ detailsReason: z.string().optional(),
+ rejectionReason: z.string().optional(),
+ pcrResponseDate: z.date().optional(),
+ vendorId: z.number({
+ required_error: "협력업체를 선택해주세요.",
+ }),
+})
+
+type CreatePcrFormValues = z.infer<typeof createPcrSchema>
+
+interface CreatePcrDialogProps {
+ open?: boolean
+ onOpenChange?: (open: boolean) => void
+ isEvcpPage?: boolean
+ onSuccess?: () => void
+ currentVendorId?: number // Partners 페이지에서 현재 사용자의 vendorId
+}
+
+interface Vendor {
+ id: number
+ vendorName: string
+ vendorCode: string
+}
+
+export function CreatePcrDialog({
+ open,
+ onOpenChange,
+ isEvcpPage = false,
+ onSuccess,
+ currentVendorId,
+}: CreatePcrDialogProps) {
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [internalOpen, setInternalOpen] = React.useState(false)
+ const [vendors, setVendors] = React.useState<Vendor[]>([])
+ const [vendorsLoading, setVendorsLoading] = React.useState(false)
+ const [vendorSearchOpen, setVendorSearchOpen] = React.useState(false)
+
+ const dialogOpen = open !== undefined ? open : internalOpen
+ const setDialogOpen = onOpenChange || setInternalOpen
+
+ const form = useForm<CreatePcrFormValues>({
+ resolver: zodResolver(createPcrSchema),
+ defaultValues: {
+ changeType: "OTHER",
+ contractCurrency: "KRW",
+ pcrRequestDate: new Date(),
+ },
+ })
+
+ const loadVendors = React.useCallback(async () => {
+ try {
+ setVendorsLoading(true)
+
+ let result
+ if (!isEvcpPage && currentVendorId) {
+ // Partners 페이지: 특정 vendorId만 조회
+ result = await getVendorsForPcr({
+ vendorId: currentVendorId,
+ limit: 1
+ })
+ } else {
+ // EvcP 페이지: 모든 협력업체 조회
+ result = await getVendorsForPcr({
+ limit: 1000
+ })
+ }
+
+ if (result.success) {
+ setVendors(result.data.map(vendor => ({
+ id: vendor.id,
+ vendorName: vendor.vendorName,
+ vendorCode: vendor.vendorCode || "",
+ })))
+
+ // Partners 페이지에서는 자동으로 해당 협력업체 선택
+ if (!isEvcpPage && currentVendorId && result.data.length > 0) {
+ form.setValue("vendorId", currentVendorId)
+ }
+ } else {
+ toast.error(result.error || "협력업체 목록을 불러오는 중 오류가 발생했습니다.")
+ }
+ } catch (error) {
+ console.error("협력업체 로드 오류:", error)
+ toast.error("협력업체 목록을 불러오는 중 오류가 발생했습니다.")
+ } finally {
+ setVendorsLoading(false)
+ }
+ }, [isEvcpPage, currentVendorId, form])
+
+ // 협력업체 데이터 로드
+ React.useEffect(() => {
+ if (dialogOpen) {
+ loadVendors()
+ }
+ }, [dialogOpen, loadVendors])
+
+ async function onSubmit(data: CreatePcrFormValues) {
+ try {
+ setIsLoading(true)
+
+ const result = await createPcrPo({
+ ...data,
+ pcrApprovalStatus: "PENDING",
+ contractCurrency: data.contractCurrency || "KRW",
+ changeType: data.changeType || "OTHER",
+ })
+
+ if (result.success) {
+ toast.success("PCR이 성공적으로 생성되었습니다.")
+ form.reset()
+ setDialogOpen(false)
+ onSuccess?.()
+ } else {
+ toast.error(result.error || "PCR 생성에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("PCR 생성 오류:", error)
+ toast.error("PCR 생성 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <DialogTrigger asChild>
+ <Button variant="default" size="sm" className="gap-2">
+ <Plus className="size-4" />
+ PCR 생성
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>PCR 생성</DialogTitle>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+ {/* 기본 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">기본 정보</h3>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="changeType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>변경 구분</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="변경 구분을 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {Object.entries(PCR_CHANGE_TYPES).map(([key, value]) => (
+ <SelectItem key={key} value={key}>
+ {value}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="pcrRequestDate"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>PCR 요청일자 *</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, "yyyy년 MM월 dd일", { 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() || date < new Date("1900-01-01")
+ }
+ initialFocus
+ />
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ {/* 협력업체 선택 */}
+ <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={cn(
+ "w-full justify-between",
+ !field.value && "text-muted-foreground"
+ )}
+ >
+ {field.value
+ ? vendors.find((vendor) => vendor.id === field.value)?.vendorName
+ : "협력업체를 선택하세요"}
+ <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="협력업체 검색..." />
+ <CommandEmpty>
+ {vendorsLoading ? "로딩 중..." : "협력업체를 찾을 수 없습니다."}
+ </CommandEmpty>
+ <CommandGroup>
+ <CommandList>
+ {vendors.map((vendor) => (
+ <CommandItem
+ key={vendor.id}
+ value={`${vendor.vendorName} ${vendor.vendorCode}`}
+ onSelect={() => {
+ form.setValue("vendorId", vendor.id)
+ setVendorSearchOpen(false)
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ field.value === vendor.id ? "opacity-100" : "opacity-0"
+ )}
+ />
+ <div className="flex flex-col">
+ <span className="font-medium">{vendor.vendorName}</span>
+ {vendor.vendorCode && (
+ <span className="text-sm text-muted-foreground">
+ 코드: {vendor.vendorCode}
+ </span>
+ )}
+ </div>
+ </CommandItem>
+ ))}
+ </CommandList>
+ </CommandGroup>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="poContractNumber"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PO/계약번호 *</FormLabel>
+ <FormControl>
+ <Input placeholder="PO/계약번호를 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="revItemNumber"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>Rev./품번</FormLabel>
+ <FormControl>
+ <Input placeholder="Rev./품번을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ <FormField
+ control={form.control}
+ name="project"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>프로젝트</FormLabel>
+ <FormControl>
+ <Input placeholder="프로젝트명을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="details"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>상세</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="상세 내용을 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ {/* 담당자 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">담당자 정보</h3>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="purchaseContractManager"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>구매/계약 담당자</FormLabel>
+ <FormControl>
+ <Input placeholder="담당자명을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="pcrCreator"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PCR 생성자</FormLabel>
+ <FormControl>
+ <Input placeholder="생성자명을 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* 금액 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">금액 정보</h3>
+ <div className="grid grid-cols-3 gap-4">
+ <FormField
+ control={form.control}
+ name="contractCurrency"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>계약 통화</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="통화를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="KRW">KRW</SelectItem>
+ <SelectItem value="USD">USD</SelectItem>
+ <SelectItem value="EUR">EUR</SelectItem>
+ <SelectItem value="JPY">JPY</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="poContractAmountBefore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PO/계약금액 (전)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ placeholder="금액을 입력하세요"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="poContractAmountAfter"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PO/계약금액 (후)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ placeholder="금액을 입력하세요"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? Number(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* 사유 정보 섹션 */}
+ <div className="space-y-4">
+ <h3 className="text-lg font-medium">사유 정보</h3>
+ <div className="space-y-4">
+ <FormField
+ control={form.control}
+ name="pcrReason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>PCR 사유</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="PCR 사유를 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="detailsReason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>상세 사유</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="상세 사유를 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="rejectionReason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>거절 사유</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="거절 사유를 입력하세요"
+ className="resize-none"
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* PCR 회신일자 */}
+ <div className="space-y-4">
+ <FormField
+ control={form.control}
+ name="pcrResponseDate"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>PCR 회신일</FormLabel>
+ <Popover>
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ className={cn(
+ "w-[200px] pl-3 text-left font-normal",
+ !field.value && "text-muted-foreground"
+ )}
+ >
+ {field.value ? (
+ format(field.value, "yyyy년 MM월 dd일", { 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>
+ )}
+ />
+ </div>
+
+ {/* 버튼들 */}
+ <div className="flex justify-end gap-4 pt-4">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => setDialogOpen(false)}
+ disabled={isLoading}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isLoading}>
+ {isLoading ? (
+ <>
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+ 생성 중...
+ </>
+ ) : (
+ "PCR 생성"
+ )}
+ </Button>
+ </div>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/lib/pcr/table/detail-table/create-pcr-pr-dialog.tsx b/lib/pcr/table/detail-table/create-pcr-pr-dialog.tsx
new file mode 100644
index 00000000..1244d179
--- /dev/null
+++ b/lib/pcr/table/detail-table/create-pcr-pr-dialog.tsx
@@ -0,0 +1,598 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import * as z from "zod"
+import { Button } from "@/components/ui/button"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Input } from "@/components/ui/input"
+import { Textarea } from "@/components/ui/textarea"
+import { Calendar } from "@/components/ui/calendar"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+import { CalendarIcon } from "lucide-react"
+import { format } from "date-fns"
+import { cn } from "@/lib/utils"
+import { toast } from "sonner"
+import { PcrPoData } from "@/lib/pcr/types"
+import { createPcrPrAction } from "@/lib/pcr/actions"
+
+const pcrPrSchema = z.object({
+ materialNumber: z.string().min(1, "자재번호는 필수입니다"),
+ materialDetails: z.string().optional(),
+ quantityBefore: z.number().optional(),
+ quantityAfter: z.number().optional(),
+ weightBefore: z.number().optional(),
+ weightAfter: z.number().optional(),
+ subcontractorWeightBefore: z.number().optional(),
+ subcontractorWeightAfter: z.number().optional(),
+ supplierWeightBefore: z.number().optional(),
+ supplierWeightAfter: z.number().optional(),
+ specDrawingBefore: z.string().optional(),
+ specDrawingAfter: z.string().optional(),
+ initialPoContractDate: z.date().optional(),
+ specChangeDate: z.date().optional(),
+ poContractModifiedDate: z.date().optional(),
+ confirmationDate: z.date().optional(),
+ designManager: z.string().optional(),
+})
+
+type PcrPrFormData = z.infer<typeof pcrPrSchema>
+
+interface CreatePcrPrDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ selectedPcrPo: PcrPoData | null
+ onSuccess: () => void
+}
+
+export function CreatePcrPrDialog({
+ open,
+ onOpenChange,
+ selectedPcrPo,
+ onSuccess,
+}: CreatePcrPrDialogProps) {
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+
+ const form = useForm<PcrPrFormData>({
+ resolver: zodResolver(pcrPrSchema),
+ defaultValues: {
+ materialNumber: "",
+ materialDetails: "",
+ quantityBefore: undefined,
+ quantityAfter: undefined,
+ weightBefore: undefined,
+ weightAfter: undefined,
+ subcontractorWeightBefore: undefined,
+ subcontractorWeightAfter: undefined,
+ supplierWeightBefore: undefined,
+ supplierWeightAfter: undefined,
+ specDrawingBefore: "",
+ specDrawingAfter: "",
+ initialPoContractDate: undefined,
+ specChangeDate: undefined,
+ poContractModifiedDate: undefined,
+ confirmationDate: undefined,
+ designManager: "",
+ },
+ })
+
+ const onSubmit = async (data: PcrPrFormData) => {
+ if (!selectedPcrPo) {
+ toast.error("선택된 PCR_PO가 없습니다")
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ const result = await createPcrPrAction({
+ ...data,
+ poContractNumber: selectedPcrPo.poContractNumber,
+ })
+
+ if (result.success) {
+ toast.success("PCR_PR이 성공적으로 생성되었습니다")
+ form.reset()
+ onOpenChange(false)
+ onSuccess()
+ } else {
+ toast.error(result.error || "PCR_PR 생성에 실패했습니다")
+ }
+ } catch (error) {
+ console.error("PCR_PR 생성 오류:", error)
+ toast.error("PCR_PR 생성 중 오류가 발생했습니다")
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={onOpenChange}>
+ <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
+ <DialogHeader>
+ <DialogTitle>PCR_PR 생성</DialogTitle>
+ <DialogDescription>
+ PO/계약번호: {selectedPcrPo?.poContractNumber}
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+ {/* 기본 정보 섹션 */}
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="materialNumber"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>자재번호 *</FormLabel>
+ <FormControl>
+ <Input placeholder="자재번호를 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="designManager"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>설계 담당자</FormLabel>
+ <FormControl>
+ <Input placeholder="설계 담당자를 입력하세요" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+
+ <FormField
+ control={form.control}
+ name="materialDetails"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>자재내역</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="자재내역을 입력하세요"
+ className="resize-none"
+ rows={3}
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 수량 정보 섹션 */}
+ <div className="space-y-4">
+ <h4 className="text-sm font-medium text-muted-foreground">수량 정보</h4>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="quantityBefore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>수량 (변경전)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="quantityAfter"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>수량 (변경후)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* 중량 정보 섹션 */}
+ <div className="space-y-4">
+ <h4 className="text-sm font-medium text-muted-foreground">중량 정보</h4>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="weightBefore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>중량 (변경전)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="weightAfter"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>중량 (변경후)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="subcontractorWeightBefore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>사급중량 (변경전)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="subcontractorWeightAfter"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>사급중량 (변경후)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="supplierWeightBefore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>도급중량 (변경전)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="supplierWeightAfter"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>도급중량 (변경후)</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ step="0.001"
+ placeholder="0.000"
+ {...field}
+ onChange={(e) => field.onChange(e.target.value ? parseFloat(e.target.value) : undefined)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* SPEC 도면 정보 섹션 */}
+ <div className="space-y-4">
+ <h4 className="text-sm font-medium text-muted-foreground">SPEC 도면 정보</h4>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="specDrawingBefore"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>SPEC 도면 (변경전)</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="SPEC 도면 정보를 입력하세요"
+ className="resize-none"
+ rows={2}
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ <FormField
+ control={form.control}
+ name="specDrawingAfter"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>SPEC 도면 (변경후)</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="SPEC 도면 정보를 입력하세요"
+ className="resize-none"
+ rows={2}
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* 날짜 정보 섹션 */}
+ <div className="space-y-4">
+ <h4 className="text-sm font-medium text-muted-foreground">날짜 정보</h4>
+ <div className="grid grid-cols-2 gap-4">
+ <FormField
+ control={form.control}
+ name="initialPoContractDate"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>최초 PO/계약 일</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, "yyyy년 MM월 dd일")
+ ) : (
+ <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="specChangeDate"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>SPEC 변경 일</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, "yyyy년 MM월 dd일")
+ ) : (
+ <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="poContractModifiedDate"
+ render={({ field }) => (
+ <FormItem className="flex flex-col">
+ <FormLabel>PO/계약 수정 일</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, "yyyy년 MM월 dd일")
+ ) : (
+ <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="confirmationDate"
+ 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, "yyyy년 MM월 dd일")
+ ) : (
+ <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>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isSubmitting}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isSubmitting}>
+ {isSubmitting ? "생성 중..." : "생성"}
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+}
diff --git a/lib/pcr/table/detail-table/pcr-detail-column.tsx b/lib/pcr/table/detail-table/pcr-detail-column.tsx
new file mode 100644
index 00000000..664f37ef
--- /dev/null
+++ b/lib/pcr/table/detail-table/pcr-detail-column.tsx
@@ -0,0 +1,333 @@
+import { ColumnDef } from "@tanstack/react-table"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { PcrPrData } from "@/lib/pcr/types"
+import { FileIcon, Download } from "lucide-react"
+import { downloadFile } from "@/lib/file-download"
+
+export function getPcrDetailColumns(): ColumnDef<PcrPrData>[] {
+ return [
+ {
+ accessorKey: "materialNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재번호" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("materialNumber") as string
+ return (
+ <div className="font-medium">
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "materialDetails",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="자재내역" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("materialDetails") as string
+ return (
+ <div className="max-w-[200px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "quantityBefore",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="수량(변경전)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("quantityBefore") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? value.toLocaleString() : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "quantityAfter",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="수량(변경후)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("quantityAfter") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? value.toLocaleString() : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "weightBefore",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="중량(변경전)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("weightBefore") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? `${value.toLocaleString()} kg` : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "weightAfter",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="중량(변경후)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("weightAfter") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? `${value.toLocaleString()} kg` : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "subcontractorWeightBefore",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="사급중량(변경전)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("subcontractorWeightBefore") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? `${value.toLocaleString()} kg` : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "subcontractorWeightAfter",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="사급중량(변경후)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("subcontractorWeightAfter") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? `${value.toLocaleString()} kg` : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "supplierWeightBefore",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="도급중량(변경전)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("supplierWeightBefore") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? `${value.toLocaleString()} kg` : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "supplierWeightAfter",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="도급중량(변경후)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("supplierWeightAfter") as number
+ return (
+ <div className="text-right font-mono text-sm">
+ {value ? `${value.toLocaleString()} kg` : "-"}
+ </div>
+ )
+ },
+ },
+ {
+ id: "attachments",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="첨부파일" />
+ ),
+ cell: ({ row }) => {
+ const attachments = row.original.attachments || []
+ const beforeAttachment = attachments.find(att => att.type === 'BEFORE')
+ const afterAttachment = attachments.find(att => att.type === 'AFTER')
+
+ if (!beforeAttachment && !afterAttachment) {
+ return (
+ <div className="text-center text-muted-foreground">
+ <FileIcon className="size-4 mx-auto" />
+ </div>
+ )
+ }
+
+ return (
+ <div className="flex gap-1 justify-center">
+ {/* 변경전 첨부파일 */}
+ {beforeAttachment && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => {
+ downloadFile(beforeAttachment.filePath, beforeAttachment.fileName)
+ }}
+ title={`변경전: ${beforeAttachment.fileName}`}
+ className="h-6 w-6 p-0"
+ >
+ <Download className="size-3" />
+ </Button>
+ )}
+
+ {/* 변경후 첨부파일 */}
+ {afterAttachment && (
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={() => {
+ downloadFile(afterAttachment.filePath, afterAttachment.fileName)
+ }}
+ title={`변경후: ${afterAttachment.fileName}`}
+ className="h-6 w-6 p-0"
+ >
+ <Download className="size-3" />
+ </Button>
+ )}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "initialPoContractDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="최초 PO/계약일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("initialPoContractDate") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "specChangeDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="SPEC 변경일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("specChangeDate") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "poContractModifiedDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PO/계약수정일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("poContractModifiedDate") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "confirmationDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="확인일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("confirmationDate") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "designManager",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="설계담당자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("designManager") as string
+ return (
+ <div className="max-w-[120px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "poContractNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PO/계약번호" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("poContractNumber") as string
+ return (
+ <div className="font-medium">
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "createdAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("createdAt") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ ]
+}
diff --git a/lib/pcr/table/detail-table/pcr-detail-table.tsx b/lib/pcr/table/detail-table/pcr-detail-table.tsx
new file mode 100644
index 00000000..65a07146
--- /dev/null
+++ b/lib/pcr/table/detail-table/pcr-detail-table.tsx
@@ -0,0 +1,121 @@
+"use client"
+
+import * as React from "react"
+import { useEffect, useState, useCallback } from "react"
+import { getPcrPrListByPoContractNumber } from "@/lib/pcr/service"
+import { PcrPoData, PcrPrData } from "@/lib/pcr/types"
+import { ClientDataTable } from "@/components/client-data-table/data-table"
+import { getPcrDetailColumns } from "./pcr-detail-column"
+import { PcrDetailToolbarAction } from "./pcr-detail-toolbar-action"
+import { toast } from "sonner"
+import { Skeleton } from "@/components/ui/skeleton"
+import { Badge } from "@/components/ui/badge"
+
+// 프로퍼티 정의
+interface PcrDetailTablesProps {
+ selectedPcrPo: PcrPoData | null
+ maxHeight?: string | number
+ isPartnersPage?: boolean
+}
+
+export function PcrDetailTables({ selectedPcrPo, maxHeight, isPartnersPage = false }: PcrDetailTablesProps) {
+ // 상태 관리
+ const [isLoading, setIsLoading] = useState(false)
+ const [details, setDetails] = useState<PcrPrData[]>([])
+
+ // 데이터 새로고침 함수
+ const handleRefreshData = useCallback(async () => {
+ if (!selectedPcrPo?.poContractNumber) {
+ setDetails([])
+ return
+ }
+
+ try {
+ setIsLoading(true)
+
+ const result = await getPcrPrListByPoContractNumber(selectedPcrPo.poContractNumber)
+
+ // 데이터 변환
+ const transformedData = result.map(item => ({
+ ...item,
+ // 필요한 추가 필드 변환
+ }))
+
+ setDetails(transformedData)
+ } catch (error) {
+ console.error("PCR_PR 데이터 로드 오류:", error)
+ setDetails([])
+ toast.error("PCR_PR 데이터를 불러오는 중 오류가 발생했습니다")
+ } finally {
+ setIsLoading(false)
+ }
+ }, [selectedPcrPo?.poContractNumber])
+
+ // selectedPcrPo가 변경될 때 데이터 로드
+ useEffect(() => {
+ handleRefreshData()
+ }, [handleRefreshData])
+
+ // 칼럼 정의
+ const columns = React.useMemo(() =>
+ getPcrDetailColumns(),
+ []
+ )
+
+ if (!selectedPcrPo) {
+ return (
+ <div className="flex items-center justify-center h-full text-muted-foreground">
+ PCR_PO를 선택하세요
+ </div>
+ )
+ }
+
+ // 로딩 중인 경우
+ if (isLoading) {
+ return (
+ <div className="p-4 space-y-4">
+ <Skeleton className="h-8 w-1/2" />
+ <Skeleton className="h-24 w-full" />
+ <Skeleton className="h-48 w-full" />
+ </div>
+ )
+ }
+
+ return (
+ <div className="h-full overflow-hidden">
+ {/* 헤더 정보 */}
+ <div className="p-4 bg-muted/50 border-b">
+ <div className="flex items-center justify-between">
+ <div className="space-y-1">
+ <h3 className="text-sm font-medium">
+ PO/계약번호: {selectedPcrPo.poContractNumber}
+ </h3>
+ <p className="text-xs text-muted-foreground">
+ 프로젝트: {selectedPcrPo.project || "N/A"}
+ </p>
+ </div>
+ <div className="flex items-center gap-2">
+ <Badge variant="outline">
+ {details.length}건
+ </Badge>
+ <PcrDetailToolbarAction
+ selectedPcrPo={selectedPcrPo}
+ onRefresh={handleRefreshData}
+ isPartnersPage={isPartnersPage}
+ />
+ </div>
+ </div>
+ </div>
+
+ {/* 테이블 표시 */}
+ <ClientDataTable
+ columns={columns}
+ data={details}
+ maxHeight={maxHeight}
+ emptyStateMessage="PCR_PR 데이터가 없습니다. 새 항목을 생성해보세요."
+ />
+ </div>
+ )
+}
+
+
diff --git a/lib/pcr/table/detail-table/pcr-detail-toolbar-action.tsx b/lib/pcr/table/detail-table/pcr-detail-toolbar-action.tsx
new file mode 100644
index 00000000..92829055
--- /dev/null
+++ b/lib/pcr/table/detail-table/pcr-detail-toolbar-action.tsx
@@ -0,0 +1,79 @@
+"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import { Plus, Loader2, RefreshCw } from "lucide-react"
+import { CreatePcrPrDialog } from "./create-pcr-pr-dialog"
+import { PcrPoData } from "@/lib/pcr/types"
+
+interface PcrDetailToolbarActionProps {
+ selectedPcrPo: PcrPoData | null
+ onRefresh: () => void
+ isPartnersPage?: boolean
+}
+
+export function PcrDetailToolbarAction({
+ selectedPcrPo,
+ onRefresh,
+ isPartnersPage = false,
+}: PcrDetailToolbarActionProps) {
+ const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
+ const [isRefreshing, setIsRefreshing] = React.useState(false)
+
+ const handleRefresh = async () => {
+ setIsRefreshing(true)
+ try {
+ await onRefresh()
+ } finally {
+ setIsRefreshing(false)
+ }
+ }
+
+ const handleCreateSuccess = () => {
+ onRefresh()
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+ {/* 새로고침 버튼 */}
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={handleRefresh}
+ disabled={isRefreshing}
+ className="gap-2"
+ >
+ {isRefreshing ? (
+ <Loader2 className="size-4 animate-spin" aria-hidden="true" />
+ ) : (
+ <RefreshCw className="size-4" aria-hidden="true" />
+ )}
+ <span>새로고침</span>
+ </Button>
+
+ {/* PCR_PR 생성 버튼 - Partners 페이지에서는 표시하지 않음 */}
+ {!isPartnersPage && (
+ <>
+ <Button
+ variant="default"
+ size="sm"
+ onClick={() => setCreateDialogOpen(true)}
+ disabled={!selectedPcrPo}
+ className="gap-2"
+ >
+ <Plus className="size-4" />
+ PCR_PR 생성
+ </Button>
+
+ {/* PCR_PR 생성 다이얼로그 */}
+ <CreatePcrPrDialog
+ open={createDialogOpen}
+ onOpenChange={setCreateDialogOpen}
+ selectedPcrPo={selectedPcrPo}
+ onSuccess={handleCreateSuccess}
+ />
+ </>
+ )}
+ </div>
+ )
+}
diff --git a/lib/pcr/table/edit-pcr-sheet.tsx b/lib/pcr/table/edit-pcr-sheet.tsx
new file mode 100644
index 00000000..df625515
--- /dev/null
+++ b/lib/pcr/table/edit-pcr-sheet.tsx
@@ -0,0 +1,237 @@
+"use client"
+
+import * as React from "react"
+import { useForm } from "react-hook-form"
+import { zodResolver } from "@hookform/resolvers/zod"
+import * as z from "zod"
+import { Button } from "@/components/ui/button"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetFooter,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Textarea } from "@/components/ui/textarea"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { toast } from "sonner"
+import { PcrPoData } from "@/lib/pcr/types"
+import { updatePcrRejectionReasonAction } from "@/lib/pcr/actions"
+
+const pcrEditSchema = z.object({
+ rejectionReasonType: z.string().optional(),
+ customRejectionReason: z.string().optional(),
+})
+
+const REJECTION_REASON_OPTIONS = [
+ { value: "제작완료", label: "제작완료" },
+ { value: "납품 준비 중", label: "납품 준비 중" },
+ { value: "제작을 위한 원소재 준비 완료", label: "제작을 위한 원소재 준비 완료" },
+ { value: "협력사 거래중지로 인한 승인 불가", label: "협력사 거래중지로 인한 승인 불가" },
+ { value: "기타", label: "기타" },
+]
+
+type PcrEditFormData = z.infer<typeof pcrEditSchema>
+
+interface EditPcrSheetProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ pcrData: PcrPoData | null
+ onSuccess: () => void
+}
+
+export function EditPcrSheet({
+ open,
+ onOpenChange,
+ pcrData,
+ onSuccess,
+}: EditPcrSheetProps) {
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+
+ const form = useForm<PcrEditFormData>({
+ resolver: zodResolver(pcrEditSchema),
+ defaultValues: {
+ rejectionReason: "",
+ },
+ })
+
+ // PCR 데이터가 변경될 때 폼 초기화
+ React.useEffect(() => {
+ if (pcrData) {
+ const rejectionReason = pcrData.rejectionReason || ""
+
+ // 기존 rejectionReason을 파싱해서 미리 정의된 옵션인지 확인
+ const predefinedOption = REJECTION_REASON_OPTIONS.find(option =>
+ option.value === rejectionReason
+ )
+
+ if (predefinedOption) {
+ form.reset({
+ rejectionReasonType: rejectionReason,
+ customRejectionReason: "",
+ })
+ } else {
+ form.reset({
+ rejectionReasonType: "기타",
+ customRejectionReason: rejectionReason,
+ })
+ }
+ }
+ }, [pcrData, form])
+
+ const onSubmit = async (data: PcrEditFormData) => {
+ if (!pcrData) {
+ toast.error("PCR 데이터가 없습니다")
+ return
+ }
+
+ setIsSubmitting(true)
+ try {
+ // rejectionReason 조합
+ let rejectionReason = ""
+ if (data.rejectionReasonType === "기타") {
+ rejectionReason = data.customRejectionReason || ""
+ } else {
+ rejectionReason = data.rejectionReasonType || ""
+ }
+
+ const result = await updatePcrRejectionReasonAction({
+ id: pcrData.id,
+ rejectionReason: rejectionReason,
+ })
+
+ if (result.success) {
+ toast.success("PCR이 성공적으로 수정되었습니다")
+ onOpenChange(false)
+ onSuccess()
+ } else {
+ toast.error(result.error || "PCR 수정에 실패했습니다")
+ }
+ } catch (error) {
+ console.error("PCR 수정 오류:", error)
+ toast.error("PCR 수정 중 오류가 발생했습니다")
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ if (!pcrData) return null
+
+ return (
+ <Sheet open={open} onOpenChange={onOpenChange}>
+ <SheetContent className="w-[400px] sm:w-[540px]">
+ <SheetHeader>
+ <SheetTitle>PCR 수정</SheetTitle>
+ <SheetDescription>
+ PO/계약번호: {pcrData.poContractNumber}
+ </SheetDescription>
+ </SheetHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
+ {/* 읽기 전용 정보 표시 */}
+ <div className="space-y-4">
+ <div className="grid grid-cols-2 gap-4">
+ <div>
+ <label className="text-sm font-medium">프로젝트</label>
+ <p className="text-sm text-muted-foreground">{pcrData.project || "-"}</p>
+ </div>
+ <div>
+ <label className="text-sm font-medium">변경구분</label>
+ <p className="text-sm text-muted-foreground">{pcrData.changeType}</p>
+ </div>
+ </div>
+
+ <div>
+ <label className="text-sm font-medium">PCR 사유</label>
+ <p className="text-sm text-muted-foreground">{pcrData.pcrReason || "-"}</p>
+ </div>
+
+ <div>
+ <label className="text-sm font-medium">상세 사유</label>
+ <p className="text-sm text-muted-foreground">{pcrData.detailsReason || "-"}</p>
+ </div>
+ </div>
+
+ {/* 편집 가능한 필드 */}
+ <FormField
+ control={form.control}
+ name="rejectionReasonType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>거절 사유 *</FormLabel>
+ <Select onValueChange={field.onChange} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="거절 사유를 선택하세요" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {REJECTION_REASON_OPTIONS.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 기타 선택 시 텍스트 입력 필드 */}
+ {form.watch("rejectionReasonType") === "기타" && (
+ <FormField
+ control={form.control}
+ name="customRejectionReason"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>기타 거절 사유</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="거절 사유를 입력하세요"
+ className="resize-none"
+ rows={3}
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ <SheetFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => onOpenChange(false)}
+ disabled={isSubmitting}
+ >
+ 취소
+ </Button>
+ <Button type="submit" disabled={isSubmitting}>
+ {isSubmitting ? "수정 중..." : "수정"}
+ </Button>
+ </SheetFooter>
+ </form>
+ </Form>
+ </SheetContent>
+ </Sheet>
+ )
+}
diff --git a/lib/pcr/table/pcr-table-column.tsx b/lib/pcr/table/pcr-table-column.tsx
new file mode 100644
index 00000000..aa4936ce
--- /dev/null
+++ b/lib/pcr/table/pcr-table-column.tsx
@@ -0,0 +1,416 @@
+import { ColumnDef } from "@tanstack/react-table"
+import { Badge } from "@/components/ui/badge"
+import { Button } from "@/components/ui/button"
+import { Checkbox } from "@/components/ui/checkbox"
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu"
+import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
+import { PcrPoData, PCR_APPROVAL_STATUS_CONFIG, PCR_CHANGE_TYPE_CONFIG } from "@/lib/pcr/types"
+import { MoreHorizontal, Eye, Edit, Trash2 } from "lucide-react"
+import type { DataTableRowAction } from "@/types/table"
+import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"
+
+interface GetColumnsProps {
+ setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<PcrPoData> | null>>
+ isEvcpPage?: boolean; // EvcP 페이지인지 여부
+}
+
+export function getColumns({ setRowAction, isEvcpPage = false }: GetColumnsProps): ColumnDef<PcrPoData>[] {
+ const columns: ColumnDef<PcrPoData>[] = [
+ {
+ id: "select",
+ header: ({ table }) => (
+ <Checkbox
+ checked={table.getSelectedRowModel().rows.length > 0}
+ onCheckedChange={(value) => {
+ if (value) {
+ // 하나만 선택 가능하도록 현재 행만 선택
+ table.getRowModel().rows.forEach((row) => {
+ if (row.getIsSelected()) {
+ row.toggleSelected(false);
+ }
+ });
+ } else {
+ // 모든 선택 해제
+ table.toggleAllPageRowsSelected(false);
+ }
+ }}
+ aria-label="행 선택"
+ className="translate-y-[2px]"
+ />
+ ),
+ cell: ({ row, table }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => {
+ if (value) {
+ // 다른 모든 행 선택 해제 후 현재 행만 선택
+ table.getRowModel().rows.forEach((r) => {
+ if (r.id !== row.id) {
+ r.toggleSelected(false);
+ }
+ });
+ }
+ row.toggleSelected(!!value);
+ }}
+ aria-label="행 선택"
+ className="translate-y-[2px]"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: "pcrApprovalStatus",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PCR 승인상태" />
+ ),
+ cell: ({ row }) => {
+ const status = row.getValue("pcrApprovalStatus") as string
+ const config = PCR_APPROVAL_STATUS_CONFIG[status as keyof typeof PCR_APPROVAL_STATUS_CONFIG]
+
+ if (!config) {
+ return <Badge variant="outline">{status}</Badge>
+ }
+
+ return (
+ <Badge
+ variant={config.variant as any}
+ className={config.color}
+ >
+ {config.label}
+ </Badge>
+ )
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ },
+ },
+ {
+ accessorKey: "changeType",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="변경구분" />
+ ),
+ cell: ({ row }) => {
+ const changeType = row.getValue("changeType") as string
+ const config = PCR_CHANGE_TYPE_CONFIG[changeType as keyof typeof PCR_CHANGE_TYPE_CONFIG]
+
+ if (!config) {
+ return <Badge variant="outline">{changeType}</Badge>
+ }
+
+ return (
+ <Badge variant="outline">
+ {config.label}
+ </Badge>
+ )
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ },
+ },
+ {
+ accessorKey: "poContractNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PO/계약번호" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("poContractNumber") as string
+ return (
+ <div className="font-medium">
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "project",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="프로젝트" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("project") as string
+ return (
+ <div className="max-w-[200px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "pcrRequestDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PCR 요청일자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("pcrRequestDate") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "revItemNumber",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="Rev./품번" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("revItemNumber") as string
+ return (
+ <div className="max-w-[150px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "purchaseContractManager",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="구매/계약담당자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("purchaseContractManager") as string
+ return (
+ <div className="max-w-[120px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "pcrCreator",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PCR 생성자" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("pcrCreator") as string
+ return (
+ <div className="max-w-[120px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "poContractAmountBefore",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PO/계약금액(전)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("poContractAmountBefore") as number
+ const currency = row.original.contractCurrency
+
+ if (!value) return "-"
+
+ return (
+ <div className="text-right font-mono text-sm">
+ {currency} {value.toLocaleString()}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "poContractAmountAfter",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PO/계약금액(후)" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("poContractAmountAfter") as number
+ const currency = row.original.contractCurrency
+
+ if (!value) return "-"
+
+ return (
+ <div className="text-right font-mono text-sm">
+ {currency} {value.toLocaleString()}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "contractCurrency",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="계약통화" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("contractCurrency") as string
+ return (
+ <div className="text-center">
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ // EvcP 페이지에서만 협력업체 정보 표시
+ ...(isEvcpPage ? [{
+ accessorKey: "vendorName" as const,
+ header: ({ column }: { column: any }) => (
+ <DataTableColumnHeaderSimple column={column} title="협력업체" />
+ ),
+ cell: ({ row }: { row: any }) => {
+ const vendorName = row.getValue("vendorName") as string
+ const vendorCode = row.original.vendorCode as string
+ const displayText = vendorName
+ ? vendorCode
+ ? `${vendorName} (${vendorCode})`
+ : vendorName
+ : "-"
+
+ return (
+ <div className="max-w-[150px] truncate" title={displayText}>
+ {displayText}
+ </div>
+ )
+ },
+ }] : []),
+ {
+ accessorKey: "details",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="상세" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("details") as string
+ return (
+ <div className="max-w-[200px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "pcrReason",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PCR 사유" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("pcrReason") as string
+ return (
+ <div className="max-w-[150px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "detailsReason",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="상세 사유" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("detailsReason") as string
+ return (
+ <div className="max-w-[150px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "rejectionReason",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="거절 사유" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("rejectionReason") as string
+ return (
+ <div className="max-w-[150px] truncate" title={value}>
+ {value || "-"}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "pcrResponseDate",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="PCR 회신일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("pcrResponseDate") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ {
+ accessorKey: "createdAt",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="생성일" />
+ ),
+ cell: ({ row }) => {
+ const value = row.getValue("createdAt") as Date
+ if (!value) return "-"
+
+ return (
+ <div className="text-sm">
+ {value.toLocaleDateString('ko-KR', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit'
+ })}
+ </div>
+ )
+ },
+ },
+ {
+ id: "actions",
+ header: ({ column }) => (
+ <DataTableColumnHeaderSimple column={column} title="작업" />
+ ),
+ cell: ({ row }) => {
+ const pcrPo = row.original
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <MoreHorizontal className="size-4" />
+ <span className="sr-only">메뉴 열기</span>
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-[160px]">
+ <DropdownMenuLabel>작업</DropdownMenuLabel>
+ <DropdownMenuSeparator />
+
+ <DropdownMenuItem
+ onClick={() => setRowAction({ row, type: "update" })}
+ >
+ <Edit className="mr-2 size-4" />
+ 수정
+ </DropdownMenuItem>
+
+ </DropdownMenuContent>
+ </DropdownMenu>
+ )
+ },
+ enableSorting: false,
+ enableHiding: false,
+ },
+ ]
+
+ return columns
+}
diff --git a/lib/pcr/table/pcr-table-toolbar-actions.tsx b/lib/pcr/table/pcr-table-toolbar-actions.tsx
new file mode 100644
index 00000000..3e2394fb
--- /dev/null
+++ b/lib/pcr/table/pcr-table-toolbar-actions.tsx
@@ -0,0 +1,120 @@
+"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import { Loader2, RefreshCw, Download, CheckCircle, XCircle } from "lucide-react"
+import type { Table } from "@tanstack/react-table"
+import { CreatePcrDialog } from "./create-pcr-dialog"
+import { ApproveRejectPcrDialog } from "./approve-reject-pcr-dialog"
+import { PcrPoData } from "@/lib/pcr/types"
+
+interface PcrTableToolbarActionsProps<TData> {
+ selection: Table<TData>
+ onRefresh: () => void
+ isEvcpPage?: boolean
+ isPartnersPage?: boolean
+ currentVendorId?: number
+}
+
+export function PcrTableToolbarActions<TData>({
+ selection,
+ onRefresh,
+ isEvcpPage = false,
+ isPartnersPage = false,
+ currentVendorId,
+}: PcrTableToolbarActionsProps<TData>) {
+ const [approveDialogOpen, setApproveDialogOpen] = React.useState(false)
+ const [rejectDialogOpen, setRejectDialogOpen] = React.useState(false)
+ const [selectedPcr, setSelectedPcr] = React.useState<PcrPoData | null>(null)
+
+ // 선택된 행들 가져오기
+ const selectedRows = selection.getSelectedRowModel().rows
+ const selectedPcrData = selectedRows.length === 1 ? (selectedRows[0].original as PcrPoData) : null
+
+ // 승인/거절 가능 여부 확인
+ // const canApproveOrReject = selectedPcrData && selectedPcrData.pcrApprovalStatus === '승인대기'
+ const canApproveOrReject = selectedPcrData // 임시로 주석 처리하여 테스트
+
+
+
+ const handleExport = () => {
+ // 추후 구현
+ console.log("Export functionality to be implemented")
+ }
+
+ const handleApprove = () => {
+ if (selectedPcrData) {
+ setSelectedPcr(selectedPcrData)
+ setApproveDialogOpen(true)
+ }
+ }
+
+ const handleReject = () => {
+ if (selectedPcrData) {
+ setSelectedPcr(selectedPcrData)
+ setRejectDialogOpen(true)
+ }
+ }
+
+ const handleActionSuccess = () => {
+ // 액션 성공 시 선택 해제
+ selection.resetRowSelection()
+ // revalidatePath로 인해 자동으로 페이지 새로고침됨
+ }
+
+ return (
+ <div className="flex items-center gap-2">
+
+ {/* 승인 버튼 */}
+ <Button
+ variant="default"
+ size="sm"
+ onClick={handleApprove}
+ disabled={!canApproveOrReject}
+ className="gap-2 bg-green-600 hover:bg-green-700"
+ >
+ <CheckCircle className="size-4" />
+ 승인
+ </Button>
+
+ {/* 거절 버튼 */}
+ <Button
+ variant="destructive"
+ size="sm"
+ onClick={handleReject}
+ disabled={!canApproveOrReject}
+ className="gap-2"
+ >
+ <XCircle className="size-4" />
+ 거절
+ </Button>
+
+ {/* PCR 생성 다이얼로그 - Partners 페이지에서는 표시하지 않음 */}
+ {!isPartnersPage && (
+ <CreatePcrDialog
+ isEvcpPage={isEvcpPage}
+ currentVendorId={currentVendorId}
+ onSuccess={onRefresh}
+ />
+ )}
+
+ {/* 승인 다이얼로그 */}
+ <ApproveRejectPcrDialog
+ open={approveDialogOpen}
+ onOpenChange={setApproveDialogOpen}
+ pcrData={selectedPcr}
+ actionType="approve"
+ onSuccess={handleActionSuccess}
+ />
+
+ {/* 거절 다이얼로그 */}
+ <ApproveRejectPcrDialog
+ open={rejectDialogOpen}
+ onOpenChange={setRejectDialogOpen}
+ pcrData={selectedPcr}
+ actionType="reject"
+ onSuccess={handleActionSuccess}
+ />
+ </div>
+ )
+}
diff --git a/lib/pcr/table/pcr-table.tsx b/lib/pcr/table/pcr-table.tsx
new file mode 100644
index 00000000..6538e820
--- /dev/null
+++ b/lib/pcr/table/pcr-table.tsx
@@ -0,0 +1,396 @@
+"use client"
+
+import * as React from "react"
+import { useSearchParams } from "next/navigation"
+import type {
+ DataTableRowAction,
+} from "@/types/table"
+import {
+ ResizablePanelGroup,
+ ResizablePanel,
+ ResizableHandle,
+} from "@/components/ui/resizable"
+
+import { useDataTable } from "@/hooks/use-data-table"
+import { DataTable } from "@/components/data-table/data-table"
+import { getColumns } from "./pcr-table-column"
+import { useEffect, useMemo } from "react"
+import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar"
+import { PcrTableToolbarActions } from "./pcr-table-toolbar-actions"
+import { getPcrPoList } from "@/lib/pcr/service"
+import { useTablePresets } from "@/components/data-table/use-table-presets"
+import { PcrDetailTables } from "./detail-table/pcr-detail-table"
+import { EditPcrSheet } from "./edit-pcr-sheet"
+import { cn } from "@/lib/utils"
+import { PcrPoData } from "@/lib/pcr/types"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+} from "@/types/table"
+
+interface PcrTableProps {
+ tableData: Awaited<ReturnType<typeof getPcrPoList>>
+ className?: string;
+ calculatedHeight?: string;
+ isEvcpPage?: boolean; // EvcP 페이지인지 여부
+ isPartnersPage?: boolean; // Partners 페이지인지 여부
+ currentVendorId?: number; // Partners 페이지에서 현재 사용자의 vendorId
+}
+
+export function PcrTable({
+ tableData,
+ className,
+ calculatedHeight,
+ isEvcpPage = false,
+ isPartnersPage = false,
+ currentVendorId,
+}: PcrTableProps) {
+ const searchParams = useSearchParams()
+
+
+ // 선택된 PCR_PO 상태
+ const [selectedPcrPo, setSelectedPcrPo] = React.useState<PcrPoData | null>(null)
+
+ // Edit sheet 상태
+ const [editSheetOpen, setEditSheetOpen] = React.useState(false)
+ const [editingPcr, setEditingPcr] = React.useState<PcrPoData | null>(null)
+
+
+ // 패널 collapse 상태
+ const [panelHeight, setPanelHeight] = React.useState<number>(55)
+
+ // RFQListTable 컴포넌트 내부의 rowAction 처리 부분 수정
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<PcrPoData> | null>(null)
+
+ // 고정 높이 설정을 위한 상수 (실제 측정값으로 조정 필요)
+ const LAYOUT_HEADER_HEIGHT = 64 // Layout Header 높이
+ const LAYOUT_FOOTER_HEIGHT = 60 // Layout Footer 높이 (있다면 실제 값)
+ const LOCAL_HEADER_HEIGHT = 72 // 로컬 헤더 바 높이 (p-4 + border)
+
+ console.log(calculatedHeight)
+
+ // 테이블 컨텐츠 높이 - 전달받은 높이에서 로컬 헤더 제외
+ const FIXED_TABLE_HEIGHT = calculatedHeight
+ ? `calc(${calculatedHeight} - ${LOCAL_HEADER_HEIGHT}px)`
+ : `calc(100vh - ${LAYOUT_HEADER_HEIGHT + LAYOUT_FOOTER_HEIGHT + LOCAL_HEADER_HEIGHT+76}px)` // fallback
+
+ // 데이터는 props로 직접 전달받음
+
+ // 초기 설정 정의
+ const initialSettings = React.useMemo(() => ({
+ page: parseInt(searchParams?.get('page') || '1'),
+ perPage: parseInt(searchParams?.get('perPage') || '10'),
+ sort: searchParams?.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }],
+ columnVisibility: {},
+ columnOrder: [],
+ pinnedColumns: { left: [], right: [] },
+ filters: [],
+ joinOperator: "and" as const,
+ basicFilters: [],
+ basicJoinOperator: "and" as const,
+ search: "",
+ }), [searchParams])
+
+ // DB 기반 프리셋 훅 사용
+ const {
+ getCurrentSettings,
+ } = useTablePresets<PcrPoData>('pcr-po-table', initialSettings)
+
+
+ // 행 액션 처리
+ useEffect(() => {
+ if (rowAction) {
+ switch (rowAction.type) {
+ case "select":
+ // 객체 참조 안정화를 위해 필요한 필드만 추출
+ const pcrPoData = rowAction.row.original;
+ console.log("Row action select - PCR_PO 데이터:", pcrPoData)
+ setSelectedPcrPo({
+ id: pcrPoData.id,
+ no: pcrPoData.no,
+ pcrApprovalStatus: pcrPoData.pcrApprovalStatus,
+ changeType: pcrPoData.changeType,
+ details: pcrPoData.details || undefined,
+ project: pcrPoData.project || undefined,
+ pcrRequestDate: pcrPoData.pcrRequestDate,
+ poContractNumber: pcrPoData.poContractNumber,
+ revItemNumber: pcrPoData.revItemNumber || undefined,
+ purchaseContractManager: pcrPoData.purchaseContractManager || undefined,
+ pcrCreator: pcrPoData.pcrCreator || undefined,
+ poContractAmountBefore: pcrPoData.poContractAmountBefore ? Number(pcrPoData.poContractAmountBefore) : undefined as number | undefined,
+ poContractAmountAfter: pcrPoData.poContractAmountAfter ? Number(pcrPoData.poContractAmountAfter) : undefined as number | undefined,
+ contractCurrency: pcrPoData.contractCurrency || "KRW",
+ pcrReason: pcrPoData.pcrReason || undefined,
+ detailsReason: pcrPoData.detailsReason || undefined,
+ rejectionReason: pcrPoData.rejectionReason || undefined,
+ pcrResponseDate: pcrPoData.pcrResponseDate || undefined,
+ vendorId: pcrPoData.vendorId || undefined,
+ vendorName: pcrPoData.vendorName || undefined,
+ createdBy: pcrPoData.createdBy,
+ updatedBy: pcrPoData.updatedBy,
+ createdAt: pcrPoData.createdAt,
+ updatedAt: pcrPoData.updatedAt,
+ });
+ break;
+ case "update":
+ // PCR_PO 수정 시트 열기
+ setEditingPcr(rowAction.row.original)
+ setEditSheetOpen(true)
+ break;
+ case "delete":
+ console.log("Delete PCR_PO:", rowAction.row.original)
+ break;
+ }
+ setRowAction(null)
+ }
+ }, [rowAction])
+
+ const columns = React.useMemo(
+ () => getColumns({
+ setRowAction,
+ isEvcpPage,
+ }),
+ [setRowAction, isEvcpPage]
+ )
+
+ // 필터 필드 정의
+ const filterFields: DataTableFilterField<PcrPoData>[] = []
+
+ const advancedFilterFields: DataTableAdvancedFilterField<PcrPoData>[] = [
+ {
+ id: "pcrApprovalStatus",
+ label: "PCR 승인상태",
+ type: "multi-select",
+ options: [
+ { label: "승인대기", value: "승인대기" },
+ { label: "승인완료", value: "승인완료" },
+ { label: "거절", value: "거절" },
+ { label: "취소", value: "취소" },
+ ],
+ },
+ {
+ id: "changeType",
+ label: "변경구분",
+ type: "multi-select",
+ options: [
+ { label: "수량변경", value: "QUANTITY" },
+ { label: "금액변경", value: "AMOUNT" },
+ { label: "기간변경", value: "PERIOD" },
+ { label: "품목변경", value: "ITEM" },
+ { label: "기타", value: "OTHER" },
+ ],
+ },
+ {
+ id: "poContractNumber",
+ label: "PO/계약번호",
+ type: "text",
+ },
+ {
+ id: "project",
+ label: "프로젝트",
+ type: "text",
+ },
+ ...(isEvcpPage ? [{
+ id: "vendorName" as const,
+ label: "협력업체",
+ type: "text" as const,
+ }] : []),
+ {
+ id: "pcrRequestDate",
+ label: "PCR 요청일자",
+ type: "date",
+ },
+ {
+ id: "createdAt",
+ label: "생성일",
+ type: "date",
+ },
+ ]
+
+
+ // 현재 설정 가져오기
+ const currentSettings = useMemo(() => {
+ return getCurrentSettings()
+ }, [getCurrentSettings])
+
+ // useDataTable 초기 상태 설정
+ const initialState = useMemo(() => {
+ return {
+ sorting: initialSettings.sort.filter((sortItem: any) => {
+ const columnExists = columns.some((col: any) => col.accessorKey === sortItem.id)
+ return columnExists
+ }) as any,
+ columnVisibility: currentSettings.columnVisibility,
+ columnPinning: currentSettings.pinnedColumns,
+ }
+ }, [currentSettings, initialSettings.sort, columns])
+
+ // useDataTable 훅 설정
+ const { table } = useDataTable({
+ data: tableData?.data || [],
+ columns: columns as any,
+ pageCount: tableData?.pageCount || 0,
+ filterFields,
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState,
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ columnResizeMode: "onEnd",
+ })
+
+ // 선택된 행들 감시하여 selectedPcrPo 설정 (checkbox selection)
+ React.useEffect(() => {
+ if (table) {
+ const selectedRows = table.getSelectedRowModel().rows
+ if (selectedRows.length === 1) {
+ const pcrPoData = selectedRows[0].original
+ const selectedData = {
+ id: pcrPoData.id,
+ no: (pcrPoData as any).no,
+ pcrApprovalStatus: pcrPoData.pcrApprovalStatus || "",
+ changeType: pcrPoData.changeType || "",
+ details: pcrPoData.details || undefined,
+ project: pcrPoData.project || undefined,
+ pcrRequestDate: pcrPoData.pcrRequestDate,
+ poContractNumber: pcrPoData.poContractNumber,
+ revItemNumber: pcrPoData.revItemNumber || undefined,
+ purchaseContractManager: pcrPoData.purchaseContractManager || undefined,
+ pcrCreator: pcrPoData.pcrCreator || undefined,
+ poContractAmountBefore: pcrPoData.poContractAmountBefore ? Number(pcrPoData.poContractAmountBefore) : undefined as number | undefined,
+ poContractAmountAfter: pcrPoData.poContractAmountAfter ? Number(pcrPoData.poContractAmountAfter) : undefined as number | undefined,
+ contractCurrency: pcrPoData.contractCurrency || "KRW",
+ pcrReason: pcrPoData.pcrReason || undefined,
+ detailsReason: pcrPoData.detailsReason || undefined,
+ rejectionReason: pcrPoData.rejectionReason || undefined,
+ pcrResponseDate: pcrPoData.pcrResponseDate || undefined,
+ vendorId: pcrPoData.vendorId || undefined,
+ vendorName: pcrPoData.vendorName || undefined,
+ createdBy: pcrPoData.createdBy,
+ updatedBy: pcrPoData.updatedBy,
+ createdAt: pcrPoData.createdAt,
+ updatedAt: pcrPoData.updatedAt,
+ }
+ setSelectedPcrPo(selectedData)
+ } else if (selectedRows.length === 0) {
+ // 선택이 해제되었을 때는 selectedPcrPo를 null로 설정하지 않음
+ // row action select가 우선권을 가짐
+ }
+ }
+ }, [table?.getSelectedRowModel().rows])
+
+
+ return (
+ <div
+ className={cn("flex flex-col relative", className)}
+ style={{ height: calculatedHeight }}
+ >
+
+ {/* Main Content */}
+ <div
+ className="flex flex-col"
+ style={{
+ height: '100%'
+ }}
+ >
+ {/* Header Bar - 고정 높이 */}
+ <div
+ className="flex items-center justify-between p-4 bg-background border-b"
+ style={{
+ height: `${LOCAL_HEADER_HEIGHT}px`,
+ flexShrink: 0
+ }}
+ >
+
+ {/* Right side info */}
+ <div className="text-sm text-muted-foreground">
+ {tableData && (
+ <span>총 {tableData.totalCount || 0}건</span>
+ )}
+ </div>
+ </div>
+
+ {/* Table Content Area - 계산된 높이 사용 */}
+ <div
+ className="relative bg-background"
+ style={{
+ height: FIXED_TABLE_HEIGHT,
+ display: 'grid',
+ gridTemplateRows: '1fr',
+ gridTemplateColumns: '1fr'
+ }}
+ >
+ <ResizablePanelGroup
+ direction="vertical"
+ className="w-full h-full"
+ >
+ <ResizablePanel
+ defaultSize={60}
+ minSize={25}
+ maxSize={75}
+ collapsible={false}
+ onResize={(size) => {
+ setPanelHeight(size)
+ }}
+ className="flex flex-col overflow-hidden"
+ >
+ {/* 상단 테이블 영역 */}
+ <div className="flex-1 min-h-0 overflow-hidden">
+ <DataTable
+ table={table}
+ maxHeight={`${panelHeight*0.5}vh`}
+ >
+ <DataTableAdvancedToolbar
+ table={table as any}
+ filterFields={advancedFilterFields}
+ shallow={false}
+ >
+ <div className="flex items-center gap-2">
+ <PcrTableToolbarActions
+ selection={table}
+ onRefresh={() => {}}
+ isEvcpPage={isEvcpPage}
+ isPartnersPage={isPartnersPage}
+ currentVendorId={currentVendorId}
+ />
+ </div>
+ </DataTableAdvancedToolbar>
+ </DataTable>
+ </div>
+ </ResizablePanel>
+
+ <ResizableHandle withHandle />
+
+ <ResizablePanel
+ minSize={25}
+ defaultSize={40}
+ collapsible={false}
+ className="flex flex-col overflow-hidden"
+ >
+ {/* 하단 상세 테이블 영역 */}
+ <div className="flex-1 min-h-0 overflow-hidden bg-background">
+ <PcrDetailTables
+ selectedPcrPo={selectedPcrPo}
+ maxHeight={`${(100-panelHeight)*0.4}vh`}
+ isPartnersPage={isPartnersPage}
+ />
+ </div>
+ </ResizablePanel>
+ </ResizablePanelGroup>
+ </div>
+ </div>
+
+ {/* PCR 수정 시트 */}
+ <EditPcrSheet
+ open={editSheetOpen}
+ onOpenChange={setEditSheetOpen}
+ pcrData={editingPcr}
+ onSuccess={() => {
+ // 데이터 새로고침
+ window.location.reload()
+ }}
+ />
+ </div>
+ )
+}