summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-19 09:44:28 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-19 09:44:28 +0000
commit95bbe9c583ff841220da1267630e7b2025fc36dc (patch)
tree5e3d5bb3302530bbaa7f7abbe8c9cf8193ccbd4c /lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
parent0eb030580b5cbe5f03d570c3c9d8c519bac3b783 (diff)
(대표님) 20250619 1844 KST 작업사항
Diffstat (limited to 'lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx')
-rw-r--r--lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx772
1 files changed, 772 insertions, 0 deletions
diff --git a/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
new file mode 100644
index 00000000..5704cba1
--- /dev/null
+++ b/lib/evaluation-target-list/table/manual-create-evaluation-target-dialog.tsx
@@ -0,0 +1,772 @@
+"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 { Check, ChevronsUpDown, Loader2, Search } 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 {
+ Command,
+ CommandEmpty,
+ CommandGroup,
+ CommandInput,
+ CommandItem,
+ CommandList,
+} from "@/components/ui/command"
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover"
+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 { cn } from "@/lib/utils"
+
+import {
+ createEvaluationTarget,
+ getAvailableVendors,
+ getAvailableReviewers,
+ getDepartmentInfo,
+ type CreateEvaluationTargetInput,
+} from "../service"
+import { EVALUATION_TARGET_FILTER_OPTIONS, getDefaultEvaluationYear } from "../validation"
+import { useSession } from "next-auth/react"
+
+// 폼 스키마 정의
+const createEvaluationTargetSchema = z.object({
+ evaluationYear: z.number().min(2020).max(2030),
+ division: z.enum(["OCEAN", "SHIPYARD"]),
+ vendorId: z.number().min(1, "벤더를 선택해주세요"),
+ materialType: z.enum(["EQUIPMENT", "BULK", "EQUIPMENT_BULK"]),
+ adminComment: z.string().optional(),
+ // L/D 클레임 정보
+ ldClaimCount: z.number().min(0).optional(),
+ ldClaimAmount: z.number().min(0).optional(),
+ ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(),
+ reviewers: z.array(
+ z.object({
+ departmentCode: z.string(),
+ reviewerUserId: z.number().min(1, "담당자를 선택해주세요"),
+ })
+ ).min(1, "최소 1명의 담당자를 지정해주세요"),
+})
+
+type CreateEvaluationTargetFormValues = z.infer<typeof createEvaluationTargetSchema>
+
+interface ManualCreateEvaluationTargetDialogProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+}
+
+export function ManualCreateEvaluationTargetDialog({
+ open,
+ onOpenChange,
+}: ManualCreateEvaluationTargetDialogProps) {
+ const router = useRouter()
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const { data: session } = useSession()
+ const userId = React.useMemo(() => {
+ return session?.user?.id ? Number(session.user.id) : 1;
+ }, [session]);
+
+ // 벤더 관련 상태
+ const [vendors, setVendors] = React.useState<Array<{ id: number, vendorCode: string, vendorName: string }>>([])
+ const [vendorSearch, setVendorSearch] = React.useState("")
+ const [vendorOpen, setVendorOpen] = React.useState(false)
+ const [isLoadingVendors, setIsLoadingVendors] = React.useState(false)
+
+ // 담당자 관련 상태
+ const [reviewers, setReviewers] = React.useState<Array<{ id: number, name: string, email: string }>>([])
+ const [isLoadingReviewers, setIsLoadingReviewers] = React.useState(false)
+
+ // 각 부서별 담당자 선택 상태
+ const [reviewerSearches, setReviewerSearches] = React.useState<Record<string, string>>({})
+ const [reviewerOpens, setReviewerOpens] = React.useState<Record<string, boolean>>({})
+
+ // 부서 정보 상태
+ const [departments, setDepartments] = React.useState<Array<{ code: string, name: string, key: string }>>([])
+
+ const form = useForm<CreateEvaluationTargetFormValues>({
+ resolver: zodResolver(createEvaluationTargetSchema),
+ defaultValues: {
+ evaluationYear: getDefaultEvaluationYear(),
+ division: "OCEAN",
+ vendorId: 0,
+ materialType: "EQUIPMENT",
+ adminComment: "",
+ ldClaimCount: 0,
+ ldClaimAmount: 0,
+ ldClaimCurrency: "KRW",
+ reviewers: [], // 초기에는 빈 배열, useEffect에서 설정
+ },
+ })
+
+ // 부서 정보 로드
+ const loadDepartments = React.useCallback(async () => {
+ try {
+ const departmentList = await getDepartmentInfo()
+ setDepartments(departmentList)
+ } catch (error) {
+ console.error("Error loading departments:", error)
+ toast.error("부서 정보를 불러오는데 실패했습니다.")
+ }
+ }, []) // form 의존성 제거
+
+ // 벤더 목록 로드
+ const loadVendors = React.useCallback(async (search?: string) => {
+ setIsLoadingVendors(true)
+ try {
+ const vendorList = await getAvailableVendors(search)
+ setVendors(vendorList)
+ } catch (error) {
+ console.error("Error loading vendors:", error)
+ toast.error("벤더 목록을 불러오는데 실패했습니다.")
+ } finally {
+ setIsLoadingVendors(false)
+ }
+ }, [])
+
+ // 담당자 목록 로드
+ const loadReviewers = React.useCallback(async () => {
+ setIsLoadingReviewers(true)
+ try {
+ const reviewerList = await getAvailableReviewers()
+ setReviewers(reviewerList)
+ } catch (error) {
+ console.error("Error loading reviewers:", error)
+ toast.error("담당자 목록을 불러오는데 실패했습니다.")
+ } finally {
+ setIsLoadingReviewers(false)
+ }
+ }, [])
+
+ // 다이얼로그 열릴 때 데이터 로드
+ React.useEffect(() => {
+ if (open) {
+ loadDepartments()
+ loadVendors()
+ loadReviewers()
+ }
+ }, [open]) // 함수 의존성 제거
+
+ // 부서 정보가 로드되면 reviewers 기본값 설정
+ React.useEffect(() => {
+ if (departments.length > 0 && open) {
+ const currentReviewers = form.getValues("reviewers")
+
+ // 이미 설정되어 있으면 다시 설정하지 않음
+ if (currentReviewers.length === 0) {
+ const defaultReviewers = departments.map(dept => ({
+ departmentCode: dept.code,
+ reviewerUserId: 0,
+ }))
+ form.setValue('reviewers', defaultReviewers)
+ }
+ }
+ }, [departments, open]) // form 의존성 제거하고 조건 추가
+
+ console.log(departments)
+
+ // 벤더 검색
+ React.useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ if (vendorSearch || vendorOpen) {
+ loadVendors(vendorSearch)
+ }
+ }, 300)
+
+ return () => clearTimeout(timeoutId)
+ }, [vendorSearch, vendorOpen]) // loadVendors 의존성 제거
+
+ // 폼 제출
+ async function onSubmit(data: CreateEvaluationTargetFormValues) {
+ console.log("Form submitted with data:", data) // 디버깅용
+ setIsSubmitting(true)
+ try {
+ // 담당자가 지정되지 않은 부서 제외
+ const validReviewers = data.reviewers.filter(r => r.reviewerUserId > 0)
+
+ if (validReviewers.length === 0) {
+ toast.error("최소 1명의 담당자를 지정해주세요.")
+ return
+ }
+
+ const input: CreateEvaluationTargetInput = {
+ ...data,
+ reviewers: validReviewers,
+ }
+
+ console.log(input,"client")
+
+ const result = await createEvaluationTarget(input, userId)
+
+ if (result.success) {
+ toast.success(result.message)
+ onOpenChange(false)
+ // 폼과 상태 초기화
+ form.reset({
+ evaluationYear: getDefaultEvaluationYear(),
+ division: "OCEAN",
+ vendorId: 0,
+ materialType: "EQUIPMENT",
+ adminComment: "",
+ ldClaimCount: 0,
+ ldClaimAmount: 0,
+ ldClaimCurrency: "KRW",
+ reviewers: [],
+ })
+ setDepartments([])
+ setVendors([])
+ setReviewers([])
+ setVendorSearch("")
+ setReviewerSearches({})
+ setReviewerOpens({})
+ router.refresh()
+ } else {
+ toast.error(result.error || "평가 대상 생성에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("Error creating evaluation target:", error)
+ toast.error("평가 대상 생성 중 오류가 발생했습니다.")
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ // 다이얼로그 닫기 핸들러
+ const handleOpenChange = (open: boolean) => {
+ onOpenChange(open)
+
+ // 다이얼로그가 닫힐 때 상태 초기화
+ if (!open) {
+ form.reset({
+ evaluationYear: getDefaultEvaluationYear(),
+ division: "OCEAN",
+ vendorId: 0,
+ materialType: "EQUIPMENT",
+ adminComment: "",
+ ldClaimCount: 0,
+ ldClaimAmount: 0,
+ ldClaimCurrency: "KRW",
+ reviewers: [],
+ })
+ setDepartments([])
+ setVendors([])
+ setReviewers([])
+ setVendorSearch("")
+ setReviewerSearches({})
+ setReviewerOpens({})
+ }
+ }
+
+ // 선택된 벤더 정보
+ const selectedVendor = vendors.find(v => v.id === form.watch("vendorId"))
+
+ // 현재 선택된 자재구분
+ const currentMaterialType = form.watch("materialType")
+
+ // BULK 자재일 때 비활성화할 부서 코드들
+ const BULK_DISABLED_DEPARTMENTS = ["DESIGN_EVAL", "CS_EVAL"] // 설계(DES), CS 부서 코드 (실제 코드에 맞게 수정 필요)
+
+ // 담당자 검색 필터링
+ const getFilteredReviewers = (search: string) => {
+ if (!search) return reviewers
+ return reviewers.filter(reviewer =>
+ reviewer.name.toLowerCase().includes(search.toLowerCase()) ||
+ reviewer.email.toLowerCase().includes(search.toLowerCase())
+ )
+ }
+
+ // 부서가 비활성화되어야 하는지 확인
+ const isDepartmentDisabled = (departmentCode: string) => {
+ return currentMaterialType === "BULK" && BULK_DISABLED_DEPARTMENTS.includes(departmentCode)
+ }
+
+ // 자재구분 변경 시 BULK 비활성화 부서들의 담당자 초기화
+ React.useEffect(() => {
+ if (currentMaterialType === "BULK") {
+ const currentReviewers = form.getValues("reviewers")
+ const updatedReviewers = currentReviewers.map(reviewer => {
+ if (BULK_DISABLED_DEPARTMENTS.includes(reviewer.departmentCode)) {
+ return { ...reviewer, reviewerUserId: 0 }
+ }
+ return reviewer
+ })
+ form.setValue("reviewers", updatedReviewers)
+ }
+ }, [currentMaterialType, form])
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogContent className="max-w-lg flex flex-col h-[90vh]">
+ {/* 고정 헤더 */}
+ <DialogHeader className="flex-shrink-0">
+ <DialogTitle>평가 대상 수동 생성</DialogTitle>
+ <DialogDescription>
+ 새로운 평가 대상을 수동으로 생성하고 담당자를 지정합니다.
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* Form을 전체 콘텐츠를 감싸도록 수정 */}
+ <Form {...form}>
+ <form
+ onSubmit={form.handleSubmit(onSubmit)}
+ className="flex flex-col flex-1"
+ id="evaluation-target-form"
+ >
+ {/* 스크롤 가능한 콘텐츠 영역 */}
+ <div className="flex-1 overflow-y-auto">
+ <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="evaluationYear"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>평가년도</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ {...field}
+ onChange={(e) => field.onChange(parseInt(e.target.value))}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 구분 */}
+ <FormField
+ control={form.control}
+ name="division"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>구분</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="구분 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {EVALUATION_TARGET_FILTER_OPTIONS.DIVISIONS.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </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="벤더 검색..."
+ value={vendorSearch}
+ onValueChange={setVendorSearch}
+ />
+ <CommandList>
+ <CommandEmpty>
+ {isLoadingVendors ? (
+ <div className="flex items-center gap-2">
+ <Loader2 className="h-4 w-4 animate-spin" />
+ 로딩 중...
+ </div>
+ ) : (
+ "검색 결과가 없습니다."
+ )}
+ </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="materialType"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>자재구분</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="자재구분 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {EVALUATION_TARGET_FILTER_OPTIONS.MATERIAL_TYPES.map((option) => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 관리자 의견 */}
+ <FormField
+ control={form.control}
+ name="adminComment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>관리자 의견 (선택사항)</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="관리자 의견을 입력하세요..."
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+ </Card>
+
+ {/* L/D 클레임 정보 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">L/D 클레임 정보</CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 지연 배송(Late Delivery) 클레임 관련 정보를 입력하세요.
+ </p>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ <div className="grid grid-cols-3 gap-4">
+ {/* 클레임 건수 */}
+ <FormField
+ control={form.control}
+ name="ldClaimCount"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>클레임 건수</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min="0"
+ placeholder="0"
+ {...field}
+ onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 클레임 금액 */}
+ <FormField
+ control={form.control}
+ name="ldClaimAmount"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>클레임 금액</FormLabel>
+ <FormControl>
+ <Input
+ type="number"
+ min="0"
+ step="0.01"
+ placeholder="0"
+ {...field}
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 통화단위 */}
+ <FormField
+ control={form.control}
+ name="ldClaimCurrency"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>통화단위</FormLabel>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="통화 선택" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="KRW">KRW (원)</SelectItem>
+ <SelectItem value="USD">USD (달러)</SelectItem>
+ <SelectItem value="EUR">EUR (유로)</SelectItem>
+ <SelectItem value="JPY">JPY (엔)</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 담당자 지정 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">담당자 지정</CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 각 부서별로 담당자를 지정해주세요. 최소 1명 이상 지정해야 합니다.
+ </p>
+ </CardHeader>
+ <CardContent>
+ {departments.length === 0 ? (
+ <div className="flex items-center justify-center py-8">
+ <div className="flex items-center gap-2 text-muted-foreground">
+ <Loader2 className="h-4 w-4 animate-spin" />
+ 부서 정보를 불러오는 중...
+ </div>
+ </div>
+ ) : (
+ <div className="space-y-4">
+ {departments.map((department, index) => {
+ const selectedReviewer = reviewers.find(r => r.id === form.watch(`reviewers.${index}.reviewerUserId`))
+ const filteredReviewers = getFilteredReviewers(reviewerSearches[department.code] || "")
+ const isDisabled = isDepartmentDisabled(department.code)
+
+ return (
+ <FormField
+ key={department.code}
+ control={form.control}
+ name={`reviewers.${index}.reviewerUserId`}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className={isDisabled ? "text-muted-foreground" : ""}>
+ {department.name}
+ {isDisabled && (
+ <span className="text-xs ml-2 text-muted-foreground">
+ (벌크 자재 시 비활성화)
+ </span>
+ )}
+ </FormLabel>
+ <Popover
+ open={!isDisabled && (reviewerOpens[department.code] || false)}
+ onOpenChange={(open) => !isDisabled && setReviewerOpens(prev => ({...prev, [department.code]: open}))}
+ >
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={reviewerOpens[department.code]}
+ className={cn(
+ "w-full justify-between",
+ isDisabled && "opacity-50 cursor-not-allowed"
+ )}
+ disabled={isDisabled}
+ >
+ {selectedReviewer && !isDisabled ? (
+ <span className="flex items-center gap-2">
+ <span>{selectedReviewer.name}</span>
+ <span className="text-xs text-muted-foreground">
+ ({selectedReviewer.email})
+ </span>
+ </span>
+ ) : (
+ isDisabled ? "벌크 자재 시 비활성화" : "담당자 선택 (선택사항)"
+ )}
+ <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="담당자 검색..."
+ value={reviewerSearches[department.code] || ""}
+ onValueChange={(value) => setReviewerSearches(prev => ({...prev, [department.code]: value}))}
+ />
+ <CommandList>
+ <CommandEmpty>
+ {isLoadingReviewers ? (
+ <div className="flex items-center gap-2">
+ <Loader2 className="h-4 w-4 animate-spin" />
+ 로딩 중...
+ </div>
+ ) : (
+ "검색 결과가 없습니다."
+ )}
+ </CommandEmpty>
+ <CommandGroup>
+ <CommandItem
+ value="선택 안함"
+ onSelect={() => {
+ field.onChange(0)
+ setReviewerOpens(prev => ({...prev, [department.code]: false}))
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ field.value === 0 ? "opacity-100" : "opacity-0"
+ )}
+ />
+ 선택 안함
+ </CommandItem>
+ {filteredReviewers.map((reviewer) => (
+ <CommandItem
+ key={reviewer.id}
+ value={`${reviewer.name} ${reviewer.email}`}
+ onSelect={() => {
+ field.onChange(reviewer.id)
+ setReviewerOpens(prev => ({...prev, [department.code]: false}))
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ reviewer.id === field.value ? "opacity-100" : "opacity-0"
+ )}
+ />
+ <div className="flex items-center gap-2">
+ <span>{reviewer.name}</span>
+ <span className="text-xs text-muted-foreground">
+ ({reviewer.email})
+ </span>
+ </div>
+ </CommandItem>
+ ))}
+ </CommandGroup>
+ </CommandList>
+ </Command>
+ </PopoverContent>
+ </Popover>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )
+ })}
+ </div>
+ )}
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+
+ {/* 고정 버튼 영역 */}
+ <div className="flex-shrink-0 flex justify-end gap-3 pt-4 border-t">
+ <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>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file