From aa86729f9a2ab95346a2851e3837de1c367aae17 Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 20 Jun 2025 11:37:31 +0000
Subject: (대표님) 20250620 작업사항
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../table/evaluation-target-action-dialogs.tsx     | 384 +++++++++++++++++++++
 1 file changed, 384 insertions(+)
 create mode 100644 lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx

(limited to 'lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx')

diff --git a/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx
new file mode 100644
index 00000000..47af419d
--- /dev/null
+++ b/lib/evaluation-target-list/table/evaluation-target-action-dialogs.tsx
@@ -0,0 +1,384 @@
+// evaluation-target-action-dialogs.tsx
+"use client"
+
+import * as React from "react"
+import { Loader2, AlertTriangle, Check, X, MessageSquare } from "lucide-react"
+import { toast } from "sonner"
+
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogFooter,
+  AlertDialogHeader,
+  AlertDialogTitle,
+} from "@/components/ui/alert-dialog"
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogFooter,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { Label } from "@/components/ui/label"
+import { Badge } from "@/components/ui/badge"
+
+import { 
+  confirmEvaluationTargets, 
+  excludeEvaluationTargets, 
+  requestEvaluationReview 
+} from "../service"
+import { EvaluationTargetWithDepartments } from "@/db/schema"
+
+// ----------------------------------------------------------------
+// 확정 컨펌 다이얼로그
+// ----------------------------------------------------------------
+interface ConfirmTargetsDialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  targets: EvaluationTargetWithDepartments[]
+  onSuccess?: () => void
+}
+
+export function ConfirmTargetsDialog({
+  open,
+  onOpenChange,
+  targets,
+  onSuccess
+}: ConfirmTargetsDialogProps) {
+  const [isLoading, setIsLoading] = React.useState(false)
+
+  // 확정 가능한 대상들 (consensusStatus가 true인 것들)
+  const confirmableTargets = targets.filter(
+    t => t.status === "PENDING" && t.consensusStatus === true
+  )
+
+  const handleConfirm = async () => {
+    if (confirmableTargets.length === 0) return
+
+    setIsLoading(true)
+    try {
+      const targetIds = confirmableTargets.map(t => t.id)
+      const result = await confirmEvaluationTargets(targetIds)
+
+      if (result.success) {
+        toast.success(result.message)
+        onSuccess?.()
+        onOpenChange(false)
+      } else {
+        toast.error(result.error)
+      }
+    } catch (error) {
+      toast.error("확정 처리 중 오류가 발생했습니다.")
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  return (
+    <AlertDialog open={open} onOpenChange={onOpenChange}>
+      <AlertDialogContent>
+        <AlertDialogHeader>
+          <AlertDialogTitle className="flex items-center gap-2">
+            <Check className="h-5 w-5 text-green-600" />
+            평가 대상 확정
+          </AlertDialogTitle>
+          <AlertDialogDescription asChild>
+            <div className="space-y-3">
+              <p>
+                선택된 {targets.length}개 항목 중{" "}
+                <span className="font-semibold text-green-600">
+                  {confirmableTargets.length}개 항목
+                </span>
+                을 확정하시겠습니까?
+              </p>
+              
+              {confirmableTargets.length !== targets.length && (
+                <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+                  <p className="text-sm text-yellow-800">
+                    <AlertTriangle className="h-4 w-4 inline mr-1" />
+                    의견 일치 상태인 대기중 항목만 확정 가능합니다.
+                    ({targets.length - confirmableTargets.length}개 항목 제외됨)
+                  </p>
+                </div>
+              )}
+
+              {confirmableTargets.length > 0 && (
+                <div className="max-h-32 overflow-y-auto">
+                  <div className="text-sm space-y-1">
+                    {confirmableTargets.slice(0, 5).map(target => (
+                      <div key={target.id} className="flex items-center gap-2">
+                        <Badge variant="outline" className="text-xs">
+                          {target.vendorCode}
+                        </Badge>
+                        <span className="text-xs">{target.vendorName}</span>
+                      </div>
+                    ))}
+                    {confirmableTargets.length > 5 && (
+                      <p className="text-xs text-muted-foreground">
+                        ...외 {confirmableTargets.length - 5}개
+                      </p>
+                    )}
+                  </div>
+                </div>
+              )}
+            </div>
+          </AlertDialogDescription>
+        </AlertDialogHeader>
+        <AlertDialogFooter>
+          <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
+          <AlertDialogAction
+            onClick={handleConfirm}
+            disabled={isLoading || confirmableTargets.length === 0}
+            className="bg-green-600 hover:bg-green-700"
+          >
+            {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+            확정 ({confirmableTargets.length})
+          </AlertDialogAction>
+        </AlertDialogFooter>
+      </AlertDialogContent>
+    </AlertDialog>
+  )
+}
+
+// ----------------------------------------------------------------
+// 제외 컨펌 다이얼로그
+// ----------------------------------------------------------------
+interface ExcludeTargetsDialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  targets: EvaluationTargetWithDepartments[]
+  onSuccess?: () => void
+}
+
+export function ExcludeTargetsDialog({
+  open,
+  onOpenChange,
+  targets,
+  onSuccess
+}: ExcludeTargetsDialogProps) {
+  const [isLoading, setIsLoading] = React.useState(false)
+
+  // 제외 가능한 대상들 (PENDING 상태인 것들)
+  const excludableTargets = targets.filter(t => t.status === "PENDING")
+
+  const handleExclude = async () => {
+    if (excludableTargets.length === 0) return
+
+    setIsLoading(true)
+    try {
+      const targetIds = excludableTargets.map(t => t.id)
+      const result = await excludeEvaluationTargets(targetIds)
+
+      if (result.success) {
+        toast.success(result.message)
+        onSuccess?.()
+        onOpenChange(false)
+      } else {
+        toast.error(result.error)
+      }
+    } catch (error) {
+      toast.error("제외 처리 중 오류가 발생했습니다.")
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  return (
+    <AlertDialog open={open} onOpenChange={onOpenChange}>
+      <AlertDialogContent>
+        <AlertDialogHeader>
+          <AlertDialogTitle className="flex items-center gap-2">
+            <X className="h-5 w-5 text-red-600" />
+            평가 대상 제외
+          </AlertDialogTitle>
+          <AlertDialogDescription asChild>
+            <div className="space-y-3">
+              <p>
+                선택된 {targets.length}개 항목 중{" "}
+                <span className="font-semibold text-red-600">
+                  {excludableTargets.length}개 항목
+                </span>
+                을 제외하시겠습니까?
+              </p>
+
+              {excludableTargets.length !== targets.length && (
+                <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+                  <p className="text-sm text-yellow-800">
+                    <AlertTriangle className="h-4 w-4 inline mr-1" />
+                    대기중 상태인 항목만 제외 가능합니다.
+                    ({targets.length - excludableTargets.length}개 항목 제외됨)
+                  </p>
+                </div>
+              )}
+
+              {excludableTargets.length > 0 && (
+                <div className="max-h-32 overflow-y-auto">
+                  <div className="text-sm space-y-1">
+                    {excludableTargets.slice(0, 5).map(target => (
+                      <div key={target.id} className="flex items-center gap-2">
+                        <Badge variant="outline" className="text-xs">
+                          {target.vendorCode}
+                        </Badge>
+                        <span className="text-xs">{target.vendorName}</span>
+                      </div>
+                    ))}
+                    {excludableTargets.length > 5 && (
+                      <p className="text-xs text-muted-foreground">
+                        ...외 {excludableTargets.length - 5}개
+                      </p>
+                    )}
+                  </div>
+                </div>
+              )}
+            </div>
+          </AlertDialogDescription>
+        </AlertDialogHeader>
+        <AlertDialogFooter>
+          <AlertDialogCancel disabled={isLoading}>취소</AlertDialogCancel>
+          <AlertDialogAction
+            onClick={handleExclude}
+            disabled={isLoading || excludableTargets.length === 0}
+            className="bg-red-600 hover:bg-red-700"
+          >
+            {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+            제외 ({excludableTargets.length})
+          </AlertDialogAction>
+        </AlertDialogFooter>
+      </AlertDialogContent>
+    </AlertDialog>
+  )
+}
+
+// ----------------------------------------------------------------
+// 의견 요청 다이얼로그
+// ----------------------------------------------------------------
+interface RequestReviewDialogProps {
+  open: boolean
+  onOpenChange: (open: boolean) => void
+  targets: EvaluationTargetWithDepartments[]
+  onSuccess?: () => void
+}
+
+export function RequestReviewDialog({
+  open,
+  onOpenChange,
+  targets,
+  onSuccess
+}: RequestReviewDialogProps) {
+  const [isLoading, setIsLoading] = React.useState(false)
+  const [message, setMessage] = React.useState("")
+
+  // 의견 요청 가능한 대상들 (PENDING 상태인 것들)
+  const reviewableTargets = targets.filter(t => t.status === "PENDING")
+
+  // 담당자 이메일들 수집
+  const reviewerEmails = React.useMemo(() => {
+    const emails = new Set<string>()
+    reviewableTargets.forEach(target => {
+      if (target.orderReviewerEmail) emails.add(target.orderReviewerEmail)
+      if (target.procurementReviewerEmail) emails.add(target.procurementReviewerEmail)
+      if (target.qualityReviewerEmail) emails.add(target.qualityReviewerEmail)
+      if (target.designReviewerEmail) emails.add(target.designReviewerEmail)
+      if (target.csReviewerEmail) emails.add(target.csReviewerEmail)
+    })
+    return Array.from(emails)
+  }, [reviewableTargets])
+
+  const handleRequestReview = async () => {
+    if (reviewableTargets.length === 0) return
+
+    setIsLoading(true)
+    try {
+      const targetIds = reviewableTargets.map(t => t.id)
+      const result = await requestEvaluationReview(targetIds, message)
+
+      if (result.success) {
+        toast.success(result.message)
+        onSuccess?.()
+        onOpenChange(false)
+        setMessage("")
+      } else {
+        toast.error(result.error)
+      }
+    } catch (error) {
+      toast.error("의견 요청 중 오류가 발생했습니다.")
+    } finally {
+      setIsLoading(false)
+    }
+  }
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="sm:max-w-md">
+        <DialogHeader>
+          <DialogTitle className="flex items-center gap-2">
+            <MessageSquare className="h-5 w-5 text-blue-600" />
+            평가 의견 요청
+          </DialogTitle>
+          <DialogDescription>
+            선택된 평가 대상에 대한 의견을 담당자들에게 요청합니다.
+          </DialogDescription>
+        </DialogHeader>
+
+        <div className="space-y-4">
+          {/* 요약 정보 */}
+          <div className="p-3 bg-blue-50 rounded-lg border border-blue-200">
+            <div className="text-sm space-y-1">
+              <p>
+                <span className="font-medium">요청 대상:</span> {reviewableTargets.length}개 평가 항목
+              </p>
+              <p>
+                <span className="font-medium">받는 사람:</span> {reviewerEmails.length}명의 담당자
+              </p>
+            </div>
+          </div>
+
+          {/* 메시지 입력 */}
+          <div className="space-y-2">
+            <Label htmlFor="review-message">추가 메시지 (선택사항)</Label>
+            <Textarea
+              id="review-message"
+              placeholder="담당자들에게 전달할 추가 메시지를 입력하세요..."
+              value={message}
+              onChange={(e) => setMessage(e.target.value)}
+              rows={3}
+            />
+          </div>
+
+          {reviewableTargets.length !== targets.length && (
+            <div className="p-3 bg-yellow-50 rounded-lg border border-yellow-200">
+              <p className="text-sm text-yellow-800">
+                <AlertTriangle className="h-4 w-4 inline mr-1" />
+                대기중 상태인 항목만 의견 요청 가능합니다.
+                ({targets.length - reviewableTargets.length}개 항목 제외됨)
+              </p>
+            </div>
+          )}
+        </div>
+
+        <DialogFooter>
+          <Button
+            variant="outline"
+            onClick={() => onOpenChange(false)}
+            disabled={isLoading}
+          >
+            취소
+          </Button>
+          <Button
+            onClick={handleRequestReview}
+            disabled={isLoading || reviewableTargets.length === 0}
+          >
+            {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+            의견 요청 발송
+          </Button>
+        </DialogFooter>
+      </DialogContent>
+    </Dialog>
+  )
+}
\ No newline at end of file
-- 
cgit v1.2.3

