diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/pq-input/pq-input-tabs.tsx | 90 | ||||
| -rw-r--r-- | components/pq-input/pq-review-wrapper.tsx | 78 | ||||
| -rw-r--r-- | components/signup/join-form.tsx | 49 |
3 files changed, 205 insertions, 12 deletions
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..efb078e0 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 { FileList, FileListHeader, FileListInfo, FileListItem, FileListName, FileListDescription, FileListAction, FileListIcon } from "@/components/ui/file-list" // import * as ExcelJS from 'exceljs'; // import { saveAs } from "file-saver"; @@ -49,13 +50,15 @@ 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() @@ -96,6 +99,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 코멘트를 로컬 상태에 초기화 @@ -482,8 +511,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 +565,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"> diff --git a/components/signup/join-form.tsx b/components/signup/join-form.tsx index 6885279a..c6281b24 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> |
