summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:41:06 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:41:06 +0000
commit4dc27e9495b005b29b4d7a2ad404c8c0644769eb (patch)
tree1d74ddd3bea33ea67745aeb4f092b7df6c6ef5cb
parent459873f983cf1468f778109df4c7953c5d40743d (diff)
(최겸) 실사 컬럼 수정 및 업데이트 변경, 협력업체 PQ 초대 로직 변경
-rw-r--r--lib/vendor-investigation/table/investigation-table-columns.tsx12
-rw-r--r--lib/vendor-investigation/table/investigation-table.tsx6
-rw-r--r--lib/vendor-investigation/table/update-investigation-sheet.tsx10
-rw-r--r--lib/vendor-investigation/validations.ts12
-rw-r--r--lib/vendors/service.ts173
-rw-r--r--lib/vendors/table/request-pq-dialog.tsx286
-rw-r--r--lib/vendors/table/vendors-table-toolbar-actions.tsx17
7 files changed, 405 insertions, 111 deletions
diff --git a/lib/vendor-investigation/table/investigation-table-columns.tsx b/lib/vendor-investigation/table/investigation-table-columns.tsx
index 6146d940..88b6644f 100644
--- a/lib/vendor-investigation/table/investigation-table-columns.tsx
+++ b/lib/vendor-investigation/table/investigation-table-columns.tsx
@@ -295,10 +295,14 @@ function formatStatus(status: string): string {
function formatEnumValue(value: string): string {
switch (value) {
// Evaluation types
- case "SITE_AUDIT":
- return "실사의뢰평가"
- case "QM_SELF_AUDIT":
- return "QM자체평가"
+ case "PURCHASE_SELF_EVAL":
+ return "구매자체평가"
+ case "DOCUMENT_EVAL":
+ return "서류평가"
+ case "PRODUCT_INSPECTION":
+ return "제품검사평가"
+ case "SITE_VISIT_EVAL":
+ return "방문실사평가"
// Evaluation results
case "APPROVED":
diff --git a/lib/vendor-investigation/table/investigation-table.tsx b/lib/vendor-investigation/table/investigation-table.tsx
index d5dc05ac..660a8507 100644
--- a/lib/vendor-investigation/table/investigation-table.tsx
+++ b/lib/vendor-investigation/table/investigation-table.tsx
@@ -102,8 +102,10 @@ export function VendorsInvestigationTable({ promises }: VendorsTableProps) {
label: "평가 유형",
type: "select",
options: [
- { label: "실사의뢰평가", value: "SITE_AUDIT" },
- { label: "QM자체평가", value: "QM_SELF_AUDIT" },
+ { label: "구매자체평가", value: "PURCHASE_SELF_EVAL" },
+ { label: "서류평가", value: "DOCUMENT_EVAL" },
+ { label: "제품검사평가", value: "PRODUCT_INSPECTION" },
+ { label: "방문실사평가", value: "SITE_VISIT_EVAL" },
]
},
{
diff --git a/lib/vendor-investigation/table/update-investigation-sheet.tsx b/lib/vendor-investigation/table/update-investigation-sheet.tsx
index 29f0fa92..fbaf000e 100644
--- a/lib/vendor-investigation/table/update-investigation-sheet.tsx
+++ b/lib/vendor-investigation/table/update-investigation-sheet.tsx
@@ -125,7 +125,7 @@ export function UpdateVendorInvestigationSheet({
investigationStatus: investigation?.investigationStatus ?? "PLANNED",
evaluationType: investigation?.evaluationType ?? undefined,
investigationAddress: investigation?.investigationAddress ?? "",
- investigationMethod: investigation?.investigationMethod ?? "",
+ investigationMethod: investigation?.investigationMethod ?? undefined,
forecastedAt: investigation?.forecastedAt ?? undefined,
requestedAt: investigation?.requestedAt ?? undefined,
confirmedAt: investigation?.confirmedAt ?? undefined,
@@ -145,7 +145,7 @@ export function UpdateVendorInvestigationSheet({
investigationStatus: investigation.investigationStatus || "PLANNED",
evaluationType: investigation.evaluationType ?? undefined,
investigationAddress: investigation.investigationAddress ?? "",
- investigationMethod: investigation.investigationMethod ?? "",
+ investigationMethod: investigation.investigationMethod ?? undefined,
forecastedAt: investigation.forecastedAt ?? undefined,
requestedAt: investigation.requestedAt ?? undefined,
confirmedAt: investigation.confirmedAt ?? undefined,
@@ -573,8 +573,10 @@ export function UpdateVendorInvestigationSheet({
</SelectTrigger>
<SelectContent>
<SelectGroup>
- <SelectItem value="SITE_AUDIT">실사의뢰평가</SelectItem>
- <SelectItem value="QM_SELF_AUDIT">QM자체평가</SelectItem>
+ <SelectItem value="PURCHASE_SELF_EVAL">구매자체평가</SelectItem>
+ <SelectItem value="DOCUMENT_EVAL">서류평가</SelectItem>
+ <SelectItem value="PRODUCT_INSPECTION">제품검사평가</SelectItem>
+ <SelectItem value="SITE_VISIT_EVAL">방문실사평가</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
diff --git a/lib/vendor-investigation/validations.ts b/lib/vendor-investigation/validations.ts
index d04f100f..e4ec2b52 100644
--- a/lib/vendor-investigation/validations.ts
+++ b/lib/vendor-investigation/validations.ts
@@ -33,8 +33,8 @@ export const searchParamsInvestigationCache = createSearchParamsCache({
// Fields specific to vendor investigations
// -----------------------------------------------------------------
- // investigationStatus: PLANNED, IN_PROGRESS, COMPLETED, CANCELED
- investigationStatus: parseAsStringEnum(["PLANNED", "IN_PROGRESS", "COMPLETED", "CANCELED"]),
+ // investigationStatus: PLANNED, IN_PROGRESS, COMPLETED, CANCELED, RESULT_SENT
+ investigationStatus: parseAsStringEnum(["PLANNED", "IN_PROGRESS", "COMPLETED", "CANCELED", "RESULT_SENT"]),
// In case you also want to filter by vendorName, vendorCode, etc.
vendorName: parseAsString.withDefault(""),
@@ -64,12 +64,12 @@ export const updateVendorInvestigationSchema = z.object({
investigationId: z.number({
required_error: "Investigation ID is required",
}),
- investigationStatus: z.enum(["PLANNED", "IN_PROGRESS", "COMPLETED", "CANCELED"], {
+ investigationStatus: z.enum(["PLANNED", "IN_PROGRESS", "COMPLETED", "CANCELED", "RESULT_SENT"], {
required_error: "실사 상태를 선택해주세요.",
}),
- evaluationType: z.enum(["SITE_AUDIT", "QM_SELF_AUDIT"]).optional(),
+ evaluationType: z.enum(["PURCHASE_SELF_EVAL", "DOCUMENT_EVAL", "PRODUCT_INSPECTION", "SITE_VISIT_EVAL"]).optional(),
investigationAddress: z.string().optional(),
- investigationMethod: z.string().max(100, "실사 방법은 100자 이내로 입력해주세요.").optional(),
+ investigationMethod: z.enum(["PURCHASE_SELF_EVAL", "DOCUMENT_EVAL", "PRODUCT_INSPECTION", "SITE_VISIT_EVAL"]).optional(),
// 날짜 필드들을 string에서 Date로 변환하도록 수정
forecastedAt: z.union([
@@ -97,7 +97,7 @@ export const updateVendorInvestigationSchema = z.object({
.min(0, "평가 점수는 0점 이상이어야 합니다.")
.max(100, "평가 점수는 100점 이하여야 합니다.")
.optional(),
- evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "REJECTED"]).optional(),
+ evaluationResult: z.enum(["APPROVED", "SUPPLEMENT", "REJECTED", "RESULT_SENT"]).optional(),
investigationNotes: z.string().max(1000, "QM 의견은 1000자 이내로 입력해주세요.").optional(),
attachments: z.any().optional(), // File 업로드를 위한 필드
})
diff --git a/lib/vendors/service.ts b/lib/vendors/service.ts
index 853b3701..e3309786 100644
--- a/lib/vendors/service.ts
+++ b/lib/vendors/service.ts
@@ -1654,14 +1654,20 @@ export async function generatePQNumber(isProject: boolean = false) {
}
}
-export async function requestPQVendors(input: ApproveVendorsInput & { userId: number }) {
+export async function requestPQVendors(input: ApproveVendorsInput & {
+ userId: number,
+ agreements?: Record<string, boolean>,
+ dueDate?: string | null,
+ type?: "GENERAL" | "PROJECT" | "NON_INSPECTION",
+ extraNote?: string,
+ pqItems?: string
+}) {
unstable_noStore();
-
+
const session = await getServerSession(authOptions);
const requesterId = session?.user?.id ? Number(session.user.id) : null;
try {
- // 프로젝트 정보 가져오기 (projectId가 있는 경우)
let projectInfo = null;
if (input.projectId) {
const project = await db
@@ -1673,95 +1679,72 @@ export async function requestPQVendors(input: ApproveVendorsInput & { userId: nu
.from(projects)
.where(eq(projects.id, input.projectId))
.limit(1);
-
+
if (project.length > 0) {
projectInfo = project[0];
}
}
-
- // 트랜잭션 내에서 협력업체 상태 업데이트 및 이메일 발송
+
const result = await db.transaction(async (tx) => {
- // 0. 업데이트 전 협력업체 상태 조회
const vendorsBeforeUpdate = await tx
- .select({
- id: vendors.id,
- status: vendors.status,
- })
+ .select({ id: vendors.id, status: vendors.status })
.from(vendors)
.where(inArray(vendors.id, input.ids));
-
- // 1. 협력업체 상태 업데이트
+
const [updated] = await tx
.update(vendors)
- .set({
- status: "IN_PQ",
- updatedAt: new Date()
- })
+ .set({ status: "IN_PQ", updatedAt: new Date() })
.where(inArray(vendors.id, input.ids))
.returning();
-
- // 2. 업데이트된 협력업체 정보 조회
+
const updatedVendors = await tx
- .select({
- id: vendors.id,
- vendorName: vendors.vendorName,
- email: vendors.email,
- })
+ .select({ id: vendors.id, vendorName: vendors.vendorName, email: vendors.email })
.from(vendors)
.where(inArray(vendors.id, input.ids));
-
- // 3. vendorPQSubmissions 테이블에 레코드 추가 (프로젝트 PQ와 일반 PQ 모두)
- const pqType = input.projectId ? "PROJECT" : "GENERAL";
+
+ const pqType = input.type;
const currentDate = new Date();
-
- // 기존 PQ 요청이 있는지 확인 (중복 방지)
+
const existingSubmissions = await tx
- .select({
- vendorId: vendorPQSubmissions.vendorId,
- projectId: vendorPQSubmissions.projectId,
- type: vendorPQSubmissions.type
- })
+ .select({ vendorId: vendorPQSubmissions.vendorId })
.from(vendorPQSubmissions)
.where(
and(
inArray(vendorPQSubmissions.vendorId, input.ids),
- eq(vendorPQSubmissions.type, pqType),
- input.projectId
+ pqType ? eq(vendorPQSubmissions.type, pqType) : undefined,
+ input.projectId
? eq(vendorPQSubmissions.projectId, input.projectId)
: isNull(vendorPQSubmissions.projectId)
)
);
-
- // 중복되지 않는 벤더에 대해서만 새 PQ 요청 생성
- const existingVendorIds = new Set(existingSubmissions.map(s => s.vendorId));
- const newVendorIds = input.ids.filter(id => !existingVendorIds.has(id));
-
+
+ const existingVendorIds = new Set(existingSubmissions.map((s) => s.vendorId));
+ const newVendorIds = input.ids.filter((id) => !existingVendorIds.has(id));
+
if (newVendorIds.length > 0) {
- // 각 벤더별로 유니크한 PQ 번호 생성 및 저장
const vendorPQDataPromises = newVendorIds.map(async (vendorId) => {
- // PQ 번호 생성 (프로젝트 PQ인지 여부 전달)
const pqNumber = await generatePQNumber(pqType === "PROJECT");
-
- return {
- vendorId,
- pqNumber, // 생성된 PQ 번호 저장
- projectId: input.projectId || null,
- type: pqType,
- status: "REQUESTED",
- requesterId: input.userId || requesterId, // 요청자 ID 저장
- createdAt: currentDate,
- updatedAt: currentDate,
- };
+
+ return {
+ vendorId,
+ pqNumber,
+ projectId: input.projectId || null,
+ type: pqType,
+ status: "REQUESTED",
+ requesterId: input.userId || requesterId,
+ dueDate: input.dueDate ? new Date(input.dueDate) : null,
+ agreements: input.agreements ?? {},
+ pqItems: input.pqItems || null,
+ createdAt: currentDate,
+ updatedAt: currentDate,
+ };
});
-
- // 모든 PQ 번호 생성 완료 대기
+
const vendorPQData = await Promise.all(vendorPQDataPromises);
-
- // 트랜잭션 내에서 데이터 삽입
+
await tx.insert(vendorPQSubmissions).values(vendorPQData);
}
- // 4. 로그 기록
await Promise.all(
vendorsBeforeUpdate.map(async (vendorBefore) => {
await tx.insert(vendorsLogs).values({
@@ -1770,25 +1753,23 @@ export async function requestPQVendors(input: ApproveVendorsInput & { userId: nu
action: "status_change",
oldStatus: vendorBefore.status,
newStatus: "IN_PQ",
- comment: input.projectId
- ? `Project PQ requested (Project: ${projectInfo?.projectCode || input.projectId})`
+ comment: input.projectId
+ ? `Project PQ requested (Project: ${projectInfo?.projectCode || input.projectId})`
: "General PQ requested",
});
})
);
const headersList = await headers();
- const host = headersList.get('host') || 'localhost:3000';
+ const host = headersList.get("host") || "localhost:3000";
- // 5. 각 벤더에게 이메일 발송
await Promise.all(
updatedVendors.map(async (vendor) => {
- if (!vendor.email) return; // 이메일이 없으면 스킵
-
+ if (!vendor.email) return;
+
try {
- const userLang = "en"; // 기본값, 필요시 협력업체 언어 설정에서 가져오기
-
- // PQ 번호 조회 (이메일에 포함하기 위해)
+ const userLang = "en";
+
const vendorPQ = await tx
.select({ pqNumber: vendorPQSubmissions.pqNumber })
.from(vendorPQSubmissions)
@@ -1796,59 +1777,76 @@ export async function requestPQVendors(input: ApproveVendorsInput & { userId: nu
and(
eq(vendorPQSubmissions.vendorId, vendor.id),
eq(vendorPQSubmissions.type, pqType),
- input.projectId
+ input.projectId
? eq(vendorPQSubmissions.projectId, input.projectId)
: isNull(vendorPQSubmissions.projectId)
)
)
.limit(1)
- .then(rows => rows[0]);
-
- // 프로젝트 PQ인지 일반 PQ인지에 따라 제목 변경
+ .then((rows) => rows[0]);
+
const subject = input.projectId
- ? `[eVCP] You are invited to submit Project PQ ${vendorPQ?.pqNumber || ''} for ${projectInfo?.projectCode || 'a project'}`
- : `[eVCP] You are invited to submit PQ ${vendorPQ?.pqNumber || ''}`;
-
- // 로그인 URL에 프로젝트 ID 추가 (프로젝트 PQ인 경우)
+ ? `[eVCP] You are invited to submit Project PQ ${vendorPQ?.pqNumber || ""} for ${projectInfo?.projectCode || "a project"}`
+ : input.type === "NON_INSPECTION"
+ ? `[eVCP] You are invited to submit Non-Inspection PQ ${vendorPQ?.pqNumber || ""}`
+ : `[eVCP] You are invited to submit PQ ${vendorPQ?.pqNumber || ""}`;
+
const baseLoginUrl = `${host}/partners/pq`;
const loginUrl = input.projectId
? `${baseLoginUrl}?projectId=${input.projectId}`
: baseLoginUrl;
-
+
+ // 체크된 계약 항목 배열 생성
+ const contracts = input.agreements
+ ? Object.entries(input.agreements)
+ .filter(([_, checked]) => checked)
+ .map(([name, _]) => name)
+ : [];
+
+ // PQ 대상 품목
+ const pqItems = input.pqItems || " - ";
+
await sendEmail({
to: vendor.email,
subject,
- template: input.projectId ? "project-pq" : "pq", // 프로젝트별 템플릿 사용
+ template: input.projectId ? "project-pq" : input.type === "NON_INSPECTION" ? "non-inspection-pq" : "pq",
context: {
vendorName: vendor.vendorName,
+ vendorContact: "", // 담당자 정보가 없으므로 빈 문자열
+ pqNumber: vendorPQ?.pqNumber || "",
+ senderName: session?.user?.name || "eVCP",
+ senderEmail: session?.user?.email || "noreply@evcp.com",
+ dueDate: input.dueDate ? new Date(input.dueDate).toLocaleDateString('ko-KR') : "",
+ pqItems,
+ contracts,
+ extraNote: input.extraNote || "",
+ currentYear: new Date().getFullYear().toString(),
loginUrl,
language: userLang,
- projectCode: projectInfo?.projectCode || '',
- projectName: projectInfo?.projectName || '',
+ projectCode: projectInfo?.projectCode || "",
+ projectName: projectInfo?.projectName || "",
hasProject: !!input.projectId,
- pqNumber: vendorPQ?.pqNumber || '', // PQ 번호 추가
+ pqType: input.type || "GENERAL",
},
});
} catch (emailError) {
console.error(`Failed to send email to vendor ${vendor.id}:`, emailError);
- // 이메일 전송 실패는 전체 트랜잭션을 실패시키지 않음
}
})
);
-
+
return updated;
});
-
- // 캐시 무효화
+
revalidateTag("vendors");
revalidateTag("vendor-status-counts");
revalidateTag("vendor-pq-submissions");
-
+
if (input.projectId) {
revalidateTag(`project-${input.projectId}`);
revalidateTag(`project-pq-submissions-${input.projectId}`);
}
-
+
return { data: result, error: null };
} catch (err) {
console.error("Error requesting PQ from vendors:", err);
@@ -1856,6 +1854,7 @@ export async function requestPQVendors(input: ApproveVendorsInput & { userId: nu
}
}
+
interface SendVendorsInput {
ids: number[];
}
diff --git a/lib/vendors/table/request-pq-dialog.tsx b/lib/vendors/table/request-pq-dialog.tsx
new file mode 100644
index 00000000..6d477d9f
--- /dev/null
+++ b/lib/vendors/table/request-pq-dialog.tsx
@@ -0,0 +1,286 @@
+"use client"
+
+import * as React from "react"
+import { type Row } from "@tanstack/react-table"
+import { Loader, SendHorizonal } 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 {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Checkbox } from "@/components/ui/checkbox"
+import { Label } from "@/components/ui/label"
+import { Vendor } from "@/db/schema/vendors"
+import { requestPQVendors } from "../service"
+import { getProjectsWithPQList } from "@/lib/pq/service"
+import type { Project } from "@/lib/pq/service"
+import { useSession } from "next-auth/react"
+import { DatePicker } from "@/components/ui/date-picker"
+
+interface RequestPQDialogProps extends React.ComponentPropsWithoutRef<typeof Dialog> {
+ vendors: Row<Vendor>["original"][]
+ showTrigger?: boolean
+ onSuccess?: () => void
+}
+
+const AGREEMENT_LIST = [
+ "준법서약",
+ "표준하도급계약",
+ "안전보건관리계약",
+ "윤리규범 준수 서약",
+ "동반성장협약",
+ "내국신용장 미개설 합의",
+ "기술자료 제출 기본 동의",
+ "GTC 합의",
+]
+
+export function RequestPQDialog({ vendors, showTrigger = true, onSuccess, ...props }: RequestPQDialogProps) {
+ const [isApprovePending, startApproveTransition] = React.useTransition()
+ const isDesktop = useMediaQuery("(min-width: 640px)")
+ const { data: session } = useSession()
+
+ const [type, setType] = React.useState<"GENERAL" | "PROJECT" | "NON_INSPECTION" | null>(null)
+ const [dueDate, setDueDate] = React.useState<string | null>(null)
+ const [projects, setProjects] = React.useState<Project[]>([])
+ const [selectedProjectId, setSelectedProjectId] = React.useState<number | null>(null)
+ const [agreements, setAgreements] = React.useState<Record<string, boolean>>({})
+ const [extraNote, setExtraNote] = React.useState<string>("")
+ const [pqItems, setPqItems] = React.useState<string>("")
+ const [isLoadingProjects, setIsLoadingProjects] = React.useState(false)
+
+ React.useEffect(() => {
+ if (type === "PROJECT") {
+ setIsLoadingProjects(true)
+ getProjectsWithPQList().then(setProjects).catch(() => toast.error("프로젝트 로딩 실패"))
+ .finally(() => setIsLoadingProjects(false))
+ }
+ }, [type])
+
+ React.useEffect(() => {
+ if (!props.open) {
+ setType(null)
+ setSelectedProjectId(null)
+ setAgreements({})
+ setDueDate(null)
+ setPqItems("")
+ setExtraNote("")
+ }
+ }, [props.open])
+
+ const onApprove = () => {
+ if (!type) return toast.error("PQ 유형을 선택하세요.")
+ if (type === "PROJECT" && !selectedProjectId) return toast.error("프로젝트를 선택하세요.")
+ if (!dueDate) return toast.error("마감일을 선택하세요.")
+ if (!session?.user?.id) return toast.error("인증 실패")
+
+ startApproveTransition(async () => {
+ const { error } = await requestPQVendors({
+ ids: vendors.map((v) => v.id),
+ userId: Number(session.user.id),
+ agreements,
+ dueDate,
+ projectId: type === "PROJECT" ? selectedProjectId : null,
+ type: type || "GENERAL",
+ extraNote,
+ pqItems,
+ })
+
+ if (error) {
+ toast.error(error)
+ return
+ }
+
+ props.onOpenChange?.(false)
+ toast.success("PQ가 성공적으로 요청되었습니다")
+ onSuccess?.()
+ })
+ }
+
+ const dialogContent = (
+ <div className="space-y-4 py-2">
+ {/* 선택된 협력업체 정보 */}
+ <div className="space-y-2">
+ <Label>선택된 협력업체 ({vendors.length}개)</Label>
+ <div className="max-h-40 overflow-y-auto border rounded-md p-3 space-y-2">
+ {vendors.map((vendor) => (
+ <div key={vendor.id} className="flex items-center justify-between text-sm">
+ <div className="flex-1">
+ <div className="font-medium">{vendor.vendorName}</div>
+ <div className="text-muted-foreground">
+ {vendor.vendorCode} • {vendor.email || "이메일 없음"}
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ <div className="space-y-2">
+ <Label htmlFor="type">PQ 종류 선택</Label>
+ <Select onValueChange={(val: "GENERAL" | "PROJECT" | "NON_INSPECTION") => setType(val)} value={type ?? undefined}>
+ <SelectTrigger id="type"><SelectValue placeholder="PQ 종류를 선택하세요" /></SelectTrigger>
+ <SelectContent>
+ <SelectItem value="GENERAL">일반 PQ</SelectItem>
+ <SelectItem value="PROJECT">프로젝트 PQ</SelectItem>
+ <SelectItem value="NON_INSPECTION">미실사 PQ</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ {type === "PROJECT" && (
+ <div className="space-y-2">
+ <Label htmlFor="project">프로젝트 선택</Label>
+ <Select onValueChange={(val) => setSelectedProjectId(Number(val))}>
+ <SelectTrigger id="project">
+ <SelectValue placeholder="프로젝트 선택" />
+ </SelectTrigger>
+ <SelectContent>
+ {isLoadingProjects ? (
+ <SelectItem value="loading" disabled>로딩 중...</SelectItem>
+ ) : projects.map((p) => (
+ <SelectItem key={p.id} value={p.id.toString()}>{p.projectCode} - {p.projectName}</SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ )}
+
+ {/* 마감일 입력 */}
+ <div className="space-y-2">
+ <Label htmlFor="dueDate">PQ 제출 마감일</Label>
+ <DatePicker
+ date={dueDate ? new Date(dueDate) : undefined}
+ onSelect={(date?: Date) => setDueDate(date ? date.toISOString().slice(0, 10) : "")}
+ placeholder="마감일 선택"
+ />
+ </div>
+
+ {/* PQ 대상품목 */}
+ <div className="space-y-2">
+ <Label htmlFor="pqItems">PQ 대상품목</Label>
+ <textarea
+ id="pqItems"
+ value={pqItems}
+ onChange={(e) => setPqItems(e.target.value)}
+ placeholder="PQ 대상품목을 입력하세요 (선택사항)"
+ className="w-full rounded-md border px-3 py-2 text-sm min-h-20 resize-none"
+ />
+ </div>
+
+ {/* 추가 안내사항 */}
+ <div className="space-y-2">
+ <Label htmlFor="extraNote">추가 안내사항</Label>
+ <textarea
+ id="extraNote"
+ value={extraNote}
+ onChange={(e) => setExtraNote(e.target.value)}
+ placeholder="추가 안내사항을 입력하세요 (선택사항)"
+ className="w-full rounded-md border px-3 py-2 text-sm min-h-20 resize-none"
+ />
+ </div>
+
+ <div className="space-y-2">
+ <Label>계약 항목 선택</Label>
+ {AGREEMENT_LIST.map((label) => (
+ <div key={label} className="flex items-center gap-2">
+ <Checkbox
+ id={label}
+ checked={agreements[label] || false}
+ onCheckedChange={(val) =>
+ setAgreements((prev) => ({ ...prev, [label]: Boolean(val) }))
+ }
+ />
+ <Label htmlFor={label}>{label}</Label>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+
+ if (isDesktop) {
+ return (
+ <Dialog {...props}>
+ {showTrigger && (
+ <DialogTrigger asChild>
+ <Button variant="outline" size="sm" className="gap-2">
+ <SendHorizonal className="size-4" /> PQ 요청 ({vendors.length})
+ </Button>
+ </DialogTrigger>
+ )}
+ <DialogContent className="max-h-[80vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>PQ 요청</DialogTitle>
+ <DialogDescription>
+ <span className="font-medium">{vendors.length}</span>
+ {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 PQ를 요청합니다.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="flex-1 overflow-y-auto">
+ {dialogContent}
+ </div>
+ <DialogFooter>
+ <DialogClose asChild><Button variant="outline">취소</Button></DialogClose>
+ <Button onClick={onApprove} disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId)}>
+ {isApprovePending && <Loader className="mr-2 size-4 animate-spin" />}요청하기
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+ }
+
+ return (
+ <Drawer {...props}>
+ {showTrigger && (
+ <DrawerTrigger asChild>
+ <Button variant="outline" size="sm" className="gap-2">
+ <SendHorizonal className="size-4" /> PQ 요청 ({vendors.length})
+ </Button>
+ </DrawerTrigger>
+ )}
+ <DrawerContent className="max-h-[80vh] flex flex-col">
+ <DrawerHeader>
+ <DrawerTitle>PQ 요청</DrawerTitle>
+ <DrawerDescription>
+ <span className="font-medium">{vendors.length}</span>
+ {vendors.length === 1 ? "개 협력업체" : "개 협력업체들"}에게 PQ를 요청합니다.
+ </DrawerDescription>
+ </DrawerHeader>
+ <div className="flex-1 overflow-y-auto px-4">
+ {dialogContent}
+ </div>
+ <DrawerFooter>
+ <DrawerClose asChild><Button variant="outline">취소</Button></DrawerClose>
+ <Button onClick={onApprove} disabled={isApprovePending || !type || (type === "PROJECT" && !selectedProjectId)}>
+ {isApprovePending && <Loader className="mr-2 size-4 animate-spin" />}요청하기
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ )
+}
diff --git a/lib/vendors/table/vendors-table-toolbar-actions.tsx b/lib/vendors/table/vendors-table-toolbar-actions.tsx
index 12f1dfcd..6d5f7425 100644
--- a/lib/vendors/table/vendors-table-toolbar-actions.tsx
+++ b/lib/vendors/table/vendors-table-toolbar-actions.tsx
@@ -17,6 +17,7 @@ import {
import { VendorWithType } from "@/db/schema/vendors"
import { ApproveVendorsDialog } from "./approve-vendor-dialog"
import { RequestPQVendorsDialog } from "./request-vendor-pg-dialog"
+import { RequestPQDialog } from "./request-pq-dialog"
import { RequestProjectPQDialog } from "./request-project-pq-dialog"
import { SendVendorsDialog } from "./send-vendor-dialog"
import { RequestVendorsInvestigateDialog } from "./request-vendor-investigate-dialog"
@@ -75,8 +76,8 @@ export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActions
.filter(vendor => vendor.status === "PQ_APPROVED");
}, [table.getFilteredSelectedRowModel().rows]);
- // 프로젝트 PQ를 보낼 수 있는 협력업체 상태 필터링
- const projectPQEligibleVendors = React.useMemo(() => {
+ // 통합 PQ를 보낼 수 있는 협력업체 상태 필터링
+ const pqEligibleVendors = React.useMemo(() => {
return table
.getFilteredSelectedRowModel()
.rows
@@ -153,17 +154,17 @@ export function VendorsTableToolbarActions({ table }: VendorsTableToolbarActions
)}
{/* 일반 PQ 요청: IN_REVIEW 상태인 협력업체가 있을 때만 표시 */}
- {inReviewVendors.length > 0 && (
+ {/* {inReviewVendors.length > 0 && (
<RequestPQVendorsDialog
vendors={inReviewVendors}
onSuccess={() => table.toggleAllRowsSelected(false)}
/>
- )}
+ )} */}
- {/* 프로젝트 PQ 요청: 적격 상태의 협력업체가 있을 때만 표시 */}
- {projectPQEligibleVendors.length > 0 && (
- <RequestProjectPQDialog
- vendors={projectPQEligibleVendors}
+ {/* 통합 PQ 요청: 적격 상태의 협력업체가 있을 때만 표시 */}
+ {pqEligibleVendors.length > 0 && (
+ <RequestPQDialog
+ vendors={pqEligibleVendors}
onSuccess={() => table.toggleAllRowsSelected(false)}
/>
)}