diff options
| author | joonhoekim <26rote@gmail.com> | 2025-10-27 09:49:34 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-10-27 09:49:34 +0900 |
| commit | 65a68325658401dd8a90ea900c1542c17c63d7ce (patch) | |
| tree | 49af55547359ac62e921bbc2b57751f6e33b0e32 | |
| parent | bd1e72048a435655e9fced65c2c9dbe58568f47d (diff) | |
(김준회) swp-upload 다이얼로그로 결과 알림, vendorCode 자동 선택 처리
| -rw-r--r-- | app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx | 14 | ||||
| -rw-r--r-- | components/ship-vendor-document/revision-validation.tsx | 6 | ||||
| -rw-r--r-- | config/menuConfig.ts | 2 | ||||
| -rw-r--r-- | lib/swp/table/swp-table-toolbar.tsx | 179 | ||||
| -rw-r--r-- | lib/swp/table/swp-upload-result-dialog.tsx | 108 |
5 files changed, 209 insertions, 100 deletions
diff --git a/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx index 2431259d..ba78bfdf 100644 --- a/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx +++ b/app/[lng]/partners/(partners)/swp-document-upload/vendor-document-page.tsx @@ -12,6 +12,7 @@ import { fetchVendorDocuments, fetchVendorProjects, fetchVendorSwpStats, + getVendorSessionInfo, } from "@/lib/swp/vendor-actions"; import { type SwpTableFilters, type SwpDocumentWithStats } from "@/lib/swp/actions"; @@ -51,6 +52,12 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP uploaded_files: 0, last_sync: null as Date | null, }); + const [vendorInfo, setVendorInfo] = useState<{ + vendorId: number; + vendorCode: string; + vendorName: string; + companyId: number; + } | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); @@ -71,8 +78,9 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP setIsLoading(true); setError(null); - // 병렬로 데이터 로드 - const [projectsData, statsData, documentsData] = await Promise.all([ + // 병렬로 데이터 로드 (벤더 정보 포함) + const [vendorInfoData, projectsData, statsData, documentsData] = await Promise.all([ + getVendorSessionInfo(), fetchVendorProjects(), fetchVendorSwpStats(), fetchVendorDocuments({ @@ -82,6 +90,7 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP }), ]); + setVendorInfo(vendorInfoData); setProjects(projectsData); setStats(statsData); setDocuments(documentsData.data); @@ -207,6 +216,7 @@ export default function VendorDocumentPage({ searchParams }: VendorDocumentPageP filters={filters} onFiltersChange={handleFiltersChange} projects={projects} + vendorCode={vendorInfo?.vendorCode} /> </CardHeader> <CardContent> diff --git a/components/ship-vendor-document/revision-validation.tsx b/components/ship-vendor-document/revision-validation.tsx index 96067400..27e25eba 100644 --- a/components/ship-vendor-document/revision-validation.tsx +++ b/components/ship-vendor-document/revision-validation.tsx @@ -203,9 +203,9 @@ export const formatB3RevisionInput = (value: string): string => { const numPart = upperValue.slice(1).replace(/\D/g, '') if (numPart) { const num = parseInt(numPart, 10) - // 1-99 범위 체크 - if (num >= 1 && num <= 99) { - // 01-09는 0을 붙이고, 10-99는 그대로 + // 0-99 범위 체크 (R00 허용) + if (num >= 0 && num <= 99) { + // 00-09는 0을 붙이고, 10-99는 그대로 return `R${num.toString().padStart(2, '0')}` } } diff --git a/config/menuConfig.ts b/config/menuConfig.ts index 30ada08b..d28b1838 100644 --- a/config/menuConfig.ts +++ b/config/menuConfig.ts @@ -995,7 +995,7 @@ export const mainNavVendor: MenuSection[] = [ }, { titleKey: "menu.vendor.engineering.document_submission", - href: `/partners/document-upload`, + href: `/partners/swp-document-upload`, descriptionKey: "menu.vendor.engineering.document_submission_desc", groupKey: "groups.offshore", }, diff --git a/lib/swp/table/swp-table-toolbar.tsx b/lib/swp/table/swp-table-toolbar.tsx index 03082b26..fc8337f5 100644 --- a/lib/swp/table/swp-table-toolbar.tsx +++ b/lib/swp/table/swp-table-toolbar.tsx @@ -4,13 +4,6 @@ import { useState, useTransition, useMemo } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { Popover, PopoverContent, PopoverTrigger, @@ -23,17 +16,20 @@ import { useRouter } from "next/navigation"; import { cn } from "@/lib/utils"; import { useRef } from "react"; import { SwpUploadHelpDialog } from "./swp-help-dialog"; +import { SwpUploadResultDialog } from "./swp-upload-result-dialog"; interface SwpTableToolbarProps { filters: SwpTableFilters; onFiltersChange: (filters: SwpTableFilters) => void; projects?: Array<{ PROJ_NO: string; PROJ_NM: string }>; + vendorCode?: string; // 벤더가 접속했을 때 고정할 벤더 코드 } export function SwpTableToolbar({ filters, onFiltersChange, projects = [], + vendorCode, }: SwpTableToolbarProps) { const [isSyncing, startSync] = useTransition(); const [isUploading, startUpload] = useTransition(); @@ -43,6 +39,8 @@ export function SwpTableToolbar({ const [projectSearchOpen, setProjectSearchOpen] = useState(false); const [projectSearch, setProjectSearch] = useState(""); const fileInputRef = useRef<HTMLInputElement>(null); + const [uploadResults, setUploadResults] = useState<Array<{ fileName: string; success: boolean; error?: string }>>([]); + const [showResultDialog, setShowResultDialog] = useState(false); // 동기화 핸들러 const handleSync = () => { @@ -96,7 +94,7 @@ export function SwpTableToolbar({ const handleUploadFiles = () => { // 프로젝트와 벤더 코드 체크 const projectNo = localFilters.projNo; - const vndrCd = localFilters.vndrCd; + const vndrCd = vendorCode || localFilters.vndrCd; if (!projectNo) { toast({ @@ -130,7 +128,7 @@ export function SwpTableToolbar({ } const projectNo = localFilters.projNo!; - const vndrCd = localFilters.vndrCd!; + const vndrCd = vendorCode || localFilters.vndrCd!; startUpload(async () => { try { @@ -153,41 +151,27 @@ export function SwpTableToolbar({ // 서버 액션 호출 const result = await uploadSwpFilesAction(projectNo, vndrCd, fileInfos); - if (result.success) { - toast({ - title: "업로드 완료", - description: result.message, - }); + // 결과 저장 및 다이얼로그 표시 + setUploadResults(result.details); + setShowResultDialog(true); - // 페이지 새로고침 + // 성공한 파일이 있으면 페이지 새로고침 + const successCount = result.details.filter((d) => d.success).length; + if (successCount > 0) { router.refresh(); - } else { - toast({ - variant: "destructive", - title: "업로드 실패", - description: result.message, - }); - } - - // 실패한 파일이 있으면 상세 정보 표시 - const failedFiles = result.details.filter((d) => !d.success); - if (failedFiles.length > 0) { - console.error("실패한 파일:", failedFiles); - failedFiles.forEach((f) => { - toast({ - variant: "destructive", - title: `${f.fileName} 업로드 실패`, - description: f.error || "알 수 없는 오류", - }); - }); } } catch (error) { console.error("파일 업로드 실패:", error); - toast({ - variant: "destructive", - title: "업로드 실패", - description: error instanceof Error ? error.message : "알 수 없는 오류", - }); + + // 예외 발생 시에도 결과 다이얼로그 표시 + const errorResults = Array.from(selectedFiles).map((file) => ({ + fileName: file.name, + success: false, + error: error instanceof Error ? error.message : "알 수 없는 오류", + })); + + setUploadResults(errorResults); + setShowResultDialog(true); } finally { // 파일 입력 초기화 if (fileInputRef.current) { @@ -222,46 +206,59 @@ export function SwpTableToolbar({ }, [projects, projectSearch]); return ( - <div className="space-y-4"> - {/* 상단 액션 바 */} - <div className="flex items-center justify-between"> - <div className="flex items-center gap-2"> - <Button - onClick={handleSync} - disabled={isSyncing || !localFilters.projNo} - size="sm" - > - <RefreshCw className={`h-4 w-4 mr-2 ${isSyncing ? "animate-spin" : ""}`} /> - {isSyncing ? "동기화 중..." : "SWP 동기화"} - </Button> - - </div> + <> + {/* 업로드 결과 다이얼로그 */} + <SwpUploadResultDialog + open={showResultDialog} + onOpenChange={setShowResultDialog} + results={uploadResults} + /> + + <div className="space-y-4"> + {/* 상단 액션 바 */} + <div className="flex items-center justify-between"> + <div className="flex items-center gap-2"> + <Button + onClick={handleSync} + disabled={isSyncing || !localFilters.projNo} + size="sm" + > + <RefreshCw className={`h-4 w-4 mr-2 ${isSyncing ? "animate-spin" : ""}`} /> + {isSyncing ? "동기화 중..." : "SWP 동기화"} + </Button> + </div> - <div className="text-sm text-muted-foreground"> - SWP 문서 관리 시스템 - </div> - <div className="flex items-center gap-2"> - <input - ref={fileInputRef} - type="file" - multiple - className="hidden" - onChange={handleFileChange} - accept="*/*" - /> - <Button - variant="outline" - size="sm" - onClick={handleUploadFiles} - disabled={isUploading || !localFilters.projNo || !localFilters.vndrCd} - > - <Upload className={`h-4 w-4 mr-2 ${isUploading ? "animate-pulse" : ""}`} /> - {isUploading ? "업로드 중..." : "파일 업로드"} - </Button> + <div className="text-sm text-muted-foreground"> + SWP 문서 관리 시스템 + </div> - <SwpUploadHelpDialog /> + <div className="flex items-center gap-2"> + {/* 벤더만 파일 업로드 기능 사용 가능 */} + {vendorCode && ( + <> + <input + ref={fileInputRef} + type="file" + multiple + className="hidden" + onChange={handleFileChange} + accept="*/*" + /> + <Button + variant="outline" + size="sm" + onClick={handleUploadFiles} + disabled={isUploading || !localFilters.projNo || (!vendorCode && !localFilters.vndrCd)} + > + <Upload className={`h-4 w-4 mr-2 ${isUploading ? "animate-pulse" : ""}`} /> + {isUploading ? "업로드 중..." : "파일 업로드"} + </Button> + + <SwpUploadHelpDialog /> + </> + )} + </div> </div> - </div> {/* 검색 필터 */} <div className="rounded-lg border p-4 space-y-4"> @@ -423,33 +420,26 @@ export function SwpTableToolbar({ <Input id="vndrCd" placeholder="업체 코드" - value={localFilters.vndrCd || ""} + value={vendorCode || localFilters.vndrCd || ""} onChange={(e) => setLocalFilters({ ...localFilters, vndrCd: e.target.value }) } + disabled={!!vendorCode} // 벤더 코드가 제공되면 입력 비활성화 + className={vendorCode ? "bg-muted" : ""} /> </div> {/* 스테이지 */} <div className="space-y-2"> <Label htmlFor="stage">스테이지</Label> - <Select - value={localFilters.stage || "__all__"} - onValueChange={(value) => - setLocalFilters({ ...localFilters, stage: value === "__all__" ? undefined : value }) + <Input + id="stage" + placeholder="스테이지 입력" + value={localFilters.stage || ""} + onChange={(e) => + setLocalFilters({ ...localFilters, stage: e.target.value }) } - > - <SelectTrigger id="stage"> - <SelectValue placeholder="전체" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="__all__">전체</SelectItem> - <SelectItem value="IFA">IFA</SelectItem> - <SelectItem value="IFC">IFC</SelectItem> - <SelectItem value="AFC">AFC</SelectItem> - <SelectItem value="BFC">BFC</SelectItem> - </SelectContent> - </Select> + /> </div> </div> @@ -459,8 +449,9 @@ export function SwpTableToolbar({ 검색 </Button> </div> + </div> </div> - </div> + </> ); } diff --git a/lib/swp/table/swp-upload-result-dialog.tsx b/lib/swp/table/swp-upload-result-dialog.tsx new file mode 100644 index 00000000..7b79fa68 --- /dev/null +++ b/lib/swp/table/swp-upload-result-dialog.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { CheckCircle2, XCircle, FileText } from "lucide-react"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +interface UploadResult { + fileName: string; + success: boolean; + error?: string; +} + +interface SwpUploadResultDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + results: UploadResult[]; +} + +export function SwpUploadResultDialog({ + open, + onOpenChange, + results, +}: SwpUploadResultDialogProps) { + const successCount = results.filter((r) => r.success).length; + const failCount = results.filter((r) => !r.success).length; + const totalCount = results.length; + + return ( + <Dialog open={open} onOpenChange={onOpenChange}> + <DialogContent className="max-w-3xl max-h-[80vh]"> + <DialogHeader> + <DialogTitle>파일 업로드 결과</DialogTitle> + <DialogDescription> + 총 {totalCount}개 파일 중 성공 {successCount}개, 실패 {failCount}개 + </DialogDescription> + </DialogHeader> + + <ScrollArea className="max-h-[500px] pr-4"> + <div className="space-y-3"> + {results.map((result, index) => ( + <div + key={index} + className={`flex items-start gap-3 p-4 rounded-lg border ${ + result.success + ? "bg-green-50 border-green-200 dark:bg-green-950 dark:border-green-800" + : "bg-red-50 border-red-200 dark:bg-red-950 dark:border-red-800" + }`} + > + <div className="flex-shrink-0 mt-0.5"> + {result.success ? ( + <CheckCircle2 className="h-5 w-5 text-green-600 dark:text-green-400" /> + ) : ( + <XCircle className="h-5 w-5 text-red-600 dark:text-red-400" /> + )} + </div> + + <div className="flex-1 min-w-0"> + <div className="flex items-center gap-2 mb-1"> + <FileText className="h-4 w-4 text-muted-foreground flex-shrink-0" /> + <span className="font-medium text-sm break-all"> + {result.fileName} + </span> + </div> + + {result.success ? ( + <p className="text-sm text-green-700 dark:text-green-300"> + 업로드 성공 + </p> + ) : ( + <div className="space-y-1"> + <p className="text-sm font-medium text-red-700 dark:text-red-300"> + 업로드 실패 + </p> + {result.error && ( + <p className="text-sm text-red-600 dark:text-red-400 break-words"> + 사유: {result.error} + </p> + )} + </div> + )} + </div> + </div> + ))} + </div> + </ScrollArea> + + <div className="flex justify-between items-center pt-4 border-t"> + <div className="text-sm text-muted-foreground"> + {failCount > 0 && ( + <span className="text-red-600 dark:text-red-400 font-medium"> + 실패한 파일을 확인하고 다시 업로드해주세요. + </span> + )} + </div> + <Button onClick={() => onOpenChange(false)}>확인</Button> + </div> + </DialogContent> + </Dialog> + ); +} + |
