From 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 25 Mar 2025 15:55:45 +0900 Subject: initial commit --- components/vendor-data/project-swicher.tsx | 116 +++++++ components/vendor-data/sidebar.tsx | 235 ++++++++++++++ .../vendor-data/tag-table/add-tag-dialog.tsx | 357 +++++++++++++++++++++ .../vendor-data/tag-table/tag-table-column.tsx | 196 +++++++++++ components/vendor-data/tag-table/tag-table.tsx | 39 +++ .../vendor-data/tag-table/tag-type-definitions.ts | 87 +++++ components/vendor-data/vendor-data-container.tsx | 231 +++++++++++++ 7 files changed, 1261 insertions(+) create mode 100644 components/vendor-data/project-swicher.tsx create mode 100644 components/vendor-data/sidebar.tsx create mode 100644 components/vendor-data/tag-table/add-tag-dialog.tsx create mode 100644 components/vendor-data/tag-table/tag-table-column.tsx create mode 100644 components/vendor-data/tag-table/tag-table.tsx create mode 100644 components/vendor-data/tag-table/tag-type-definitions.ts create mode 100644 components/vendor-data/vendor-data-container.tsx (limited to 'components/vendor-data') diff --git a/components/vendor-data/project-swicher.tsx b/components/vendor-data/project-swicher.tsx new file mode 100644 index 00000000..609de5cc --- /dev/null +++ b/components/vendor-data/project-swicher.tsx @@ -0,0 +1,116 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" + +interface ContractInfo { + contractId: number + contractName: string +} + +interface ProjectInfo { + projectId: number + projectCode: string + projectName: string + contracts: ContractInfo[] +} + +interface ProjectSwitcherProps { + isCollapsed: boolean + projects: ProjectInfo[] + + // 상위가 관리하는 "현재 선택된 contractId" + selectedContractId: number | null + + // 콜백: 사용자가 "어떤 contract"를 골랐는지 + // => 우리가 projectId도 찾아서 상위 state를 같이 갱신해야 함 + onSelectContract: (projectId: number, contractId: number) => void +} + +export function ProjectSwitcher({ + isCollapsed, + projects, + selectedContractId, + onSelectContract, +}: ProjectSwitcherProps) { + // Select value = stringified contractId + const selectValue = selectedContractId ? String(selectedContractId) : "" + + // 현재 선택된 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 } + } + } + return null + }, [projects, selectedContractId]) + + // Trigger label => 계약 이름 or placeholder + const triggerLabel = selectedContract?.contractName ?? "Select a contract" + + // onValueChange: val = String(contractId) + // => 찾으면 projectId, contractId 모두 상위로 전달 + function handleValueChange(val: string) { + const contractId = Number(val) + // Find which project has this contract + let foundProjectId = 0 + let foundContractName = "" + + for (const proj of projects) { + const found = proj.contracts.find((c) => c.contractId === contractId) + if (found) { + foundProjectId = proj.projectId + foundContractName = found.contractName + break + } + } + // 상위로 알림 + onSelectContract(foundProjectId, contractId) + } + + return ( + + ) +} \ No newline at end of file diff --git a/components/vendor-data/sidebar.tsx b/components/vendor-data/sidebar.tsx new file mode 100644 index 00000000..b9e14b65 --- /dev/null +++ b/components/vendor-data/sidebar.tsx @@ -0,0 +1,235 @@ +"use client" + +import * as React from "react" +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { + Tooltip, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/tooltip" +import { Package2, FormInput } from "lucide-react" +import { useRouter, usePathname } from "next/navigation" +import { Skeleton } from "@/components/ui/skeleton" +import { type FormInfo } from "@/lib/forms/services" + +interface PackageData { + itemId: number + itemName: string +} + +interface SidebarProps extends React.HTMLAttributes { + isCollapsed: boolean + packages: PackageData[] + selectedPackageId: number | null + onSelectPackage: (itemId: number) => void + forms: FormInfo[] + selectedForm: string | null + onSelectForm: (formName: string) => void + isLoadingForms?: boolean +} + +export function Sidebar({ + className, + isCollapsed, + packages, + selectedPackageId, + onSelectPackage, + forms, + selectedForm, + onSelectForm, + isLoadingForms = false, +}: SidebarProps) { + const router = useRouter() + const pathname = usePathname() + + /** + * --------------------------- + * 1) URL에서 현재 패키지 / 폼 코드 추출 + * --------------------------- + */ + const segments = pathname.split("/").filter(Boolean) + // 예) "/partners/vendor-data/tag/123" => ["partners","vendor-data","tag","123"] + + let currentItemId: number | null = null + let currentFormCode: string | null = null + + const tagIndex = segments.indexOf("tag") + if (tagIndex !== -1 && segments[tagIndex + 1]) { + // tag 뒤에 오는 값이 패키지 itemId + currentItemId = parseInt(segments[tagIndex + 1], 10) + } + + const formIndex = segments.indexOf("form") + if (formIndex !== -1) { + // form 뒤 첫 파라미터 => itemId, 그 다음 파라미터 => formCode + const itemSegment = segments[formIndex + 1] + const codeSegment = segments[formIndex + 2] + + if (itemSegment) { + currentItemId = parseInt(itemSegment, 10) + } + if (codeSegment) { + currentFormCode = codeSegment + } + } + + /** + * --------------------------- + * 2) 패키지 클릭 핸들러 + * --------------------------- + */ + const handlePackageClick = (itemId: number) => { + // 상위 컴포넌트 상태 업데이트 + onSelectPackage(itemId) + + // 해당 태그 페이지로 라우팅 + // 예: /vendor-data/tag/123 + const baseSegments = segments.slice(0, segments.indexOf("vendor-data") + 1).join("/") + router.push(`/${baseSegments}/tag/${itemId}`) + } + + /** + * --------------------------- + * 3) 폼 클릭 핸들러 + * --------------------------- + */ + const handleFormClick = (form: FormInfo) => { + // 패키지가 선택되어 있을 때만 동작 + if (selectedPackageId === null) return + + // 상위 컴포넌트 상태 업데이트 + onSelectForm(form.formName) + + // 해당 폼 페이지로 라우팅 + // 예: /vendor-data/form/[packageId]/[formCode] + + const baseSegments = segments.slice(0, segments.indexOf("vendor-data") + 1).join("/") + + router.push(`/${baseSegments}/form/${selectedPackageId}/${form.formCode}`) + } + + return ( +
+
+ {/* ---------- 패키지(Items) 목록 ---------- */} +
+

+ {isCollapsed ? "P" : "Package Lists"} +

+ +
+ {packages.map((pkg) => { + // URL 기준으로 active 여부 판단 + const isActive = pkg.itemId === currentItemId + + return ( +
+ {isCollapsed ? ( + + + + + + {pkg.itemName} + + + ) : ( + + )} +
+ ) + })} +
+
+
+ + + + {/* ---------- 폼 목록 ---------- */} +
+

+ {isCollapsed ? "F" : "Form Lists"} +

+ +
+ {isLoadingForms ? ( + // 로딩 중 스켈레톤 UI 표시 + Array.from({ length: 3 }).map((_, index) => ( +
+ +
+ )) + ) : forms.length === 0 ? ( +

+ (No forms loaded) +

+ ) : ( + forms.map((form) => { + // URL 기준으로 active 여부 판단 + const isActive = form.formCode === currentFormCode + + return isCollapsed ? ( + + + + + + {form.formName} + + + ) : ( + + ) + }) + )} +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/components/vendor-data/tag-table/add-tag-dialog.tsx b/components/vendor-data/tag-table/add-tag-dialog.tsx new file mode 100644 index 00000000..1321fc58 --- /dev/null +++ b/components/vendor-data/tag-table/add-tag-dialog.tsx @@ -0,0 +1,357 @@ +"use client" + +import * as React from "react" +import { useForm, useWatch } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { createTagSchema, type CreateTagSchema } from "@/lib/tags/validations" +import { createTag } from "@/lib/tags/service" +import { toast } from "sonner" +import { Loader2 } from "lucide-react" +import { cn } from "@/lib/utils" +import { useRouter } from "next/navigation" + +// Popover + Command +import { + Popover, + PopoverTrigger, + PopoverContent, +} from "@/components/ui/popover" +import { + Command, + CommandInput, + CommandList, + CommandGroup, + CommandItem, + CommandEmpty, +} from "@/components/ui/command" +import { ChevronsUpDown, Check } from "lucide-react" + +// The dynamic Tag Type definitions +import { tagTypeDefinitions } from "./tag-type-definitions" + +// Add Select component for dropdown fields +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { ScrollArea } from "@/components/ui/scroll-area" + +interface AddTagDialogProps { + selectedPackageId: number | null +} + +export function AddTagDialog({ selectedPackageId }: AddTagDialogProps) { + const [open, setOpen] = React.useState(false) + const [isPending, startTransition] = React.useTransition() + const router = useRouter() + + const form = useForm({ + resolver: zodResolver(createTagSchema), + defaultValues: { + tagType: "", // user picks + tagNo: "", // auto-generated + description: "", + functionCode: "", + seqNumber: "", + valveAcronym: "", + processUnit: "", + }, + }) + + const watchAll = useWatch({ control: form.control }) + + // 1) Find the selected tag type definition + const currentTagTypeDef = React.useMemo(() => { + return tagTypeDefinitions.find((def) => def.id === watchAll.tagType) || null + }, [watchAll.tagType]) + + // 2) Whenever the user changes sub-fields, re-generate `tagNo` + React.useEffect(() => { + if (!currentTagTypeDef) { + // if no type selected, no auto-generation + return + } + + // Prevent infinite loop by excluding tagNo from the watched dependencies + // This is crucial because setting tagNo would trigger another update + const { tagNo, ...fieldsToWatch } = watchAll + + const newTagNo = currentTagTypeDef.generateTagNo(fieldsToWatch as CreateTagSchema) + + // Only update if different to avoid unnecessary re-renders + if (form.getValues("tagNo") !== newTagNo) { + form.setValue("tagNo", newTagNo, { shouldValidate: false }) + } + }, [currentTagTypeDef, watchAll, form]) + + // Check if tag number is valid (doesn't contain '??' and is not empty) + const isTagNoValid = React.useMemo(() => { + const tagNo = form.getValues("tagNo"); + return tagNo && tagNo.trim() !== "" && !tagNo.includes("??"); + }, [form, watchAll.tagNo]); + + // onSubmit + async function onSubmit(data: CreateTagSchema) { + startTransition(async () => { + if (!selectedPackageId) { + toast.error("No selectedPackageId.") + return + } + + const result = await createTag(data, selectedPackageId) + if ("error" in result) { + toast.error(`Error: ${result.error}`) + return + } + + toast.success("Tag created successfully!") + form.reset() + setOpen(false) + router.refresh() + + }) + } + + function handleDialogOpenChange(nextOpen: boolean) { + if (!nextOpen) { + form.reset() + } + setOpen(nextOpen) + } + + // 3) TagType selection UI (like your Command menu) + function renderTagTypeSelector(field: any) { + const [popoverOpen, setPopoverOpen] = React.useState(false) + return ( + + Tag Type + + + + + + + + + + No tag type found. + + {tagTypeDefinitions.map((def,index) => ( + { + field.onChange(def.id) // store the 'id' + setPopoverOpen(false) + }} + value={def.id} + > + {def.label} + + + ))} + + + + + + + + + ) + } + + // 4) Render sub-fields based on currentTagTypeDef + // Updated to handle different field types (text, select) + function renderSubFields() { + if (!currentTagTypeDef) return null + + return currentTagTypeDef.subFields.map((subField, index) => ( + + ( + + {subField.label} + + {subField.type === "select" && subField.options ? ( + + ) : ( + + )} + + {subField.formatHint && ( +

+ {subField.formatHint} +

+ )} + +
+ )} + /> + )) + } + + return ( + + + + + + + + Add New Tag + + Select a Tag Type and fill in sub-fields. The Tag No will be generated automatically. + + + + +
+ +
+ {/* Tag Type - Outside ScrollArea as it's always visible */} + renderTagTypeSelector(field)} + /> +
+ + {/* ScrollArea for dynamic fields */} + +
+ {/* sub-fields from the selected tagType */} + {renderSubFields()} + + {/* Tag No (auto-generated) */} + ( + + Tag No (auto-generated) + + + + + + )} + /> + + {/* Description (optional) */} + ( + + Description + + field.onChange(e.target.value)} + /> + + + + )} + /> +
+
+
+ +
+ + + + + + + +
+
+ ) +} \ No newline at end of file diff --git a/components/vendor-data/tag-table/tag-table-column.tsx b/components/vendor-data/tag-table/tag-table-column.tsx new file mode 100644 index 00000000..a22611cf --- /dev/null +++ b/components/vendor-data/tag-table/tag-table-column.tsx @@ -0,0 +1,196 @@ +"use client" + +import * as React from "react" +import { ColumnDef } from "@tanstack/react-table" +import type { Row } from "@tanstack/react-table" +import { numericFilter } from "@/lib/data-table" +import { ClientDataTableColumnHeaderSimple } from "@/components/client-data-table/data-table-column-simple-header" +import { formatDate } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, + } from "@/components/ui/dropdown-menu" +import { Ellipsis } from "lucide-react" +import { Tag } from "@/types/vendorData" + + +export interface DataTableRowAction { + row: Row + type: 'open' | "update" | "delete" +} + +interface GetColumnsProps { + setRowAction: React.Dispatch | null>> +} + +export function getColumns({ + setRowAction, + }: GetColumnsProps): ColumnDef[] { + return [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + className="translate-y-0.5" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + className="translate-y-0.5" + /> + ), + enableSorting: false, + enableHiding: false, + size: 40, + }, + + { + accessorKey: "tagNo", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.getValue("tagNo")}
, + meta: { + excelHeader: "Tag No" + }, + }, + { + accessorKey: "description", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.getValue("description")}
, + meta: { + excelHeader: "Tag Descripiton" + }, + }, + { + accessorKey: "tagType", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.getValue("tagType")}
, + meta: { + excelHeader: "Tag Type" + }, + }, + { + id: "validation", + header: "Error", + cell: ({ row }) =>
, + }, + + { + accessorKey: "createdAt", + header: ({ column }) => ( + + ), + cell: ({ cell }) => formatDate(cell.getValue() as Date), + meta: { + excelHeader: "created At" + }, + }, + { + accessorKey: "updatedAt", + header: ({ column }) => ( + + ), + cell: ({ cell }) => formatDate(cell.getValue() as Date), + meta: { + excelHeader: "updated At" + }, + }, + + { + id: "actions", + cell: function Cell({ row }) { + const [isUpdatePending, startUpdateTransition] = React.useTransition() + + return ( + + + + + + setRowAction({ row, type: "update" })} + > + Edit + + + Labels + + {/* { + startUpdateTransition(() => { + toast.promise( + updateTask({ + id: row.original.id, + label: value as Task["label"], + }), + { + loading: "Updating...", + success: "Label updated", + error: (err) => getErrorMessage(err), + } + ) + }) + }} + > + {tasks.label.enumValues.map((label) => ( + + {label} + + ))} + */} + + + + setRowAction({ row, type: "delete" })} + > + Delete + ⌘⌫ + + + + ) + }, + size: 40, + }, + ] + } + \ No newline at end of file diff --git a/components/vendor-data/tag-table/tag-table.tsx b/components/vendor-data/tag-table/tag-table.tsx new file mode 100644 index 00000000..a449529f --- /dev/null +++ b/components/vendor-data/tag-table/tag-table.tsx @@ -0,0 +1,39 @@ +"use client" + +import * as React from "react" +import { ClientDataTable } from "@/components/client-data-table/data-table" +import { DataTableRowAction, getColumns } from "./tag-table-column" +import { Tag as TagData } from "@/types/vendorData" +import { DataTableAdvancedFilterField } from "@/types/table" +import { AddTagDialog } from "./add-tag-dialog" + +interface TagTableProps { + data: TagData[] +} + +/** + * TagTable: Tag 데이터를 표시하는 표 + */ +export function TagTable({ data }: TagTableProps) { + + const [rowAction, setRowAction] = + React.useState | null>(null) + + const columns = React.useMemo( + () => getColumns({ setRowAction }), + [setRowAction] + ) + + const advancedFilterFields: DataTableAdvancedFilterField[] = [] + + return ( + <> + + + + ) +} \ No newline at end of file diff --git a/components/vendor-data/tag-table/tag-type-definitions.ts b/components/vendor-data/tag-table/tag-type-definitions.ts new file mode 100644 index 00000000..e5d04eab --- /dev/null +++ b/components/vendor-data/tag-table/tag-type-definitions.ts @@ -0,0 +1,87 @@ +import { CreateTagSchema } from "@/lib/tags/validations" + +/** + * Each "Tag Type" has: + * - id, label + * - subFields[]: + * -- name (form field name) + * -- label (UI label) + * -- placeholder? + * -- type: "select" | "text" + * -- options?: { value: string; label: string; }[] (for dropdown) + * -- optional "regex" or "formatHint" for display + * - generateTagNo: function + */ +export const tagTypeDefinitions = [ + { + id: "EquipmentNumbering", + label: "Equipment Numbering", + subFields: [ + { + name: "functionCode", + label: "Function", + placeholder: "", + type: "select", + // Example options: + options: [ + { value: "PM", label: "Pump" }, + { value: "AA", label: "Pneumatic Motor" }, + ], + // or if you want a regex or format hint: + formatHint: "2 letters, e.g. PM", + }, + { + name: "seqNumber", + label: "Seq. Number", + placeholder: "001", + type: "text", + formatHint: "3 digits", + }, + ], + generateTagNo: (values: CreateTagSchema) => { + const fc = values.functionCode || "??" + const seq = values.seqNumber || "000" + return `${fc}-${seq}` + }, + }, + { + id: "Valve", + label: "Valve", + subFields: [ + { + name: "valveAcronym", + label: "Valve Acronym", + placeholder: "", + type: "select", + options: [ + { value: "VB", label: "Ball Valve" }, + { value: "VAR", label: "Auto Recirculation Valve" }, + ], + }, + { + name: "processUnit", + label: "Process Unit (2 digits)", + placeholder: "01", + type: "select", + options: [ + { value: "01", label: "Firewater System" }, + { value: "02", label: "Liquefaction Unit" }, + ], + }, + { + name: "seqNumber", + label: "Seq. Number", + placeholder: "001", + type: "text", + formatHint: "3 digits", + }, + ], + generateTagNo: (values: CreateTagSchema) => { + const va = values.valveAcronym || "??" + const pu = values.processUnit || "??" + const seq= values.seqNumber || "000" + return `${va}-${pu}${seq}` + }, + }, + // ... more types from your API ... +] \ No newline at end of file diff --git a/components/vendor-data/vendor-data-container.tsx b/components/vendor-data/vendor-data-container.tsx new file mode 100644 index 00000000..69c22b79 --- /dev/null +++ b/components/vendor-data/vendor-data-container.tsx @@ -0,0 +1,231 @@ +"use client" + +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 { useParams, usePathname, useRouter } from "next/navigation" +import { getFormsByContractItemId, type FormInfo } from "@/lib/forms/services" +import { Separator } from "@/components/ui/separator" + +interface PackageData { + itemId: number + itemName: string +} + +interface ContractData { + contractId: number + contractName: string + packages: PackageData[] +} + +interface ProjectData { + projectId: number + projectCode: string + projectName: string + contracts: ContractData[] +} + +interface VendorDataContainerProps { + projects: ProjectData[] + defaultLayout?: number[] + defaultCollapsed?: boolean + navCollapsedSize: number + children: React.ReactNode +} + +function getTagIdFromPathname(path: string): number | null { + // 태그 패턴 검사 (/tag/123) + const tagMatch = path.match(/\/tag\/(\d+)/) + if (tagMatch) return parseInt(tagMatch[1], 10) + + // 폼 패턴 검사 (/form/123/...) + const formMatch = path.match(/\/form\/(\d+)/) + if (formMatch) return parseInt(formMatch[1], 10) + + return null +} +export function VendorDataContainer({ + projects, + defaultLayout = [20, 80], + defaultCollapsed = false, + navCollapsedSize, + children +}: VendorDataContainerProps) { + const pathname = usePathname() + const router = useRouter() + const tagIdNumber = getTagIdFromPathname(pathname) + + const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed) + // 폼 로드 요청 추적 + const lastRequestIdRef = React.useRef(0) + + // 기본 상태 + const [selectedProjectId, setSelectedProjectId] = React.useState(projects[0]?.projectId || 0) + const [selectedContractId, setSelectedContractId] = React.useState( + projects[0]?.contracts[0]?.contractId || 0 + ) + // URL에서 들어온 tagIdNumber를 우선으로 설정하기 위해 초기에 null로 두고, 뒤에서 useEffect로 세팅 + const [selectedPackageId, setSelectedPackageId] = React.useState(null) + + const [formList, setFormList] = React.useState([]) + const [selectedFormCode, setSelectedFormCode] = React.useState(null) + const [isLoadingForms, setIsLoadingForms] = React.useState(false) + + // 현재 선택된 프로젝트/계약/패키지 + const currentProject = projects.find((p) => p.projectId === selectedProjectId) ?? projects[0] + const currentContract = currentProject?.contracts.find((c) => c.contractId === selectedContractId) + ?? currentProject?.contracts[0] + + const isTagOrFormRoute = pathname.includes("/tag/") || pathname.includes("/form/") + 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]) + + // (1) 새로고침 시 URL 파라미터(tagIdNumber) → selectedPackageId 세팅 + // URL에 tagIdNumber가 있으면 그걸 우선으로, 아니면 기존 로직대로 '첫 번째 패키지' 사용 + React.useEffect(() => { + if (!currentContract) return + + if (tagIdNumber) { + setSelectedPackageId(tagIdNumber) + } else { + // tagIdNumber가 없으면, 현재 계약의 첫 번째 패키지로 + if (currentContract.packages?.length) { + setSelectedPackageId(currentContract.packages[0].itemId) + } else { + setSelectedPackageId(null) + } + } + }, [tagIdNumber, currentContract]) + + // (2) 프로젝트 변경 시 계약 초기화 + React.useEffect(() => { + if (currentProject?.contracts.length) { + setSelectedContractId(currentProject.contracts[0].contractId) + } else { + setSelectedContractId(0) + } + }, [currentProject]) + + // // (3) selectedPackageId 바뀔 때 URL도 같이 업데이트 + // React.useEffect(() => { + // if (!selectedPackageId) return + // const basePath = pathname.includes("/partners/") + // ? "/partners/vendor-data/tag/" + // : "/vendor-data/tag/" + + // router.push(`${basePath}${selectedPackageId}`) + // }, [selectedPackageId, router, pathname]) + + // (4) 패키지 ID가 정해질 때마다 폼 로딩 + React.useEffect(() => { + const packageId = getTagIdFromPathname(pathname) + + if (packageId) { + setSelectedPackageId(packageId) + + // URL에서 패키지 ID를 얻었을 때 즉시 폼 로드 + loadFormsList(packageId); + } else if (currentContract?.packages?.length) { + setSelectedPackageId(currentContract.packages[0].itemId) + } + }, [pathname, currentContract]) + + // 폼 로드 함수를 컴포넌트 내부에 정의하고 재사용 + const loadFormsList = async (packageId: number) => { + if (!packageId) return; + + setIsLoadingForms(true); + try { + const result = await getFormsByContractItemId(packageId); + setFormList(result.forms || []); + } catch (error) { + console.error("폼 로딩 오류:", error); + setFormList([]); + } finally { + setIsLoadingForms(false); + } + }; + // 핸들러들 + function handleSelectContract(projId: number, cId: number) { + setSelectedProjectId(projId) + setSelectedContractId(cId) + } + + function handleSelectPackage(itemId: number) { + setSelectedPackageId(itemId) + } + + function handleSelectForm(formName: string) { + const form = formList.find((f) => f.formName === formName) + if (form) { + setSelectedFormCode(form.formCode) + } + } + + return ( + + + setIsCollapsed(true)} + onResize={() => setIsCollapsed(false)} + className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")} + > +
+ +
+ + f.formCode === selectedFormCode)?.formName || null + : null + } + onSelectForm={handleSelectForm} + isLoadingForms={isLoadingForms} + className="hidden lg:block" + /> +
+ + + + +
+
+

Package: {currentPackageName}

+
+ {children} +
+
+
+
+ ) +} \ No newline at end of file -- cgit v1.2.3