summaryrefslogtreecommitdiff
path: root/components/vendor-data-plant
diff options
context:
space:
mode:
authorjoonhoekim <26rote@gmail.com>2025-11-27 17:53:34 +0900
committerjoonhoekim <26rote@gmail.com>2025-11-27 17:53:34 +0900
commit5870b73785715d1585531e655c06d8c068eb64ac (patch)
tree1d19e1482f5210cc56e778158b51e810f9717c46 /components/vendor-data-plant
parent95984e67b8d57fbe1431fcfedf3bb682f28416b3 (diff)
(김준회) Revert "(대표님) EDP 작업사항"
태그 가져오기 실패 등 에러로 인한 Revert 처리
Diffstat (limited to 'components/vendor-data-plant')
-rw-r--r--components/vendor-data-plant/project-swicher.tsx163
-rw-r--r--components/vendor-data-plant/sidebar.tsx479
-rw-r--r--components/vendor-data-plant/vendor-data-container.tsx523
3 files changed, 763 insertions, 402 deletions
diff --git a/components/vendor-data-plant/project-swicher.tsx b/components/vendor-data-plant/project-swicher.tsx
index 9b8f9bea..d3123709 100644
--- a/components/vendor-data-plant/project-swicher.tsx
+++ b/components/vendor-data-plant/project-swicher.tsx
@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
-import { Check, ChevronsUpDown } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
@@ -17,103 +16,149 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
+import { Check, ChevronsUpDown, Loader2 } from "lucide-react"
-interface PackageData {
- packageCode: string
- packageName: string | null
+interface ContractInfo {
+ contractId: number
+ contractName: string
}
-interface ProjectData {
+interface ProjectInfo {
projectId: number
projectCode: string
projectName: string
- projectType: string
- packages: PackageData[]
+ contracts: ContractInfo[]
}
interface ProjectSwitcherProps {
isCollapsed: boolean
- projects: ProjectData[]
- selectedProjectId: number
- selectedPackageCode: string | null
- onSelectPackage: (projectId: number, packageCode: string) => void
+ projects: ProjectInfo[]
+
+ // 상위가 관리하는 "현재 선택된 contractId"
+ selectedContractId: number | null
+
+ // 콜백: 사용자가 "어떤 contract"를 골랐는지
+ // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함
+ onSelectContract: (projectId: number, contractId: number) => void
+
+ // 로딩 상태 (선택사항)
+ isLoading?: boolean
}
export function ProjectSwitcher({
isCollapsed,
projects,
- selectedProjectId,
- selectedPackageCode,
- onSelectPackage,
+ selectedContractId,
+ onSelectContract,
+ isLoading = false,
}: ProjectSwitcherProps) {
- const [open, setOpen] = React.useState(false)
+ const [popoverOpen, setPopoverOpen] = React.useState(false)
+ const [searchTerm, setSearchTerm] = React.useState("")
- // 현재 선택된 프로젝트와 패키지 정보
- const selectedProject = projects.find(p => p.projectId === selectedProjectId)
- const selectedPackage = selectedProject?.packages.find(
- pkg => pkg.packageCode === selectedPackageCode
- )
+ // 현재 선택된 contract 객체 찾기
+ const selectedContract = React.useMemo(() => {
+ if (!selectedContractId) return null
+ for (const proj of projects) {
+ const found = proj.contracts.find((c) => c.contractId === selectedContractId)
+ if (found) {
+ return { ...found, projectId: proj.projectId, projectName: proj.projectName }
+ }
+ }
+ return null
+ }, [projects, selectedContractId])
+
+ // Trigger label => 계약 이름 or placeholder
+ const triggerLabel = selectedContract?.contractName ?? "Select a contract"
+ // 검색어에 따른 필터링된 프로젝트/계약 목록
+ const filteredProjects = React.useMemo(() => {
+ if (!searchTerm) return projects
+
+ return projects.map(project => ({
+ ...project,
+ contracts: project.contracts.filter(contract =>
+ contract.contractName.toLowerCase().includes(searchTerm.toLowerCase()) ||
+ project.projectName.toLowerCase().includes(searchTerm.toLowerCase())
+ )
+ })).filter(project => project.contracts.length > 0)
+ }, [projects, searchTerm])
- console.log(projects,"projects")
+ // 계약 선택 핸들러
+ function handleSelectContract(projectId: number, contractId: number) {
+ onSelectContract(projectId, contractId)
+ setPopoverOpen(false)
+ setSearchTerm("") // 검색어 초기화
+ }
- const displayText = selectedPackage
- ? `${selectedProject?.projectCode} - ${selectedPackage.packageCode}`
- : selectedProject?.projectCode || "Select Package"
+ // 총 계약 수 계산 (빈 상태 표시용)
+ const totalContracts = filteredProjects.reduce((sum, project) => sum + project.contracts.length, 0)
return (
- <Popover open={open} onOpenChange={setOpen}>
+ <Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
<PopoverTrigger asChild>
<Button
+ type="button"
variant="outline"
- role="combobox"
- aria-expanded={open}
- aria-label="Select a package"
- className={cn("w-full justify-between", isCollapsed && "w-[50px]")}
+ className={cn(
+ "justify-between relative",
+ isCollapsed ? "h-9 w-9 shrink-0 items-center justify-center p-0" : "w-full h-9"
+ )}
+ disabled={isLoading}
+ aria-label="Select Contract"
>
- {isCollapsed ? (
- <ChevronsUpDown className="h-4 w-4" />
+ {isLoading ? (
+ <>
+ <span className={cn(isCollapsed && "hidden")}>Loading...</span>
+ <Loader2 className={cn("h-4 w-4 animate-spin", !isCollapsed && "ml-2")} />
+ </>
) : (
<>
- <span className="truncate">{displayText}</span>
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+ <span className={cn("truncate flex-grow text-left", isCollapsed && "hidden")}>
+ {triggerLabel}
+ </span>
+ <ChevronsUpDown className={cn("h-4 w-4 opacity-50 flex-shrink-0", isCollapsed && "hidden")} />
</>
)}
</Button>
</PopoverTrigger>
- <PopoverContent className="w-[300px] p-0">
+
+ <PopoverContent className="w-[320px] p-0" align="start">
<Command>
- <CommandInput placeholder="Search package..." />
- <CommandList>
- <CommandEmpty>No package found.</CommandEmpty>
- {projects.map((project) => (
- <CommandGroup key={project.projectId} heading={project.projectName}>
- {project.packages.map((pkg) => (
+ <CommandInput
+ placeholder="Search contracts..."
+ value={searchTerm}
+ onValueChange={setSearchTerm}
+ />
+
+ <CommandList
+ className="max-h-[320px]"
+ onWheel={(e) => {
+ e.stopPropagation() // 이벤트 전파 차단
+ const target = e.currentTarget
+ target.scrollTop += e.deltaY // 직접 스크롤 처리
+ }}
+ >
+ <CommandEmpty>
+ {totalContracts === 0 ? "No contracts found." : "No search results."}
+ </CommandEmpty>
+
+ {filteredProjects.map((project) => (
+ <CommandGroup key={project.projectCode} heading={project.projectName}>
+ {project.contracts.map((contract) => (
<CommandItem
- key={`${project.projectId}-${pkg.packageCode}`}
- onSelect={() => {
- onSelectPackage(project.projectId, pkg.packageCode)
- setOpen(false)
- }}
- className="text-sm"
+ key={contract.contractId}
+ onSelect={() => handleSelectContract(project.projectId, contract.contractId)}
+ value={`${project.projectName} ${contract.contractName}`}
+ className="truncate"
+ title={contract.contractName}
>
+ <span className="truncate">{contract.contractName}</span>
<Check
className={cn(
- "mr-2 h-4 w-4",
- selectedProjectId === project.projectId &&
- selectedPackageCode === pkg.packageCode
- ? "opacity-100"
- : "opacity-0"
+ "ml-auto h-4 w-4 flex-shrink-0",
+ selectedContractId === contract.contractId ? "opacity-100" : "opacity-0"
)}
/>
- <div className="flex flex-col">
- <span className="font-medium">{pkg.packageCode}</span>
- {pkg.packageName && (
- <span className="text-xs text-muted-foreground">
- {pkg.packageName}
- </span>
- )}
- </div>
</CommandItem>
))}
</CommandGroup>
diff --git a/components/vendor-data-plant/sidebar.tsx b/components/vendor-data-plant/sidebar.tsx
index b746e69d..31ee6dc7 100644
--- a/components/vendor-data-plant/sidebar.tsx
+++ b/components/vendor-data-plant/sidebar.tsx
@@ -10,265 +10,304 @@ import {
TooltipTrigger,
TooltipContent,
} from "@/components/ui/tooltip"
-import { List, FormInput, FileText } from "lucide-react"
+import { Package2, FormInput } from "lucide-react"
+import { useRouter, usePathname } from "next/navigation"
import { Skeleton } from "@/components/ui/skeleton"
-import { getEngineeringForms, getIMForms } from "@/lib/tags-plant/service"
+import { type FormInfo } from "@/lib/forms/services"
-interface FormInfo {
- formCode: string
- formName: string
+interface PackageData {
+ itemId: number
+ itemName: string
}
interface SidebarProps extends React.HTMLAttributes<HTMLDivElement> {
isCollapsed: boolean
- selectedPackageCode: string | null
- selectedFormCode: string | null
- currentMode: "master" | "engineering" | "im" | null
- projectCode: string // 추가
- onMasterTagListClick: () => void
- onEngineeringFormClick: (formCode: string) => void
- onIMFormClick: (formCode: string) => void
+ packages: PackageData[]
+ selectedPackageId: number | null
+ selectedProjectId: number | null
+ selectedContractId: number | null
+ onSelectPackage: (itemId: number) => void
+ forms?: FormInfo[]
+ onSelectForm: (formName: string) => void
+ isLoadingForms?: boolean
+ mode: "IM" | "ENG"
}
export function Sidebar({
className,
isCollapsed,
- selectedPackageCode,
- selectedFormCode,
- currentMode,
- projectCode, // 추가
- onMasterTagListClick,
- onEngineeringFormClick,
- onIMFormClick,
+ packages,
+ selectedPackageId,
+ selectedProjectId,
+ selectedContractId,
+ onSelectPackage,
+ forms,
+ onSelectForm,
+ isLoadingForms = false,
+ mode = "IM",
}: SidebarProps) {
- const [engineeringForms, setEngineeringForms] = React.useState<FormInfo[]>([])
- const [imForms, setIMForms] = React.useState<FormInfo[]>([])
- const [isLoadingEngineering, setIsLoadingEngineering] = React.useState(false)
- const [isLoadingIM, setIsLoadingIM] = React.useState(false)
+ const router = useRouter()
+ const rawPathname = usePathname()
+ const pathname = rawPathname ?? ""
- // Engineering 폼 로드
- React.useEffect(() => {
- if (!selectedPackageCode || !projectCode) {
- setEngineeringForms([])
- return
- }
+ /**
+ * ---------------------------
+ * 1) URL에서 현재 패키지 / 폼 코드 추출
+ * ---------------------------
+ */
+ const segments = pathname.split("/").filter(Boolean)
- const loadEngineeringForms = async () => {
- setIsLoadingEngineering(true)
- try {
- const result = await getEngineeringForms(projectCode, selectedPackageCode)
- setEngineeringForms(result)
- } catch (error) {
- console.error("Engineering 폼 로딩 오류:", error)
- setEngineeringForms([])
- } finally {
- setIsLoadingEngineering(false)
- }
- }
+ let currentItemId: number | null = null
+ let currentFormCode: string | null = null
- loadEngineeringForms()
- }, [selectedPackageCode, projectCode])
+ const tagIndex = segments.indexOf("tag")
+ if (tagIndex !== -1 && segments[tagIndex + 1]) {
+ currentItemId = parseInt(segments[tagIndex + 1], 10)
+ }
- // IM 폼 로드
- React.useEffect(() => {
- if (!selectedPackageCode || !projectCode) {
- setIMForms([])
- return
- }
+ const formIndex = segments.indexOf("form")
+ if (formIndex !== -1) {
+ const itemSegment = segments[formIndex + 1]
+ const codeSegment = segments[formIndex + 2]
- const loadIMForms = async () => {
- setIsLoadingIM(true)
- try {
- const result = await getIMForms(projectCode, selectedPackageCode)
- setIMForms(result)
- } catch (error) {
- console.error("IM 폼 로딩 오류:", error)
- setIMForms([])
- } finally {
- setIsLoadingIM(false)
- }
+ if (itemSegment) {
+ currentItemId = parseInt(itemSegment, 10)
+ }
+ if (codeSegment) {
+ currentFormCode = codeSegment
}
+ }
- loadIMForms()
- }, [selectedPackageCode, projectCode])
+ /**
+ * ---------------------------
+ * 2) 패키지 클릭 핸들러 (IM 모드)
+ * ---------------------------
+ */
+ const handlePackageClick = (itemId: number) => {
+ // 상위 컴포넌트 상태 업데이트
+ onSelectPackage(itemId)
- const isMasterActive = currentMode === "master"
- const isPackageSelected = selectedPackageCode !== null
+ // 해당 태그 페이지로 라우팅
+ // 예: /vendor-data-plant/tag/123
+ const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/tag/${itemId}`)
+ }
+
+ /**
+ * ---------------------------
+ * 3) 폼 클릭 핸들러 (IM 모드만 사용)
+ * ---------------------------
+ */
+ const handleFormClick = (form: FormInfo) => {
+ // IM 모드에서만 사용
+ if (selectedPackageId === null) return;
+
+ onSelectForm(form.formName)
+
+ const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/${selectedPackageId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`)
+ }
+
+ /**
+ * ---------------------------
+ * 4) 패키지 클릭 핸들러 (ENG 모드)
+ * ---------------------------
+ */
+ const handlePackageUnderFormClick = (form: FormInfo, pkg: PackageData) => {
+ onSelectForm(form.formName)
+ onSelectPackage(pkg.itemId)
+
+ const baseSegments = segments.slice(0, segments.indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/${pkg.itemId}/${form.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`)
+ }
return (
<div className={cn("pb-12", className)}>
<div className="space-y-4 py-4">
- {/* Master Tag List */}
- <div className="py-1">
- <h2 className="relative px-7 text-lg font-semibold tracking-tight">
- {isCollapsed ? "M" : "Master"}
- </h2>
- <div className="space-y-1 p-2">
- {isCollapsed ? (
- <Tooltip delayDuration={0}>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isMasterActive && "bg-accent text-accent-foreground"
- )}
- onClick={onMasterTagListClick}
- disabled={!isPackageSelected}
- >
- <List className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent side="right">
- Master Tag List
- </TooltipContent>
- </Tooltip>
- ) : (
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isMasterActive && "bg-accent text-accent-foreground"
- )}
- onClick={onMasterTagListClick}
- disabled={!isPackageSelected}
- >
- <List className="mr-2 h-4 w-4" />
- Master Tag List
- </Button>
- )}
- </div>
- </div>
+ {/* ---------- 패키지(Items) 목록 - IM 모드에서만 표시 ---------- */}
+ {mode === "IM" && (
+ <>
+ <div className="py-1">
+ <h2 className="relative px-7 text-lg font-semibold tracking-tight">
+ {isCollapsed ? "P" : "Package Lists"}
+ </h2>
+ <ScrollArea className="h-[150px] px-1">
+ <div className="space-y-1 p-2">
+ {packages.map((pkg) => {
+ const isActive = pkg.itemId === currentItemId
- <Separator />
+ return (
+ <div key={pkg.itemId}>
+ {isCollapsed ? (
+ <Tooltip delayDuration={0}>
+ <TooltipTrigger asChild>
+ <Button
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handlePackageClick(pkg.itemId)}
+ >
+ <Package2 className="mr-2 h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent side="right">
+ {pkg.itemName}
+ </TooltipContent>
+ </Tooltip>
+ ) : (
+ <Button
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handlePackageClick(pkg.itemId)}
+ >
+ <Package2 className="mr-2 h-4 w-4" />
+ {pkg.itemName}
+ </Button>
+ )}
+ </div>
+ )
+ })}
+ </div>
+ </ScrollArea>
+ </div>
+ <Separator />
+ </>
+ )}
- {/* Engineering Forms */}
+ {/* ---------- 폼 목록 (IM 모드) / 패키지와 폼 목록 (ENG 모드) ---------- */}
<div className="py-1">
<h2 className="relative px-7 text-lg font-semibold tracking-tight">
- {isCollapsed ? "E" : "Engineering"}
+ {isCollapsed
+ ? (mode === "IM" ? "F" : "P")
+ : (mode === "IM" ? "Form Lists" : "Package Lists")
+ }
</h2>
- <ScrollArea className="h-[250px] px-1">
+ <ScrollArea className={cn(
+ "px-1",
+ mode === "IM" ? "h-[300px]" : "h-[450px]"
+ )}>
<div className="space-y-1 p-2">
- {isLoadingEngineering ? (
+ {isLoadingForms ? (
Array.from({ length: 3 }).map((_, index) => (
- <div key={`eng-skeleton-${index}`} className="px-2 py-1.5">
+ <div key={`form-skeleton-${index}`} className="px-2 py-1.5">
<Skeleton className="h-8 w-full" />
</div>
))
- ) : !isPackageSelected ? (
- <p className="text-sm text-muted-foreground px-2">
- Select a package first
- </p>
- ) : engineeringForms.length === 0 ? (
- <p className="text-sm text-muted-foreground px-2">
- No forms available
- </p>
- ) : (
- engineeringForms.map((form) => {
- const isActive =
- currentMode === "engineering" &&
- form.formCode === selectedFormCode
+ ) : mode === "IM" ? (
+ // =========== IM 모드: 폼만 표시 ===========
+ !forms || forms.length === 0 ? (
+ <p className="text-sm text-muted-foreground px-2">
+ (No forms loaded)
+ </p>
+ ) : (
+ forms.map((form) => {
+ const isFormActive = form.formCode === currentFormCode
+ const isDisabled = currentItemId === null
- return isCollapsed ? (
- <Tooltip key={form.formCode} delayDuration={0}>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
- )}
- onClick={() => onEngineeringFormClick(form.formCode)}
- >
- <FormInput className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent side="right">
+ return isCollapsed ? (
+ <Tooltip key={form.formCode} delayDuration={0}>
+ <TooltipTrigger asChild>
+ <Button
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isFormActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handleFormClick(form)}
+ disabled={isDisabled}
+ >
+ <FormInput className="mr-2 h-4 w-4" />
+ </Button>
+ </TooltipTrigger>
+ <TooltipContent side="right">
+ {form.formName}
+ </TooltipContent>
+ </Tooltip>
+ ) : (
+ <Button
+ key={form.formCode}
+ variant="ghost"
+ className={cn(
+ "w-full justify-start font-normal",
+ isFormActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handleFormClick(form)}
+ disabled={isDisabled}
+ >
+ <FormInput className="mr-2 h-4 w-4" />
{form.formName}
- </TooltipContent>
- </Tooltip>
- ) : (
- <Button
- key={form.formCode}
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
- )}
- onClick={() => onEngineeringFormClick(form.formCode)}
- >
- <FormInput className="mr-2 h-4 w-4" />
- {form.formName}
- </Button>
- )
- })
- )}
- </div>
- </ScrollArea>
- </div>
-
- <Separator />
-
- {/* IM Forms */}
- <div className="py-1">
- <h2 className="relative px-7 text-lg font-semibold tracking-tight">
- {isCollapsed ? "I" : "IM"}
- </h2>
- <ScrollArea className="h-[250px] px-1">
- <div className="space-y-1 p-2">
- {isLoadingIM ? (
- Array.from({ length: 3 }).map((_, index) => (
- <div key={`im-skeleton-${index}`} className="px-2 py-1.5">
- <Skeleton className="h-8 w-full" />
- </div>
- ))
- ) : !isPackageSelected ? (
- <p className="text-sm text-muted-foreground px-2">
- Select a package first
- </p>
- ) : imForms.length === 0 ? (
- <p className="text-sm text-muted-foreground px-2">
- No forms available
- </p>
+ </Button>
+ )
+ })
+ )
) : (
- imForms.map((form) => {
- const isActive =
- currentMode === "im" &&
- form.formCode === selectedFormCode
+ // =========== ENG 모드: 패키지 > 폼 계층 구조 ===========
+ packages.length === 0 ? (
+ <p className="text-sm text-muted-foreground px-2">
+ (No packages loaded)
+ </p>
+ ) : (
+ packages.map((pkg) => (
+ <div key={pkg.itemId} className="space-y-1">
+ {isCollapsed ? (
+ <Tooltip delayDuration={0}>
+ <TooltipTrigger asChild>
+ <div className="px-2 py-1">
+ <Package2 className="h-4 w-4" />
+ </div>
+ </TooltipTrigger>
+ <TooltipContent side="right">
+ {pkg.itemName}
+ </TooltipContent>
+ </Tooltip>
+ ) : (
+ <>
+ {/* 패키지 이름 (클릭 불가능한 라벨) */}
+ <div className="flex items-center px-2 py-1 text-sm font-medium">
+ <Package2 className="mr-2 h-4 w-4" />
+ {pkg.itemName}
+ </div>
+
+ {/* 폼 목록 바로 표시 */}
+ <div className="ml-6 space-y-1">
+ {!forms || forms.length === 0 ? (
+ <p className="text-xs text-muted-foreground px-2 py-1">
+ No forms available
+ </p>
+ ) : (
+ forms.map((form) => {
+ const isFormPackageActive =
+ pkg.itemId === currentItemId &&
+ form.formCode === currentFormCode
- return isCollapsed ? (
- <Tooltip key={form.formCode} delayDuration={0}>
- <TooltipTrigger asChild>
- <Button
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
- )}
- onClick={() => onIMFormClick(form.formCode)}
- >
- <FileText className="h-4 w-4" />
- </Button>
- </TooltipTrigger>
- <TooltipContent side="right">
- {form.formName}
- </TooltipContent>
- </Tooltip>
- ) : (
- <Button
- key={form.formCode}
- variant="ghost"
- className={cn(
- "w-full justify-start font-normal",
- isActive && "bg-accent text-accent-foreground"
+ return (
+ <Button
+ key={`${pkg.itemId}-${form.formCode}`}
+ variant="ghost"
+ size="sm"
+ className={cn(
+ "w-full justify-start font-normal text-sm",
+ isFormPackageActive && "bg-accent text-accent-foreground"
+ )}
+ onClick={() => handlePackageUnderFormClick(form, pkg)}
+ >
+ <FormInput className="mr-2 h-3 w-3" />
+ {form.formName}
+ </Button>
+ )
+ })
+ )}
+ </div>
+ </>
)}
- onClick={() => onIMFormClick(form.formCode)}
- >
- <FileText className="mr-2 h-4 w-4" />
- {form.formName}
- </Button>
- )
- })
+ </div>
+ ))
+ )
)}
</div>
</ScrollArea>
diff --git a/components/vendor-data-plant/vendor-data-container.tsx b/components/vendor-data-plant/vendor-data-container.tsx
index 7ce831df..60ec2c94 100644
--- a/components/vendor-data-plant/vendor-data-container.tsx
+++ b/components/vendor-data-plant/vendor-data-container.tsx
@@ -4,14 +4,28 @@ import * as React from "react"
import { TooltipProvider } from "@/components/ui/tooltip"
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"
import { cn } from "@/lib/utils"
+import { ProjectSwitcher } from "./project-swicher"
import { Sidebar } from "./sidebar"
-import { usePathname, useRouter } from "next/navigation"
+import { usePathname, useRouter, useSearchParams } from "next/navigation"
+import { getFormsByContractItemId, type FormInfo } from "@/lib/forms/services"
import { Separator } from "@/components/ui/separator"
-import { ProjectSwitcher } from "./project-swicher"
+import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
+import { ScrollArea } from "@/components/ui/scroll-area"
+import { Button } from "@/components/ui/button"
+import { FormInput } from "lucide-react"
+import { Skeleton } from "@/components/ui/skeleton"
+import { selectedModeAtom } from '@/atoms'
+import { useAtom } from 'jotai'
interface PackageData {
- packageCode: string
- packageName: string | null
+ itemId: number
+ itemName: string
+}
+
+interface ContractData {
+ contractId: number
+ contractName: string
+ packages: PackageData[]
}
interface ProjectData {
@@ -19,7 +33,7 @@ interface ProjectData {
projectCode: string
projectName: string
projectType: string
- packages: PackageData[]
+ contracts: ContractData[]
}
interface VendorDataContainerProps {
@@ -30,39 +44,18 @@ interface VendorDataContainerProps {
children: React.ReactNode
}
-function getInfoFromPathname(path: string | null): {
- projectCode: string | null
- packageCode: string | null
- formCode: string | null
- mode: "master" | "engineering" | "im" | null
-} {
- if (!path) return { projectCode: null, packageCode: null, formCode: null, mode: null }
-
- const segments = path.split("/").filter(Boolean)
- const vendorDataIndex = segments.indexOf("vendor-data-plant")
-
- if (vendorDataIndex === -1) {
- return { projectCode: null, packageCode: null, formCode: null, mode: null }
- }
+function getTagIdFromPathname(path: string | null): number | null {
+ if (!path) return null;
- const projectCode = segments[vendorDataIndex + 1] || null
- const packageCode = segments[vendorDataIndex + 2] || null
+ // 태그 패턴 검사 (/tag/123)
+ const tagMatch = path.match(/\/tag\/(\d+)/)
+ if (tagMatch) return parseInt(tagMatch[1], 10)
- // /eng/{formCode} 또는 /im/{formCode} 패턴 체크
- const modeSegment = segments[vendorDataIndex + 3]
- const formCode = segments[vendorDataIndex + 4] || null
+ // 폼 패턴 검사 (/form/123/...)
+ const formMatch = path.match(/\/form\/(\d+)/)
+ if (formMatch) return parseInt(formMatch[1], 10)
- let mode: "master" | "engineering" | "im" | null = null
-
- if (modeSegment === "eng") {
- mode = "engineering"
- } else if (modeSegment === "im") {
- mode = "im"
- } else if (projectCode && packageCode && !modeSegment) {
- mode = "master"
- }
-
- return { projectCode, packageCode, formCode, mode }
+ return null
}
export function VendorDataContainer({
@@ -74,106 +67,267 @@ export function VendorDataContainer({
}: VendorDataContainerProps) {
const pathname = usePathname()
const router = useRouter()
+ const searchParams = useSearchParams()
- // 상태 관리
+ const tagIdNumber = getTagIdFromPathname(pathname)
+
+ // 기본 상태
const [selectedProjectId, setSelectedProjectId] = React.useState(projects[0]?.projectId || 0)
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed)
- const [selectedPackageCode, setSelectedPackageCode] = React.useState<string | null>(null)
+ const [selectedContractId, setSelectedContractId] = React.useState(
+ projects[0]?.contracts[0]?.contractId || 0
+ )
+ const [selectedPackageId, setSelectedPackageId] = React.useState<number | null>(null)
+ const [formList, setFormList] = React.useState<FormInfo[]>([])
const [selectedFormCode, setSelectedFormCode] = React.useState<string | null>(null)
- const [currentMode, setCurrentMode] = React.useState<"master" | "engineering" | "im" | null>(null)
+ const [isLoadingForms, setIsLoadingForms] = React.useState(false)
- // 현재 선택된 프로젝트
+ console.log(selectedPackageId,"selectedPackageId")
+
+
+ // 현재 선택된 프로젝트/계약/패키지
const currentProject = projects.find((p) => p.projectId === selectedProjectId) ?? projects[0]
+ const currentContract = currentProject?.contracts.find((c) => c.contractId === selectedContractId)
+ ?? currentProject?.contracts[0]
+
+ // 프로젝트 타입 확인 - ship인 경우 항상 ENG 모드
+ const isShipProject = currentProject?.projectType === "ship"
+
+ const [selectedMode, setSelectedMode] = useAtom(selectedModeAtom)
+
+ // URL에서 모드 추출 (ship 프로젝트면 무조건 ENG로, 아니면 URL 또는 기본값)
+ const modeFromUrl = searchParams?.get('mode')
+ const initialMode ="ENG"
+
+ // 모드 초기화 (기존의 useState 초기값 대신)
+ React.useEffect(() => {
+ setSelectedMode(initialMode as "IM" | "ENG")
+ }, [initialMode, setSelectedMode])
+
+ const isTagOrFormRoute = pathname ? (pathname.includes("/tag/") || pathname.includes("/form/")) : false
+ const currentPackageName = isTagOrFormRoute
+ ? currentContract?.packages.find((pkg) => pkg.itemId === selectedPackageId)?.itemName || "None"
+ : "None"
+
+ // 폼 목록에서 고유한 폼 이름만 추출
+ const formNames = React.useMemo(() => {
+ return [...new Set(formList.map((form) => form.formName))]
+ }, [formList])
+
+ // URL에서 현재 폼 코드 추출
+ const getCurrentFormCode = (path: string): string | null => {
+ const segments = path.split("/").filter(Boolean)
+ const formIndex = segments.indexOf("form")
+ if (formIndex !== -1 && segments[formIndex + 2]) {
+ return segments[formIndex + 2]
+ }
+ return null
+ }
+
+ const currentFormCode = React.useMemo(() => {
+ return pathname ? getCurrentFormCode(pathname) : null
+ }, [pathname])
- // URL 변경 시 상태 동기화
+ // URL에서 모드가 변경되면 상태도 업데이트 (ship 프로젝트가 아닐 때만)
React.useEffect(() => {
- const { projectCode, packageCode, formCode, mode } = getInfoFromPathname(pathname)
-
- if (projectCode && packageCode) {
- // 프로젝트 찾기
- const project = projects.find(p => p.projectCode === projectCode)
- if (project) {
- setSelectedProjectId(project.projectId)
- setSelectedPackageCode(packageCode)
+ if (!isShipProject) {
+ const modeFromUrl = searchParams?.get('mode')
+ if (modeFromUrl === "ENG" || modeFromUrl === "IM") {
+ setSelectedMode(modeFromUrl)
}
}
-
- if (formCode) {
- setSelectedFormCode(formCode)
- } else {
- setSelectedFormCode(null)
+ }, [searchParams, isShipProject])
+
+ // 프로젝트 타입이 변경될 때 모드 업데이트
+ React.useEffect(() => {
+ if (isShipProject) {
+ setSelectedMode("ENG")
+
+ // URL 모드 파라미터도 업데이트
+ const url = new URL(window.location.href);
+ url.searchParams.set('mode', 'ENG');
+ router.replace(url.pathname + url.search);
}
-
- if (mode) {
- setCurrentMode(mode)
+ }, [isShipProject, router])
+
+ // (1) 새로고침 시 URL 파라미터(tagIdNumber) → selectedPackageId 세팅
+ React.useEffect(() => {
+ if (!currentContract) return
+
+ if (tagIdNumber) {
+ setSelectedPackageId(tagIdNumber)
} else {
- setCurrentMode(null)
+ // tagIdNumber가 없으면, 현재 계약의 첫 번째 패키지로
+ if (currentContract.packages?.length) {
+ setSelectedPackageId(currentContract.packages[0].itemId)
+ } else {
+ setSelectedPackageId(null)
+ }
}
- }, [pathname, projects])
-
- // 베이스 URL 생성 헬퍼
- const getBaseUrl = () => {
- const segments = pathname?.split("/").filter(Boolean) || []
- const vendorDataIndex = segments.indexOf("vendor-data-plant")
- if (vendorDataIndex === -1) return ""
- return "/" + segments.slice(0, vendorDataIndex + 1).join("/")
- }
+ }, [tagIdNumber, currentContract])
- // 프로젝트 및 패키지 선택 핸들러
- const handleSelectPackage = (projectId: number, packageCode: string) => {
- const project = projects.find(p => p.projectId === projectId)
- if (!project) return
-
- setSelectedProjectId(projectId)
- setSelectedPackageCode(packageCode)
- setSelectedFormCode(null)
- setCurrentMode("master")
+ // (2) 프로젝트 변경 시 계약 초기화
+ // React.useEffect(() => {
+ // if (currentProject?.contracts.length) {
+ // setSelectedContractId(currentProject.contracts[0].contractId)
+ // } else {
+ // setSelectedContractId(0)
+ // }
+ // }, [currentProject])
+
+ // (3) 패키지 ID와 모드가 변경될 때마다 폼 로딩
+ React.useEffect(() => {
+ const packageId = getTagIdFromPathname(pathname)
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${packageCode}`)
- }
+ if (packageId) {
+ setSelectedPackageId(packageId)
+
+ // URL에서 패키지 ID를 얻었을 때 즉시 폼 로드
+ loadFormsList(packageId, selectedMode);
+ } else if (currentContract?.packages?.length) {
+ const firstPackageId = currentContract.packages[0].itemId;
+ setSelectedPackageId(firstPackageId);
+ loadFormsList(firstPackageId, selectedMode);
+ }
+ }, [pathname, currentContract, selectedMode])
- // Master Tag List 클릭 핸들러
- const handleMasterTagListClick = () => {
- if (!selectedPackageCode) return
+ // 모드에 따른 폼 로드 함수
+ const loadFormsList = async (packageId: number, mode: "IM" | "ENG") => {
+ if (!packageId) return;
- const project = projects.find(p => p.projectId === selectedProjectId)
- if (!project) return
+ setIsLoadingForms(true);
+ try {
+ const result = await getFormsByContractItemId(packageId, mode);
+ setFormList(result.forms || []);
+ } catch (error) {
+ console.error(`폼 로딩 오류 (${mode} 모드):`, error);
+ setFormList([]);
+ } finally {
+ setIsLoadingForms(false);
+ }
+ };
+
+ // 핸들러들
+// 수정된 handleSelectContract 함수
+async function handleSelectContract(projId: number, cId: number) {
+ setSelectedProjectId(projId)
+ setSelectedContractId(cId)
+
+ // 선택된 계약의 첫 번째 패키지 찾기
+ const selectedProject = projects.find(p => p.projectId === projId)
+ const selectedContract = selectedProject?.contracts.find(c => c.contractId === cId)
+
+ if (selectedContract?.packages?.length) {
+ const firstPackageId = selectedContract.packages[0].itemId
+ setSelectedPackageId(firstPackageId)
- setCurrentMode("master")
+ // ENG 모드로 폼 목록 로드
+ setIsLoadingForms(true)
+ try {
+ const result = await getFormsByContractItemId(firstPackageId, "ENG")
+ setFormList(result.forms || [])
+
+ // 첫 번째 폼이 있으면 자동 선택 및 네비게이션
+ if (result.forms && result.forms.length > 0) {
+ const firstForm = result.forms[0]
+ setSelectedFormCode(firstForm.formCode)
+
+ // ENG 모드로 설정
+ setSelectedMode("ENG")
+
+ // 첫 번째 폼으로 네비게이션
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${projId}/${cId}?mode=ENG`)
+ } else {
+ // 폼이 없는 경우에도 ENG 모드로 설정
+ setSelectedMode("ENG")
+ setSelectedFormCode(null)
+
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/")
+ router.push(`/${baseSegments}/form/0/0/${projId}/${cId}?mode=ENG`)
+ }
+ } catch (error) {
+ console.error("폼 로딩 오류:", error)
+ setFormList([])
+ setSelectedFormCode(null)
+
+ // 오류 발생 시에도 ENG 모드로 설정
+ setSelectedMode("ENG")
+ } finally {
+ setIsLoadingForms(false)
+ }
+ } else {
+ // 패키지가 없는 경우
+ setSelectedPackageId(null)
+ setFormList([])
setSelectedFormCode(null)
-
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}`)
+ setSelectedMode("ENG")
}
-
- // Engineering 폼 클릭 핸들러
- const handleEngineeringFormClick = (formCode: string) => {
- if (!selectedPackageCode) return
-
- const project = projects.find(p => p.projectId === selectedProjectId)
- if (!project) return
-
- setCurrentMode("engineering")
- setSelectedFormCode(formCode)
-
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}/eng/${formCode}`)
+}
+
+ function handleSelectPackage(itemId: number) {
+ setSelectedPackageId(itemId)
}
+
+ function handleSelectForm(formName: string) {
+ const form = formList.find((f) => f.formName === formName)
+ if (form) {
+ setSelectedFormCode(form.formCode)
+ }
+ }
+
+ // 모드 변경 핸들러
+// 모드 변경 핸들러
+const handleModeChange = async (mode: "IM" | "ENG") => {
+ // ship 프로젝트인 경우 모드 변경 금지
+ if (isShipProject && mode !== "ENG") return;
- // IM 폼 클릭 핸들러
- const handleIMFormClick = (formCode: string) => {
- if (!selectedPackageCode) return
-
- const project = projects.find(p => p.projectId === selectedProjectId)
- if (!project) return
-
- setCurrentMode("im")
- setSelectedFormCode(formCode)
+ setSelectedMode(mode);
+
+ // 모드가 변경될 때 자동 네비게이션
+ if (currentContract?.packages?.length) {
+ const firstPackageId = currentContract.packages[0].itemId;
- const baseUrl = getBaseUrl()
- router.push(`${baseUrl}/${project.projectCode}/${selectedPackageCode}/im/${formCode}`)
+ if (mode === "IM") {
+ // IM 모드: 첫 번째 패키지로 이동
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/");
+ router.push(`/${baseSegments}/tag/${firstPackageId}?mode=${mode}`);
+ } else {
+ // ENG 모드: 폼 목록을 먼저 로드
+ setIsLoadingForms(true);
+ try {
+ const result = await getFormsByContractItemId(firstPackageId, mode);
+ setFormList(result.forms || []);
+
+ // 폼이 있으면 첫 번째 폼으로 이동
+ if (result.forms && result.forms.length > 0) {
+ const firstForm = result.forms[0];
+ setSelectedFormCode(firstForm.formCode);
+
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/");
+ router.push(`/${baseSegments}/form/0/${firstForm.formCode}/${selectedProjectId}/${selectedContractId}?mode=${mode}`);
+ } else {
+ // 폼이 없으면 모드만 변경
+ const baseSegments = pathname?.split("/").filter(Boolean).slice(0, pathname.split("/").filter(Boolean).indexOf("vendor-data-plant") + 1).join("/");
+ router.push(`/${baseSegments}/form/0/0/${selectedProjectId}/${selectedContractId}?mode=${mode}`);
+ }
+ } catch (error) {
+ console.error(`폼 로딩 오류 (${mode} 모드):`, error);
+ // 오류 발생 시 모드만 변경
+ const url = new URL(window.location.href);
+ url.searchParams.set('mode', mode);
+ router.replace(url.pathname + url.search);
+ } finally {
+ setIsLoadingForms(false);
+ }
+ }
+ } else {
+ // 패키지가 없는 경우, 모드만 변경
+ const url = new URL(window.location.href);
+ url.searchParams.set('mode', mode);
+ router.replace(url.pathname + url.search);
}
+};
return (
<TooltipProvider delayDuration={0}>
@@ -197,28 +351,151 @@ export function VendorDataContainer({
<ProjectSwitcher
isCollapsed={isCollapsed}
projects={projects}
- selectedProjectId={selectedProjectId}
- selectedPackageCode={selectedPackageCode}
- onSelectPackage={handleSelectPackage}
+ selectedContractId={selectedContractId}
+ onSelectContract={handleSelectContract}
/>
</div>
<Separator />
- <Sidebar
- isCollapsed={isCollapsed}
- selectedPackageCode={selectedPackageCode}
- selectedFormCode={selectedFormCode}
- currentMode={currentMode}
- onMasterTagListClick={handleMasterTagListClick}
- onEngineeringFormClick={handleEngineeringFormClick}
- onIMFormClick={handleIMFormClick}
- />
+ {!isCollapsed ? (
+ isShipProject ? (
+ // 프로젝트 타입이 ship인 경우: 탭 없이 ENG 모드 사이드바만 바로 표시
+ <div className="mt-0">
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedProjectId={selectedProjectId}
+ selectedContractId={selectedContractId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode="ENG"
+ className="hidden lg:block"
+ />
+ </div>
+ ) : (
+ // 프로젝트 타입이 ship이 아닌 경우: 기존 탭 UI 표시
+ <Tabs
+ defaultValue={initialMode}
+ value={selectedMode}
+ onValueChange={(value) => handleModeChange(value as "IM" | "ENG")}
+ className="w-full"
+ >
+ <TabsList className="w-full">
+ <TabsTrigger value="ENG" className="flex-1">Engineering</TabsTrigger>
+ <TabsTrigger value="IM" className="flex-1">Handover</TabsTrigger>
+
+ </TabsList>
+
+ <TabsContent value="IM" className="mt-0">
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedContractId={selectedContractId}
+ selectedProjectId={selectedProjectId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode="IM"
+ className="hidden lg:block"
+ />
+ </TabsContent>
+
+ <TabsContent value="ENG" className="mt-0">
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedContractId={selectedContractId}
+ selectedProjectId={selectedProjectId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode="ENG"
+ className="hidden lg:block"
+ />
+ </TabsContent>
+ </Tabs>
+ )
+ ) : (
+ // 접혀있을 때 UI
+ <>
+ {!isShipProject && (
+ // ship 프로젝트가 아닐 때만 모드 선택 버튼 표시
+ <div className="flex justify-center space-x-1 my-2">
+
+ <Button
+ variant={selectedMode === "ENG" ? "default" : "ghost"}
+ size="sm"
+ className="h-8 px-2"
+ onClick={() => handleModeChange("ENG")}
+ >
+ Engineering
+ </Button>
+ <Button
+ variant={selectedMode === "IM" ? "default" : "ghost"}
+ size="sm"
+ className="h-8 px-2"
+ onClick={() => handleModeChange("IM")}
+ >
+ Handover
+ </Button>
+ </div>
+ )}
+
+ <Sidebar
+ isCollapsed={isCollapsed}
+ packages={currentContract?.packages || []}
+ selectedPackageId={selectedPackageId}
+ selectedProjectId={selectedProjectId}
+ selectedContractId={selectedContractId}
+ onSelectPackage={handleSelectPackage}
+ forms={formList}
+ selectedForm={
+ selectedFormCode
+ ? formList.find((f) => f.formCode === selectedFormCode)?.formName || null
+ : null
+ }
+ onSelectForm={handleSelectForm}
+ isLoadingForms={isLoadingForms}
+ mode={isShipProject ? "ENG" : selectedMode}
+ className="hidden lg:block"
+ />
+ </>
+ )}
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={defaultLayout[1]} minSize={40}>
<div className="p-4 h-full overflow-auto flex flex-col">
+ <div className="flex items-center justify-between mb-4">
+ <h2 className="text-lg font-bold">
+ {isShipProject || selectedMode === "ENG"
+ ? "Engineering Mode"
+ : `Package: ${currentPackageName}`}
+ </h2>
+ </div>
{children}
</div>
</ResizablePanel>