summaryrefslogtreecommitdiff
path: root/lib/swp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/swp')
-rw-r--r--lib/swp/table/swp-table-toolbar.tsx179
-rw-r--r--lib/swp/table/swp-upload-result-dialog.tsx108
2 files changed, 193 insertions, 94 deletions
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>
+ );
+}
+