diff options
Diffstat (limited to 'lib/site-visit')
| -rw-r--r-- | lib/site-visit/client-site-visit-wrapper.tsx | 2 | ||||
| -rw-r--r-- | lib/site-visit/service.ts | 5 | ||||
| -rw-r--r-- | lib/site-visit/site-visit-detail-dialog.tsx | 520 | ||||
| -rw-r--r-- | lib/site-visit/vendor-info-sheet.tsx | 58 |
4 files changed, 321 insertions, 264 deletions
diff --git a/lib/site-visit/client-site-visit-wrapper.tsx b/lib/site-visit/client-site-visit-wrapper.tsx index e2664ac3..f2655475 100644 --- a/lib/site-visit/client-site-visit-wrapper.tsx +++ b/lib/site-visit/client-site-visit-wrapper.tsx @@ -100,6 +100,7 @@ interface SiteVisitRequest { // 협력업체 정보
vendorInfo?: {
id: number
+ country?: string
siteVisitRequestId: number
factoryName: string
factoryLocation: string
@@ -495,6 +496,7 @@ export function ClientSiteVisitWrapper({ hasAttachments: siteVisitRequests.find(r => r.id === selectedSiteVisitRequestId)?.vendorInfo?.hasAttachments || false,
otherInfo: siteVisitRequests.find(r => r.id === selectedSiteVisitRequestId)?.vendorInfo?.otherInfo || "",
} : null}
+ vendorCountry={siteVisitRequests.find(r => r.id === selectedSiteVisitRequestId)?.vendorInfo?.country || ""}
/>
)}
diff --git a/lib/site-visit/service.ts b/lib/site-visit/service.ts index 684e73f1..54f2bf54 100644 --- a/lib/site-visit/service.ts +++ b/lib/site-visit/service.ts @@ -593,6 +593,7 @@ export async function getSiteVisitRequestAction(investigationId: number) { vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
vendorEmail: vendors.email,
+ vendorCountry: vendors.country,
})
.from(siteVisitRequests)
.leftJoin(
@@ -658,7 +659,9 @@ export async function getSiteVisitRequestAction(investigationId: number) { ...item,
shiAttendees: item.shiAttendees as Record<string, unknown> | null,
vendorRequests: item.vendorRequests as Record<string, unknown> | null,
- vendorInfo,
+ vendorInfo: vendorInfo
+ ? { ...vendorInfo, country: (item as any).vendorCountry || "" }
+ : null,
shiAttachments,
};
})
diff --git a/lib/site-visit/site-visit-detail-dialog.tsx b/lib/site-visit/site-visit-detail-dialog.tsx index 7788454a..634d2aef 100644 --- a/lib/site-visit/site-visit-detail-dialog.tsx +++ b/lib/site-visit/site-visit-detail-dialog.tsx @@ -1,261 +1,261 @@ -"use client"
-
-import * as React from "react"
-import { format } from "date-fns"
-import { ko } from "date-fns/locale"
-import { FileText, Download } from "lucide-react"
-import { toast } from "sonner"
-
-import { Button } from "@/components/ui/button"
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog"
-import { Separator } from "@/components/ui/separator"
-import { formatDate } from "../utils"
-
-interface SiteVisitRequest {
- id: number
- investigationId: number
- requesterId: number | null
- inspectionDuration: string | null
- requestedStartDate: Date | null
- requestedEndDate: Date | null
- shiAttendees: Record<string, unknown> | null
- shiAttendeeDetails?: string | null
- vendorRequests: Record<string, unknown> | null
- additionalRequests: string | null
- status: string
- sentAt: Date | null
- createdAt: Date
- updatedAt: Date
-
- // 실사정보
- investigationMethod: string | null // QM담당자가 작성한 실사방법
- investigationAddress: string | null
- investigationNotes: string | null
- forecastedAt: Date | null
- actualAt: Date | null
- result: string | null
- resultNotes: string | null
-
- // PQ 정보
- pqItems: string | null | Array<{itemCode: string, itemName: string}>
-
- // 요청자 정보
- requesterName: string | null
- requesterEmail: string | null
- requesterTitle: string | null
-
- // QM 매니저 정보
- qmManagerName: string | null
- qmManagerEmail: string | null
- qmManagerTitle: string | null
-
- // 협력업체 정보
- vendorInfo?: {
- id: number
- siteVisitRequestId: number
- factoryName: string
- factoryLocation: string
- factoryAddress: string
- factoryPicName: string
- factoryPicPhone: string
- factoryPicEmail: string
- factoryDirections: string | null
- accessProcedure: string | null
- hasAttachments: boolean
- otherInfo: string | null
- submittedAt: Date
- submittedBy: number
- createdAt: Date
- updatedAt: Date
- } | null
-
- // SHI 첨부파일
- shiAttachments?: Array<{
- id: number
- siteVisitRequestId: number
- vendorSiteVisitInfoId: number | null
- fileName: string
- originalFileName: string
- filePath: string
- fileSize: number
- mimeType: string
- createdAt: Date
- updatedAt: Date
- }> | null
-}
-
-interface SiteVisitDetailDialogProps {
- isOpen: boolean
- onOpenChange: (open: boolean) => void
- selectedRequest: SiteVisitRequest | null
-}
-
-export function SiteVisitDetailDialog({
- isOpen,
- onOpenChange,
- selectedRequest,
-}: SiteVisitDetailDialogProps) {
-
- return (
- <Dialog open={isOpen} onOpenChange={onOpenChange}>
- <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
- <DialogHeader>
- <DialogTitle>방문실사 상세 정보</DialogTitle>
- <DialogDescription>
- 작성한 방문실사 정보의 상세 내용입니다.
- </DialogDescription>
- </DialogHeader>
-
- {selectedRequest && (
- <div className="space-y-6">
- {/* 기본 정보 */}
-
- {/* 협력업체 정보 */}
- {selectedRequest.vendorInfo && (
- <>
- <Separator />
- <div>
- <h3 className="font-semibold mb-2">작성한 협력업체 정보</h3>
- <div className="bg-muted p-4 rounded-md">
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- <div className="space-y-4">
- <div>
- <h4 className="font-semibold mb-2">공장 기본 정보</h4>
- <div className="space-y-2 text-sm">
- <div><span className="font-medium">공장명:</span> {selectedRequest.vendorInfo.factoryName}</div>
- <div><span className="font-medium">공장위치:</span> {selectedRequest.vendorInfo.factoryLocation}</div>
- <div><span className="font-medium">공장주소:</span> {selectedRequest.vendorInfo.factoryAddress}</div>
- </div>
- </div>
-
- <div>
- <h4 className="font-semibold mb-2">공장 담당자 정보</h4>
- <div className="space-y-2 text-sm">
- <div><span className="font-medium">이름:</span> {selectedRequest.vendorInfo.factoryPicName}</div>
- <div><span className="font-medium">전화번호:</span> {selectedRequest.vendorInfo.factoryPicPhone}</div>
- <div><span className="font-medium">이메일:</span> {selectedRequest.vendorInfo.factoryPicEmail}</div>
- </div>
- </div>
- </div>
-
- <div className="space-y-4">
- {selectedRequest.vendorInfo.factoryDirections && (
- <div>
- <h4 className="font-semibold mb-2">공장 가는 법</h4>
- <div className="bg-background p-3 rounded-md">
- <p className="text-sm whitespace-pre-wrap">{selectedRequest.vendorInfo.factoryDirections}</p>
- </div>
- </div>
- )}
-
- {selectedRequest.vendorInfo.accessProcedure && (
- <div>
- <h4 className="font-semibold mb-2">공장 출입절차</h4>
- <div className="bg-background p-3 rounded-md">
- <p className="text-sm whitespace-pre-wrap">{selectedRequest.vendorInfo.accessProcedure}</p>
- </div>
- </div>
- )}
- </div>
- </div>
-
- {/* 기타 정보 */}
- {selectedRequest.vendorInfo.otherInfo && (
- <div className="mt-6">
- <h4 className="font-semibold mb-2">기타 정보</h4>
- <div className="bg-background p-3 rounded-md">
- <p className="text-sm whitespace-pre-wrap">{selectedRequest.vendorInfo.otherInfo}</p>
- </div>
- </div>
- )}
-
- {/* 제출 정보 */}
- <div className="mt-6">
- <h4 className="font-semibold mb-2">제출 정보</h4>
- <div className="space-y-2 text-sm">
- <div><span className="font-medium">제출일:</span> {formatDate(selectedRequest.vendorInfo.submittedAt, "kr")}</div>
- <div><span className="font-medium">첨부파일:</span> {selectedRequest.vendorInfo.hasAttachments ? "있음" : "없음"}</div>
- </div>
- </div>
- </div>
- </div>
- </>
- )}
-
- <Separator />
-
- {/* SHI 첨부파일 */}
- {selectedRequest.shiAttachments && selectedRequest.shiAttachments.length > 0 && (
- <>
- <div>
- <h3 className="font-semibold mb-2">SHI 첨부파일 ({selectedRequest.shiAttachments.length}개)</h3>
- <div className="bg-muted p-4 rounded-md">
- <div className="space-y-2">
- {selectedRequest.shiAttachments.map((attachment) => (
- <div key={attachment.id} className="flex items-center justify-between p-2 border rounded-md">
- <div className="flex items-center space-x-2 flex-1 min-w-0">
- <FileText className="h-4 w-4 text-muted-foreground" />
- <span className="text-sm truncate">{attachment.originalFileName}</span>
- <span className="text-xs text-muted-foreground">
- ({Math.round((attachment.fileSize || 0) / 1024)}KB)
- </span>
- </div>
- <Button
- type="button"
- variant="outline"
- size="sm"
- className="p-2"
- onClick={async () => {
- try {
- const { downloadFile } = await import('@/lib/file-download')
- await downloadFile(attachment.filePath, attachment.originalFileName || '', {
- showToast: true,
- onError: (error) => {
- console.error('다운로드 오류:', error)
- toast.error("다운로드 실패: " + error)
- },
- onSuccess: (fileName, fileSize) => {
- console.log(`다운로드 성공: ${fileName} (${fileSize} bytes)`)
- }
- })
- } catch (error) {
- console.error('다운로드 오류:', error)
- toast.error("파일 다운로드 중 오류가 발생했습니다.")
- }
- }}
- aria-label="파일 다운로드"
- >
- <Download className="h-4 w-4" />
- </Button>
- </div>
- ))}
- </div>
- </div>
- </div>
- </>
- )}
-
- {/* 추가 요청사항 */}
- {selectedRequest.additionalRequests && (
- <>
- <div>
- <h3 className="font-semibold mb-2">SHI 추가 요청사항</h3>
- <div className="bg-muted p-4 rounded-md">
- <p className="text-sm whitespace-pre-wrap">{selectedRequest.additionalRequests}</p>
- </div>
- </div>
- <Separator />
- </>
- )}
- </div>
- )}
- </DialogContent>
- </Dialog>
- )
+"use client" + +import * as React from "react" +import { format } from "date-fns" +import { ko } from "date-fns/locale" +import { FileText, Download } from "lucide-react" +import { toast } from "sonner" + +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Separator } from "@/components/ui/separator" +import { formatDate } from "../utils" + +interface SiteVisitRequest { + id: number + investigationId: number + requesterId: number | null + inspectionDuration: string | null + requestedStartDate: Date | null + requestedEndDate: Date | null + shiAttendees: Record<string, unknown> | null + shiAttendeeDetails?: string | null + vendorRequests: Record<string, unknown> | null + additionalRequests: string | null + status: string + sentAt: Date | null + createdAt: Date + updatedAt: Date + + // 실사정보 + investigationMethod: string | null // QM담당자가 작성한 실사방법 + investigationAddress: string | null + investigationNotes: string | null + forecastedAt: Date | null + actualAt: Date | null + result: string | null + resultNotes: string | null + + // PQ 정보 + pqItems: string | null | Array<{itemCode: string, itemName: string}> + + // 요청자 정보 + requesterName: string | null + requesterEmail: string | null + requesterTitle: string | null + + // QM 매니저 정보 + qmManagerName: string | null + qmManagerEmail: string | null + qmManagerTitle: string | null + + // 협력업체 정보 + vendorInfo?: { + id: number + siteVisitRequestId: number + factoryName: string + factoryLocation: string + factoryAddress: string + factoryPicName: string + factoryPicPhone: string + factoryPicEmail: string + factoryDirections: string | null + accessProcedure: string | null + hasAttachments: boolean + otherInfo: string | null + submittedAt: Date + submittedBy: number + createdAt: Date + updatedAt: Date + } | null + + // SHI 첨부파일 + shiAttachments?: Array<{ + id: number + siteVisitRequestId: number + vendorSiteVisitInfoId: number | null + fileName: string + originalFileName: string + filePath: string + fileSize: number + mimeType: string + createdAt: Date + updatedAt: Date + }> | null +} + +interface SiteVisitDetailDialogProps { + isOpen: boolean + onOpenChange: (open: boolean) => void + selectedRequest: SiteVisitRequest | null +} + +export function SiteVisitDetailDialog({ + isOpen, + onOpenChange, + selectedRequest, +}: SiteVisitDetailDialogProps) { + + return ( + <Dialog open={isOpen} onOpenChange={onOpenChange}> + <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto"> + <DialogHeader> + <DialogTitle>방문실사 상세 정보</DialogTitle> + <DialogDescription> + 작성한 방문실사 정보의 상세 내용입니다. + </DialogDescription> + </DialogHeader> + + {selectedRequest && ( + <div className="space-y-6"> + {/* 기본 정보 */} + + {/* 협력업체 정보 */} + {selectedRequest.vendorInfo && ( + <> + <Separator /> + <div> + <h3 className="font-semibold mb-2">작성한 협력업체 정보</h3> + <div className="bg-muted p-4 rounded-md"> + <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> + <div className="space-y-4"> + <div> + <h4 className="font-semibold mb-2">공장 기본 정보</h4> + <div className="space-y-2 text-sm"> + <div><span className="font-medium">공장명:</span> {selectedRequest.vendorInfo.factoryName}</div> + <div><span className="font-medium">공장위치:</span> {selectedRequest.vendorInfo.factoryLocation}</div> + <div><span className="font-medium">공장주소:</span> {selectedRequest.vendorInfo.factoryAddress}</div> + </div> + </div> + + <div> + <h4 className="font-semibold mb-2">공장 담당자 정보</h4> + <div className="space-y-2 text-sm"> + <div><span className="font-medium">이름:</span> {selectedRequest.vendorInfo.factoryPicName}</div> + <div><span className="font-medium">전화번호:</span> {selectedRequest.vendorInfo.factoryPicPhone}</div> + <div><span className="font-medium">이메일:</span> {selectedRequest.vendorInfo.factoryPicEmail}</div> + </div> + </div> + </div> + + <div className="space-y-4"> + {selectedRequest.vendorInfo.factoryDirections && ( + <div> + <h4 className="font-semibold mb-2">공장 가는 법</h4> + <div className="bg-background p-3 rounded-md"> + <p className="text-sm whitespace-pre-wrap">{selectedRequest.vendorInfo.factoryDirections}</p> + </div> + </div> + )} + + {selectedRequest.vendorInfo.accessProcedure && ( + <div> + <h4 className="font-semibold mb-2">공장 출입절차</h4> + <div className="bg-background p-3 rounded-md"> + <p className="text-sm whitespace-pre-wrap">{selectedRequest.vendorInfo.accessProcedure}</p> + </div> + </div> + )} + </div> + </div> + + {/* 기타 정보 */} + {selectedRequest.vendorInfo.otherInfo && ( + <div className="mt-6"> + <h4 className="font-semibold mb-2">기타 정보</h4> + <div className="bg-background p-3 rounded-md"> + <p className="text-sm whitespace-pre-wrap">{selectedRequest.vendorInfo.otherInfo}</p> + </div> + </div> + )} + + {/* 제출 정보 */} + <div className="mt-6"> + <h4 className="font-semibold mb-2">제출 정보</h4> + <div className="space-y-2 text-sm"> + <div><span className="font-medium">제출일:</span> {formatDate(selectedRequest.vendorInfo.submittedAt, "kr")}</div> + <div><span className="font-medium">첨부파일:</span> {selectedRequest.vendorInfo.hasAttachments ? "있음" : "없음"}</div> + </div> + </div> + </div> + </div> + </> + )} + + <Separator /> + + {/* SHI 첨부파일 */} + {selectedRequest.shiAttachments && selectedRequest.shiAttachments.length > 0 && ( + <> + <div> + <h3 className="font-semibold mb-2">SHI 첨부파일 ({selectedRequest.shiAttachments.length}개)</h3> + <div className="bg-muted p-4 rounded-md"> + <div className="space-y-2"> + {selectedRequest.shiAttachments.map((attachment) => ( + <div key={attachment.id} className="flex items-center justify-between p-2 border rounded-md"> + <div className="flex items-center space-x-2 flex-1 min-w-0"> + <FileText className="h-4 w-4 text-muted-foreground" /> + <span className="text-sm truncate">{attachment.originalFileName}</span> + <span className="text-xs text-muted-foreground"> + ({Math.round((attachment.fileSize || 0) / 1024)}KB) + </span> + </div> + <Button + type="button" + variant="outline" + size="sm" + className="p-2" + onClick={async () => { + try { + const { downloadFile } = await import('@/lib/file-download') + await downloadFile(attachment.filePath, attachment.originalFileName || '', { + showToast: true, + onError: (error) => { + console.error('다운로드 오류:', error) + toast.error("다운로드 실패: " + error) + }, + onSuccess: (fileName, fileSize) => { + console.log(`다운로드 성공: ${fileName} (${fileSize} bytes)`) + } + }) + } catch (error) { + console.error('다운로드 오류:', error) + toast.error("파일 다운로드 중 오류가 발생했습니다.") + } + }} + aria-label="파일 다운로드" + > + <Download className="h-4 w-4" /> + </Button> + </div> + ))} + </div> + </div> + </div> + </> + )} + + {/* 추가 요청사항 */} + {selectedRequest.additionalRequests && ( + <> + <div> + <h3 className="font-semibold mb-2">SHI 추가 요청사항</h3> + <div className="bg-muted p-4 rounded-md"> + <p className="text-sm whitespace-pre-wrap">{selectedRequest.additionalRequests}</p> + </div> + </div> + <Separator /> + </> + )} + </div> + )} + </DialogContent> + </Dialog> + ) }
\ No newline at end of file diff --git a/lib/site-visit/vendor-info-sheet.tsx b/lib/site-visit/vendor-info-sheet.tsx index 2a20e212..ad2fa16b 100644 --- a/lib/site-visit/vendor-info-sheet.tsx +++ b/lib/site-visit/vendor-info-sheet.tsx @@ -61,6 +61,7 @@ interface VendorInfoSheetProps { onSubmit: (data: VendorInfoFormValues & { attachments?: File[] }) => Promise<void>
siteVisitRequestId: number
initialData?: VendorInfoFormValues | null
+ vendorCountry?: string
}
export function VendorInfoSheet({
@@ -69,10 +70,12 @@ export function VendorInfoSheet({ onSubmit,
siteVisitRequestId,
initialData,
+ vendorCountry = "",
}: VendorInfoSheetProps) {
const [isPending, setIsPending] = React.useState(false)
const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
const fileInputRef = React.useRef<HTMLInputElement>(null)
+ const isDomestic = vendorCountry === "KR"
const form = useForm<VendorInfoFormValues>({
resolver: zodResolver(vendorInfoSchema),
@@ -114,6 +117,36 @@ export function VendorInfoSheet({ }
}, [isOpen, form, initialData])
+ // 도로명 주소 검색 결과 수신 (내자만 적용)
+ React.useEffect(() => {
+ if (!isOpen || !isDomestic) return
+
+ const handleMessage = (event: MessageEvent) => {
+ if (!event.data || event.data.type !== "JUSO_SELECTED") return
+ const { roadAddrPart1, roadAddrPart2, addrDetail } = event.data.payload || {}
+ const combinedRoad = [roadAddrPart1, roadAddrPart2].filter(Boolean).join(" ").trim()
+
+ if (combinedRoad) {
+ form.setValue("factoryLocation", combinedRoad, { shouldDirty: true })
+ }
+ if (addrDetail) {
+ form.setValue("factoryAddress", addrDetail, { shouldDirty: true })
+ }
+ }
+
+ window.addEventListener("message", handleMessage)
+ return () => window.removeEventListener("message", handleMessage)
+ }, [isOpen, isDomestic, form])
+
+ const handleJusoSearch = () => {
+ if (!isDomestic) return
+ window.open(
+ "/api/juso",
+ "jusoSearch",
+ "width=570,height=420,scrollbars=yes,resizable=yes"
+ )
+ }
+
// 파일 업로드 핸들러
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files
@@ -202,7 +235,25 @@ export function VendorInfoSheet({ <FormItem>
<FormLabel>실사 지역 *</FormLabel>
<FormControl>
- <Input placeholder="국가 또는 지역 (예: Finland, 부산)" {...field} disabled={isPending} />
+ <div className="flex gap-2">
+ <Input
+ placeholder="국가 또는 지역 (예: Finland, 부산)"
+ {...field}
+ disabled={isPending || isDomestic}
+ readOnly={isDomestic}
+ className={isDomestic ? "bg-muted text-muted-foreground" : undefined}
+ />
+ {isDomestic && (
+ <Button
+ type="button"
+ variant="secondary"
+ onClick={handleJusoSearch}
+ disabled={isPending}
+ >
+ 주소 검색
+ </Button>
+ )}
+ </div>
</FormControl>
<FormMessage />
</FormItem>
@@ -220,8 +271,9 @@ export function VendorInfoSheet({ <Textarea
placeholder="상세 주소를 입력하세요"
{...field}
- disabled={isPending}
- className="min-h-[80px]"
+ disabled={isPending || isDomestic}
+ readOnly={isDomestic}
+ className={isDomestic ? "bg-muted text-muted-foreground min-h-[80px]" : "min-h-[80px]"}
/>
</FormControl>
<FormMessage />
|
