summaryrefslogtreecommitdiff
path: root/lib/evaluation-target-list/table/update-evaluation-target.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-06-20 11:37:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-06-20 11:37:31 +0000
commitaa86729f9a2ab95346a2851e3837de1c367aae17 (patch)
treeb601b18b6724f2fb449c7fa9ea50cbd652a8077d /lib/evaluation-target-list/table/update-evaluation-target.tsx
parent95bbe9c583ff841220da1267630e7b2025fc36dc (diff)
(대표님) 20250620 작업사항
Diffstat (limited to 'lib/evaluation-target-list/table/update-evaluation-target.tsx')
-rw-r--r--lib/evaluation-target-list/table/update-evaluation-target.tsx760
1 files changed, 760 insertions, 0 deletions
diff --git a/lib/evaluation-target-list/table/update-evaluation-target.tsx b/lib/evaluation-target-list/table/update-evaluation-target.tsx
new file mode 100644
index 00000000..0d56addb
--- /dev/null
+++ b/lib/evaluation-target-list/table/update-evaluation-target.tsx
@@ -0,0 +1,760 @@
+"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, X } from "lucide-react"
+import { toast } from "sonner"
+import { useSession } from "next-auth/react"
+
+import { Button } from "@/components/ui/button"
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetHeader,
+ SheetTitle,
+} from "@/components/ui/sheet"
+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 {
+ updateEvaluationTarget,
+ getAvailableReviewers,
+ getDepartmentInfo,
+ type UpdateEvaluationTargetInput,
+} from "../service"
+import { EvaluationTargetWithDepartments } from "@/db/schema"
+
+// 편집 가능한 필드들에 대한 스키마
+const editEvaluationTargetSchema = z.object({
+ adminComment: z.string().optional(),
+ consolidatedComment: z.string().optional(),
+ ldClaimCount: z.number().min(0).optional(),
+ ldClaimAmount: z.number().min(0).optional(),
+ ldClaimCurrency: z.enum(["KRW", "USD", "EUR", "JPY"]).optional(),
+ consensusStatus: z.boolean().nullable().optional(),
+ orderIsApproved: z.boolean().nullable().optional(),
+ procurementIsApproved: z.boolean().nullable().optional(),
+ qualityIsApproved: z.boolean().nullable().optional(),
+ designIsApproved: z.boolean().nullable().optional(),
+ csIsApproved: z.boolean().nullable().optional(),
+ // 담당자 정보 수정
+ orderReviewerEmail: z.string().optional(),
+ procurementReviewerEmail: z.string().optional(),
+ qualityReviewerEmail: z.string().optional(),
+ designReviewerEmail: z.string().optional(),
+ csReviewerEmail: z.string().optional(),
+})
+
+type EditEvaluationTargetFormValues = z.infer<typeof editEvaluationTargetSchema>
+
+interface EditEvaluationTargetSheetProps {
+ open: boolean
+ onOpenChange: (open: boolean) => void
+ evaluationTarget: EvaluationTargetWithDepartments | null
+}
+
+// 권한 타입 정의
+type PermissionLevel = "none" | "department" | "admin"
+
+interface UserPermissions {
+ level: PermissionLevel
+ editableApprovals: string[] // 편집 가능한 approval 필드들
+}
+
+export function EditEvaluationTargetSheet({
+ open,
+ onOpenChange,
+ evaluationTarget,
+}: EditEvaluationTargetSheetProps) {
+ const router = useRouter()
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const { data: session } = useSession()
+
+ // 담당자 관련 상태
+ 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 userPermissions = React.useMemo((): UserPermissions => {
+ if (!session?.user || !evaluationTarget) {
+ return { level: "none", editableApprovals: [] }
+ }
+
+ const userEmail = session.user.email
+ const userRole = session.user.role
+
+ // 평가관리자는 모든 권한
+ if (userRole === "평가관리자") {
+ return {
+ level: "admin",
+ editableApprovals: [
+ "orderIsApproved",
+ "procurementIsApproved",
+ "qualityIsApproved",
+ "designIsApproved",
+ "csIsApproved"
+ ]
+ }
+ }
+
+ // 부서별 담당자 권한 확인
+ const editableApprovals: string[] = []
+
+ if (evaluationTarget.orderReviewerEmail === userEmail) {
+ editableApprovals.push("orderIsApproved")
+ }
+ if (evaluationTarget.procurementReviewerEmail === userEmail) {
+ editableApprovals.push("procurementIsApproved")
+ }
+ if (evaluationTarget.qualityReviewerEmail === userEmail) {
+ editableApprovals.push("qualityIsApproved")
+ }
+ if (evaluationTarget.designReviewerEmail === userEmail) {
+ editableApprovals.push("designIsApproved")
+ }
+ if (evaluationTarget.csReviewerEmail === userEmail) {
+ editableApprovals.push("csIsApproved")
+ }
+
+ return {
+ level: editableApprovals.length > 0 ? "department" : "none",
+ editableApprovals
+ }
+ }, [session, evaluationTarget])
+
+ const form = useForm<EditEvaluationTargetFormValues>({
+ resolver: zodResolver(editEvaluationTargetSchema),
+ defaultValues: {
+ adminComment: "",
+ consolidatedComment: "",
+ ldClaimCount: 0,
+ ldClaimAmount: 0,
+ ldClaimCurrency: "KRW",
+ consensusStatus: null,
+ orderIsApproved: null,
+ procurementIsApproved: null,
+ qualityIsApproved: null,
+ designIsApproved: null,
+ csIsApproved: null,
+ orderReviewerEmail: "",
+ procurementReviewerEmail: "",
+ qualityReviewerEmail: "",
+ designReviewerEmail: "",
+ csReviewerEmail: "",
+ },
+ })
+
+ // 부서 정보 로드
+ const loadDepartments = React.useCallback(async () => {
+ try {
+ const departmentList = await getDepartmentInfo()
+ setDepartments(departmentList)
+ } catch (error) {
+ console.error("Error loading departments:", error)
+ toast.error("부서 정보를 불러오는데 실패했습니다.")
+ }
+ }, [])
+
+ // 담당자 목록 로드
+ 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 && evaluationTarget) {
+ loadDepartments()
+ loadReviewers()
+
+ // 폼에 기존 데이터 설정
+ form.reset({
+ adminComment: evaluationTarget.adminComment || "",
+ consolidatedComment: evaluationTarget.consolidatedComment || "",
+ ldClaimCount: evaluationTarget.ldClaimCount || 0,
+ ldClaimAmount: parseFloat(evaluationTarget.ldClaimAmount || "0"),
+ ldClaimCurrency: evaluationTarget.ldClaimCurrency || "KRW",
+ consensusStatus: evaluationTarget.consensusStatus,
+ orderIsApproved: evaluationTarget.orderIsApproved,
+ procurementIsApproved: evaluationTarget.procurementIsApproved,
+ qualityIsApproved: evaluationTarget.qualityIsApproved,
+ designIsApproved: evaluationTarget.designIsApproved,
+ csIsApproved: evaluationTarget.csIsApproved,
+ orderReviewerEmail: evaluationTarget.orderReviewerEmail || "",
+ procurementReviewerEmail: evaluationTarget.procurementReviewerEmail || "",
+ qualityReviewerEmail: evaluationTarget.qualityReviewerEmail || "",
+ designReviewerEmail: evaluationTarget.designReviewerEmail || "",
+ csReviewerEmail: evaluationTarget.csReviewerEmail || "",
+ })
+ }
+ }, [open, evaluationTarget, form])
+
+ // 폼 제출
+ async function onSubmit(data: EditEvaluationTargetFormValues) {
+ if (!evaluationTarget) return
+
+ setIsSubmitting(true)
+ try {
+ const input: UpdateEvaluationTargetInput = {
+ id: evaluationTarget.id,
+ ...data,
+ }
+
+ console.log("Updating evaluation target:", input)
+
+ const result = await updateEvaluationTarget(input)
+
+ if (result.success) {
+ toast.success(result.message || "평가 대상이 성공적으로 수정되었습니다.")
+ onOpenChange(false)
+ router.refresh()
+ } else {
+ toast.error(result.error || "평가 대상 수정에 실패했습니다.")
+ }
+ } catch (error) {
+ console.error("Error updating evaluation target:", error)
+ toast.error("평가 대상 수정 중 오류가 발생했습니다.")
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ // 시트 닫기 핸들러
+ const handleOpenChange = (open: boolean) => {
+ onOpenChange(open)
+ if (!open) {
+ form.reset()
+ setReviewerSearches({})
+ setReviewerOpens({})
+ }
+ }
+
+ // 담당자 검색 필터링
+ 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 canEditField = (fieldName: string): boolean => {
+ if (userPermissions.level === "admin") return true
+ if (userPermissions.level === "none") return false
+
+ // 부서 담당자는 자신의 approval만 편집 가능
+ return userPermissions.editableApprovals.includes(fieldName)
+ }
+
+ // 관리자 전용 필드 확인
+ const canEditAdminFields = (): boolean => {
+ return userPermissions.level === "admin"
+ }
+
+ if (!evaluationTarget) {
+ return null
+ }
+
+ // 권한이 없는 경우
+ if (userPermissions.level === "none") {
+ return (
+ <Sheet open={open} onOpenChange={handleOpenChange}>
+ <SheetContent className="sm:max-w-lg overflow-y-auto">
+ <SheetHeader>
+ <SheetTitle>평가 대상 수정</SheetTitle>
+ <SheetDescription>
+ 권한이 없어 수정할 수 없습니다.
+ </SheetDescription>
+ </SheetHeader>
+ <div className="mt-6 p-4 bg-muted rounded-lg text-center">
+ <p className="text-sm text-muted-foreground">
+ 이 평가 대상을 수정할 권한이 없습니다.
+ </p>
+ </div>
+ </SheetContent>
+ </Sheet>
+ )
+ }
+
+ return (
+ <Sheet open={open} onOpenChange={handleOpenChange}>
+ <SheetContent className="flex flex-col h-full sm:max-w-xl">
+ <SheetHeader className="flex-shrink-0 text-left pb-6">
+ <SheetTitle>평가 대상 수정</SheetTitle>
+ <SheetDescription>
+ 평가 대상 정보를 수정합니다.
+ {userPermissions.level === "department" && (
+ <div className="mt-2 p-2 bg-blue-50 rounded text-sm">
+ <strong>부서 담당자 권한:</strong> 해당 부서의 평가 항목만 수정 가능합니다.
+ </div>
+ )}
+ {userPermissions.level === "admin" && (
+ <div className="mt-2 p-2 bg-green-50 rounded text-sm">
+ <strong>평가관리자 권한:</strong> 모든 항목을 수정할 수 있습니다.
+ </div>
+ )}
+ </SheetDescription>
+ </SheetHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0">
+ {/* 스크롤 가능한 컨텐츠 영역 */}
+ <div className="flex-1 overflow-y-auto pr-2 -mr-2">
+ <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 text-sm">
+ <div>
+ <span className="font-medium">평가년도:</span> {evaluationTarget.evaluationYear}
+ </div>
+ <div>
+ <span className="font-medium">구분:</span> {evaluationTarget.division === "PLANT" ? "해양" : "조선"}
+ </div>
+ <div>
+ <span className="font-medium">벤더 코드:</span> {evaluationTarget.vendorCode}
+ </div>
+ <div>
+ <span className="font-medium">벤더명:</span> {evaluationTarget.vendorName}
+ </div>
+ <div>
+ <span className="font-medium">자재구분:</span> {evaluationTarget.materialType}
+ </div>
+ <div>
+ <span className="font-medium">상태:</span> {evaluationTarget.status}
+ </div>
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* L/D 클레임 정보 (관리자만) */}
+ {canEditAdminFields() && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">L/D 클레임 정보</CardTitle>
+ </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"
+ {...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"
+ {...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} value={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue />
+ </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>
+ </CardHeader>
+ <CardContent className="space-y-4">
+
+ {/* 의견 일치 여부 (관리자만) */}
+ {canEditAdminFields() && (
+ <FormField
+ control={form.control}
+ name="consensusStatus"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>의견 일치 여부</FormLabel>
+ <Select
+ onValueChange={(value) => {
+ if (value === "null") field.onChange(null)
+ else field.onChange(value === "true")
+ }}
+ value={field.value === null ? "null" : field.value.toString()}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="null">검토 중</SelectItem>
+ <SelectItem value="true">의견 일치</SelectItem>
+ <SelectItem value="false">의견 불일치</SelectItem>
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 각 부서별 평가 */}
+ <div className="grid grid-cols-1 gap-4">
+ {[
+ { key: "orderIsApproved", label: "주문 부서 평가", email: evaluationTarget.orderReviewerEmail },
+ { key: "procurementIsApproved", label: "조달 부서 평가", email: evaluationTarget.procurementReviewerEmail },
+ { key: "qualityIsApproved", label: "품질 부서 평가", email: evaluationTarget.qualityReviewerEmail },
+ { key: "designIsApproved", label: "설계 부서 평가", email: evaluationTarget.designReviewerEmail },
+ { key: "csIsApproved", label: "CS 부서 평가", email: evaluationTarget.csReviewerEmail },
+ ].map(({ key, label, email }) => (
+ <FormField
+ key={key}
+ control={form.control}
+ name={key as keyof EditEvaluationTargetFormValues}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel className="flex items-center justify-between">
+ <span>{label}</span>
+ {email && (
+ <span className="text-xs text-muted-foreground">
+ 담당자: {email}
+ </span>
+ )}
+ </FormLabel>
+ <Select
+ onValueChange={(value) => {
+ if (value === "null") field.onChange(null)
+ else field.onChange(value === "true")
+ }}
+ value={field.value === null ? "null" : field.value?.toString()}
+ disabled={!canEditField(key)}
+ >
+ <FormControl>
+ <SelectTrigger className={!canEditField(key) ? "opacity-50" : ""}>
+ <SelectValue />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ <SelectItem value="null">대기중</SelectItem>
+ <SelectItem value="true">평가대상 맞음</SelectItem>
+ <SelectItem value="false">평가대상 아님</SelectItem>
+ </SelectContent>
+ </Select>
+ {!canEditField(key) && (
+ <p className="text-xs text-muted-foreground">
+ 편집 권한이 없습니다.
+ </p>
+ )}
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ ))}
+ </div>
+ </CardContent>
+ </Card>
+
+ {/* 의견 */}
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">의견</CardTitle>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {/* 관리자 의견 (관리자만) */}
+ {canEditAdminFields() && (
+ <FormField
+ control={form.control}
+ name="adminComment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>관리자 의견</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="관리자 의견을 입력하세요..."
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ )}
+
+ {/* 종합 의견 (모든 권한자) */}
+ <FormField
+ control={form.control}
+ name="consolidatedComment"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>종합 의견</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="종합 의견을 입력하세요..."
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </CardContent>
+ </Card>
+
+ {/* 담당자 변경 (관리자만) */}
+ {canEditAdminFields() && departments.length > 0 && (
+ <Card>
+ <CardHeader>
+ <CardTitle className="text-lg">담당자 변경</CardTitle>
+ <p className="text-sm text-muted-foreground">
+ 각 부서별 담당자를 변경할 수 있습니다.
+ </p>
+ </CardHeader>
+ <CardContent className="space-y-4">
+ {[
+ { key: "orderReviewerEmail", label: "주문 부서 담당자", current: evaluationTarget.orderReviewerEmail },
+ { key: "procurementReviewerEmail", label: "조달 부서 담당자", current: evaluationTarget.procurementReviewerEmail },
+ { key: "qualityReviewerEmail", label: "품질 부서 담당자", current: evaluationTarget.qualityReviewerEmail },
+ { key: "designReviewerEmail", label: "설계 부서 담당자", current: evaluationTarget.designReviewerEmail },
+ { key: "csReviewerEmail", label: "CS 부서 담당자", current: evaluationTarget.csReviewerEmail },
+ ].map(({ key, label, current }) => {
+ const selectedReviewer = reviewers.find(r => r.email === form.watch(key as keyof EditEvaluationTargetFormValues))
+ const filteredReviewers = getFilteredReviewers(reviewerSearches[key] || "")
+
+ return (
+ <FormField
+ key={key}
+ control={form.control}
+ name={key as keyof EditEvaluationTargetFormValues}
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>
+ {label}
+ {current && (
+ <span className="text-xs text-muted-foreground ml-2">
+ (현재: {current})
+ </span>
+ )}
+ </FormLabel>
+ <Popover
+ open={reviewerOpens[key] || false}
+ onOpenChange={(open) => setReviewerOpens(prev => ({...prev, [key]: open}))}
+ >
+ <PopoverTrigger asChild>
+ <FormControl>
+ <Button
+ variant="outline"
+ role="combobox"
+ aria-expanded={reviewerOpens[key]}
+ className="w-full justify-between"
+ >
+ {selectedReviewer ? (
+ <span className="flex items-center gap-2">
+ <span>{selectedReviewer.name}</span>
+ <span className="text-xs text-muted-foreground">
+ ({selectedReviewer.email})
+ </span>
+ </span>
+ ) : field.value ? (
+ field.value
+ ) : (
+ "담당자 선택..."
+ )}
+ <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[key] || ""}
+ onValueChange={(value) => setReviewerSearches(prev => ({...prev, [key]: 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("")
+ setReviewerOpens(prev => ({ ...prev, [key]: false }))
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ !field.value ? "opacity-100" : "opacity-0"
+ )}
+ />
+ 선택 안함
+ </CommandItem>
+ {filteredReviewers.map((reviewer) => (
+ <CommandItem
+ key={reviewer.id}
+ value={`${reviewer.name} ${reviewer.email}`}
+ onSelect={() => {
+ field.onChange(reviewer.email)
+ setReviewerOpens(prev => ({...prev, [key]: false}))
+ }}
+ >
+ <Check
+ className={cn(
+ "mr-2 h-4 w-4",
+ reviewer.email === 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>
+ )}
+ />
+ )
+ })}
+ </CardContent>
+ </Card>
+ )}
+ </div>
+ </div>
+
+ {/* 고정된 버튼 영역 */}
+ <div className="flex-shrink-0 flex justify-end gap-3 pt-6 mt-6 border-t bg-background">
+ <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>
+ </SheetContent>
+ </Sheet>
+ )
+} \ No newline at end of file