summaryrefslogtreecommitdiff
path: root/lib/legal-review/status
diff options
context:
space:
mode:
Diffstat (limited to 'lib/legal-review/status')
-rw-r--r--lib/legal-review/status/create-legal-work-dialog.tsx506
-rw-r--r--lib/legal-review/status/delete-legal-works-dialog.tsx152
-rw-r--r--lib/legal-review/status/legal-table copy.tsx583
-rw-r--r--lib/legal-review/status/legal-table.tsx546
-rw-r--r--lib/legal-review/status/legal-work-detail-dialog.tsx409
-rw-r--r--lib/legal-review/status/legal-work-filter-sheet.tsx897
-rw-r--r--lib/legal-review/status/legal-works-columns.tsx222
-rw-r--r--lib/legal-review/status/legal-works-toolbar-actions.tsx286
-rw-r--r--lib/legal-review/status/request-review-dialog.tsx983
-rw-r--r--lib/legal-review/status/update-legal-work-dialog.tsx385
10 files changed, 0 insertions, 4969 deletions
diff --git a/lib/legal-review/status/create-legal-work-dialog.tsx b/lib/legal-review/status/create-legal-work-dialog.tsx
deleted file mode 100644
index 0ee1c430..00000000
--- a/lib/legal-review/status/create-legal-work-dialog.tsx
+++ /dev/null
@@ -1,506 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import * as z from "zod"
-import { Loader2, Check, ChevronsUpDown, Calendar, User } from "lucide-react"
-import { toast } from "sonner"
-
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "@/components/ui/command"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Switch } from "@/components/ui/switch"
-import { cn } from "@/lib/utils"
-import { getVendorsForSelection } from "@/lib/b-rfq/service"
-import { createLegalWork } from "../service"
-import { useSession } from "next-auth/react"
-
-interface CreateLegalWorkDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- onSuccess?: () => void
- onDataChange?: () => void
-}
-
-// legalWorks 테이블에 맞춘 단순화된 폼 스키마
-const createLegalWorkSchema = z.object({
- category: z.enum(["CP", "GTC", "기타"]),
- vendorId: z.number().min(1, "벤더를 선택해주세요"),
- isUrgent: z.boolean().default(false),
- requestDate: z.string().min(1, "답변요청일을 선택해주세요"),
- expectedAnswerDate: z.string().optional(),
- reviewer: z.string().min(1, "검토요청자를 입력해주세요"),
-})
-
-type CreateLegalWorkFormValues = z.infer<typeof createLegalWorkSchema>
-
-interface Vendor {
- id: number
- vendorName: string
- vendorCode: string
- country: string
- taxId: string
- status: string
-}
-
-export function CreateLegalWorkDialog({
- open,
- onOpenChange,
- onSuccess,
- onDataChange
-}: CreateLegalWorkDialogProps) {
- const router = useRouter()
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [vendors, setVendors] = React.useState<Vendor[]>([])
- const [vendorsLoading, setVendorsLoading] = React.useState(false)
- const [vendorOpen, setVendorOpen] = React.useState(false)
- const { data: session } = useSession()
-
- const userName = React.useMemo(() => {
- return session?.user?.name || "";
- }, [session]);
-
- const userEmail = React.useMemo(() => {
- return session?.user?.email || "";
- }, [session]);
-
- const defaultReviewer = React.useMemo(() => {
- if (userName && userEmail) {
- return `${userName} (${userEmail})`;
- } else if (userName) {
- return userName;
- } else if (userEmail) {
- return userEmail;
- }
- return "";
- }, [userName, userEmail]);
-
- 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)
- }
- }, [])
-
- // 오늘 날짜 + 7일 후를 기본 답변요청일로 설정
- const getDefaultRequestDate = () => {
- const date = new Date()
- date.setDate(date.getDate() + 7)
- return date.toISOString().split('T')[0]
- }
-
- // 답변요청일 + 3일 후를 기본 답변예정일로 설정
- const getDefaultExpectedDate = (requestDate: string) => {
- if (!requestDate) return ""
- const date = new Date(requestDate)
- date.setDate(date.getDate() + 3)
- return date.toISOString().split('T')[0]
- }
-
- const form = useForm<CreateLegalWorkFormValues>({
- resolver: zodResolver(createLegalWorkSchema),
- defaultValues: {
- category: "CP",
- vendorId: 0,
- isUrgent: false,
- requestDate: getDefaultRequestDate(),
- expectedAnswerDate: "",
- reviewer: defaultReviewer,
- },
- })
-
- React.useEffect(() => {
- if (open) {
- loadVendors()
- }
- }, [open, loadVendors])
-
- // 세션 정보가 로드되면 검토요청자 필드 업데이트
- React.useEffect(() => {
- if (defaultReviewer) {
- form.setValue("reviewer", defaultReviewer)
- }
- }, [defaultReviewer, form])
-
- // 답변요청일 변경시 답변예정일 자동 설정
- const requestDate = form.watch("requestDate")
- React.useEffect(() => {
- if (requestDate) {
- const expectedDate = getDefaultExpectedDate(requestDate)
- form.setValue("expectedAnswerDate", expectedDate)
- }
- }, [requestDate, form])
-
- // 폼 제출 - 서버 액션 적용
- async function onSubmit(data: CreateLegalWorkFormValues) {
- console.log("Form submitted with data:", data)
- setIsSubmitting(true)
-
- try {
- // legalWorks 테이블에 맞춘 데이터 구조
- const legalWorkData = {
- ...data,
- // status는 서버에서 "검토요청"으로 설정
- // consultationDate는 서버에서 오늘 날짜로 설정
- // hasAttachment는 서버에서 false로 설정
- }
-
- const result = await createLegalWork(legalWorkData)
-
- if (result.success) {
- toast.success(result.data?.message || "법무업무가 성공적으로 등록되었습니다.")
- onOpenChange(false)
- form.reset({
- category: "CP",
- vendorId: 0,
- isUrgent: false,
- requestDate: getDefaultRequestDate(),
- expectedAnswerDate: "",
- reviewer: defaultReviewer,
- })
- onSuccess?.()
- onDataChange?.()
- router.refresh()
- } else {
- toast.error(result.error || "등록 중 오류가 발생했습니다.")
- }
- } catch (error) {
- console.error("Error creating legal work:", error)
- toast.error("등록 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- }
- }
-
- // 다이얼로그 닫기 핸들러
- const handleOpenChange = (open: boolean) => {
- onOpenChange(open)
- if (!open) {
- form.reset({
- category: "CP",
- vendorId: 0,
- isUrgent: false,
- requestDate: getDefaultRequestDate(),
- expectedAnswerDate: "",
- reviewer: defaultReviewer,
- })
- }
- }
-
- // 선택된 벤더 정보
- const selectedVendor = vendors.find(v => v.id === form.watch("vendorId"))
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="max-w-2xl h-[80vh] p-0 flex flex-col">
- {/* 고정 헤더 */}
- <div className="flex-shrink-0 p-6 border-b">
- <DialogHeader>
- <DialogTitle>법무업무 신규 등록</DialogTitle>
- <DialogDescription>
- 새로운 법무업무를 등록합니다. 상세한 검토 요청은 등록 후 별도로 진행할 수 있습니다.
- </DialogDescription>
- </DialogHeader>
- </div>
-
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col flex-1 min-h-0"
- >
- {/* 스크롤 가능한 콘텐츠 영역 */}
- <div className="flex-1 overflow-y-auto p-6">
- <div className="space-y-6">
- {/* 기본 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">기본 정보</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- <div className="grid grid-cols-2 gap-4">
- {/* 구분 */}
- <FormField
- control={form.control}
- name="category"
- render={({ field }) => (
- <FormItem>
- <FormLabel>구분</FormLabel>
- <Select onValueChange={field.onChange} defaultValue={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="구분 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="CP">CP</SelectItem>
- <SelectItem value="GTC">GTC</SelectItem>
- <SelectItem value="기타">기타</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 긴급여부 */}
- <FormField
- control={form.control}
- name="isUrgent"
- render={({ field }) => (
- <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
- <div className="space-y-0.5">
- <FormLabel className="text-base">긴급 요청</FormLabel>
- <div className="text-sm text-muted-foreground">
- 긴급 처리가 필요한 경우 체크
- </div>
- </div>
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- </FormItem>
- )}
- />
- </div>
-
- {/* 벤더 선택 */}
- <FormField
- control={form.control}
- name="vendorId"
- render={({ field }) => (
- <FormItem>
- <FormLabel>벤더</FormLabel>
- <Popover open={vendorOpen} onOpenChange={setVendorOpen}>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- aria-expanded={vendorOpen}
- className="w-full justify-between"
- >
- {selectedVendor ? (
- <span className="flex items-center gap-2">
- <Badge variant="outline">{selectedVendor.vendorCode}</Badge>
- {selectedVendor.vendorName}
- </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="벤더 검색..." />
- <CommandList
- onWheel={(e) => {
- e.stopPropagation(); // 이벤트 전파 차단
- const target = e.currentTarget;
- target.scrollTop += e.deltaY; // 직접 스크롤 처리
- }}>
- <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
- <CommandGroup>
- {vendors.map((vendor) => (
- <CommandItem
- key={vendor.id}
- value={`${vendor.vendorCode} ${vendor.vendorName}`}
- onSelect={() => {
- field.onChange(vendor.id)
- setVendorOpen(false)
- }}
- >
- <Check
- className={cn(
- "mr-2 h-4 w-4",
- vendor.id === field.value ? "opacity-100" : "opacity-0"
- )}
- />
- <div className="flex items-center gap-2">
- <Badge variant="outline">{vendor.vendorCode}</Badge>
- <span>{vendor.vendorName}</span>
- </div>
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
- </CardContent>
- </Card>
-
- {/* 담당자 및 일정 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <Calendar className="h-5 w-5" />
- 담당자 및 일정
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {/* 검토요청자 */}
- <FormField
- control={form.control}
- name="reviewer"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center gap-2">
- <User className="h-4 w-4" />
- 검토요청자
- </FormLabel>
- <FormControl>
- <Input
- placeholder={defaultReviewer || "검토요청자 이름을 입력하세요"}
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- <div className="grid grid-cols-2 gap-4">
- {/* 답변요청일 */}
- <FormField
- control={form.control}
- name="requestDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel>답변요청일</FormLabel>
- <FormControl>
- <Input
- type="date"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 답변예정일 */}
- <FormField
- control={form.control}
- name="expectedAnswerDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel>답변예정일 (선택사항)</FormLabel>
- <FormControl>
- <Input
- type="date"
- {...field}
- />
- </FormControl>
- <div className="text-xs text-muted-foreground">
- 답변요청일 기준으로 자동 설정됩니다
- </div>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- </CardContent>
- </Card>
-
- {/* 안내 메시지 */}
- <Card className="bg-blue-50 border-blue-200">
- <CardContent className="pt-6">
- <div className="flex items-start gap-3">
- <div className="h-2 w-2 rounded-full bg-blue-500 mt-2"></div>
- <div className="space-y-1">
- <p className="text-sm font-medium text-blue-900">
- 법무업무 등록 안내
- </p>
- <p className="text-sm text-blue-700">
- 기본 정보 등록 후, 목록에서 해당 업무를 선택하여 상세한 검토 요청을 진행할 수 있습니다.
- </p>
- <p className="text-xs text-blue-600">
- • 상태: "검토요청"으로 자동 설정<br/>
- • 의뢰일: 오늘 날짜로 자동 설정<br/>
- • 법무답변자: 나중에 배정
- </p>
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- </div>
-
- {/* 고정 버튼 영역 */}
- <div className="flex-shrink-0 border-t bg-background p-6">
- <div className="flex justify-end gap-3">
- <Button
- type="button"
- variant="outline"
- onClick={() => handleOpenChange(false)}
- disabled={isSubmitting}
- >
- 취소
- </Button>
- <Button
- type="submit"
- disabled={isSubmitting}
- >
- {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- 등록
- </Button>
- </div>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/legal-review/status/delete-legal-works-dialog.tsx b/lib/legal-review/status/delete-legal-works-dialog.tsx
deleted file mode 100644
index 665dafc2..00000000
--- a/lib/legal-review/status/delete-legal-works-dialog.tsx
+++ /dev/null
@@ -1,152 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type LegalWorksDetailView } from "@/db/schema"
-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 { useRouter } from "next/navigation"
-
-import { removeLegalWorks } from "../service"
-
-interface DeleteLegalWorksDialogProps
- extends React.ComponentPropsWithoutRef<typeof Dialog> {
- legalWorks: Row<LegalWorksDetailView>["original"][]
- showTrigger?: boolean
- onSuccess?: () => void
-}
-
-export function DeleteLegalWorksDialog({
- legalWorks,
- showTrigger = true,
- onSuccess,
- ...props
-}: DeleteLegalWorksDialogProps) {
- const [isDeletePending, startDeleteTransition] = React.useTransition()
- const isDesktop = useMediaQuery("(min-width: 640px)")
- const router = useRouter()
-
- function onDelete() {
- startDeleteTransition(async () => {
- const { error } = await removeLegalWorks({
- ids: legalWorks.map((work) => work.id),
- })
-
- if (error) {
- toast.error(error)
- return
- }
-
- props.onOpenChange?.(false)
- router.refresh()
- toast.success("법무업무가 삭제되었습니다")
- onSuccess?.()
- })
- }
-
- if (isDesktop) {
- return (
- <Dialog {...props}>
- {showTrigger ? (
- <DialogTrigger asChild>
- <Button variant="outline" size="sm">
- <Trash className="mr-2 size-4" aria-hidden="true" />
- 삭제 ({legalWorks.length})
- </Button>
- </DialogTrigger>
- ) : null}
- <DialogContent>
- <DialogHeader>
- <DialogTitle>정말로 삭제하시겠습니까?</DialogTitle>
- <DialogDescription>
- 이 작업은 되돌릴 수 없습니다. 선택한{" "}
- <span className="font-medium">{legalWorks.length}</span>
- 건의 법무업무가 완전히 삭제됩니다.
- </DialogDescription>
- </DialogHeader>
- <DialogFooter className="gap-2 sm:space-x-0">
- <DialogClose asChild>
- <Button variant="outline">취소</Button>
- </DialogClose>
- <Button
- aria-label="Delete selected legal works"
- 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" />
- 삭제 ({legalWorks.length})
- </Button>
- </DrawerTrigger>
- ) : null}
- <DrawerContent>
- <DrawerHeader>
- <DrawerTitle>정말로 삭제하시겠습니까?</DrawerTitle>
- <DrawerDescription>
- 이 작업은 되돌릴 수 없습니다. 선택한{" "}
- <span className="font-medium">{legalWorks.length}</span>
- 건의 법무업무가 완전히 삭제됩니다.
- </DrawerDescription>
- </DrawerHeader>
- <DrawerFooter className="gap-2 sm:space-x-0">
- <DrawerClose asChild>
- <Button variant="outline">취소</Button>
- </DrawerClose>
- <Button
- aria-label="Delete selected legal works"
- 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/legal-review/status/legal-table copy.tsx b/lib/legal-review/status/legal-table copy.tsx
deleted file mode 100644
index 92abfaf6..00000000
--- a/lib/legal-review/status/legal-table copy.tsx
+++ /dev/null
@@ -1,583 +0,0 @@
-// ============================================================================
-// legal-works-table.tsx - EvaluationTargetsTable을 정확히 복사해서 수정
-// ============================================================================
-"use client";
-
-import * as React from "react";
-import { useSearchParams } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} 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 { getLegalWorks } from "../service";
-import { cn } from "@/lib/utils";
-import { useTablePresets } from "@/components/data-table/use-table-presets";
-import { TablePresetManager } from "@/components/data-table/data-table-preset";
-import { getLegalWorksColumns } from "./legal-works-columns";
-import { LegalWorksTableToolbarActions } from "./legal-works-toolbar-actions";
-import { LegalWorkFilterSheet } from "./legal-work-filter-sheet";
-import { LegalWorksDetailView } from "@/db/schema";
-import { EditLegalWorkSheet } from "./update-legal-work-dialog";
-import { LegalWorkDetailDialog } from "./legal-work-detail-dialog";
-import { DeleteLegalWorksDialog } from "./delete-legal-works-dialog";
-
-/* -------------------------------------------------------------------------- */
-/* Stats Card */
-/* -------------------------------------------------------------------------- */
-function LegalWorksStats({ data }: { data: LegalWorksDetailView[] }) {
- const stats = React.useMemo(() => {
- const total = data.length;
- const pending = data.filter(item => item.status === '검토요청').length;
- const assigned = data.filter(item => item.status === '담당자배정').length;
- const inProgress = data.filter(item => item.status === '검토중').length;
- const completed = data.filter(item => item.status === '답변완료').length;
- const urgent = data.filter(item => item.isUrgent).length;
-
- return { total, pending, assigned, inProgress, completed, urgent };
- }, [data]);
-
- if (stats.total === 0) {
- return (
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6">
- <Card className="col-span-full">
- <CardContent className="pt-6 text-center text-sm text-muted-foreground">
- 등록된 법무업무가 없습니다.
- </CardContent>
- </Card>
- </div>
- );
- }
-
- return (
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6">
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">총 건수</CardTitle>
- <Badge variant="outline">전체</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{stats.total.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- 긴급 {stats.urgent}건
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">검토요청</CardTitle>
- <Badge variant="secondary">대기</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-blue-600">{stats.pending.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.pending / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">담당자배정</CardTitle>
- <Badge variant="secondary">진행</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-yellow-600">{stats.assigned.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.assigned / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">검토중</CardTitle>
- <Badge variant="secondary">진행</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-orange-600">{stats.inProgress.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.inProgress / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">답변완료</CardTitle>
- <Badge variant="default">완료</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-green-600">{stats.completed.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.completed / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
- </div>
- );
-}
-
-/* -------------------------------------------------------------------------- */
-/* LegalWorksTable */
-/* -------------------------------------------------------------------------- */
-interface LegalWorksTableProps {
- promises: Promise<[Awaited<ReturnType<typeof getLegalWorks>>]>;
- currentYear?: number; // ✅ EvaluationTargetsTable의 evaluationYear와 동일한 역할
- className?: string;
-}
-
-export function LegalWorksTable({ promises, currentYear = new Date().getFullYear(), className }: LegalWorksTableProps) {
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<LegalWorksDetailView> | null>(null);
- const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false);
- const searchParams = useSearchParams();
-
- // ✅ EvaluationTargetsTable과 정확히 동일한 외부 필터 상태
- const [externalFilters, setExternalFilters] = React.useState<any[]>([]);
- const [externalJoinOperator, setExternalJoinOperator] = React.useState<"and" | "or">("and");
-
- // ✅ EvaluationTargetsTable과 정확히 동일한 필터 핸들러
- const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => {
- console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator);
- setExternalFilters(filters);
- setExternalJoinOperator(joinOperator);
- setIsFilterPanelOpen(false);
- }, []);
-
- const searchString = React.useMemo(
- () => searchParams.toString(),
- [searchParams]
- );
-
- const getSearchParam = React.useCallback(
- (key: string, def = "") =>
- new URLSearchParams(searchString).get(key) ?? def,
- [searchString]
- );
-
- // ✅ EvaluationTargetsTable과 정확히 동일한 URL 필터 변경 감지 및 데이터 새로고침
- React.useEffect(() => {
- const refetchData = async () => {
- try {
- setIsDataLoading(true);
-
- // 현재 URL 파라미터 기반으로 새 검색 파라미터 생성
- const currentFilters = getSearchParam("filters");
- const currentJoinOperator = getSearchParam("joinOperator", "and");
- const currentPage = parseInt(getSearchParam("page", "1"));
- const currentPerPage = parseInt(getSearchParam("perPage", "10"));
- const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }];
- const currentSearch = getSearchParam("search", "");
-
- const searchParams = {
- filters: currentFilters ? JSON.parse(currentFilters) : [],
- joinOperator: currentJoinOperator as "and" | "or",
- page: currentPage,
- perPage: currentPerPage,
- sort: currentSort,
- search: currentSearch,
- // ✅ currentYear 추가 (EvaluationTargetsTable의 evaluationYear와 동일)
- currentYear: currentYear
- };
-
- console.log("=== 새 데이터 요청 ===", searchParams);
-
- // 서버 액션 직접 호출
- const newData = await getLegalWorks(searchParams);
- setTableData(newData);
-
- console.log("=== 데이터 업데이트 완료 ===", newData.data.length, "건");
- } catch (error) {
- console.error("데이터 새로고침 오류:", error);
- } finally {
- setIsDataLoading(false);
- }
- };
-
- // 필터나 검색 파라미터가 변경되면 데이터 새로고침 (디바운스 적용)
- const timeoutId = setTimeout(() => {
- // 필터, 검색, 페이지네이션, 정렬 중 하나라도 변경되면 새로고침
- const hasChanges = getSearchParam("filters") ||
- getSearchParam("search") ||
- getSearchParam("page") !== "1" ||
- getSearchParam("perPage") !== "10" ||
- getSearchParam("sort");
-
- if (hasChanges) {
- refetchData();
- }
- }, 300); // 디바운스 시간 단축
-
- return () => clearTimeout(timeoutId);
- }, [searchString, currentYear, getSearchParam]); // ✅ EvaluationTargetsTable과 정확히 동일한 의존성
-
- const refreshData = React.useCallback(async () => {
- try {
- setIsDataLoading(true);
-
- // 현재 URL 파라미터로 데이터 새로고침
- const currentFilters = getSearchParam("filters");
- const currentJoinOperator = getSearchParam("joinOperator", "and");
- const currentPage = parseInt(getSearchParam("page", "1"));
- const currentPerPage = parseInt(getSearchParam("perPage", "10"));
- const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }];
- const currentSearch = getSearchParam("search", "");
-
- const searchParams = {
- filters: currentFilters ? JSON.parse(currentFilters) : [],
- joinOperator: currentJoinOperator as "and" | "or",
- page: currentPage,
- perPage: currentPerPage,
- sort: currentSort,
- search: currentSearch,
- currentYear: currentYear
- };
-
- const newData = await getLegalWorks(searchParams);
- setTableData(newData);
-
- console.log("=== 데이터 새로고침 완료 ===", newData.data.length, "건");
- } catch (error) {
- console.error("데이터 새로고침 오류:", error);
- } finally {
- setIsDataLoading(false);
- }
- }, [currentYear, getSearchParam]); // ✅ EvaluationTargetsTable과 동일한 의존성
-
- /* --------------------------- layout refs --------------------------- */
- const containerRef = React.useRef<HTMLDivElement>(null);
- const [containerTop, setContainerTop] = React.useState(0);
-
- const updateContainerBounds = React.useCallback(() => {
- if (containerRef.current) {
- const rect = containerRef.current.getBoundingClientRect()
- const newTop = rect.top
- setContainerTop(prevTop => {
- if (Math.abs(prevTop - newTop) > 1) { // 1px 이상 차이날 때만 업데이트
- return newTop
- }
- return prevTop
- })
- }
- }, [])
-
- React.useEffect(() => {
- updateContainerBounds();
-
- const handleResize = () => {
- updateContainerBounds();
- };
-
- window.addEventListener('resize', handleResize);
- window.addEventListener('scroll', updateContainerBounds);
-
- return () => {
- window.removeEventListener('resize', handleResize);
- window.removeEventListener('scroll', updateContainerBounds);
- };
- }, [updateContainerBounds]);
-
- /* ---------------------- 데이터 상태 관리 ---------------------- */
- // 초기 데이터 설정
- const [initialPromiseData] = React.use(promises);
-
- // ✅ 테이블 데이터 상태 추가
- const [tableData, setTableData] = React.useState(initialPromiseData);
- const [isDataLoading, setIsDataLoading] = React.useState(false);
-
- const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => {
- try {
- const value = getSearchParam(key);
- return value ? JSON.parse(value) : defaultValue;
- } catch {
- return defaultValue;
- }
- }, [getSearchParam]);
-
- const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
- return parseSearchParamHelper(key, defaultValue);
- };
-
- /* ---------------------- 초기 설정 ---------------------------- */
- const initialSettings = React.useMemo(() => ({
- page: parseInt(getSearchParam("page", "1")),
- perPage: parseInt(getSearchParam("perPage", "10")),
- sort: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }],
- filters: parseSearchParam("filters", []),
- joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and",
- search: getSearchParam("search", ""),
- columnVisibility: {},
- columnOrder: [],
- pinnedColumns: { left: [], right: ["actions"] },
- groupBy: [],
- expandedRows: [],
- }), [getSearchParam, parseSearchParam]);
-
- /* --------------------- 프리셋 훅 ------------------------------ */
- const {
- presets,
- activePresetId,
- hasUnsavedChanges,
- isLoading: presetsLoading,
- createPreset,
- applyPreset,
- updatePreset,
- deletePreset,
- setDefaultPreset,
- renamePreset,
- getCurrentSettings,
- } = useTablePresets<LegalWorksDetailView>(
- "legal-works-table",
- initialSettings
- );
-
- /* --------------------- 컬럼 ------------------------------ */
- const columns = React.useMemo(() => getLegalWorksColumns({ setRowAction }), [setRowAction]);
-
- /* 기본 필터 */
- const filterFields: DataTableFilterField<LegalWorksDetailView>[] = [
- { id: "vendorCode", label: "벤더 코드" },
- { id: "vendorName", label: "벤더명" },
- { id: "status", label: "상태" },
- ];
-
- /* 고급 필터 */
- const advancedFilterFields: DataTableAdvancedFilterField<LegalWorksDetailView>[] = [
- {
- id: "category", label: "구분", type: "select", options: [
- { label: "CP", value: "CP" },
- { label: "GTC", value: "GTC" },
- { label: "기타", value: "기타" }
- ]
- },
- {
- id: "status", label: "상태", type: "select", options: [
- { label: "검토요청", value: "검토요청" },
- { label: "담당자배정", value: "담당자배정" },
- { label: "검토중", value: "검토중" },
- { label: "답변완료", value: "답변완료" },
- { label: "재검토요청", value: "재검토요청" },
- { label: "보류", value: "보류" },
- { label: "취소", value: "취소" }
- ]
- },
- { id: "vendorCode", label: "벤더 코드", type: "text" },
- { id: "vendorName", label: "벤더명", type: "text" },
- {
- id: "isUrgent", label: "긴급여부", type: "select", options: [
- { label: "긴급", value: "true" },
- { label: "일반", value: "false" }
- ]
- },
- {
- id: "reviewDepartment", label: "검토부문", type: "select", options: [
- { label: "준법문의", value: "준법문의" },
- { label: "법무검토", value: "법무검토" }
- ]
- },
- {
- id: "inquiryType", label: "문의종류", type: "select", options: [
- { label: "국내계약", value: "국내계약" },
- { label: "국내자문", value: "국내자문" },
- { label: "해외계약", value: "해외계약" },
- { label: "해외자문", value: "해외자문" }
- ]
- },
- { id: "reviewer", label: "검토요청자", type: "text" },
- { id: "legalResponder", label: "법무답변자", type: "text" },
- { id: "requestDate", label: "답변요청일", type: "date" },
- { id: "consultationDate", label: "의뢰일", type: "date" },
- { id: "expectedAnswerDate", label: "답변예정일", type: "date" },
- { id: "legalCompletionDate", label: "법무완료일", type: "date" },
- { id: "createdAt", label: "생성일", type: "date" },
- ];
-
- /* current settings */
- const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]);
-
- const initialState = React.useMemo(() => {
- return {
- sorting: initialSettings.sort.filter(sortItem => {
- const columnExists = columns.some(col => 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,
- pageCount: tableData.pageCount,
- rowCount: tableData.total || tableData.data.length,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState,
- getRowId: (row) => String(row.id),
- shallow: false,
- clearOnDefault: true,
- });
-
- /* ---------------------- helper ------------------------------ */
- const getActiveFilterCount = React.useCallback(() => {
- try {
- // URL에서 현재 필터 수 확인
- const filtersParam = getSearchParam("filters");
- if (filtersParam) {
- const filters = JSON.parse(filtersParam);
- return Array.isArray(filters) ? filters.length : 0;
- }
- return 0;
- } catch {
- return 0;
- }
- }, [getSearchParam]);
-
- const FILTER_PANEL_WIDTH = 400;
-
- /* ---------------------------- JSX ---------------------------- */
- return (
- <>
- {/* Filter Panel */}
- <div
- className={cn(
- "fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden",
- isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0"
- )}
- style={{
- width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : "0px",
- top: `${containerTop}px`,
- height: `calc(100vh - ${containerTop}px)`
- }}
- >
- <LegalWorkFilterSheet
- isOpen={isFilterPanelOpen}
- onClose={() => setIsFilterPanelOpen(false)}
- onFiltersApply={handleFiltersApply}
- isLoading={false}
- />
- </div>
-
- {/* Main Container */}
- <div ref={containerRef} className={cn("relative w-full overflow-hidden", className)}>
- <div className="flex w-full h-full">
- <div
- className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out"
- style={{
- width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : "100%",
- marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : "0px",
- }}
- >
- {/* Header */}
- <div className="flex items-center justify-between p-4 bg-background shrink-0">
- <Button
- variant="outline"
- size="sm"
- onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
- className="flex items-center shadow-sm"
- >
- {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
- {getActiveFilterCount() > 0 && (
- <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
- {getActiveFilterCount()}
- </span>
- )}
- </Button>
- <div className="text-sm text-muted-foreground">
- 총 {tableData.total || tableData.data.length}건
- </div>
- </div>
-
- {/* Stats */}
- <div className="px-4">
- <LegalWorksStats data={tableData.data} />
- </div>
-
- {/* Table */}
- <div className="flex-1 overflow-hidden relative" style={{ height: "calc(100vh - 500px)" }}>
- {isDataLoading && (
- <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-10 flex items-center justify-center">
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
- <div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
- 필터링 중...
- </div>
- </div>
- )}
- <DataTable table={table} className="h-full">
- {/* ✅ EvaluationTargetsTable과 정확히 동일한 DataTableAdvancedToolbar */}
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- debounceMs={300}
- shallow={false}
- externalFilters={externalFilters}
- externalJoinOperator={externalJoinOperator}
- onFiltersChange={(filters, joinOperator) => {
- console.log("=== 필터 변경 감지 ===", filters, joinOperator);
- }}
- >
- <div className="flex items-center gap-2">
- <TablePresetManager<LegalWorksDetailView>
- presets={presets}
- activePresetId={activePresetId}
- currentSettings={currentSettings}
- hasUnsavedChanges={hasUnsavedChanges}
- isLoading={presetsLoading}
- onCreatePreset={createPreset}
- onUpdatePreset={updatePreset}
- onDeletePreset={deletePreset}
- onApplyPreset={applyPreset}
- onSetDefaultPreset={setDefaultPreset}
- onRenamePreset={renamePreset}
- />
-
- <LegalWorksTableToolbarActions table={table} onRefresh={refreshData} />
- </div>
- </DataTableAdvancedToolbar>
- </DataTable>
-
- {/* 편집 다이얼로그 */}
- <EditLegalWorkSheet
- open={rowAction?.type === "update"}
- onOpenChange={() => setRowAction(null)}
- work={rowAction?.row.original ?? null}
- onSuccess={() => {
- rowAction?.row.toggleSelected(false);
- refreshData();
- }}
- />
-
- <LegalWorkDetailDialog
- open={rowAction?.type === "view"}
- onOpenChange={(open) => !open && setRowAction(null)}
- work={rowAction?.row.original || null}
- />
-
- <DeleteLegalWorksDialog
- open={rowAction?.type === "delete"}
- onOpenChange={(open) => !open && setRowAction(null)}
- legalWorks={rowAction?.row.original ? [rowAction.row.original] : []}
- showTrigger={false}
- onSuccess={() => {
- setRowAction(null);
- refreshData();
- }}
- />
- </div>
- </div>
- </div>
- </div>
- </>
- );
-} \ No newline at end of file
diff --git a/lib/legal-review/status/legal-table.tsx b/lib/legal-review/status/legal-table.tsx
deleted file mode 100644
index 4df3568c..00000000
--- a/lib/legal-review/status/legal-table.tsx
+++ /dev/null
@@ -1,546 +0,0 @@
-// ============================================================================
-// components/evaluation-targets-table.tsx (CLIENT COMPONENT)
-// ─ 정리된 버전 ─
-// ============================================================================
-"use client";
-
-import * as React from "react";
-import { useSearchParams } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { HelpCircle, PanelLeftClose, PanelLeftOpen } from "lucide-react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Badge } from "@/components/ui/badge";
-import { Skeleton } from "@/components/ui/skeleton";
-import type {
- DataTableAdvancedFilterField,
- DataTableFilterField,
- DataTableRowAction,
-} 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 { cn } from "@/lib/utils";
-import { useTablePresets } from "@/components/data-table/use-table-presets";
-import { TablePresetManager } from "@/components/data-table/data-table-preset";
-import { LegalWorksDetailView } from "@/db/schema";
-import { LegalWorksTableToolbarActions } from "./legal-works-toolbar-actions";
-import { getLegalWorks } from "../service";
-import { getLegalWorksColumns } from "./legal-works-columns";
-import { LegalWorkFilterSheet } from "./legal-work-filter-sheet";
-import { EditLegalWorkSheet } from "./update-legal-work-dialog";
-import { LegalWorkDetailDialog } from "./legal-work-detail-dialog";
-import { DeleteLegalWorksDialog } from "./delete-legal-works-dialog";
-
-
-/* -------------------------------------------------------------------------- */
-/* Stats Card */
-/* -------------------------------------------------------------------------- */
-function LegalWorksStats({ data }: { data: LegalWorksDetailView[] }) {
- const stats = React.useMemo(() => {
- const total = data.length;
- const pending = data.filter(item => item.status === '검토요청').length;
- const assigned = data.filter(item => item.status === '담당자배정').length;
- const inProgress = data.filter(item => item.status === '검토중').length;
- const completed = data.filter(item => item.status === '답변완료').length;
- const urgent = data.filter(item => item.isUrgent).length;
-
- return { total, pending, assigned, inProgress, completed, urgent };
- }, [data]);
-
- if (stats.total === 0) {
- return (
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6">
- <Card className="col-span-full">
- <CardContent className="pt-6 text-center text-sm text-muted-foreground">
- 등록된 법무업무가 없습니다.
- </CardContent>
- </Card>
- </div>
- );
- }
-
- return (
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-5 mb-6">
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">총 건수</CardTitle>
- <Badge variant="outline">전체</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold">{stats.total.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- 긴급 {stats.urgent}건
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">검토요청</CardTitle>
- <Badge variant="secondary">대기</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-blue-600">{stats.pending.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.pending / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">담당자배정</CardTitle>
- <Badge variant="secondary">진행</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-yellow-600">{stats.assigned.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.assigned / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">검토중</CardTitle>
- <Badge variant="secondary">진행</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-orange-600">{stats.inProgress.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.inProgress / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
-
- <Card>
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
- <CardTitle className="text-sm font-medium">답변완료</CardTitle>
- <Badge variant="default">완료</Badge>
- </CardHeader>
- <CardContent>
- <div className="text-2xl font-bold text-green-600">{stats.completed.toLocaleString()}</div>
- <div className="text-xs text-muted-foreground mt-1">
- {stats.total ? Math.round((stats.completed / stats.total) * 100) : 0}% of total
- </div>
- </CardContent>
- </Card>
- </div>
- );
-}
-
-/* -------------------------------------------------------------------------- */
-/* EvaluationTargetsTable */
-/* -------------------------------------------------------------------------- */
-interface LegalWorksTableProps {
- promises: Promise<[Awaited<ReturnType<typeof getLegalWorks>>]>;
- currentYear: number;
- className?: string;
-}
-
-export function LegalWorksTable({ promises, currentYear = new Date().getFullYear(), className }: LegalWorksTableProps) {
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<LegalWorksDetailView> | null>(null);
- const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false);
- const searchParams = useSearchParams();
-
- // ✅ 외부 필터 상태 (폼에서 전달받은 필터)
- const [externalFilters, setExternalFilters] = React.useState<any[]>([]);
- const [externalJoinOperator, setExternalJoinOperator] = React.useState<"and" | "or">("and");
-
- // ✅ 폼에서 전달받은 필터를 처리하는 핸들러
- const handleFiltersApply = React.useCallback((filters: any[], joinOperator: "and" | "or") => {
- console.log("=== 폼에서 필터 전달받음 ===", filters, joinOperator);
- setExternalFilters(filters);
- setExternalJoinOperator(joinOperator);
- // 필터 적용 후 패널 닫기
- setIsFilterPanelOpen(false);
- }, []);
-
-
- const searchString = React.useMemo(
- () => searchParams.toString(),
- [searchParams]
- );
-
- const getSearchParam = React.useCallback(
- (key: string, def = "") =>
- new URLSearchParams(searchString).get(key) ?? def,
- [searchString]
- );
-
-
- // ✅ URL 필터 변경 감지 및 데이터 새로고침
- React.useEffect(() => {
- const refetchData = async () => {
- try {
- setIsDataLoading(true);
-
- // 현재 URL 파라미터 기반으로 새 검색 파라미터 생성
- const currentFilters = getSearchParam("filters");
- const currentJoinOperator = getSearchParam("joinOperator", "and");
- const currentPage = parseInt(getSearchParam("page", "1"));
- const currentPerPage = parseInt(getSearchParam("perPage", "10"));
- const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }];
- const currentSearch = getSearchParam("search", "");
-
- const searchParams = {
- filters: currentFilters ? JSON.parse(currentFilters) : [],
- joinOperator: currentJoinOperator as "and" | "or",
- page: currentPage,
- perPage: currentPerPage,
- sort: currentSort,
- search: currentSearch,
- currentYear: currentYear
- };
-
- console.log("=== 새 데이터 요청 ===", searchParams);
-
- // 서버 액션 직접 호출
- const newData = await getLegalWorks(searchParams);
- setTableData(newData);
-
- console.log("=== 데이터 업데이트 완료 ===", newData.data.length, "건");
- } catch (error) {
- console.error("데이터 새로고침 오류:", error);
- } finally {
- setIsDataLoading(false);
- }
- };
-
- /* ---------------------- 검색 파라미터 안전 처리 ---------------------- */
-
- // 필터나 검색 파라미터가 변경되면 데이터 새로고침 (디바운스 적용)
- const timeoutId = setTimeout(() => {
- // 필터, 검색, 페이지네이션, 정렬 중 하나라도 변경되면 새로고침
- const hasChanges = getSearchParam("filters") ||
- getSearchParam("search") ||
- getSearchParam("page") !== "1" ||
- getSearchParam("perPage") !== "10" ||
- getSearchParam("sort");
-
- if (hasChanges) {
- refetchData();
- }
- }, 300); // 디바운스 시간 단축
-
- return () => clearTimeout(timeoutId);
- }, [searchString, currentYear, getSearchParam]);
-
- const refreshData = React.useCallback(async () => {
- try {
- setIsDataLoading(true);
-
- // 현재 URL 파라미터로 데이터 새로고침
- const currentFilters = getSearchParam("filters");
- const currentJoinOperator = getSearchParam("joinOperator", "and");
- const currentPage = parseInt(getSearchParam("page", "1"));
- const currentPerPage = parseInt(getSearchParam("perPage", "10"));
- const currentSort = getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }];
- const currentSearch = getSearchParam("search", "");
-
- const searchParams = {
- filters: currentFilters ? JSON.parse(currentFilters) : [],
- joinOperator: currentJoinOperator as "and" | "or",
- page: currentPage,
- perPage: currentPerPage,
- sort: currentSort,
- search: currentSearch,
- currentYear: currentYear
- };
-
- const newData = await getLegalWorks(searchParams);
- setTableData(newData);
-
- console.log("=== 데이터 새로고침 완료 ===", newData.data.length, "건");
- } catch (error) {
- console.error("데이터 새로고침 오류:", error);
- } finally {
- setIsDataLoading(false);
- }
- }, [currentYear, getSearchParam]);
-
- /* --------------------------- layout refs --------------------------- */
- const containerRef = React.useRef<HTMLDivElement>(null);
- const [containerTop, setContainerTop] = React.useState(0);
-
- const updateContainerBounds = React.useCallback(() => {
- if (containerRef.current) {
- const rect = containerRef.current.getBoundingClientRect()
- const newTop = rect.top
- setContainerTop(prevTop => {
- if (Math.abs(prevTop - newTop) > 1) { // 1px 이상 차이날 때만 업데이트
- return newTop
- }
- return prevTop
- })
- }
- }, [])
- React.useEffect(() => {
- updateContainerBounds();
-
- const handleResize = () => {
- updateContainerBounds();
- };
-
- window.addEventListener('resize', handleResize);
- window.addEventListener('scroll', updateContainerBounds);
-
- return () => {
- window.removeEventListener('resize', handleResize);
- window.removeEventListener('scroll', updateContainerBounds);
- };
- }, [updateContainerBounds]);
-
- /* ---------------------- 데이터 상태 관리 ---------------------- */
- // 초기 데이터 설정
- const [initialPromiseData] = React.use(promises);
-
- // ✅ 테이블 데이터 상태 추가
- const [tableData, setTableData] = React.useState(initialPromiseData);
- const [isDataLoading, setIsDataLoading] = React.useState(false);
-
- const parseSearchParamHelper = React.useCallback((key: string, defaultValue: any): any => {
- try {
- const value = getSearchParam(key);
- return value ? JSON.parse(value) : defaultValue;
- } catch {
- return defaultValue;
- }
- }, [getSearchParam]);
-
- const parseSearchParam = <T,>(key: string, defaultValue: T): T => {
- return parseSearchParamHelper(key, defaultValue);
- };
-
- /* ---------------------- 초기 설정 ---------------------------- */
- const initialSettings = React.useMemo(() => ({
- page: parseInt(getSearchParam("page", "1")),
- perPage: parseInt(getSearchParam("perPage", "10")),
- sort: getSearchParam('sort') ? JSON.parse(getSearchParam('sort')!) : [{ id: "createdAt", desc: true }],
- filters: parseSearchParam("filters", []),
- joinOperator: (getSearchParam("joinOperator") as "and" | "or") || "and",
- search: getSearchParam("search", ""),
- columnVisibility: {},
- columnOrder: [],
- pinnedColumns: { left: [], right: ["actions"] },
- groupBy: [],
- expandedRows: [],
- }), [getSearchParam, parseSearchParam]);
-
- /* --------------------- 프리셋 훅 ------------------------------ */
- const {
- presets,
- activePresetId,
- hasUnsavedChanges,
- isLoading: presetsLoading,
- createPreset,
- applyPreset,
- updatePreset,
- deletePreset,
- setDefaultPreset,
- renamePreset,
- getCurrentSettings,
- } = useTablePresets<LegalWorksDetailView>(
- "legal-review-table",
- initialSettings
- );
-
-
-
- /* --------------------- 컬럼 ------------------------------ */
- const columns = React.useMemo(() => getLegalWorksColumns({ setRowAction }), [setRowAction]);
-
- /* 기본 필터 */
- const filterFields: DataTableFilterField<LegalWorksDetailView>[] = [
- { id: "vendorCode", label: "벤더 코드" },
- { id: "vendorName", label: "벤더명" },
- { id: "status", label: "상태" },
- ];
-
- /* 고급 필터 */
- const advancedFilterFields: DataTableAdvancedFilterField<LegalWorksDetailView>[] = [
- ];
-
- /* current settings */
- const currentSettings = React.useMemo(() => getCurrentSettings(), [getCurrentSettings]);
-
- const initialState = React.useMemo(() => {
- return {
- sorting: initialSettings.sort.filter(sortItem => {
- const columnExists = columns.some(col => 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,
- pageCount: tableData.pageCount,
- rowCount: tableData.total || tableData.data.length,
- filterFields,
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState,
- getRowId: (row) => String(row.id),
- shallow: false,
- clearOnDefault: true,
- });
-
- /* ---------------------- helper ------------------------------ */
- const getActiveFilterCount = React.useCallback(() => {
- try {
- // URL에서 현재 필터 수 확인
- const filtersParam = getSearchParam("filters");
- if (filtersParam) {
- const filters = JSON.parse(filtersParam);
- return Array.isArray(filters) ? filters.length : 0;
- }
- return 0;
- } catch {
- return 0;
- }
- }, [getSearchParam]);
-
- const FILTER_PANEL_WIDTH = 400;
-
- /* ---------------------------- JSX ---------------------------- */
- return (
- <>
- {/* Filter Panel */}
- <div
- className={cn(
- "fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden",
- isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0"
- )}
- style={{
- width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : "0px",
- top: `${containerTop}px`,
- height: `calc(100vh - ${containerTop}px)`
- }}
- >
- <LegalWorkFilterSheet
- isOpen={isFilterPanelOpen}
- onClose={() => setIsFilterPanelOpen(false)}
- onFiltersApply={handleFiltersApply} // ✅ 필터 적용 콜백 전달
- isLoading={false}
- />
- </div>
-
- {/* Main Container */}
- <div ref={containerRef} className={cn("relative w-full overflow-hidden", className)}>
- <div className="flex w-full h-full">
- <div
- className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out"
- style={{
- width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : "100%",
- marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : "0px",
- }}
- >
- {/* Header */}
- <div className="flex items-center justify-between p-4 bg-background shrink-0">
- <Button
- variant="outline"
- size="sm"
- onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)}
- className="flex items-center shadow-sm"
- >
- {isFilterPanelOpen ? <PanelLeftClose className="size-4" /> : <PanelLeftOpen className="size-4" />}
- {getActiveFilterCount() > 0 && (
- <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs">
- {getActiveFilterCount()}
- </span>
- )}
- </Button>
- <div className="text-sm text-muted-foreground">
- 총 {tableData.total || tableData.data.length}건
- </div>
- </div>
-
- {/* Stats */}
- <div className="px-4">
- <LegalWorksStats data={tableData.data} />
-
- </div>
-
- {/* Table */}
- <div className="flex-1 overflow-hidden relative" style={{ height: "calc(100vh - 500px)" }}>
- {isDataLoading && (
- <div className="absolute inset-0 bg-background/50 backdrop-blur-sm z-10 flex items-center justify-center">
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
- <div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
- 필터링 중...
- </div>
- </div>
- )}
- <DataTable table={table} className="h-full">
- {/* ✅ 확장된 DataTableAdvancedToolbar 사용 */}
- <DataTableAdvancedToolbar
- table={table}
- filterFields={advancedFilterFields}
- debounceMs={300}
- shallow={false}
- externalFilters={externalFilters}
- externalJoinOperator={externalJoinOperator}
- onFiltersChange={(filters, joinOperator) => {
- console.log("=== 필터 변경 감지 ===", filters, joinOperator);
- }}
- >
- <div className="flex items-center gap-2">
- <TablePresetManager<LegalWorksDetailView>
- presets={presets}
- activePresetId={activePresetId}
- currentSettings={currentSettings}
- hasUnsavedChanges={hasUnsavedChanges}
- isLoading={presetsLoading}
- onCreatePreset={createPreset}
- onUpdatePreset={updatePreset}
- onDeletePreset={deletePreset}
- onApplyPreset={applyPreset}
- onSetDefaultPreset={setDefaultPreset}
- onRenamePreset={renamePreset}
- />
-
- <LegalWorksTableToolbarActions table={table}onRefresh={refreshData} />
- </div>
- </DataTableAdvancedToolbar>
- </DataTable>
-
- {/* 다이얼로그들 */}
- <EditLegalWorkSheet
- open={rowAction?.type === "update"}
- onOpenChange={() => setRowAction(null)}
- work={rowAction?.row.original || null}
- onSuccess={() => {
- rowAction?.row.toggleSelected(false);
- refreshData();
- }}
- />
-
- <LegalWorkDetailDialog
- open={rowAction?.type === "view"}
- onOpenChange={(open) => !open && setRowAction(null)}
- work={rowAction?.row.original || null}
- />
-
- <DeleteLegalWorksDialog
- open={rowAction?.type === "delete"}
- onOpenChange={(open) => !open && setRowAction(null)}
- legalWorks={rowAction?.row.original ? [rowAction.row.original] : []}
- showTrigger={false}
- onSuccess={() => {
- setRowAction(null);
- refreshData();
- }}
- />
-
- </div>
- </div>
- </div>
- </div>
- </>
- );
-} \ No newline at end of file
diff --git a/lib/legal-review/status/legal-work-detail-dialog.tsx b/lib/legal-review/status/legal-work-detail-dialog.tsx
deleted file mode 100644
index 23ceccb2..00000000
--- a/lib/legal-review/status/legal-work-detail-dialog.tsx
+++ /dev/null
@@ -1,409 +0,0 @@
-"use client";
-
-import * as React from "react";
-import {
- Eye,
- FileText,
- Building,
- User,
- Calendar,
- Clock,
- MessageSquare,
- CheckCircle,
- ShieldCheck,
-} from "lucide-react";
-
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { Badge } from "@/components/ui/badge";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { Separator } from "@/components/ui/separator";
-import { formatDate } from "@/lib/utils";
-import { LegalWorksDetailView } from "@/db/schema";
-
-// -----------------------------------------------------------------------------
-// TYPES
-// -----------------------------------------------------------------------------
-
-type LegalWorkData = LegalWorksDetailView;
-
-interface LegalWorkDetailDialogProps {
- open: boolean;
- onOpenChange: (open: boolean) => void;
- work: LegalWorkData | null;
-}
-
-// -----------------------------------------------------------------------------
-// HELPERS
-// -----------------------------------------------------------------------------
-
-// 상태별 배지 스타일
-const getStatusBadgeVariant = (status: string) => {
- switch (status) {
- case "검토요청":
- return "bg-blue-100 text-blue-800 border-blue-200";
- case "담당자배정":
- return "bg-yellow-100 text-yellow-800 border-yellow-200";
- case "검토중":
- return "bg-orange-100 text-orange-800 border-orange-200";
- case "답변완료":
- return "bg-green-100 text-green-800 border-green-200";
- case "재검토요청":
- return "bg-purple-100 text-purple-800 border-purple-200";
- case "보류":
- return "bg-gray-100 text-gray-800 border-gray-200";
- case "취소":
- return "bg-red-100 text-red-800 border-red-200";
- default:
- return "bg-gray-100 text-gray-800 border-gray-200";
- }
-};
-
-export function LegalWorkDetailDialog({
- open,
- onOpenChange,
- work,
-}: LegalWorkDetailDialogProps) {
- if (!work) return null;
-
- // ---------------------------------------------------------------------------
- // CONDITIONAL FLAGS
- // ---------------------------------------------------------------------------
-
- const isLegalReview = work.reviewDepartment === "법무검토";
- const isCompliance = work.reviewDepartment === "준법문의";
-
- const isDomesticContract = work.inquiryType === "국내계약";
- const isDomesticAdvisory = work.inquiryType === "국내자문";
- const isOverseasContract = work.inquiryType === "해외계약";
- const isOverseasAdvisory = work.inquiryType === "해외자문";
-
- const isContractTypeActive =
- isDomesticContract || isOverseasContract || isOverseasAdvisory;
- const isDomesticContractFieldsActive = isDomesticContract;
- const isFactualRelationActive = isDomesticAdvisory || isOverseasAdvisory;
- const isOverseasFieldsActive = isOverseasContract || isOverseasAdvisory;
-
- // ---------------------------------------------------------------------------
- // RENDER
- // ---------------------------------------------------------------------------
-
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl h-[90vh] p-0 flex flex-col">
- {/* 헤더 */}
- <div className="flex-shrink-0 p-6 border-b">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <Eye className="h-5 w-5" /> 법무업무 상세보기
- </DialogTitle>
- <DialogDescription>
- 법무업무 #{work.id}의 상세 정보를 확인합니다.
- </DialogDescription>
- </DialogHeader>
- </div>
-
- {/* 본문 */}
- <ScrollArea className="flex-1 p-6">
- <div className="space-y-6">
- {/* 1. 기본 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2 text-lg">
- <FileText className="h-5 w-5" /> 기본 정보
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-2 gap-6 text-sm">
- <div className="space-y-4">
- <div className="flex items-center gap-2">
- <span className="font-medium text-muted-foreground">업무 ID:</span>
- <Badge variant="outline">#{work.id}</Badge>
- </div>
- <div className="flex items-center gap-2">
- <span className="font-medium text-muted-foreground">구분:</span>
- <Badge
- variant={
- work.category === "CP"
- ? "default"
- : work.category === "GTC"
- ? "secondary"
- : "outline"
- }
- >
- {work.category}
- </Badge>
- {work.isUrgent && (
- <Badge variant="destructive" className="text-xs">
- 긴급
- </Badge>
- )}
- </div>
- <div className="flex items-center gap-2">
- <span className="font-medium text-muted-foreground">상태:</span>
- <Badge
- className={getStatusBadgeVariant(work.status)}
- variant="outline"
- >
- {work.status}
- </Badge>
- </div>
- </div>
- <div className="space-y-4">
- <div className="flex items-center gap-2">
- <Building className="h-4 w-4 text-muted-foreground" />
- <span className="font-medium text-muted-foreground">벤더:</span>
- <span>
- {work.vendorCode} - {work.vendorName}
- </span>
- </div>
- <div className="flex items-center gap-2">
- <Calendar className="h-4 w-4 text-muted-foreground" />
- <span className="font-medium text-muted-foreground">의뢰일:</span>
- <span>{formatDate(work.consultationDate, "KR")}</span>
- </div>
- <div className="flex items-center gap-2">
- <Clock className="h-4 w-4 text-muted-foreground" />
- <span className="font-medium text-muted-foreground">답변요청일:</span>
- <span>{formatDate(work.requestDate, "KR")}</span>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
-
- {/* 2. 담당자 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2 text-lg">
- <User className="h-5 w-5" /> 담당자 정보
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-2 gap-6 text-sm">
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">검토요청자</span>
- <p>{work.reviewer || "미지정"}</p>
- </div>
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">법무답변자</span>
- <p>{work.legalResponder || "미배정"}</p>
- </div>
- {work.expectedAnswerDate && (
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">답변예정일</span>
- <p>{formatDate(work.expectedAnswerDate, "KR")}</p>
- </div>
- )}
- {work.legalCompletionDate && (
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">법무완료일</span>
- <p>{formatDate(work.legalCompletionDate, "KR")}</p>
- </div>
- )}
- </div>
- </CardContent>
- </Card>
-
- {/* 3. 법무업무 상세 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2 text-lg">
- <ShieldCheck className="h-5 w-5" /> 법무업무 상세 정보
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4 text-sm">
- <div className="grid grid-cols-2 gap-6">
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">검토부문</span>
- <Badge variant="outline">{work.reviewDepartment}</Badge>
- </div>
- {work.inquiryType && (
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">문의종류</span>
- <Badge variant="secondary">{work.inquiryType}</Badge>
- </div>
- )}
- {isCompliance && (
- <div className="space-y-2 col-span-2">
- <span className="font-medium text-muted-foreground">공개여부</span>
- <Badge variant={work.isPublic ? "default" : "outline"}>
- {work.isPublic ? "공개" : "비공개"}
- </Badge>
- </div>
- )}
- </div>
-
- {/* 법무검토 전용 필드 */}
- {isLegalReview && (
- <>
- {work.contractProjectName && (
- <>
- <Separator className="my-2" />
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">
- 계약명 / 프로젝트명
- </span>
- <p>{work.contractProjectName}</p>
- </div>
- </>
- )}
-
- {/* 계약서 종류 */}
- {isContractTypeActive && work.contractType && (
- <div className="space-y-2">
- <span className="font-medium text-muted-foreground">계약서 종류</span>
- <Badge variant="outline" className="max-w-max">
- {work.contractType}
- </Badge>
- </div>
- )}
-
- {/* 국내계약 전용 필드 */}
- {isDomesticContractFieldsActive && (
- <div className="grid grid-cols-2 gap-6 mt-4">
- {work.contractCounterparty && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">
- 계약상대방
- </span>
- <p>{work.contractCounterparty}</p>
- </div>
- )}
- {work.counterpartyType && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">
- 계약상대방 구분
- </span>
- <p>{work.counterpartyType}</p>
- </div>
- )}
- {work.contractPeriod && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">계약기간</span>
- <p>{work.contractPeriod}</p>
- </div>
- )}
- {work.contractAmount && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">계약금액</span>
- <p>{work.contractAmount}</p>
- </div>
- )}
- </div>
- )}
-
- {/* 사실관계 */}
- {isFactualRelationActive && work.factualRelation && (
- <div className="space-y-2 mt-4">
- <span className="font-medium text-muted-foreground">사실관계</span>
- <p className="whitespace-pre-wrap">{work.factualRelation}</p>
- </div>
- )}
-
- {/* 해외 전용 필드 */}
- {isOverseasFieldsActive && (
- <div className="grid grid-cols-2 gap-6 mt-4">
- {work.projectNumber && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">프로젝트번호</span>
- <p>{work.projectNumber}</p>
- </div>
- )}
- {work.shipownerOrderer && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">선주 / 발주처</span>
- <p>{work.shipownerOrderer}</p>
- </div>
- )}
- {work.projectType && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">프로젝트종류</span>
- <p>{work.projectType}</p>
- </div>
- )}
- {work.governingLaw && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">준거법</span>
- <p>{work.governingLaw}</p>
- </div>
- )}
- </div>
- )}
- </>
- )}
- </CardContent>
- </Card>
-
- {/* 4. 요청 내용 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2 text-lg">
- <MessageSquare className="h-5 w-5" /> 요청 내용
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4 text-sm">
- {work.title && (
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">제목</span>
- <p className="font-medium">{work.title}</p>
- </div>
- )}
- <Separator />
- <div className="space-y-1">
- <span className="font-medium text-muted-foreground">상세 내용</span>
- <div className="bg-muted/30 rounded-lg p-4">
- {work.requestContent ? (
- <div className="prose prose-sm max-w-none">
- <div
- dangerouslySetInnerHTML={{ __html: work.requestContent }}
- />
- </div>
- ) : (
- <p className="italic text-muted-foreground">요청 내용이 없습니다.</p>
- )}
- </div>
- </div>
- {work.attachmentCount > 0 && (
- <div className="flex items-center gap-2">
- <FileText className="h-4 w-4" /> 첨부파일 {work.attachmentCount}개
- </div>
- )}
- </CardContent>
- </Card>
-
- {/* 5. 답변 내용 */}
- <Card>
- <CardHeader>
- <CardTitle className="flex items-center gap-2 text-lg">
- <CheckCircle className="h-5 w-5" /> 답변 내용
- </CardTitle>
- </CardHeader>
- <CardContent className="space-y-4 text-sm">
- <div className="bg-green-50 border border-green-200 rounded-lg p-4">
- {work.responseContent ? (
- <div className="prose prose-sm max-w-none">
- <div
- dangerouslySetInnerHTML={{ __html: work.responseContent }}
- />
- </div>
- ) : (
- <p className="italic text-muted-foreground">
- 아직 답변이 등록되지 않았습니다.
- </p>
- )}
- </div>
- </CardContent>
- </Card>
- </div>
- </ScrollArea>
- </DialogContent>
- </Dialog>
- );
-}
diff --git a/lib/legal-review/status/legal-work-filter-sheet.tsx b/lib/legal-review/status/legal-work-filter-sheet.tsx
deleted file mode 100644
index 4ac877a9..00000000
--- a/lib/legal-review/status/legal-work-filter-sheet.tsx
+++ /dev/null
@@ -1,897 +0,0 @@
-"use client"
-
-import { useTransition, useState } from "react"
-import { useRouter } from "next/navigation"
-import { z } from "zod"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import { Search, X } from "lucide-react"
-import { customAlphabet } from "nanoid"
-
-import { Button } from "@/components/ui/button"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import { Input } from "@/components/ui/input"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { cn } from "@/lib/utils"
-import { LEGAL_WORK_FILTER_OPTIONS } from "@/types/legal"
-
-// nanoid 생성기
-const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6)
-
-// 법무업무 필터 스키마 정의
-const legalWorkFilterSchema = z.object({
- category: z.string().optional(),
- status: z.string().optional(),
- isUrgent: z.string().optional(),
- reviewDepartment: z.string().optional(),
- inquiryType: z.string().optional(),
- reviewer: z.string().optional(),
- legalResponder: z.string().optional(),
- vendorCode: z.string().optional(),
- vendorName: z.string().optional(),
- requestDateFrom: z.string().optional(),
- requestDateTo: z.string().optional(),
- consultationDateFrom: z.string().optional(),
- consultationDateTo: z.string().optional(),
-})
-
-type LegalWorkFilterFormValues = z.infer<typeof legalWorkFilterSchema>
-
-interface LegalWorkFilterSheetProps {
- isOpen: boolean;
- onClose: () => void;
- onFiltersApply: (filters: any[], joinOperator: "and" | "or") => void;
- isLoading?: boolean;
-}
-
-export function LegalWorkFilterSheet({
- isOpen,
- onClose,
- onFiltersApply,
- isLoading = false
-}: LegalWorkFilterSheetProps) {
- const router = useRouter()
- const [isPending, startTransition] = useTransition()
- const [joinOperator, setJoinOperator] = useState<"and" | "or">("and")
-
- // 폼 상태 초기화
- const form = useForm<LegalWorkFilterFormValues>({
- resolver: zodResolver(legalWorkFilterSchema),
- defaultValues: {
- category: "",
- status: "",
- isUrgent: "",
- reviewDepartment: "",
- inquiryType: "",
- reviewer: "",
- legalResponder: "",
- vendorCode: "",
- vendorName: "",
- requestDateFrom: "",
- requestDateTo: "",
- consultationDateFrom: "",
- consultationDateTo: "",
- },
- })
-
- // ✅ 폼 제출 핸들러 - 필터 배열 생성 및 전달
- async function onSubmit(data: LegalWorkFilterFormValues) {
- startTransition(async () => {
- try {
- const newFilters = []
-
- // 구분 필터
- if (data.category?.trim()) {
- newFilters.push({
- id: "category",
- value: data.category.trim(),
- type: "select",
- operator: "eq",
- rowId: generateId()
- })
- }
-
- // 상태 필터
- if (data.status?.trim()) {
- newFilters.push({
- id: "status",
- value: data.status.trim(),
- type: "select",
- operator: "eq",
- rowId: generateId()
- })
- }
-
- // 긴급여부 필터
- if (data.isUrgent?.trim()) {
- newFilters.push({
- id: "isUrgent",
- value: data.isUrgent.trim() === "true",
- type: "select",
- operator: "eq",
- rowId: generateId()
- })
- }
-
- // 검토부문 필터
- if (data.reviewDepartment?.trim()) {
- newFilters.push({
- id: "reviewDepartment",
- value: data.reviewDepartment.trim(),
- type: "select",
- operator: "eq",
- rowId: generateId()
- })
- }
-
- // 문의종류 필터
- if (data.inquiryType?.trim()) {
- newFilters.push({
- id: "inquiryType",
- value: data.inquiryType.trim(),
- type: "select",
- operator: "eq",
- rowId: generateId()
- })
- }
-
- // 요청자 필터
- if (data.reviewer?.trim()) {
- newFilters.push({
- id: "reviewer",
- value: data.reviewer.trim(),
- type: "text",
- operator: "iLike",
- rowId: generateId()
- })
- }
-
- // 법무답변자 필터
- if (data.legalResponder?.trim()) {
- newFilters.push({
- id: "legalResponder",
- value: data.legalResponder.trim(),
- type: "text",
- operator: "iLike",
- rowId: generateId()
- })
- }
-
- // 벤더 코드 필터
- if (data.vendorCode?.trim()) {
- newFilters.push({
- id: "vendorCode",
- value: data.vendorCode.trim(),
- type: "text",
- operator: "iLike",
- rowId: generateId()
- })
- }
-
- // 벤더명 필터
- if (data.vendorName?.trim()) {
- newFilters.push({
- id: "vendorName",
- value: data.vendorName.trim(),
- type: "text",
- operator: "iLike",
- rowId: generateId()
- })
- }
-
- // 검토 요청일 범위 필터
- if (data.requestDateFrom?.trim() && data.requestDateTo?.trim()) {
- // 범위 필터 (시작일과 종료일 모두 있는 경우)
- newFilters.push({
- id: "requestDate",
- value: [data.requestDateFrom.trim(), data.requestDateTo.trim()],
- type: "date",
- operator: "between",
- rowId: generateId()
- })
- } else if (data.requestDateFrom?.trim()) {
- // 시작일만 있는 경우 (이후 날짜)
- newFilters.push({
- id: "requestDate",
- value: data.requestDateFrom.trim(),
- type: "date",
- operator: "gte",
- rowId: generateId()
- })
- } else if (data.requestDateTo?.trim()) {
- // 종료일만 있는 경우 (이전 날짜)
- newFilters.push({
- id: "requestDate",
- value: data.requestDateTo.trim(),
- type: "date",
- operator: "lte",
- rowId: generateId()
- })
- }
-
- // 의뢰일 범위 필터
- if (data.consultationDateFrom?.trim() && data.consultationDateTo?.trim()) {
- // 범위 필터 (시작일과 종료일 모두 있는 경우)
- newFilters.push({
- id: "consultationDate",
- value: [data.consultationDateFrom.trim(), data.consultationDateTo.trim()],
- type: "date",
- operator: "between",
- rowId: generateId()
- })
- } else if (data.consultationDateFrom?.trim()) {
- // 시작일만 있는 경우 (이후 날짜)
- newFilters.push({
- id: "consultationDate",
- value: data.consultationDateFrom.trim(),
- type: "date",
- operator: "gte",
- rowId: generateId()
- })
- } else if (data.consultationDateTo?.trim()) {
- // 종료일만 있는 경우 (이전 날짜)
- newFilters.push({
- id: "consultationDate",
- value: data.consultationDateTo.trim(),
- type: "date",
- operator: "lte",
- rowId: generateId()
- })
- }
-
- console.log("=== 생성된 필터들 ===", newFilters);
- console.log("=== 조인 연산자 ===", joinOperator);
-
- // ✅ 부모 컴포넌트에 필터 전달
- onFiltersApply(newFilters, joinOperator);
-
- console.log("=== 필터 적용 완료 ===");
- } catch (error) {
- console.error("법무업무 필터 적용 오류:", error);
- }
- })
- }
-
- // ✅ 필터 초기화 핸들러
- function handleReset() {
- // 1. 폼 초기화
- form.reset({
- category: "",
- status: "",
- isUrgent: "",
- reviewDepartment: "",
- inquiryType: "",
- reviewer: "",
- legalResponder: "",
- vendorCode: "",
- vendorName: "",
- requestDateFrom: "",
- requestDateTo: "",
- consultationDateFrom: "",
- consultationDateTo: "",
- });
-
- // 2. 조인 연산자 초기화
- setJoinOperator("and");
-
- // 3. URL 파라미터 초기화 (필터를 빈 배열로 설정)
- const currentUrl = new URL(window.location.href);
- const newSearchParams = new URLSearchParams(currentUrl.search);
-
- // 필터 관련 파라미터 초기화
- newSearchParams.set("filters", JSON.stringify([]));
- newSearchParams.set("joinOperator", "and");
- newSearchParams.set("page", "1");
- newSearchParams.delete("search"); // 검색어 제거
-
- // URL 업데이트
- router.replace(`${currentUrl.pathname}?${newSearchParams.toString()}`);
-
- // 4. 빈 필터 배열 전달 (즉시 UI 업데이트를 위해)
- onFiltersApply([], "and");
-
- console.log("=== 필터 완전 초기화 완료 ===");
- }
-
- if (!isOpen) {
- return null;
- }
-
- return (
- <div className="flex flex-col h-full max-h-full bg-[#F5F7FB] px-6 sm:px-8" style={{backgroundColor:"#F5F7FB", paddingLeft:"2rem", paddingRight:"2rem"}}>
- {/* Filter Panel Header */}
- <div className="flex items-center justify-between px-6 min-h-[60px] shrink-0">
- <h3 className="text-lg font-semibold whitespace-nowrap">법무업무 검색 필터</h3>
- <Button
- variant="ghost"
- size="icon"
- onClick={onClose}
- className="h-8 w-8"
- >
- <X className="size-4" />
- </Button>
- </div>
-
- {/* Join Operator Selection */}
- <div className="px-6 shrink-0">
- <label className="text-sm font-medium">조건 결합 방식</label>
- <Select
- value={joinOperator}
- onValueChange={(value: "and" | "or") => setJoinOperator(value)}
- >
- <SelectTrigger className="h-8 w-[180px] mt-2 bg-white">
- <SelectValue placeholder="조건 결합 방식" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="and">모든 조건 충족 (AND)</SelectItem>
- <SelectItem value="or">하나라도 충족 (OR)</SelectItem>
- </SelectContent>
- </Select>
- </div>
-
- <Form {...form}>
- <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col h-full min-h-0">
- {/* Scrollable content area */}
- <div className="flex-1 min-h-0 overflow-y-auto px-6 pb-4">
- <div className="space-y-4 pt-2">
-
- {/* 구분 */}
- <FormField
- control={form.control}
- name="category"
- render={({ field }) => (
- <FormItem>
- <FormLabel>구분</FormLabel>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <FormControl>
- <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
- <div className="flex justify-between w-full">
- <SelectValue placeholder="구분 선택" />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="h-4 w-4 -mr-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("category", "");
- }}
- >
- <X className="size-3" />
- </Button>
- )}
- </div>
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {LEGAL_WORK_FILTER_OPTIONS.categories.map(option => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 상태 */}
- <FormField
- control={form.control}
- name="status"
- render={({ field }) => (
- <FormItem>
- <FormLabel>상태</FormLabel>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <FormControl>
- <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
- <div className="flex justify-between w-full">
- <SelectValue placeholder="상태 선택" />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="h-4 w-4 -mr-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("status", "");
- }}
- >
- <X className="size-3" />
- </Button>
- )}
- </div>
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {LEGAL_WORK_FILTER_OPTIONS.statuses.map(option => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 긴급여부 */}
- <FormField
- control={form.control}
- name="isUrgent"
- render={({ field }) => (
- <FormItem>
- <FormLabel>긴급여부</FormLabel>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <FormControl>
- <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
- <div className="flex justify-between w-full">
- <SelectValue placeholder="긴급여부 선택" />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="h-4 w-4 -mr-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("isUrgent", "");
- }}
- >
- <X className="size-3" />
- </Button>
- )}
- </div>
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="true">긴급</SelectItem>
- <SelectItem value="false">일반</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 검토부문 */}
- <FormField
- control={form.control}
- name="reviewDepartment"
- render={({ field }) => (
- <FormItem>
- <FormLabel>검토부문</FormLabel>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <FormControl>
- <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
- <div className="flex justify-between w-full">
- <SelectValue placeholder="검토부문 선택" />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="h-4 w-4 -mr-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("reviewDepartment", "");
- }}
- >
- <X className="size-3" />
- </Button>
- )}
- </div>
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {LEGAL_WORK_FILTER_OPTIONS.reviewDepartments.map(option => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 문의종류 */}
- <FormField
- control={form.control}
- name="inquiryType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>문의종류</FormLabel>
- <Select
- value={field.value}
- onValueChange={field.onChange}
- >
- <FormControl>
- <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
- <div className="flex justify-between w-full">
- <SelectValue placeholder="문의종류 선택" />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="h-4 w-4 -mr-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("inquiryType", "");
- }}
- >
- <X className="size-3" />
- </Button>
- )}
- </div>
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {LEGAL_WORK_FILTER_OPTIONS.inquiryTypes.map(option => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 요청자 */}
- <FormField
- control={form.control}
- name="reviewer"
- render={({ field }) => (
- <FormItem>
- <FormLabel>요청자</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- placeholder="요청자명 입력"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("reviewer", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 법무답변자 */}
- <FormField
- control={form.control}
- name="legalResponder"
- render={({ field }) => (
- <FormItem>
- <FormLabel>법무답변자</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- placeholder="법무답변자명 입력"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("legalResponder", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 벤더 코드 */}
- <FormField
- control={form.control}
- name="vendorCode"
- render={({ field }) => (
- <FormItem>
- <FormLabel>벤더 코드</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- placeholder="벤더 코드 입력"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("vendorCode", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 벤더명 */}
- <FormField
- control={form.control}
- name="vendorName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>벤더명</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- placeholder="벤더명 입력"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("vendorName", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 검토 요청일 범위 */}
- <div className="space-y-2">
- <label className="text-sm font-medium">검토 요청일</label>
-
- {/* 시작일 */}
- <FormField
- control={form.control}
- name="requestDateFrom"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="text-xs text-muted-foreground">시작일</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- type="date"
- placeholder="시작일 선택"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("requestDateFrom", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 종료일 */}
- <FormField
- control={form.control}
- name="requestDateTo"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="text-xs text-muted-foreground">종료일</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- type="date"
- placeholder="종료일 선택"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("requestDateTo", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- {/* 의뢰일 범위 */}
- <div className="space-y-2">
- <label className="text-sm font-medium">의뢰일</label>
-
- {/* 시작일 */}
- <FormField
- control={form.control}
- name="consultationDateFrom"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="text-xs text-muted-foreground">시작일</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- type="date"
- placeholder="시작일 선택"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("consultationDateFrom", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 종료일 */}
- <FormField
- control={form.control}
- name="consultationDateTo"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="text-xs text-muted-foreground">종료일</FormLabel>
- <FormControl>
- <div className="relative">
- <Input
- type="date"
- placeholder="종료일 선택"
- {...field}
- className={cn(field.value && "pr-8", "bg-white")}
- />
- {field.value && (
- <Button
- type="button"
- variant="ghost"
- size="icon"
- className="absolute right-0 top-0 h-full px-2"
- onClick={(e) => {
- e.stopPropagation();
- form.setValue("consultationDateTo", "");
- }}
- >
- <X className="size-3.5" />
- </Button>
- )}
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
-
- </div>
- </div>
-
- {/* Fixed buttons at bottom */}
- <div className="p-4 shrink-0">
- <div className="flex gap-2 justify-end">
- <Button
- type="button"
- variant="outline"
- onClick={handleReset}
- disabled={isPending}
- className="px-4"
- >
- 초기화
- </Button>
- <Button
- type="submit"
- variant="default"
- disabled={isPending || isLoading}
- className="px-4 bg-blue-600 hover:bg-blue-700 text-white"
- >
- <Search className="size-4 mr-2" />
- {isPending || isLoading ? "조회 중..." : "조회"}
- </Button>
- </div>
- </div>
- </form>
- </Form>
- </div>
- )
-} \ No newline at end of file
diff --git a/lib/legal-review/status/legal-works-columns.tsx b/lib/legal-review/status/legal-works-columns.tsx
deleted file mode 100644
index c94b414d..00000000
--- a/lib/legal-review/status/legal-works-columns.tsx
+++ /dev/null
@@ -1,222 +0,0 @@
-// components/legal-works/legal-works-columns.tsx
-"use client";
-
-import * as React from "react";
-import { type ColumnDef } from "@tanstack/react-table";
-import { Checkbox } from "@/components/ui/checkbox";
-import { Badge } from "@/components/ui/badge";
-import { Button } from "@/components/ui/button";
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu";
-import { Ellipsis, Paperclip } from "lucide-react";
-
-import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header";
-import type { DataTableRowAction } from "@/types/table";
-import { formatDate } from "@/lib/utils";
-import { LegalWorksDetailView } from "@/db/schema";
-
-// ────────────────────────────────────────────────────────────────────────────
-// 타입
-// ────────────────────────────────────────────────────────────────────────────
-interface GetColumnsProps {
- setRowAction: React.Dispatch<
- React.SetStateAction<DataTableRowAction<LegalWorksDetailView> | null>
- >;
-}
-
-// ────────────────────────────────────────────────────────────────────────────
-// 헬퍼
-// ────────────────────────────────────────────────────────────────────────────
-const statusVariant = (status: string) => {
- const map: Record<string, string> = {
- 검토요청: "bg-blue-100 text-blue-800 border-blue-200",
- 담당자배정: "bg-yellow-100 text-yellow-800 border-yellow-200",
- 검토중: "bg-orange-100 text-orange-800 border-orange-200",
- 답변완료: "bg-green-100 text-green-800 border-green-200",
- 재검토요청: "bg-purple-100 text-purple-800 border-purple-200",
- 보류: "bg-gray-100 text-gray-800 border-gray-200",
- 취소: "bg-red-100 text-red-800 border-red-200",
- };
- return map[status] ?? "bg-gray-100 text-gray-800 border-gray-200";
-};
-
-const categoryBadge = (category: string) => (
- <Badge
- variant={
- category === "CP" ? "default" : category === "GTC" ? "secondary" : "outline"
- }
- >
- {category}
- </Badge>
-);
-
-const urgentBadge = (isUrgent: boolean) =>
- isUrgent ? (
- <Badge variant="destructive" className="text-xs px-1 py-0">
- 긴급
- </Badge>
- ) : null;
-
-const header = (title: string) =>
- ({ column }: { column: any }) =>
- <DataTableColumnHeaderSimple column={column} title={title} />;
-
-// ────────────────────────────────────────────────────────────────────────────
-// 기본 컬럼
-// ────────────────────────────────────────────────────────────────────────────
-const BASE_COLUMNS: ColumnDef<LegalWorksDetailView>[] = [
- // 선택 체크박스
- {
- id: "select",
- header: ({ table }) => (
- <Checkbox
- checked={
- table.getIsAllPageRowsSelected() ||
- (table.getIsSomePageRowsSelected() && "indeterminate")
- }
- onCheckedChange={(v) => table.toggleAllPageRowsSelected(!!v)}
- aria-label="select all"
- className="translate-y-0.5"
- />
- ),
- cell: ({ row }) => (
- <Checkbox
- checked={row.getIsSelected()}
- onCheckedChange={(v) => row.toggleSelected(!!v)}
- aria-label="select row"
- className="translate-y-0.5"
- />
- ),
- enableSorting: false,
- enableHiding: false,
- size: 40,
- },
-
- // 번호, 구분, 상태
- {
- accessorKey: "id",
- header: header("No."),
- cell: ({ row }) => (
- <div className="w-[60px] text-center font-medium">{row.getValue("id")}</div>
- ),
- size: 80,
- },
- {
- accessorKey: "category",
- header: header("구분"),
- cell: ({ row }) => categoryBadge(row.getValue("category")),
- size: 80,
- },
- {
- accessorKey: "status",
- header: header("상태"),
- cell: ({ row }) => (
- <Badge className={statusVariant(row.getValue("status"))} variant="outline">
- {row.getValue("status")}
- </Badge>
- ),
- size: 120,
- },
-
- // 벤더 코드·이름
- {
- accessorKey: "vendorCode",
- header: header("벤더 코드"),
- cell: ({ row }) => <span className="font-mono text-sm">{row.getValue("vendorCode")}</span>,
- size: 120,
- },
- {
- accessorKey: "vendorName",
- header: header("벤더명"),
- cell: ({ row }) => {
- const name = row.getValue<string>("vendorName");
- return (
- <div className="flex items-center gap-2 truncate max-w-[200px]" title={name}>
- {urgentBadge(row.original.isUrgent)}
- {name}
- </div>
- );
- },
- size: 200,
- },
-
- // 날짜·첨부
- {
- accessorKey: "requestDate",
- header: header("답변요청일"),
- cell: ({ row }) => (
- <span className="text-sm">{formatDate(row.getValue("requestDate"), "KR")}</span>
- ),
- size: 100,
- },
- {
- accessorKey: "hasAttachment",
- header: header("첨부"),
- cell: ({ row }) =>
- row.getValue<boolean>("hasAttachment") ? (
- <Paperclip className="h-4 w-4 text-muted-foreground" />
- ) : (
- <span className="text-muted-foreground">-</span>
- ),
- size: 60,
- enableSorting: false,
- },
-];
-
-// ────────────────────────────────────────────────────────────────────────────
-// 액션 컬럼
-// ────────────────────────────────────────────────────────────────────────────
-const createActionsColumn = (
- setRowAction: React.Dispatch<
- React.SetStateAction<DataTableRowAction<LegalWorksDetailView> | null>
- >
-): ColumnDef<LegalWorksDetailView> => ({
- id: "actions",
- enableHiding: false,
- size: 40,
- minSize: 40,
- cell: ({ row }) => (
- <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" />
- </Button>
- </DropdownMenuTrigger>
-
- <DropdownMenuContent align="end" className="w-40">
- <DropdownMenuItem onSelect={() => setRowAction({ row, type: "view" })}>
- 상세보기
- </DropdownMenuItem>
- {row.original.status === "신규등록" && (
- <>
- <DropdownMenuItem onSelect={() => setRowAction({ row, type: "update" })}>
- 편집
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem onSelect={() => setRowAction({ row, type: "delete" })}>
- 삭제하기
- </DropdownMenuItem>
- </>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- ),
-});
-
-// ────────────────────────────────────────────────────────────────────────────
-// 메인 함수
-// ────────────────────────────────────────────────────────────────────────────
-export function getLegalWorksColumns({
- setRowAction,
-}: GetColumnsProps): ColumnDef<LegalWorksDetailView>[] {
- return [...BASE_COLUMNS, createActionsColumn(setRowAction)];
-}
diff --git a/lib/legal-review/status/legal-works-toolbar-actions.tsx b/lib/legal-review/status/legal-works-toolbar-actions.tsx
deleted file mode 100644
index 82fbc80a..00000000
--- a/lib/legal-review/status/legal-works-toolbar-actions.tsx
+++ /dev/null
@@ -1,286 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { type Table } from "@tanstack/react-table"
-import {
- Plus,
- Send,
- Download,
- RefreshCw,
- FileText,
- MessageSquare
-} from "lucide-react"
-import { toast } from "sonner"
-import { useRouter } from "next/navigation"
-import { useSession } from "next-auth/react"
-
-import { Button } from "@/components/ui/button"
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from "@/components/ui/dropdown-menu"
-import { CreateLegalWorkDialog } from "./create-legal-work-dialog"
-import { RequestReviewDialog } from "./request-review-dialog"
-import { exportTableToExcel } from "@/lib/export"
-import { getLegalWorks } from "../service"
-import { LegalWorksDetailView } from "@/db/schema"
-import { DeleteLegalWorksDialog } from "./delete-legal-works-dialog"
-
-type LegalWorkData = LegalWorksDetailView
-
-interface LegalWorksTableToolbarActionsProps {
- table: Table<LegalWorkData>
- onRefresh?: () => void
-}
-
-export function LegalWorksTableToolbarActions({
- table,
- onRefresh
-}: LegalWorksTableToolbarActionsProps) {
- const [isLoading, setIsLoading] = React.useState(false)
- const [createDialogOpen, setCreateDialogOpen] = React.useState(false)
- const [reviewDialogOpen, setReviewDialogOpen] = React.useState(false)
- const router = useRouter()
- const { data: session } = useSession()
-
- // 사용자 ID 가져오기
- const userId = React.useMemo(() => {
- return session?.user?.id ? Number(session.user.id) : 1
- }, [session])
-
- // 선택된 행들 - 단일 선택만 허용
- const selectedRows = table.getFilteredSelectedRowModel().rows
- const hasSelection = selectedRows.length > 0
- const isSingleSelection = selectedRows.length === 1
- const isMultipleSelection = selectedRows.length > 1
-
- // 선택된 단일 work
- const selectedWork = isSingleSelection ? selectedRows[0].original : null
-
- // const canDeleateReview = selectedRows.filter(v=>v.status === '신규등록')
-
-
- const deletableRows = React.useMemo(() => {
- return selectedRows.filter(row => {
- const status = row.original.status
- return status ==="신규등록"
- })
- }, [selectedRows])
-
- const hasDeletableRows = deletableRows.length > 0
-
- // 선택된 work의 상태 확인
- const canRequestReview = selectedWork?.status === "신규등록"
- const canAssign = selectedWork?.status === "신규등록"
-
- // ----------------------------------------------------------------
- // 신규 생성
- // ----------------------------------------------------------------
- const handleCreateNew = React.useCallback(() => {
- setCreateDialogOpen(true)
- }, [])
-
- // ----------------------------------------------------------------
- // 검토 요청 (단일 선택만)
- // ----------------------------------------------------------------
- const handleRequestReview = React.useCallback(() => {
- if (!isSingleSelection) {
- toast.error("검토요청은 한 건씩만 가능합니다. 하나의 항목만 선택해주세요.")
- return
- }
-
- if (!canRequestReview) {
- toast.error("신규등록 상태인 항목만 검토요청이 가능합니다.")
- return
- }
-
- setReviewDialogOpen(true)
- }, [isSingleSelection, canRequestReview])
-
- // ----------------------------------------------------------------
- // 다이얼로그 성공 핸들러
- // ----------------------------------------------------------------
- const handleActionSuccess = React.useCallback(() => {
- table.resetRowSelection()
- onRefresh?.()
- router.refresh()
- }, [table, onRefresh, router])
-
- // ----------------------------------------------------------------
- // 내보내기 핸들러
- // ----------------------------------------------------------------
- const handleExport = React.useCallback(() => {
- exportTableToExcel(table, {
- filename: "legal-works-list",
- excludeColumns: ["select", "actions"],
- })
- }, [table])
-
- // ----------------------------------------------------------------
- // 새로고침 핸들러
- // ----------------------------------------------------------------
- const handleRefresh = React.useCallback(async () => {
- setIsLoading(true)
- try {
- onRefresh?.()
- toast.success("데이터를 새로고침했습니다.")
- } catch (error) {
- console.error("새로고침 오류:", error)
- toast.error("새로고침 중 오류가 발생했습니다.")
- } finally {
- setIsLoading(false)
- }
- }, [onRefresh])
-
- return (
- <>
- <div className="flex items-center gap-2">
-
- {hasDeletableRows&&(
- <DeleteLegalWorksDialog
- legalWorks={deletableRows.map(row => row.original)}
- showTrigger={hasDeletableRows}
- onSuccess={() => {
- table.toggleAllRowsSelected(false)
- // onRefresh?.()
- }}
- />
- )}
- {/* 신규 생성 버튼 */}
- <Button
- variant="default"
- size="sm"
- className="gap-2"
- onClick={handleCreateNew}
- disabled={isLoading}
- >
- <Plus className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">신규 등록</span>
- </Button>
-
- {/* 유틸리티 버튼들 */}
- <div className="flex items-center gap-1 border-l pl-2 ml-2">
- <Button
- variant="outline"
- size="sm"
- onClick={handleRefresh}
- disabled={isLoading}
- className="gap-2"
- >
- <RefreshCw className={`size-4 ${isLoading ? 'animate-spin' : ''}`} aria-hidden="true" />
- <span className="hidden sm:inline">새로고침</span>
- </Button>
-
- <Button
- variant="outline"
- size="sm"
- onClick={handleExport}
- className="gap-2"
- >
- <Download className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">내보내기</span>
- </Button>
- </div>
-
- {/* 선택된 항목 액션 버튼들 */}
- {hasSelection && (
- <div className="flex items-center gap-1 border-l pl-2 ml-2">
- {/* 다중 선택 경고 메시지 */}
- {isMultipleSelection && (
- <div className="text-xs text-amber-600 bg-amber-50 px-2 py-1 rounded border border-amber-200">
- 검토요청은 한 건씩만 가능합니다
- </div>
- )}
-
- {/* 검토 요청 버튼 (단일 선택시만) */}
- {isSingleSelection && (
- <Button
- variant="default"
- size="sm"
- className="gap-2 bg-blue-600 hover:bg-blue-700"
- onClick={handleRequestReview}
- disabled={isLoading || !canRequestReview}
- >
- <Send className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">
- {canRequestReview ? "검토요청" : "검토불가"}
- </span>
- </Button>
- )}
-
- {/* 추가 액션 드롭다운 */}
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- variant="outline"
- size="sm"
- className="gap-2"
- disabled={isLoading}
- >
- <MessageSquare className="size-4" aria-hidden="true" />
- <span className="hidden sm:inline">추가 작업</span>
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end">
- <DropdownMenuItem
- onClick={() => toast.info("담당자 배정 기능을 준비 중입니다.")}
- disabled={!isSingleSelection || !canAssign}
- >
- <FileText className="size-4 mr-2" />
- 담당자 배정
- </DropdownMenuItem>
- <DropdownMenuSeparator />
- <DropdownMenuItem
- onClick={() => toast.info("상태 변경 기능을 준비 중입니다.")}
- disabled={!isSingleSelection}
- >
- <RefreshCw className="size-4 mr-2" />
- 상태 변경
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- </div>
- )}
-
- {/* 선택된 항목 정보 표시 */}
- {hasSelection && (
- <div className="flex items-center gap-1 border-l pl-2 ml-2">
- <div className="text-xs text-muted-foreground">
- {isSingleSelection ? (
- <>
- 선택: #{selectedWork?.id} ({selectedWork?.category})
- {selectedWork?.vendorName && ` | ${selectedWork.vendorName}`}
- {selectedWork?.status && ` | ${selectedWork.status}`}
- </>
- ) : (
- `선택: ${selectedRows.length}건 (개별 처리 필요)`
- )}
- </div>
- </div>
- )}
- </div>
-
- {/* 다이얼로그들 */}
- {/* 신규 생성 다이얼로그 */}
- <CreateLegalWorkDialog
- open={createDialogOpen}
- onOpenChange={setCreateDialogOpen}
- onSuccess={handleActionSuccess}
- onDataChange={onRefresh}
- />
-
- {/* 검토 요청 다이얼로그 - 단일 work 전달 */}
- {selectedWork && (
- <RequestReviewDialog
- open={reviewDialogOpen}
- onOpenChange={setReviewDialogOpen}
- work={selectedWork} // 단일 객체로 변경
- onSuccess={handleActionSuccess}
- />
- )}
- </>
- )
-} \ No newline at end of file
diff --git a/lib/legal-review/status/request-review-dialog.tsx b/lib/legal-review/status/request-review-dialog.tsx
deleted file mode 100644
index d99fc0e3..00000000
--- a/lib/legal-review/status/request-review-dialog.tsx
+++ /dev/null
@@ -1,983 +0,0 @@
-"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 { Loader2, Send, FileText, Clock, Upload, X, Building, User, Calendar } from "lucide-react"
-import { toast } from "sonner"
-
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import { Input } from "@/components/ui/input"
-import { Textarea } from "@/components/ui/textarea"
-import { Badge } from "@/components/ui/badge"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Switch } from "@/components/ui/switch"
-import TiptapEditor from "@/components/qna/tiptap-editor"
-import { canRequestReview, requestReview } from "../service"
-import { LegalWorksDetailView } from "@/db/schema"
-
-type LegalWorkData = LegalWorksDetailView
-
-interface RequestReviewDialogProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- work: LegalWorkData | null
- onSuccess?: () => void
-}
-
-// 검토요청 폼 스키마
-const requestReviewSchema = z.object({
- // 기본 검토 설정
- dueDate: z.string().min(1, "검토 완료 희망일을 선택해주세요"),
- assignee: z.string().optional(),
- notificationMethod: z.enum(["email", "internal", "both"]).default("both"),
-
- // 법무업무 상세 정보
- reviewDepartment: z.enum(["준법문의", "법무검토"]),
- inquiryType: z.enum(["국내계약", "국내자문", "해외계약", "해외자문"]).optional(),
-
- // 공통 필드
- title: z.string().min(1, "제목을 선택해주세요"),
- requestContent: z.string().min(1, "요청내용을 입력해주세요"),
-
- // 준법문의 전용 필드
- isPublic: z.boolean().default(false),
-
- // 법무검토 전용 필드들
- contractProjectName: z.string().optional(),
- contractType: z.string().optional(),
- contractCounterparty: z.string().optional(),
- counterpartyType: z.enum(["법인", "개인"]).optional(),
- contractPeriod: z.string().optional(),
- contractAmount: z.string().optional(),
- factualRelation: z.string().optional(),
- projectNumber: z.string().optional(),
- shipownerOrderer: z.string().optional(),
- projectType: z.string().optional(),
- governingLaw: z.string().optional(),
-}).refine((data) => {
- // 법무검토 선택시 문의종류 필수
- if (data.reviewDepartment === "법무검토" && !data.inquiryType) {
- return false;
- }
- return true;
-}, {
- message: "법무검토 선택시 문의종류를 선택해주세요",
- path: ["inquiryType"]
-});
-
-type RequestReviewFormValues = z.infer<typeof requestReviewSchema>
-
-export function RequestReviewDialog({
- open,
- onOpenChange,
- work,
- onSuccess
-}: RequestReviewDialogProps) {
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [attachments, setAttachments] = React.useState<File[]>([])
- const [editorContent, setEditorContent] = React.useState("")
- const [canRequest, setCanRequest] = React.useState(true)
- const [requestCheckMessage, setRequestCheckMessage] = React.useState("")
- const [isCustomTitle, setIsCustomTitle] = React.useState(false)
-
- // work의 category에 따라 기본 reviewDepartment 결정
- const getDefaultReviewDepartment = () => {
- return work?.category === "CP" ? "준법문의" : "법무검토"
- }
-
- const form = useForm<RequestReviewFormValues>({
- resolver: zodResolver(requestReviewSchema),
- defaultValues: {
- dueDate: "",
- assignee: "",
- notificationMethod: "both",
- reviewDepartment: getDefaultReviewDepartment(),
- title: getDefaultReviewDepartment() === "준법문의" ? "CP검토" : "GTC검토",
- requestContent: "",
- isPublic: false,
- },
- })
-
- // work 변경시 검토요청 가능 여부 확인
- React.useEffect(() => {
- if (work && open) {
- canRequestReview(work.id).then((result) => {
- setCanRequest(result.canRequest)
- setRequestCheckMessage(result.reason || "")
- })
-
- const defaultDepartment = work.category === "CP" ? "준법문의" : "법무검토"
- form.setValue("reviewDepartment", defaultDepartment)
- }
- }, [work, open, form])
-
- // 검토부문 감시
- const reviewDepartment = form.watch("reviewDepartment")
- const inquiryType = form.watch("inquiryType")
- const titleValue = form.watch("title")
-
- // 조건부 필드 활성화 로직
- const isContractTypeActive = inquiryType && ["국내계약", "해외계약", "해외자문"].includes(inquiryType)
- const isDomesticContractFieldsActive = inquiryType === "국내계약"
- const isFactualRelationActive = inquiryType && ["국내자문", "해외자문"].includes(inquiryType)
- const isOverseasFieldsActive = inquiryType && ["해외계약", "해외자문"].includes(inquiryType)
-
- // 제목 "기타" 선택 여부 확인
- // const isTitleOther = titleValue === "기타"
-
- // 검토부문 변경시 관련 필드 초기화
- React.useEffect(() => {
- if (reviewDepartment === "준법문의") {
- setIsCustomTitle(false)
- form.setValue("inquiryType", undefined)
- // 제목 초기화 (기타 상태였거나 값이 없으면 기본값으로)
- const currentTitle = form.getValues("title")
- if (!currentTitle || currentTitle === "GTC검토") {
- form.setValue("title", "CP검토")
- }
- // 법무검토 전용 필드들 초기화
- form.setValue("contractProjectName", "")
- form.setValue("contractType", "")
- form.setValue("contractCounterparty", "")
- form.setValue("counterpartyType", undefined)
- form.setValue("contractPeriod", "")
- form.setValue("contractAmount", "")
- form.setValue("factualRelation", "")
- form.setValue("projectNumber", "")
- form.setValue("shipownerOrderer", "")
- form.setValue("projectType", "")
- form.setValue("governingLaw", "")
- } else {
- setIsCustomTitle(false)
- // 제목 초기화 (기타 상태였거나 값이 없으면 기본값으로)
- const currentTitle = form.getValues("title")
- if (!currentTitle || currentTitle === "CP검토") {
- form.setValue("title", "GTC검토")
- }
- form.setValue("isPublic", false)
- }
- }, [reviewDepartment, form])
-
- // 문의종류 변경시 관련 필드 초기화
- React.useEffect(() => {
- if (inquiryType) {
- // 계약서 종류 초기화 (옵션이 달라지므로)
- form.setValue("contractType", "")
-
- // 조건에 맞지 않는 필드들 초기화
- if (!isDomesticContractFieldsActive) {
- form.setValue("contractCounterparty", "")
- form.setValue("counterpartyType", undefined)
- form.setValue("contractPeriod", "")
- form.setValue("contractAmount", "")
- }
-
- if (!isFactualRelationActive) {
- form.setValue("factualRelation", "")
- }
-
- if (!isOverseasFieldsActive) {
- form.setValue("projectNumber", "")
- form.setValue("shipownerOrderer", "")
- form.setValue("projectType", "")
- form.setValue("governingLaw", "")
- }
- }
- }, [inquiryType, isDomesticContractFieldsActive, isFactualRelationActive, isOverseasFieldsActive, form])
-
- // 에디터 내용이 변경될 때 폼에 반영
- React.useEffect(() => {
- form.setValue("requestContent", editorContent)
- }, [editorContent, form])
-
- // 첨부파일 처리
- const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
- const files = Array.from(event.target.files || [])
- setAttachments(prev => [...prev, ...files])
- }
-
- const removeAttachment = (index: number) => {
- setAttachments(prev => prev.filter((_, i) => i !== index))
- }
-
- // 폼 제출
- async function onSubmit(data: RequestReviewFormValues) {
- if (!work) return
-
- console.log("Request review data:", data)
- console.log("Work to review:", work)
- console.log("Attachments:", attachments)
- setIsSubmitting(true)
-
- try {
- const result = await requestReview(work.id, data, attachments)
-
- if (result.success) {
- toast.success(result.data?.message || `법무업무 #${work.id}에 대한 검토요청이 완료되었습니다.`)
- onOpenChange(false)
- handleReset()
- onSuccess?.()
- } else {
- toast.error(result.error || "검토요청 중 오류가 발생했습니다.")
- }
- } catch (error) {
- console.error("Error requesting review:", error)
- toast.error("검토요청 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- }
- }
-
- // 폼 리셋 함수
- const handleReset = () => {
- const defaultDepartment = getDefaultReviewDepartment()
- setIsCustomTitle(false) // 추가
-
- form.reset({
- dueDate: "",
- assignee: "",
- notificationMethod: "both",
- reviewDepartment: defaultDepartment,
- title: defaultDepartment === "준법문의" ? "CP검토" : "GTC검토",
- requestContent: "",
- isPublic: false,
- })
- setAttachments([])
- setEditorContent("")
- }
-
- // 다이얼로그 닫기 핸들러
- const handleOpenChange = (open: boolean) => {
- onOpenChange(open)
- if (!open) {
- handleReset()
- }
- }
-
- // 제목 옵션 (검토부문에 따라 다름)
- const getTitleOptions = () => {
- if (reviewDepartment === "준법문의") {
- return [
- { value: "CP검토", label: "CP검토" },
- { value: "기타", label: "기타 (직접입력)" }
- ]
- } else {
- return [
- { value: "GTC검토", label: "GTC검토" },
- { value: "기타", label: "기타 (직접입력)" }
- ]
- }
- }
-
- // 계약서 종류 옵션 (문의종류에 따라 다름)
- const getContractTypeOptions = () => {
- if (inquiryType === "국내계약") {
- return [
- { value: "공사도급계약", label: "공사도급계약" },
- { value: "제작납품계약", label: "제작납품계약" },
- { value: "자재매매계약", label: "자재매매계약" },
- { value: "용역위탁계약", label: "용역위탁계약" },
- { value: "기술사용 및 개발계약", label: "기술사용 및 개발계약" },
- { value: "운송 및 자재관리 계약", label: "운송 및 자재관리 계약" },
- { value: "자문 등 위임계약", label: "자문 등 위임계약" },
- { value: "양해각서", label: "양해각서" },
- { value: "양수도 계약", label: "양수도 계약" },
- { value: "합의서", label: "합의서" },
- { value: "공동도급(운영)협약서", label: "공동도급(운영)협약서" },
- { value: "협정서", label: "협정서" },
- { value: "약정서", label: "약정서" },
- { value: "협의서", label: "협의서" },
- { value: "기타", label: "기타" },
- { value: "비밀유지계약서", label: "비밀유지계약서" },
- { value: "분양계약서", label: "분양계약서" },
- ]
- } else {
- // 해외계약/해외자문
- return [
- { value: "Shipbuilding Contract", label: "Shipbuilding Contract" },
- { value: "Offshore Contract (EPCI, FEED)", label: "Offshore Contract (EPCI, FEED)" },
- { value: "Supplementary / Addendum", label: "Supplementary / Addendum" },
- { value: "Subcontract / GTC / PTC / PO", label: "Subcontract / GTC / PTC / PO" },
- { value: "Novation / Assignment", label: "Novation / Assignment" },
- { value: "NDA (Confidential, Secrecy)", label: "NDA (Confidential, Secrecy)" },
- { value: "Warranty", label: "Warranty" },
- { value: "Waiver and Release", label: "Waiver and Release" },
- { value: "Bond (PG, RG, Advanced Payment)", label: "Bond (PG, RG, Advanced Payment)" },
- { value: "MOU / LOI / LOA", label: "MOU / LOI / LOA" },
- { value: "Power of Attorney (POA)", label: "Power of Attorney (POA)" },
- { value: "Commission Agreement", label: "Commission Agreement" },
- { value: "Consortium Agreement", label: "Consortium Agreement" },
- { value: "JV / JDP Agreement", label: "JV / JDP Agreement" },
- { value: "Engineering Service Contract", label: "Engineering Service Contract" },
- { value: "Consultancy Service Agreement", label: "Consultancy Service Agreement" },
- { value: "Purchase / Lease Agreement", label: "Purchase / Lease Agreement" },
- { value: "Financial / Loan / Covenant", label: "Financial / Loan / Covenant" },
- { value: "Other Contract / Agreement", label: "Other Contract / Agreement" },
- ]
- }
- }
-
- // 프로젝트 종류 옵션
- const getProjectTypeOptions = () => {
- return [
- { value: "BARGE VESSEL", label: "BARGE VESSEL" },
- { value: "BULK CARRIER", label: "BULK CARRIER" },
- { value: "CHEMICAL CARRIER", label: "CHEMICAL CARRIER" },
- { value: "FULL CONTAINER", label: "FULL CONTAINER" },
- { value: "CRUDE OIL TANKER", label: "CRUDE OIL TANKER" },
- { value: "CRUISE SHIP", label: "CRUISE SHIP" },
- { value: "DRILL SHIP", label: "DRILL SHIP" },
- { value: "FIELD DEVELOPMENT SHIP", label: "FIELD DEVELOPMENT SHIP" },
- { value: "FLOATING PRODUCTION STORAGE OFFLOADING", label: "FLOATING PRODUCTION STORAGE OFFLOADING" },
- { value: "CAR-FERRY & PASSENGER VESSEL", label: "CAR-FERRY & PASSENGER VESSEL" },
- { value: "FLOATING STORAGE OFFLOADING", label: "FLOATING STORAGE OFFLOADING" },
- { value: "HEAVY DECK CARGO", label: "HEAVY DECK CARGO" },
- { value: "PRODUCT OIL TANKER", label: "PRODUCT OIL TANKER" },
- { value: "HIGH SPEED LINER", label: "HIGH SPEED LINER" },
- { value: "JACK-UP", label: "JACK-UP" },
- { value: "LIQUEFIED NATURAL GAS CARRIER", label: "LIQUEFIED NATURAL GAS CARRIER" },
- { value: "LIQUEFIED PETROLEUM GAS CARRIER", label: "LIQUEFIED PETROLEUM GAS CARRIER" },
- { value: "MULTIPURPOSE CARGO CARRIER", label: "MULTIPURPOSE CARGO CARRIER" },
- { value: "ORE-BULK-OIL CARRIER", label: "ORE-BULK-OIL CARRIER" },
- { value: "OIL TANKER", label: "OIL TANKER" },
- { value: "OTHER VESSEL", label: "OTHER VESSEL" },
- { value: "PURE CAR CARRIER", label: "PURE CAR CARRIER" },
- { value: "PRODUCT CARRIER", label: "PRODUCT CARRIER" },
- { value: "PLATFORM", label: "PLATFORM" },
- { value: "PUSHER", label: "PUSHER" },
- { value: "REEFER TRANSPORT VESSEL", label: "REEFER TRANSPORT VESSEL" },
- { value: "ROLL-ON ROLL-OFF VESSEL", label: "ROLL-ON ROLL-OFF VESSEL" },
- { value: "SEMI RIG", label: "SEMI RIG" },
- { value: "SUPPLY ANCHOR HANDLING VESSEL", label: "SUPPLY ANCHOR HANDLING VESSEL" },
- { value: "SHUTTLE TANKER", label: "SHUTTLE TANKER" },
- { value: "SUPPLY VESSEL", label: "SUPPLY VESSEL" },
- { value: "TOPSIDE", label: "TOPSIDE" },
- { value: "TUG SUPPLY VESSEL", label: "TUG SUPPLY VESSEL" },
- { value: "VERY LARGE CRUDE OIL CARRIER", label: "VERY LARGE CRUDE OIL CARRIER" },
- { value: "WELL INTERVENTION SHIP", label: "WELL INTERVENTION SHIP" },
- { value: "WIND TURBINE INSTALLATION VESSEL", label: "WIND TURBINE INSTALLATION VESSEL" },
- { value: "기타", label: "기타" },
- ]
- }
-
- if (!work) {
- return null
- }
-
- // 검토요청 불가능한 경우 안내 메시지
- if (!canRequest) {
- return (
- <Dialog open={open} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-md">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2 text-amber-600">
- <FileText className="h-5 w-5" />
- 검토요청 불가
- </DialogTitle>
- <DialogDescription className="pt-4">
- {requestCheckMessage}
- </DialogDescription>
- </DialogHeader>
- <div className="flex justify-end pt-4">
- <Button onClick={() => onOpenChange(false)}>확인</Button>
- </div>
- </DialogContent>
- </Dialog>
- )
- }
-
- return (
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent className="max-w-4xl h-[90vh] p-0 flex flex-col">
- {/* 고정 헤더 */}
- <div className="flex-shrink-0 p-6 border-b">
- <DialogHeader>
- <DialogTitle className="flex items-center gap-2">
- <Send className="h-5 w-5" />
- 검토요청 발송
- </DialogTitle>
- <DialogDescription>
- 법무업무 #{work.id}에 대한 상세한 검토를 요청합니다.
- </DialogDescription>
- </DialogHeader>
- </div>
-
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col flex-1 min-h-0"
- >
- {/* 스크롤 가능한 콘텐츠 영역 */}
- <div className="flex-1 overflow-y-auto p-6">
- <div className="space-y-6">
- {/* 선택된 업무 정보 */}
- <Card className="bg-blue-50 border-blue-200">
- <CardHeader>
- <CardTitle className="text-lg flex items-center gap-2">
- <FileText className="h-5 w-5" />
- 검토 대상 업무
- </CardTitle>
- </CardHeader>
- <CardContent>
- <div className="grid grid-cols-2 gap-4 text-sm">
- <div className="space-y-2">
- <div className="flex items-center gap-2">
- <span className="font-medium">업무 ID:</span>
- <Badge variant="outline">#{work.id}</Badge>
- </div>
- <div className="flex items-center gap-2">
- <span className="font-medium">구분:</span>
- <Badge variant={work.category === "CP" ? "default" : "secondary"}>
- {work.category}
- </Badge>
- {work.isUrgent && (
- <Badge variant="destructive" className="text-xs">
- 긴급
- </Badge>
- )}
- </div>
- <div className="flex items-center gap-2">
- <Building className="h-4 w-4" />
- <span className="font-medium">벤더:</span>
- <span>{work.vendorCode} - {work.vendorName}</span>
- </div>
- </div>
- <div className="space-y-2">
- <div className="flex items-center gap-2">
- <User className="h-4 w-4" />
- <span className="font-medium">요청자:</span>
- <span>{work.reviewer || "미지정"}</span>
- </div>
- <div className="flex items-center gap-2">
- <Calendar className="h-4 w-4" />
- <span className="font-medium">답변요청일:</span>
- <span>{work.requestDate || "미설정"}</span>
- </div>
- <div className="flex items-center gap-2">
- <span className="font-medium">상태:</span>
- <Badge variant="outline">{work.status}</Badge>
- </div>
- </div>
- </div>
- </CardContent>
- </Card>
-
- {/* 기본 설정 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">기본 설정</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {/* 검토 완료 희망일 */}
- <FormField
- control={form.control}
- name="dueDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel className="flex items-center gap-2">
- <Clock className="h-4 w-4" />
- 검토 완료 희망일
- </FormLabel>
- <FormControl>
- <Input type="date" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </CardContent>
- </Card>
-
- {/* 법무업무 상세 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">법무업무 상세 정보</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {/* 검토부문 */}
- <FormField
- control={form.control}
- name="reviewDepartment"
- render={({ field }) => (
- <FormItem>
- <FormLabel>검토부문</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="검토부문 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="준법문의">준법문의</SelectItem>
- <SelectItem value="법무검토">법무검토</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 문의종류 (법무검토 선택시만) */}
- {reviewDepartment === "법무검토" && (
- <FormField
- control={form.control}
- name="inquiryType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>문의종류</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="문의종류 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="국내계약">국내계약</SelectItem>
- <SelectItem value="국내자문">국내자문</SelectItem>
- <SelectItem value="해외계약">해외계약</SelectItem>
- <SelectItem value="해외자문">해외자문</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
-
- {/* 제목 - 조건부 렌더링 */}
- <FormField
- control={form.control}
- name="title"
- render={({ field }) => (
- <FormItem>
- <FormLabel>제목</FormLabel>
- {!isCustomTitle ? (
- // Select 모드
- <Select
- onValueChange={(value) => {
- if (value === "기타") {
- setIsCustomTitle(true)
- field.onChange("") // 빈 값으로 초기화
- } else {
- field.onChange(value)
- }
- }}
- value={field.value}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="제목 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {getTitleOptions().map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- ) : (
- // Input 모드 (기타 선택시)
- <div className="space-y-2">
- <div className="flex items-center gap-2">
- <Badge variant="outline" className="text-xs">기타</Badge>
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => {
- const defaultTitle = reviewDepartment === "준법문의" ? "CP검토" : "GTC검토"
- form.setValue("title", defaultTitle)
- setIsCustomTitle(false) // 상태 초기화
- }}
- className="h-6 text-xs"
- >
- 선택 모드로 돌아가기
- </Button>
- </div>
- <FormControl>
- <Input
- placeholder="제목을 직접 입력하세요"
- value={field.value}
- onChange={(e) => field.onChange(e.target.value)}
- autoFocus
- />
- </FormControl>
- </div>
- )}
- <FormMessage />
- </FormItem>
- )}
-/>
-
- {/* 준법문의 전용 필드들 */}
- {reviewDepartment === "준법문의" && (
- <FormField
- control={form.control}
- name="isPublic"
- render={({ field }) => (
- <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
- <div className="space-y-0.5">
- <FormLabel className="text-base">공개여부</FormLabel>
- <div className="text-sm text-muted-foreground">
- 준법문의 공개 설정
- </div>
- </div>
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- </FormItem>
- )}
- />
- )}
-
- {/* 법무검토 전용 필드들 */}
- {reviewDepartment === "법무검토" && (
- <div className="space-y-4">
- {/* 계약명/프로젝트명 */}
- <FormField
- control={form.control}
- name="contractProjectName"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약명/프로젝트명</FormLabel>
- <FormControl>
- <Input placeholder="계약명 또는 프로젝트명 입력" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약서 종류 - 조건부 활성화 */}
- {isContractTypeActive && (
- <FormField
- control={form.control}
- name="contractType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약서 종류</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="계약서 종류 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {getContractTypeOptions().map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
-
- {/* 국내계약 전용 필드들 */}
- {isDomesticContractFieldsActive && (
- <div className="grid grid-cols-2 gap-4">
- {/* 계약상대방 */}
- <FormField
- control={form.control}
- name="contractCounterparty"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약상대방</FormLabel>
- <FormControl>
- <Input placeholder="계약상대방 입력" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약상대방 구분 */}
- <FormField
- control={form.control}
- name="counterpartyType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약상대방 구분</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="구분 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="법인">법인</SelectItem>
- <SelectItem value="개인">개인</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약기간 */}
- <FormField
- control={form.control}
- name="contractPeriod"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약기간</FormLabel>
- <FormControl>
- <Input placeholder="계약기간 입력" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 계약금액 */}
- <FormField
- control={form.control}
- name="contractAmount"
- render={({ field }) => (
- <FormItem>
- <FormLabel>계약금액</FormLabel>
- <FormControl>
- <Input placeholder="계약금액 입력" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- )}
-
- {/* 사실관계 - 조건부 활성화 */}
- {isFactualRelationActive && (
- <FormField
- control={form.control}
- name="factualRelation"
- render={({ field }) => (
- <FormItem>
- <FormLabel>사실관계</FormLabel>
- <FormControl>
- <Textarea
- placeholder="사실관계를 상세히 입력해주세요"
- className="min-h-[80px]"
- {...field}
- />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- )}
-
- {/* 해외 관련 필드들 - 조건부 활성화 */}
- {isOverseasFieldsActive && (
- <div className="grid grid-cols-2 gap-4">
- {/* 프로젝트번호 */}
- <FormField
- control={form.control}
- name="projectNumber"
- render={({ field }) => (
- <FormItem>
- <FormLabel>프로젝트번호</FormLabel>
- <FormControl>
- <Input placeholder="프로젝트번호 입력" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 선주/발주처 */}
- <FormField
- control={form.control}
- name="shipownerOrderer"
- render={({ field }) => (
- <FormItem>
- <FormLabel>선주/발주처</FormLabel>
- <FormControl>
- <Input placeholder="선주/발주처 입력" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 프로젝트종류 */}
- <FormField
- control={form.control}
- name="projectType"
- render={({ field }) => (
- <FormItem>
- <FormLabel>프로젝트종류</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="프로젝트종류 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {getProjectTypeOptions().map((option) => (
- <SelectItem key={option.value} value={option.value}>
- {option.label}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 준거법 */}
- <FormField
- control={form.control}
- name="governingLaw"
- render={({ field }) => (
- <FormItem>
- <FormLabel>준거법</FormLabel>
- <FormControl>
- <Input placeholder="준거법 입력" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </div>
- )}
- </div>
- )}
-
- {/* 요청내용 - TiptapEditor로 교체 */}
- <FormField
- control={form.control}
- name="requestContent"
- render={({ field }) => (
- <FormItem>
- <FormLabel>요청내용</FormLabel>
- <FormControl>
- <div className="min-h-[250px]">
- <TiptapEditor
- content={editorContent}
- setContent={setEditorContent}
- disabled={isSubmitting}
- height="250px"
- />
- </div>
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 첨부파일 */}
- <div className="space-y-2">
- <FormLabel>첨부파일</FormLabel>
- <div className="border-2 border-dashed border-muted-foreground/25 rounded-lg p-4">
- <input
- type="file"
- multiple
- onChange={handleFileChange}
- className="hidden"
- id="file-upload"
- />
- <label
- htmlFor="file-upload"
- className="flex flex-col items-center justify-center cursor-pointer"
- >
- <Upload className="h-8 w-8 text-muted-foreground mb-2" />
- <span className="text-sm text-muted-foreground">
- 파일을 선택하거나 여기로 드래그하세요
- </span>
- </label>
- </div>
-
- {/* 선택된 파일 목록 */}
- {attachments.length > 0 && (
- <div className="space-y-2">
- {attachments.map((file, index) => (
- <div key={index} className="flex items-center justify-between bg-muted/50 p-2 rounded">
- <span className="text-sm truncate">{file.name}</span>
- <Button
- type="button"
- variant="ghost"
- size="sm"
- onClick={() => removeAttachment(index)}
- >
- <X className="h-4 w-4" />
- </Button>
- </div>
- ))}
- </div>
- )}
- </div>
- </CardContent>
- </Card>
- </div>
- </div>
-
- {/* 고정 버튼 영역 */}
- <div className="flex-shrink-0 border-t p-6">
- <div className="flex justify-end gap-3">
- <Button
- type="button"
- variant="outline"
- onClick={() => handleOpenChange(false)}
- disabled={isSubmitting}
- >
- 취소
- </Button>
- <Button
- type="submit"
- disabled={isSubmitting}
- className="bg-blue-600 hover:bg-blue-700"
- >
- {isSubmitting ? (
- <>
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
- 발송 중...
- </>
- ) : (
- <>
- <Send className="mr-2 h-4 w-4" />
- 검토요청 발송
- </>
- )}
- </Button>
- </div>
- </div>
- </form>
- </Form>
- </DialogContent>
- </Dialog>
- )
-} \ No newline at end of file
diff --git a/lib/legal-review/status/update-legal-work-dialog.tsx b/lib/legal-review/status/update-legal-work-dialog.tsx
deleted file mode 100644
index d9157d3c..00000000
--- a/lib/legal-review/status/update-legal-work-dialog.tsx
+++ /dev/null
@@ -1,385 +0,0 @@
-"use client"
-
-import * as React from "react"
-import { useRouter } from "next/navigation"
-import { useForm } from "react-hook-form"
-import { zodResolver } from "@hookform/resolvers/zod"
-import * as z from "zod"
-import { Loader2, Check, ChevronsUpDown, Edit } from "lucide-react"
-import { toast } from "sonner"
-
-import { Button } from "@/components/ui/button"
-import {
- Sheet,
- SheetClose,
- SheetContent,
- SheetDescription,
- SheetFooter,
- SheetHeader,
- SheetTitle,
-} from "@/components/ui/sheet"
-import {
- Form,
- FormControl,
- FormField,
- FormItem,
- FormLabel,
- FormMessage,
-} from "@/components/ui/form"
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select"
-import {
- Command,
- CommandEmpty,
- CommandGroup,
- CommandInput,
- CommandItem,
- CommandList,
-} from "@/components/ui/command"
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover"
-import { Input } from "@/components/ui/input"
-import { Badge } from "@/components/ui/badge"
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
-import { Switch } from "@/components/ui/switch"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import { cn } from "@/lib/utils"
-import { getVendorsForSelection } from "@/lib/b-rfq/service"
-import { LegalWorksDetailView } from "@/db/schema"
-// import { updateLegalWork } from "../service"
-
-type LegalWorkData = LegalWorksDetailView
-
-interface EditLegalWorkSheetProps {
- open: boolean
- onOpenChange: (open: boolean) => void
- work: LegalWorkData | null
- onSuccess?: () => void
- onDataChange?: () => void
-}
-
-// 편집용 폼 스키마 (신규등록 상태에서만 기본 정보만 편집)
-const editLegalWorkSchema = z.object({
- category: z.enum(["CP", "GTC", "기타"]),
- vendorId: z.number().min(1, "벤더를 선택해주세요"),
- isUrgent: z.boolean().default(false),
- requestDate: z.string().min(1, "답변요청일을 선택해주세요"),
-})
-
-type EditLegalWorkFormValues = z.infer<typeof editLegalWorkSchema>
-
-interface Vendor {
- id: number
- vendorName: string
- vendorCode: string
- country: string
- taxId: string
- status: string
-}
-
-export function EditLegalWorkSheet({
- open,
- onOpenChange,
- work,
- onSuccess,
- onDataChange
-}: EditLegalWorkSheetProps) {
- const router = useRouter()
- const [isSubmitting, setIsSubmitting] = React.useState(false)
- const [vendors, setVendors] = React.useState<Vendor[]>([])
- const [vendorsLoading, setVendorsLoading] = React.useState(false)
- const [vendorOpen, setVendorOpen] = React.useState(false)
-
- 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)
- }
- }, [])
-
- const form = useForm<EditLegalWorkFormValues>({
- resolver: zodResolver(editLegalWorkSchema),
- defaultValues: {
- category: "CP",
- vendorId: 0,
- isUrgent: false,
- requestDate: "",
- },
- })
-
- // work 데이터가 변경될 때 폼 값 업데이트
- React.useEffect(() => {
- if (work && open) {
- form.reset({
- category: work.category as "CP" | "GTC" | "기타",
- vendorId: work.vendorId || 0,
- isUrgent: work.isUrgent || false,
- requestDate: work.requestDate ? new Date(work.requestDate).toISOString().split('T')[0] : "",
- })
- }
- }, [work, open, form])
-
- React.useEffect(() => {
- if (open) {
- loadVendors()
- }
- }, [open, loadVendors])
-
- // 폼 제출
- async function onSubmit(data: EditLegalWorkFormValues) {
- if (!work) return
-
- console.log("Updating legal work with data:", data)
- setIsSubmitting(true)
-
- try {
- const result = await updateLegalWork(work.id, data)
-
- if (result.success) {
- toast.success(result.data?.message || "법무업무가 성공적으로 수정되었습니다.")
- onOpenChange(false)
- onSuccess?.()
- onDataChange?.()
- router.refresh()
- } else {
- toast.error(result.error || "수정 중 오류가 발생했습니다.")
- }
- } catch (error) {
- console.error("Error updating legal work:", error)
- toast.error("수정 중 오류가 발생했습니다.")
- } finally {
- setIsSubmitting(false)
- }
- }
-
- // 시트 닫기 핸들러
- const handleOpenChange = (openState: boolean) => {
- onOpenChange(openState)
- if (!openState) {
- form.reset()
- }
- }
-
- // 선택된 벤더 정보
- const selectedVendor = vendors.find(v => v.id === form.watch("vendorId"))
-
- if (!work) {
- return null
- }
-
- return (
- <Sheet open={open} onOpenChange={handleOpenChange}>
- <SheetContent className="w-[600px] sm:w-[800px] p-0 flex flex-col" style={{maxWidth:900}}>
- {/* 고정 헤더 */}
- <SheetHeader className="flex-shrink-0 p-6 border-b">
- <SheetTitle className="flex items-center gap-2">
- <Edit className="h-5 w-5" />
- 법무업무 편집
- </SheetTitle>
- <SheetDescription>
- 법무업무 #{work.id}의 기본 정보를 수정합니다. (신규등록 상태에서만 편집 가능)
- </SheetDescription>
- </SheetHeader>
-
- <Form {...form}>
- <form
- onSubmit={form.handleSubmit(onSubmit)}
- className="flex flex-col flex-1 min-h-0"
- >
- {/* 스크롤 가능한 콘텐츠 영역 */}
- <ScrollArea className="flex-1 p-6">
- <div className="space-y-6">
- {/* 기본 정보 */}
- <Card>
- <CardHeader>
- <CardTitle className="text-lg">기본 정보</CardTitle>
- </CardHeader>
- <CardContent className="space-y-4">
- {/* 구분 */}
- <FormField
- control={form.control}
- name="category"
- render={({ field }) => (
- <FormItem>
- <FormLabel>구분</FormLabel>
- <Select onValueChange={field.onChange} value={field.value}>
- <FormControl>
- <SelectTrigger>
- <SelectValue placeholder="구분 선택" />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- <SelectItem value="CP">CP</SelectItem>
- <SelectItem value="GTC">GTC</SelectItem>
- <SelectItem value="기타">기타</SelectItem>
- </SelectContent>
- </Select>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 긴급여부 */}
- <FormField
- control={form.control}
- name="isUrgent"
- render={({ field }) => (
- <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
- <div className="space-y-0.5">
- <FormLabel className="text-base">긴급 요청</FormLabel>
- <div className="text-sm text-muted-foreground">
- 긴급 처리가 필요한 경우 체크
- </div>
- </div>
- <FormControl>
- <Switch
- checked={field.value}
- onCheckedChange={field.onChange}
- />
- </FormControl>
- </FormItem>
- )}
- />
-
- {/* 벤더 선택 */}
- <FormField
- control={form.control}
- name="vendorId"
- render={({ field }) => (
- <FormItem>
- <FormLabel>벤더</FormLabel>
- <Popover open={vendorOpen} onOpenChange={setVendorOpen}>
- <PopoverTrigger asChild>
- <FormControl>
- <Button
- variant="outline"
- role="combobox"
- aria-expanded={vendorOpen}
- className="w-full justify-between"
- >
- {selectedVendor ? (
- <span className="flex items-center gap-2">
- <Badge variant="outline">{selectedVendor.vendorCode}</Badge>
- {selectedVendor.vendorName}
- </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="벤더 검색..." />
- <CommandList>
- <CommandEmpty>검색 결과가 없습니다.</CommandEmpty>
- <CommandGroup>
- {vendors.map((vendor) => (
- <CommandItem
- key={vendor.id}
- value={`${vendor.vendorCode} ${vendor.vendorName}`}
- onSelect={() => {
- field.onChange(vendor.id)
- setVendorOpen(false)
- }}
- >
- <Check
- className={cn(
- "mr-2 h-4 w-4",
- vendor.id === field.value ? "opacity-100" : "opacity-0"
- )}
- />
- <div className="flex items-center gap-2">
- <Badge variant="outline">{vendor.vendorCode}</Badge>
- <span>{vendor.vendorName}</span>
- </div>
- </CommandItem>
- ))}
- </CommandGroup>
- </CommandList>
- </Command>
- </PopoverContent>
- </Popover>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 답변요청일 */}
- <FormField
- control={form.control}
- name="requestDate"
- render={({ field }) => (
- <FormItem>
- <FormLabel>답변요청일</FormLabel>
- <FormControl>
- <Input type="date" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
- </CardContent>
- </Card>
-
- {/* 안내 메시지 */}
- <Card className="bg-blue-50 border-blue-200">
- <CardContent className="pt-6">
- <div className="flex items-start gap-3">
- <div className="h-2 w-2 rounded-full bg-blue-500 mt-2"></div>
- <div className="space-y-1">
- <p className="text-sm font-medium text-blue-900">
- 편집 제한 안내
- </p>
- <p className="text-sm text-blue-700">
- 기본 정보는 '신규등록' 상태에서만 편집할 수 있습니다. 검토요청이 발송된 후에는 담당자를 통해 변경해야 합니다.
- </p>
- </div>
- </div>
- </CardContent>
- </Card>
- </div>
- </ScrollArea>
-
- {/* 고정 버튼 영역 */}
- <SheetFooter className="flex-shrink-0 border-t bg-background p-6">
- <div className="flex justify-end gap-3 w-full">
- <SheetClose asChild>
- <Button
- type="button"
- variant="outline"
- disabled={isSubmitting}
- >
- 취소
- </Button>
- </SheetClose>
- <Button
- type="submit"
- disabled={isSubmitting}
- >
- {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
- 저장
- </Button>
- </div>
- </SheetFooter>
- </form>
- </Form>
- </SheetContent>
- </Sheet>
- )
-} \ No newline at end of file