diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/additional-info/join-form.tsx | 60 | ||||
| -rw-r--r-- | components/pq-input/pq-input-tabs.tsx | 90 | ||||
| -rw-r--r-- | components/pq-input/pq-review-wrapper.tsx | 404 | ||||
| -rw-r--r-- | components/signup/join-form.tsx | 201 | ||||
| -rw-r--r-- | components/vendor-regular-registrations/document-status-dialog.tsx | 19 |
5 files changed, 523 insertions, 251 deletions
diff --git a/components/additional-info/join-form.tsx b/components/additional-info/join-form.tsx index afe38841..1642962f 100644 --- a/components/additional-info/join-form.tsx +++ b/components/additional-info/join-form.tsx @@ -223,6 +223,8 @@ export function InfoForm() { }) const isFormValid = form.formState.isValid + const watchedCountry = form.watch("country") + const isDomesticVendor = watchedCountry === "KR" // Field array for contacts const { fields: contactFields, append: addContact, remove: removeContact, replace: replaceContacts } = @@ -369,6 +371,29 @@ export function InfoForm() { fetchVendorData() }, [companyId, form, replaceContacts]) + // 도로명주소 검색 결과 수신 (내자 벤더만) + React.useEffect(() => { + if (!isDomesticVendor) return + + const handleMessage = (event: MessageEvent) => { + if (!event.data || event.data.type !== "JUSO_SELECTED") return + const { zipNo, roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {} + const road = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim() + + form.setValue("postalCode", zipNo || form.getValues("postalCode") || "", { shouldDirty: true }) + form.setValue("address", road || form.getValues("address") || "", { shouldDirty: true }) + form.setValue("addressDetail", addrDetail || form.getValues("addressDetail") || "", { shouldDirty: true }) + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [isDomesticVendor, form]) + + const handleJusoSearch = () => { + if (!isDomesticVendor) return + window.open("/api/juso", "jusoSearch", "width=570,height=420,scrollbars=yes,resizable=yes") + } + // 컴포넌트 언마운트 시 미리보기 URL 정리 (blob URL만) React.useEffect(() => { return () => { @@ -1224,11 +1249,29 @@ export function InfoForm() { name="address" render={({ field }) => ( <FormItem> - <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500"> - 주소 - </FormLabel> + <div className="flex items-center justify-between gap-2"> + <FormLabel className="after:content-['*'] after:ml-0.5 after:text-red-500"> + 주소 + </FormLabel> + {isDomesticVendor && ( + <Button + type="button" + variant="secondary" + size="sm" + onClick={handleJusoSearch} + disabled={isSubmitting} + > + 주소 검색 + </Button> + )} + </div> <FormControl> - <Input {...field} disabled={isSubmitting} /> + <Input + {...field} + disabled={isSubmitting} + readOnly={isDomesticVendor} + className={cn(isDomesticVendor && "bg-muted text-muted-foreground")} + /> </FormControl> <FormMessage /> </FormItem> @@ -1258,7 +1301,13 @@ export function InfoForm() { <FormItem> <FormLabel>우편번호</FormLabel> <FormControl> - <Input {...field} disabled={isSubmitting} placeholder="우편번호를 입력해주세요" /> + <Input + {...field} + disabled={isSubmitting} + readOnly={isDomesticVendor} + className={cn(isDomesticVendor && "bg-muted text-muted-foreground")} + placeholder="우편번호를 입력해주세요" + /> </FormControl> <FormMessage /> </FormItem> @@ -1960,7 +2009,6 @@ export function InfoForm() { assignedDepartment: registrationData.registration?.assignedDepartment, assignedUser: registrationData.registration?.assignedUser, remarks: registrationData.registration?.remarks, - safetyQualificationContent: registrationData.registration?.safetyQualificationContent || null, gtcSkipped: registrationData.registration?.gtcSkipped || false, additionalInfo: registrationData.additionalInfo, documentSubmissions: registrationData.documentStatus, diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx index df911d5e..6c9a1254 100644 --- a/components/pq-input/pq-input-tabs.tsx +++ b/components/pq-input/pq-input-tabs.tsx @@ -152,6 +152,7 @@ export function PQInputTabs({ projectData, isReadOnly = false, currentPQ, // 추가: 현재 PQ Submission 정보 + vendorCountry, }: { data: PQGroupData[] vendorId: number @@ -163,6 +164,7 @@ export function PQInputTabs({ status: string; type: string; } | null + vendorCountry?: string | null }) { const [isSaving, setIsSaving] = React.useState(false) @@ -208,6 +210,33 @@ export function PQInputTabs({ }) } + // 벤더 내자/외자 판별 (국가 코드 기반) + const isDomesticVendor = React.useMemo(() => { + if (!vendorCountry) return null; // null 이면 필터 미적용 + return vendorCountry === "KR" || vendorCountry === "한국"; + }, [vendorCountry]); + + // 벤더 유형에 따라 PQ 항목 필터링 + const filteredData: PQGroupData[] = React.useMemo(() => { + // 벤더 타입 정보가 없으면 전체 노출 + if (isDomesticVendor === null) return data; + + const filterItemByType = (item: any) => { + const itemType = item.type || "내외자"; + if (itemType === "내외자") return true; + if (itemType === "내자") return isDomesticVendor === true; + if (itemType === "외자") return isDomesticVendor === false; + return true; + }; + + return data + .map((group) => ({ + ...group, + items: group.items.filter(filterItemByType), + })) + .filter((group) => group.items.length > 0); + }, [data, isDomesticVendor]); + // 필터링 함수 const shouldShowItem = (isSaved: boolean) => { if (filterOptions.showAll) return true; @@ -223,7 +252,7 @@ export function PQInputTabs({ function createInitialFormValues(): PQFormValues { const answers: PQFormValues["answers"] = [] - data.forEach((group) => { + filteredData.forEach((group) => { // 그룹 내 아이템들을 코드 순서로 정렬 const sortedItems = sortByCode(group.items) @@ -383,7 +412,7 @@ export function PQInputTabs({ try { const answerData = form.getValues(`answers.${answerIndex}`) const criteriaId = answerData.criteriaId - const item = data.flatMap(group => group.items).find(item => item.criteriaId === criteriaId) + const item = filteredData.flatMap(group => group.items).find(item => item.criteriaId === criteriaId) const inputFormat = item?.inputFormat || "TEXT" // Validation // 모든 항목은 필수로 처리 (isRequired 제거됨) @@ -723,7 +752,14 @@ export function PQInputTabs({ {/* 프로젝트 정보 섹션 */} {renderProjectInfo()} - <Tabs defaultValue={data[0]?.groupName || ""} className="w-full"> + {filteredData.length === 0 ? ( + <div className="rounded-md border border-dashed p-6 text-sm text-muted-foreground"> + 표시할 PQ 항목이 없습니다. (벤더 내/외자 구분 필터 적용) + </div> + ) : ( + <> + + <Tabs defaultValue={filteredData[0]?.groupName || ""} className="w-full"> {/* Top Controls - Sticky Header */} <div className="sticky top-0 z-10 bg-background border-b border-border mb-4 pb-4"> {/* Item Count Display */} @@ -810,7 +846,7 @@ export function PQInputTabs({ <div className="flex justify-between items-center"> <TabsList className="grid grid-cols-4"> - {data.map((group) => { + {filteredData.map((group) => { const colorClasses = getTabColorClasses(group.groupName) return ( <TabsTrigger @@ -880,7 +916,7 @@ export function PQInputTabs({ </div> {/* Render each group */} - {data.map((group) => ( + {filteredData.map((group) => ( <TabsContent key={group.groupName} value={group.groupName}> {/* 2-column grid */} <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 pb-4"> @@ -958,6 +994,46 @@ export function PQInputTabs({ </CardHeader> <CardContent className="pt-3 space-y-3 h-full flex flex-col"> + {/* 기준 첨부 파일 */} + {item.criteriaAttachments && item.criteriaAttachments.length > 0 && ( + <div className="space-y-2"> + <FormLabel>기준 첨부파일</FormLabel> + <FileList> + {item.criteriaAttachments.map((file, idx) => ( + <FileListItem key={idx}> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <FileListName>{file.fileName}</FileListName> + {file.fileSize && ( + <FileListDescription>{prettyBytes(file.fileSize)}</FileListDescription> + )} + </FileListInfo> + <FileListAction + onClick={async () => { + try { + const { downloadFile } = await import('@/lib/file-download') + await downloadFile(file.filePath, file.fileName, { showToast: true }) + } catch (error) { + console.error('다운로드 오류:', error) + toast({ + title: "다운로드 실패", + description: "파일 다운로드 중 오류가 발생했습니다.", + variant: "destructive" + }) + } + }} + > + <Download className="h-4 w-4" /> + <span className="sr-only">Download</span> + </FileListAction> + </FileListHeader> + </FileListItem> + ))} + </FileList> + </div> + )} + {/* 프로젝트별 추가 필드 (contractInfo, additionalRequirement) */} {projectId && contractInfo && ( <div className="space-y-1"> @@ -1379,6 +1455,8 @@ export function PQInputTabs({ </TabsContent> ))} </Tabs> + </> + )} </form> {/* Confirmation Dialog */} @@ -1395,7 +1473,7 @@ export function PQInputTabs({ </DialogHeader> <div className="space-y-4 max-h-[600px] overflow-y-auto "> - {data.map((group, groupIndex) => ( + {filteredData.map((group, groupIndex) => ( <div key={groupIndex}> {group.items.map((item) => { const answerObj = form diff --git a/components/pq-input/pq-review-wrapper.tsx b/components/pq-input/pq-review-wrapper.tsx index 1e172744..ac9629cb 100644 --- a/components/pq-input/pq-review-wrapper.tsx +++ b/components/pq-input/pq-review-wrapper.tsx @@ -21,9 +21,10 @@ import { DialogTitle } from "@/components/ui/dialog" import { useToast } from "@/hooks/use-toast" -import { CheckCircle, AlertCircle, Paperclip, Square } from "lucide-react" +import { CheckCircle, AlertCircle, Paperclip, Square, Download } from "lucide-react" import { PQGroupData } from "@/lib/pq/service" -import { approvePQAction, rejectPQAction, updateSHICommentAction, approveQMReviewAction, rejectQMReviewAction, requestPqSupplementAction } from "@/lib/pq/service" +import { approvePQAction, rejectPQAction, updateSHICommentAction, approveQMReviewAction, rejectQMReviewAction, requestPqSupplementAction, approveSafetyPQAction, rejectSafetyPQAction } from "@/lib/pq/service" +import { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction, FileListIcon } from "@/components/ui/file-list" // import * as ExcelJS from 'exceljs'; // import { saveAs } from "file-saver"; @@ -49,25 +50,32 @@ interface PQReviewWrapperProps { vendorId: number pqSubmission: PQSubmission vendorInfo?: any // 협력업체 정보 (선택사항) + vendorCountry?: string | null } export function PQReviewWrapper({ pqData, vendorId, pqSubmission, - vendorInfo + vendorInfo, + vendorCountry, }: PQReviewWrapperProps) { const router = useRouter() const { toast } = useToast() + const [isSafetyApproving, setIsSafetyApproving] = React.useState(false) + const [isSafetyRejecting, setIsSafetyRejecting] = React.useState(false) const [isApproving, setIsApproving] = React.useState(false) const [isRejecting, setIsRejecting] = React.useState(false) const [isQMApproving, setIsQMApproving] = React.useState(false) const [isQMRejecting, setIsQMRejecting] = React.useState(false) + const [showSafetyApproveDialog, setShowSafetyApproveDialog] = React.useState(false) + const [showSafetyRejectDialog, setShowSafetyRejectDialog] = React.useState(false) const [showApproveDialog, setShowApproveDialog] = React.useState(false) const [showRejectDialog, setShowRejectDialog] = React.useState(false) const [showQMApproveDialog, setShowQMApproveDialog] = React.useState(false) const [showQMRejectDialog, setShowQMRejectDialog] = React.useState(false) const [showSupplementDialog, setShowSupplementDialog] = React.useState(false) + const [safetyRejectReason, setSafetyRejectReason] = React.useState("") const [rejectReason, setRejectReason] = React.useState("") const [qmRejectReason, setQmRejectReason] = React.useState("") const [supplementComment, setSupplementComment] = React.useState("") @@ -96,6 +104,32 @@ export function PQReviewWrapper({ return 0 }) } + + // 벤더 내자/외자 판별 (국가 코드 기반) + const isDomesticVendor = React.useMemo(() => { + if (!vendorCountry) return null; // 정보 없으면 필터 미적용 + return vendorCountry === "KR" || vendorCountry === "한국"; + }, [vendorCountry]); + + // 벤더 유형에 따라 PQ 항목 필터링 + const filteredData: PQGroupData[] = React.useMemo(() => { + if (isDomesticVendor === null) return pqData; + + const filterItemByType = (item: any) => { + const itemType = item.type || "내외자"; + if (itemType === "내외자") return true; + if (itemType === "내자") return isDomesticVendor === true; + if (itemType === "외자") return isDomesticVendor === false; + return true; + }; + + return pqData + .map((group) => ({ + ...group, + items: group.items.filter(filterItemByType), + })) + .filter((group) => group.items.length > 0); + }, [pqData, isDomesticVendor]); // 기존 SHI 코멘트를 로컬 상태에 초기화 @@ -111,6 +145,87 @@ export function PQReviewWrapper({ setShiComments(initialComments) }, [pqData]) + // 안전 PQ 승인 처리 + const handleSafetyApprove = async () => { + try { + setIsSafetyApproving(true) + const result = await approveSafetyPQAction({ + pqSubmissionId: pqSubmission.id, + vendorId: vendorId, + }) + + if (result.ok) { + toast({ + title: "안전 PQ 승인 완료", + description: "안전 검토가 승인되었습니다.", + }) + router.refresh() + } else { + toast({ + title: "안전 승인 실패", + description: result.error || "안전 PQ 승인 중 오류가 발생했습니다.", + variant: "destructive", + }) + } + } catch (error) { + console.error("안전 PQ 승인 오류:", error) + toast({ + title: "안전 승인 실패", + description: "안전 PQ 승인 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setIsSafetyApproving(false) + setShowSafetyApproveDialog(false) + } + } + + // 안전 PQ 거절 처리 + const handleSafetyReject = async () => { + if (!safetyRejectReason.trim()) { + toast({ + title: "거절 사유 필요", + description: "안전 거절 사유를 입력해주세요.", + variant: "destructive", + }) + return + } + + try { + setIsSafetyRejecting(true) + const result = await rejectSafetyPQAction({ + pqSubmissionId: pqSubmission.id, + vendorId: vendorId, + rejectReason: safetyRejectReason, + }) + + if (result.ok) { + toast({ + title: "안전 PQ 거절 완료", + description: "안전 검토에서 거절되었습니다.", + }) + router.refresh() + } else { + toast({ + title: "안전 거절 실패", + description: result.error || "안전 PQ 거절 중 오류가 발생했습니다.", + variant: "destructive", + }) + } + } catch (error) { + console.error("안전 PQ 거절 오류:", error) + toast({ + title: "안전 거절 실패", + description: "안전 PQ 거절 중 오류가 발생했습니다.", + variant: "destructive", + }) + } finally { + setSafetyRejectReason("") + setIsSafetyRejecting(false) + setShowSafetyRejectDialog(false) + } + } + // PQ 승인 처리 const handleApprove = async () => { try { @@ -298,141 +413,6 @@ export function PQReviewWrapper({ } } - // // Excel export 처리 - // const handleExportToExcel = async () => { - // try { - // setIsExporting(true) - - // // 워크북 생성 - // const workbook = new ExcelJS.Workbook() - // workbook.creator = 'PQ Management System' - // workbook.created = new Date() - - // // 메인 시트 생성 - // const worksheet = workbook.addWorksheet("PQ 항목") - - // // 헤더 정의 - // const headers = [ - // "그룹명", - // "코드", - // "체크포인트", - // "설명", - // "입력형식", - // "필수여부", - // "벤더답변", - // "SHI 코멘트", - // "벤더 답변", - // ] - - // // 헤더 추가 - // worksheet.addRow(headers) - - // // 헤더 스타일 적용 - // const headerRow = worksheet.getRow(1) - // headerRow.font = { bold: true } - // headerRow.fill = { - // type: 'pattern', - // pattern: 'solid', - // fgColor: { argb: 'FFE0E0E0' } - // } - // headerRow.alignment = { vertical: 'middle', horizontal: 'center' } - - // // 컬럼 너비 설정 - // worksheet.columns = [ - // { header: "그룹명", key: "groupName", width: 15 }, - // { header: "코드", key: "code", width: 12 }, - // { header: "체크포인트", key: "checkPoint", width: 30 }, - // { header: "설명", key: "description", width: 40 }, - // { header: "입력형식", key: "inputFormat", width: 12 }, - - // { header: "벤더답변", key: "answer", width: 30 }, - // { header: "SHI 코멘트", key: "shiComment", width: 30 }, - // { header: "벤더 답변", key: "vendorReply", width: 30 }, - // ] - - // // 데이터 추가 - // pqData.forEach(group => { - // group.items.forEach(item => { - // const rowData = [ - // group.groupName, - // item.code, - // item.checkPoint, - // item.description || "", - // item.inputFormat || "", - - // item.answer || "", - // item.shiComment || "", - // item.vendorReply || "", - // ] - // worksheet.addRow(rowData) - // }) - // }) - - // // 전체 셀에 테두리 추가 - // worksheet.eachRow((row, rowNumber) => { - // row.eachCell((cell) => { - // cell.border = { - // top: { style: 'thin' }, - // left: { style: 'thin' }, - // bottom: { style: 'thin' }, - // right: { style: 'thin' } - // } - // // 긴 텍스트는 자동 줄바꿈 - // cell.alignment = { - // vertical: 'top', - // horizontal: 'left', - // wrapText: true - // } - // }) - // }) - - // // 정보 시트 생성 - // const infoSheet = workbook.addWorksheet("정보") - // infoSheet.addRow(["벤더명", pqSubmission.vendorName]) - // if (pqSubmission.projectName) { - // infoSheet.addRow(["프로젝트명", pqSubmission.projectName]) - // } - // infoSheet.addRow(["생성일", new Date().toLocaleDateString('ko-KR')]) - // infoSheet.addRow(["총 항목 수", pqData.reduce((total, group) => total + group.items.length, 0)]) - - // // 정보 시트 스타일링 - // infoSheet.columns = [ - // { header: "항목", key: "item", width: 20 }, - // { header: "값", key: "value", width: 40 } - // ] - - // const infoHeaderRow = infoSheet.getRow(1) - // infoHeaderRow.font = { bold: true } - // infoHeaderRow.fill = { - // type: 'pattern', - // pattern: 'solid', - // fgColor: { argb: 'FFE6F3FF' } - // } - - // // 파일명 생성 - // const defaultFilename = pqSubmission.projectName - // ? `${pqSubmission.vendorName}_${pqSubmission.projectName}_PQ_${new Date().toISOString().slice(0, 10)}` - // : `${pqSubmission.vendorName}_PQ_${new Date().toISOString().slice(0, 10)}` - // const finalFilename = defaultFilename - - // // 파일 다운로드 - // const buffer = await workbook.xlsx.writeBuffer() - // const blob = new Blob([buffer], { - // type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - // }) - // saveAs(blob, `${finalFilename}.xlsx`) - // } catch (error) { - // console.error("Excel export 오류:", error) - // toast({ - // title: "내보내기 실패", - // description: "Excel 내보내기 중 오류가 발생했습니다.", - // variant: "destructive" - // }) - // } finally { - // setIsExporting(false) - // } - // } - // PQ 거부 처리 const handleReject = async () => { if (!rejectReason.trim()) { @@ -482,8 +462,14 @@ export function PQReviewWrapper({ return ( <div className="space-y-6"> + {filteredData.length === 0 && ( + <div className="rounded-md border border-dashed p-4 text-sm text-muted-foreground"> + 표시할 PQ 항목이 없습니다. (벤더 내/외자 구분 필터 적용) + </div> + )} + {/* 그룹별 PQ 항목 표시 */} - {pqData.map((group) => ( + {filteredData.map((group) => ( <div key={group.groupName} className="space-y-4"> <h3 className="text-lg font-medium">{group.groupName}</h3> @@ -530,6 +516,43 @@ export function PQReviewWrapper({ </div> </CardHeader> <CardContent className="space-y-4"> + {item.criteriaAttachments && item.criteriaAttachments.length > 0 && ( + <div className="space-y-2"> + <p className="text-sm font-medium">기준 첨부파일</p> + <FileList> + {item.criteriaAttachments.map((file) => ( + <FileListItem key={file.attachId}> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <FileListName>{file.fileName}</FileListName> + {file.fileSize && ( + <FileListDescription>{file.fileSize} bytes</FileListDescription> + )} + </FileListInfo> + <FileListAction + onClick={async () => { + try { + const { downloadFile } = await import('@/lib/file-download') + await downloadFile(file.filePath, file.fileName, { showToast: true }) + } catch (error) { + toast({ + title: "다운로드 실패", + description: "파일 다운로드 중 오류가 발생했습니다.", + variant: "destructive" + }) + } + }} + > + <Download className="h-4 w-4" /> + <span className="sr-only">Download</span> + </FileListAction> + </FileListHeader> + </FileListItem> + ))} + </FileList> + </div> + )} {/* 프로젝트별 추가 정보 */} {pqSubmission.projectId && item.contractInfo && ( <div className="space-y-1"> @@ -767,15 +790,42 @@ export function PQReviewWrapper({ {/* 검토 버튼 - 상태에 따라 다른 버튼 표시 */} <div className="fixed bottom-4 right-4 bg-background p-4 rounded-lg shadow-md border"> <div className="flex gap-2"> - {/* SUBMITTED 상태: 구매 담당자 승인/거절 */} + {/* SUBMITTED 상태: 안전 담당자 승인/거절 */} {pqSubmission.status === "SUBMITTED" && ( <> <Button variant="outline" + onClick={() => setShowSafetyRejectDialog(true)} + disabled={isSafetyRejecting} + > + {isSafetyRejecting ? "안전 거절 중..." : "안전 거절"} + </Button> + <Button + variant="secondary" + onClick={() => setShowSupplementDialog(true)} + disabled={isSendingSupplement} + > + 보완요청 + </Button> + <Button + variant="default" + onClick={() => setShowSafetyApproveDialog(true)} + disabled={isSafetyApproving} + > + {isSafetyApproving ? "안전 승인 중..." : "안전 승인"} + </Button> + </> + )} + + {/* SAFETY_APPROVED 상태: 구매 담당자 승인/거절 */} + {pqSubmission.status === "SAFETY_APPROVED" && ( + <> + <Button + variant="outline" onClick={() => setShowRejectDialog(true)} disabled={isRejecting} > - {isRejecting ? "거부 중..." : "거부"} + {isRejecting ? "거부 중..." : "구매 거부"} </Button> <Button variant="secondary" @@ -844,10 +894,82 @@ export function PQReviewWrapper({ <span>거절됨</span> </div> )} + + {/* SAFETY_REJECTED 상태: 안전 거절 표시 */} + {pqSubmission.status === "SAFETY_REJECTED" && ( + <div className="flex items-center gap-2 text-red-600"> + <AlertCircle className="h-4 w-4" /> + <span>안전 거절됨</span> + </div> + )} </div> </div> + {/* 안전 승인 확인 다이얼로그 */} + <Dialog open={showSafetyApproveDialog} onOpenChange={setShowSafetyApproveDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>안전 PQ 승인 확인</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 안전 검토에서 승인하시겠습니까? + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <DialogFooter> + <Button variant="outline" onClick={() => setShowSafetyApproveDialog(false)}> + 취소 + </Button> + <Button onClick={handleSafetyApprove} disabled={isSafetyApproving}> + {isSafetyApproving ? "안전 승인 중..." : "안전 승인"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + + {/* 안전 거부 확인 다이얼로그 */} + <Dialog open={showSafetyRejectDialog} onOpenChange={setShowSafetyRejectDialog}> + <DialogContent> + <DialogHeader> + <DialogTitle>안전 PQ 거부</DialogTitle> + <DialogDescription> + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 안전 검토에서 거부하는 이유를 입력해주세요. + {pqSubmission.projectId && ( + <span> 프로젝트: {pqSubmission.projectName}</span> + )} + </DialogDescription> + </DialogHeader> + <Textarea + value={safetyRejectReason} + onChange={(e) => setSafetyRejectReason(e.target.value)} + placeholder="안전 거부 사유를 입력하세요" + className="min-h-24" + /> + <DialogFooter> + <Button variant="outline" onClick={() => setShowSafetyRejectDialog(false)}> + 취소 + </Button> + <Button + variant="destructive" + onClick={handleSafetyReject} + disabled={isSafetyRejecting || !safetyRejectReason.trim()} + > + {isSafetyRejecting ? "안전 거절 중..." : "안전 거절"} + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + {/* 승인 확인 다이얼로그 */} <Dialog open={showApproveDialog} onOpenChange={setShowApproveDialog}> <DialogContent> diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx index 6885279a..973fb00c 100644 --- a/components/signup/join-form.tsx +++ b/components/signup/join-form.tsx @@ -910,6 +910,32 @@ function CompleteVendorForm({ }: VendorStepProps) { const [isSubmitting, setIsSubmitting] = useState(false); const { toast } = useToast(); + const effectiveCountry = data.country || accountData.country || ""; + const isKR = effectiveCountry === "KR"; + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + if (!event.data || event.data.type !== "JUSO_SELECTED") return; + const { zipNo, roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {}; + const combinedAddress = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim(); + onChange(prev => ({ + ...prev, + postalCode: zipNo || prev.postalCode, + address: combinedAddress || prev.address, + addressDetail: addrDetail || prev.addressDetail, + })); + }; + window.addEventListener("message", handleMessage); + return () => window.removeEventListener("message", handleMessage); + }, [onChange]); + + const handleJusoSearch = () => { + window.open( + "/api/juso", + "jusoSearch", + "width=570,height=420,scrollbars=yes,resizable=yes" + ); + }; // 담당자 관리 함수들 const addContact = () => { @@ -1259,13 +1285,28 @@ function CompleteVendorForm({ {/* 주소 */} <div> - <label className="block text-sm font-medium mb-1"> - {t('address')} <span className="text-red-500">*</span> - </label> + <div className="flex items-center justify-between gap-2 mb-1"> + <label className="block text-sm font-medium"> + {t('address')} <span className="text-red-500">*</span> + </label> + {isKR && ( + <Button + type="button" + variant="secondary" + size="sm" + onClick={handleJusoSearch} + disabled={isSubmitting} + > + 주소 검색 + </Button> + )} + </div> <Input value={data.address} onChange={(e) => handleInputChange('address', e.target.value)} disabled={isSubmitting} + readOnly={isKR} + className={cn(isKR && "bg-muted text-muted-foreground")} /> </div> @@ -1291,6 +1332,8 @@ function CompleteVendorForm({ value={data.postalCode} onChange={(e) => handleInputChange('postalCode', e.target.value)} disabled={isSubmitting} + readOnly={isKR} + className={cn(isKR && "bg-muted text-muted-foreground")} placeholder="우편번호를 입력해주세요" /> </div> @@ -1388,6 +1431,82 @@ function CompleteVendorForm({ </div> </div> + {/* 한국 사업자 정보 */} + {data.country === "KR" && ( + <div className="rounded-md border p-6 space-y-4"> + <h4 className="text-md font-semibold">{t('koreanBusinessInfo')}</h4> + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> + <div> + <label className="block text-sm font-medium mb-1"> + {t('name')} <span className="text-red-500">*</span> + </label> + <Input + value={data.representativeName} + onChange={(e) => handleInputChange('representativeName', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('representativeBirth')} <span className="text-red-500">*</span> + </label> + <Input + placeholder="YYYY-MM-DD" + value={data.representativeBirth} + onChange={(e) => handleInputChange('representativeBirth', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('representativeEmail')} <span className="text-red-500">*</span> + </label> + <Input + value={data.representativeEmail} + onChange={(e) => handleInputChange('representativeEmail', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('')} <span className="text-red-500">*</span> + </label> + <PhoneInput + value={data.representativePhone} + onChange={(value) => handleInputChange('representativePhone', value)} + countryCode="KR" + disabled={isSubmitting} + showValidation={true} + t={t} + /> + </div> + <div> + <label className="block text-sm font-medium mb-1"> + {t('corporateRegistrationNumber')} + </label> + <Input + value={data.corporateRegistrationNumber} + onChange={(e) => handleInputChange('corporateRegistrationNumber', e.target.value)} + disabled={isSubmitting} + /> + </div> + <div className="flex items-center space-x-2"> + <input + type="checkbox" + id="work-experience" + checked={data.representativeWorkExpirence} + onChange={(e) => handleInputChange('representativeWorkExpirence', e.target.checked)} + disabled={isSubmitting} + className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" + /> + <label htmlFor="work-experience" className="text-sm"> + {t('samsungWorkExperience')} + </label> + </div> + </div> + </div> + )} + {/* 담당자 정보 */} <div className="rounded-md border p-6 space-y-4"> <div className="flex items-center justify-between"> @@ -1508,82 +1627,6 @@ function CompleteVendorForm({ </div> </div> - {/* 한국 사업자 정보 */} - {data.country === "KR" && ( - <div className="rounded-md border p-6 space-y-4"> - <h4 className="text-md font-semibold">{t('koreanBusinessInfo')}</h4> - <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativeName')} <span className="text-red-500">*</span> - </label> - <Input - value={data.representativeName} - onChange={(e) => handleInputChange('representativeName', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativeBirth')} <span className="text-red-500">*</span> - </label> - <Input - placeholder="YYYY-MM-DD" - value={data.representativeBirth} - onChange={(e) => handleInputChange('representativeBirth', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativeEmail')} <span className="text-red-500">*</span> - </label> - <Input - value={data.representativeEmail} - onChange={(e) => handleInputChange('representativeEmail', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('representativePhone')} <span className="text-red-500">*</span> - </label> - <PhoneInput - value={data.representativePhone} - onChange={(value) => handleInputChange('representativePhone', value)} - countryCode="KR" - disabled={isSubmitting} - showValidation={true} - t={t} - /> - </div> - <div> - <label className="block text-sm font-medium mb-1"> - {t('corporateRegistrationNumber')} - </label> - <Input - value={data.corporateRegistrationNumber} - onChange={(e) => handleInputChange('corporateRegistrationNumber', e.target.value)} - disabled={isSubmitting} - /> - </div> - <div className="flex items-center space-x-2"> - <input - type="checkbox" - id="work-experience" - checked={data.representativeWorkExpirence} - onChange={(e) => handleInputChange('representativeWorkExpirence', e.target.checked)} - disabled={isSubmitting} - className="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500" - /> - <label htmlFor="work-experience" className="text-sm"> - {t('samsungWorkExperience')} - </label> - </div> - </div> - </div> - )} - {/* 필수 첨부 서류 */} <div className="rounded-md border p-6 space-y-6"> <h4 className="text-md font-semibold">{t('requiredDocuments')}</h4> diff --git a/components/vendor-regular-registrations/document-status-dialog.tsx b/components/vendor-regular-registrations/document-status-dialog.tsx index 02da19bf..1bd96ee8 100644 --- a/components/vendor-regular-registrations/document-status-dialog.tsx +++ b/components/vendor-regular-registrations/document-status-dialog.tsx @@ -387,25 +387,6 @@ export function DocumentStatusDialog({ </div> </div> - {/* 안전적격성 평가 */} - <div> - <h3 className="text-lg font-semibold mb-4">안전적격성 평가</h3> - <div className="p-4 border rounded-lg"> - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <StatusIcon status={!!registration.safetyQualificationContent} /> - <span>안전적격성 평가</span> - </div> - <StatusBadge status={!!registration.safetyQualificationContent} /> - </div> - {registration.safetyQualificationContent && ( - <div className="mt-3 p-3 bg-gray-50 rounded"> - <p className="text-sm">{registration.safetyQualificationContent}</p> - </div> - )} - </div> - </div> - {/* 추가 정보 */} <div> <h3 className="text-lg font-semibold mb-4">추가 정보</h3> |
