From e2ed31dd0112dc3bede53ceef9b957d2810e141e Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Wed, 27 Aug 2025 08:24:58 +0000 Subject: (김준회) 임시 관리자 페이지 - EDP 데이터 수동 관리 추가 및 세션검증 추가 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/[lng]/admin/edp/actions/contract-actions.ts | 200 +++++++++++ app/[lng]/admin/edp/actions/data-actions.ts | 155 +++++++++ .../admin/edp/components/contract-edit-form.tsx | 231 +++++++++++++ app/[lng]/admin/edp/components/contract-form.tsx | 145 ++++++++ .../edp/components/contract-items-edit-form.tsx | 204 ++++++++++++ .../admin/edp/components/contract-items-form.tsx | 230 +++++++++++++ .../admin/edp/components/contract-selector.tsx | 334 +++++++++++++++++++ app/[lng]/admin/edp/components/item-selector.tsx | 368 +++++++++++++++++++++ .../admin/edp/components/project-selector.tsx | 258 +++++++++++++++ app/[lng]/admin/edp/components/vendor-selector.tsx | 281 ++++++++++++++++ app/[lng]/admin/edp/page.tsx | 69 ++++ app/[lng]/admin/edp/types/item.ts | 18 + app/[lng]/admin/layout.tsx | 34 ++ 13 files changed, 2527 insertions(+) create mode 100644 app/[lng]/admin/edp/actions/contract-actions.ts create mode 100644 app/[lng]/admin/edp/actions/data-actions.ts create mode 100644 app/[lng]/admin/edp/components/contract-edit-form.tsx create mode 100644 app/[lng]/admin/edp/components/contract-form.tsx create mode 100644 app/[lng]/admin/edp/components/contract-items-edit-form.tsx create mode 100644 app/[lng]/admin/edp/components/contract-items-form.tsx create mode 100644 app/[lng]/admin/edp/components/contract-selector.tsx create mode 100644 app/[lng]/admin/edp/components/item-selector.tsx create mode 100644 app/[lng]/admin/edp/components/project-selector.tsx create mode 100644 app/[lng]/admin/edp/components/vendor-selector.tsx create mode 100644 app/[lng]/admin/edp/page.tsx create mode 100644 app/[lng]/admin/edp/types/item.ts create mode 100644 app/[lng]/admin/layout.tsx (limited to 'app') diff --git a/app/[lng]/admin/edp/actions/contract-actions.ts b/app/[lng]/admin/edp/actions/contract-actions.ts new file mode 100644 index 00000000..f6e1bed2 --- /dev/null +++ b/app/[lng]/admin/edp/actions/contract-actions.ts @@ -0,0 +1,200 @@ +'use server' + +import db from '@/db/db' +import { contracts, contractItems } from '@/db/schema/contract' +import { eq } from 'drizzle-orm' + +// 계약 생성 타입 정의 +export interface CreateContractData { + projectId: number + vendorId: number + contractName: string + status?: string +} + +// 계약 아이템 생성 타입 정의 +export interface CreateContractItemData { + contractId: number + itemId: number + description?: string + quantity?: number + unitPrice?: number +} + +// 계약 생성 +export async function createContract(data: CreateContractData) { + try { + // contractNo 생성 (간단한 방식으로 timestamp 기반) + const contractNo = `TEST-${Date.now()}` + + const [newContract] = await db.insert(contracts).values({ + projectId: data.projectId, + vendorId: data.vendorId, + contractNo, + contractName: data.contractName, + status: data.status || 'TEST', + }).returning({ + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + }) + + return { + success: true, + data: newContract, + message: '계약이 성공적으로 생성되었습니다.' + } + } catch (error) { + console.error('계약 생성 오류:', error) + return { + success: false, + error: '계약을 생성할 수 없습니다.' + } + } +} + +// 계약 아이템 생성 +export async function createContractItem(data: CreateContractItemData) { + try { + const [newContractItem] = await db.insert(contractItems).values({ + contractId: data.contractId, + itemId: data.itemId, + description: data.description, + quantity: data.quantity || 1, + unitPrice: data.unitPrice, + }).returning({ + id: contractItems.id, + contractId: contractItems.contractId, + itemId: contractItems.itemId, + quantity: contractItems.quantity, + }) + + return { + success: true, + data: newContractItem, + message: '계약 아이템이 성공적으로 생성되었습니다.' + } + } catch (error) { + console.error('계약 아이템 생성 오류:', error) + return { + success: false, + error: '계약 아이템을 생성할 수 없습니다.' + } + } +} + +// 여러 계약 아이템 일괄 생성 +export async function createMultipleContractItems(contractId: number, itemsData: Omit[]) { + try { + const itemsToInsert = itemsData.map(item => ({ + contractId, + itemId: item.itemId, + description: item.description, + quantity: item.quantity || 1, + unitPrice: item.unitPrice, + })) + + const newContractItems = await db.insert(contractItems).values(itemsToInsert).returning({ + id: contractItems.id, + contractId: contractItems.contractId, + itemId: contractItems.itemId, + quantity: contractItems.quantity, + }) + + return { + success: true, + data: newContractItems, + message: `${newContractItems.length}개의 계약 아이템이 성공적으로 생성되었습니다.` + } + } catch (error) { + console.error('계약 아이템 일괄 생성 오류:', error) + return { + success: false, + error: '계약 아이템을 일괄 생성할 수 없습니다.' + } + } +} + +// 계약 수정 +export async function updateContract(contractId: number, data: Partial) { + try { + const [updatedContract] = await db.update(contracts) + .set({ + projectId: data.projectId, + vendorId: data.vendorId, + contractName: data.contractName, + status: data.status, + updatedAt: new Date(), + }) + .where(eq(contracts.id, contractId)) + .returning({ + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + }) + + return { + success: true, + data: updatedContract, + message: '계약이 성공적으로 수정되었습니다.' + } + } catch (error) { + console.error('계약 수정 오류:', error) + return { + success: false, + error: '계약을 수정할 수 없습니다.' + } + } +} + +// 계약 아이템 수정 +export async function updateContractItem(contractItemId: number, data: Partial) { + try { + const [updatedContractItem] = await db.update(contractItems) + .set({ + description: data.description, + quantity: data.quantity, + unitPrice: data.unitPrice, + updatedAt: new Date(), + }) + .where(eq(contractItems.id, contractItemId)) + .returning({ + id: contractItems.id, + contractId: contractItems.contractId, + itemId: contractItems.itemId, + quantity: contractItems.quantity, + }) + + return { + success: true, + data: updatedContractItem, + message: '계약 아이템이 성공적으로 수정되었습니다.' + } + } catch (error) { + console.error('계약 아이템 수정 오류:', error) + return { + success: false, + error: '계약 아이템을 수정할 수 없습니다.' + } + } +} + +// 계약 아이템 삭제 +export async function deleteContractItem(contractItemId: number) { + try { + await db.delete(contractItems).where(eq(contractItems.id, contractItemId)) + + return { + success: true, + message: '계약 아이템이 성공적으로 삭제되었습니다.' + } + } catch (error) { + console.error('계약 아이템 삭제 오류:', error) + return { + success: false, + error: '계약 아이템을 삭제할 수 없습니다.' + } + } +} diff --git a/app/[lng]/admin/edp/actions/data-actions.ts b/app/[lng]/admin/edp/actions/data-actions.ts new file mode 100644 index 00000000..66fdc919 --- /dev/null +++ b/app/[lng]/admin/edp/actions/data-actions.ts @@ -0,0 +1,155 @@ +'use server' + +import db from '@/db/db' +import { projects } from '@/db/schema/projects' +import { vendors } from '@/db/schema/vendors' +import { items } from '@/db/schema/items' +import { contracts, contractItems } from '@/db/schema/contract' +import { eq } from 'drizzle-orm' + +// 프로젝트 목록 조회 +export async function getProjects() { + try { + const projectList = await db.select({ + id: projects.id, + code: projects.code, + name: projects.name, + type: projects.type, + }).from(projects) + + return { success: true, data: projectList } + } catch (error) { + console.error('프로젝트 조회 오류:', error) + return { success: false, error: '프로젝트를 조회할 수 없습니다.' } + } +} + +// 벤더 목록 조회 +export async function getVendors() { + try { + const vendorList = await db.select({ + id: vendors.id, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + status: vendors.status, + }).from(vendors) + + return { success: true, data: vendorList } + } catch (error) { + console.error('벤더 조회 오류:', error) + return { success: false, error: '벤더를 조회할 수 없습니다.' } + } +} + +// 아이템 목록 조회 (모든 필드 포함) +export async function getItems() { + try { + const itemList = await db.select({ + id: items.id, + ProjectNo: items.ProjectNo, + itemCode: items.itemCode, + itemName: items.itemName, + packageCode: items.packageCode, + smCode: items.smCode, + description: items.description, + parentItemCode: items.parentItemCode, + itemLevel: items.itemLevel, + deleteFlag: items.deleteFlag, + unitOfMeasure: items.unitOfMeasure, + steelType: items.steelType, + gradeMaterial: items.gradeMaterial, + changeDate: items.changeDate, + baseUnitOfMeasure: items.baseUnitOfMeasure, + }).from(items) + + return { success: true, data: itemList } + } catch (error) { + console.error('아이템 조회 오류:', error) + return { success: false, error: '아이템을 조회할 수 없습니다.' } + } +} + +// 기존 계약 목록 조회 (프로젝트 코드, 벤더명 포함) +export async function getContracts() { + try { + const contractList = await db.select({ + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + projectId: contracts.projectId, + vendorId: contracts.vendorId, + projectCode: projects.code, + projectName: projects.name, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + + return { success: true, data: contractList } + } catch (error) { + console.error('계약 조회 오류:', error) + return { success: false, error: '계약을 조회할 수 없습니다.' } + } +} + +// 특정 계약의 상세 정보 조회 +export async function getContractById(contractId: number) { + try { + const [contract] = await db.select({ + id: contracts.id, + contractNo: contracts.contractNo, + contractName: contracts.contractName, + status: contracts.status, + projectId: contracts.projectId, + vendorId: contracts.vendorId, + projectCode: projects.code, + projectName: projects.name, + vendorName: vendors.vendorName, + vendorCode: vendors.vendorCode, + }) + .from(contracts) + .leftJoin(projects, eq(contracts.projectId, projects.id)) + .leftJoin(vendors, eq(contracts.vendorId, vendors.id)) + .where(eq(contracts.id, contractId)) + + if (!contract) { + return { success: false, error: '계약을 찾을 수 없습니다.' } + } + + return { success: true, data: contract } + } catch (error) { + console.error('계약 상세 조회 오류:', error) + return { success: false, error: '계약 상세 정보를 조회할 수 없습니다.' } + } +} + +// 특정 계약의 아이템들 조회 +export async function getContractItems(contractId: number) { + try { + const contractItemList = await db.select({ + id: contractItems.id, + contractId: contractItems.contractId, + itemId: contractItems.itemId, + description: contractItems.description, + quantity: contractItems.quantity, + unitPrice: contractItems.unitPrice, + // 아이템 정보 + ProjectNo: items.ProjectNo, + itemCode: items.itemCode, + itemName: items.itemName, + packageCode: items.packageCode, + unitOfMeasure: items.unitOfMeasure, + }) + .from(contractItems) + .leftJoin(items, eq(contractItems.itemId, items.id)) + .where(eq(contractItems.contractId, contractId)) + + return { success: true, data: contractItemList } + } catch (error) { + console.error('계약 아이템 조회 오류:', error) + return { success: false, error: '계약 아이템을 조회할 수 없습니다.' } + } +} diff --git a/app/[lng]/admin/edp/components/contract-edit-form.tsx b/app/[lng]/admin/edp/components/contract-edit-form.tsx new file mode 100644 index 00000000..5a9ec03e --- /dev/null +++ b/app/[lng]/admin/edp/components/contract-edit-form.tsx @@ -0,0 +1,231 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { toast } from 'sonner' +import { updateContract } from '../actions/contract-actions' +import { getContractById } from '../actions/data-actions' +import { ProjectSelector } from './project-selector' +import { VendorSelector } from './vendor-selector' +import { ContractSelector } from './contract-selector' + +interface Project { + id: number + code: string + name: string + type: string +} + +interface Vendor { + id: number + vendorName: string + vendorCode: string | null + status: string +} + +interface Contract { + id: number + contractNo: string + contractName: string + status: string + projectId: number + vendorId: number + projectCode: string | null + projectName: string | null + vendorName: string | null + vendorCode: string | null +} + +interface ContractEditFormProps { + onContractUpdated?: (contract: { id: number; contractNo: string; contractName: string; status: string }) => void +} + +export function ContractEditForm({ onContractUpdated }: ContractEditFormProps) { + const [loading, setLoading] = useState(false) + const [selectedContract, setSelectedContract] = useState() + const [selectedProject, setSelectedProject] = useState() + const [selectedVendor, setSelectedVendor] = useState() + const [formData, setFormData] = useState({ + contractName: '', + status: 'TEST' + }) + + // 계약 선택 시 데이터 로드 + useEffect(() => { + if (selectedContract) { + loadContractData(selectedContract.id) + } + }, [selectedContract]) + + const loadContractData = async (contractId: number) => { + try { + const result = await getContractById(contractId) + if (result.success) { + const contract = result.data + setFormData({ + contractName: contract.contractName, + status: contract.status + }) + + // 프로젝트 정보 설정 + if (contract.projectId && contract.projectCode) { + setSelectedProject({ + id: contract.projectId, + code: contract.projectCode, + name: contract.projectName || '', + type: '' + }) + } + + // 벤더 정보 설정 + if (contract.vendorId && contract.vendorName) { + setSelectedVendor({ + id: contract.vendorId, + vendorName: contract.vendorName, + vendorCode: contract.vendorCode, + status: 'ACTIVE' + }) + } + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('계약 정보를 불러오는 중 오류가 발생했습니다.') + } + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!selectedContract) { + toast.error('수정할 계약을 선택해주세요.') + return + } + + if (!selectedProject || !selectedVendor || !formData.contractName.trim()) { + toast.error('모든 필수 항목을 입력해주세요.') + return + } + + setLoading(true) + try { + const result = await updateContract(selectedContract.id, { + projectId: selectedProject.id, + vendorId: selectedVendor.id, + contractName: formData.contractName, + status: formData.status + }) + + if (result.success) { + toast.success(result.message) + onContractUpdated?.(result.data) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('계약 수정 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + const handleContractSelect = (contract: Contract) => { + setSelectedContract(contract) + // 폼 초기화 + setSelectedProject(undefined) + setSelectedVendor(undefined) + setFormData({ + contractName: '', + status: 'TEST' + }) + } + + return ( + + + 계약 수정 + + 기존 계약의 정보를 수정합니다. + + + +
+
+ + +
+ + {selectedContract && ( + <> +
+
선택된 계약
+
+ [{selectedContract.contractNo}] {selectedContract.contractName} +
+
+ +
+ + +
+ +
+ + +
+ +
+ + setFormData(prev => ({ ...prev, contractName: e.target.value }))} + placeholder="계약명을 입력하세요" + /> +
+ +
+ + +
+ + + + )} +
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/components/contract-form.tsx b/app/[lng]/admin/edp/components/contract-form.tsx new file mode 100644 index 00000000..45c2f629 --- /dev/null +++ b/app/[lng]/admin/edp/components/contract-form.tsx @@ -0,0 +1,145 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { toast } from 'sonner' +import { createContract, CreateContractData } from '../actions/contract-actions' +import { ProjectSelector } from './project-selector' +import { VendorSelector } from './vendor-selector' + +interface Project { + id: number + code: string + name: string + type: string +} + +interface Vendor { + id: number + vendorName: string + vendorCode: string | null + status: string +} + +interface ContractFormProps { + onContractCreated?: (contract: { id: number; contractNo: string; contractName: string; status: string }) => void +} + +export function ContractForm({ onContractCreated }: ContractFormProps) { + const [loading, setLoading] = useState(false) + const [selectedProject, setSelectedProject] = useState() + const [selectedVendor, setSelectedVendor] = useState() + const [formData, setFormData] = useState({ + contractName: '', + status: 'TEST' + }) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!selectedProject || !selectedVendor || !formData.contractName.trim()) { + toast.error('모든 필수 항목을 입력해주세요.') + return + } + + setLoading(true) + try { + const contractData: CreateContractData = { + projectId: selectedProject.id, + vendorId: selectedVendor.id, + contractName: formData.contractName, + status: formData.status + } + + const result = await createContract(contractData) + + if (result.success) { + toast.success(result.message) + // 폼 초기화 + setSelectedProject(undefined) + setSelectedVendor(undefined) + setFormData({ + contractName: '', + status: 'TEST' + }) + // 부모 컴포넌트에 생성된 계약 정보 전달 + onContractCreated?.(result.data) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('계약 생성 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + return ( + + + 계약 생성 + + 새로운 테스트 계약을 생성합니다. + + + +
+
+ + +
+ +
+ + +
+ +
+ + setFormData(prev => ({ ...prev, contractName: e.target.value }))} + placeholder="계약명을 입력하세요" + /> +
+ +
+ + +
+ + +
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/components/contract-items-edit-form.tsx b/app/[lng]/admin/edp/components/contract-items-edit-form.tsx new file mode 100644 index 00000000..18fe75de --- /dev/null +++ b/app/[lng]/admin/edp/components/contract-items-edit-form.tsx @@ -0,0 +1,204 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' + +import { Label } from '@/components/ui/label' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { toast } from 'sonner' +import { deleteContractItem } from '../actions/contract-actions' +import { getContractItems } from '../actions/data-actions' +import { ContractSelector } from './contract-selector' +import { Trash2 } from 'lucide-react' + +interface Contract { + id: number + contractNo: string + contractName: string + status: string + projectId: number + vendorId: number + projectCode: string | null + projectName: string | null + vendorName: string | null + vendorCode: string | null +} + +interface ContractItem { + id: number + contractId: number + itemId: number + description: string | null + quantity: number + unitPrice: number | null + ProjectNo: string | null + itemCode: string | null + itemName: string | null + packageCode: string | null + unitOfMeasure: string | null +} + +interface ContractItemsEditFormProps { + preselectedContractId?: number +} + +export function ContractItemsEditForm({ preselectedContractId }: ContractItemsEditFormProps) { + const [loading, setLoading] = useState(false) + const [selectedContract, setSelectedContract] = useState() + const [contractItems, setContractItems] = useState([]) + + // 계약 선택 시 아이템들 로드 + useEffect(() => { + if (selectedContract) { + loadContractItems(selectedContract.id) + } + }, [selectedContract]) + + const loadContractItems = async (contractId: number) => { + setLoading(true) + try { + const result = await getContractItems(contractId) + if (result.success) { + setContractItems(result.data) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('계약 아이템을 불러오는 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + + + const handleDelete = async (itemId: number) => { + if (!confirm('정말로 이 계약 아이템을 삭제하시겠습니까?')) { + return + } + + setLoading(true) + try { + const result = await deleteContractItem(itemId) + + if (result.success) { + toast.success(result.message) + // 아이템 목록 새로고침 + if (selectedContract) { + await loadContractItems(selectedContract.id) + } + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('계약 아이템 삭제 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + return ( + + + 계약 아이템 삭제 + + 기존 계약의 아이템들을 삭제할 수 있습니다. + + + +
+ {/* 계약 선택 */} +
+ + +
+ + {selectedContract && ( +
+
선택된 계약
+
+ [{selectedContract.contractNo}] {selectedContract.contractName} +
+
+ )} + + {/* 계약 아이템 목록 */} + {contractItems.length > 0 && ( +
+ +
+ {contractItems.map(item => ( +
+
+
+
+ {item.itemName || `아이템 ${item.itemId}`} + {item.itemCode && ( + + {item.itemCode} + + )} + {item.unitOfMeasure && ( + + {item.unitOfMeasure} + + )} +
+ + {item.ProjectNo && ( +
+ 프로젝트: {item.ProjectNo} | 패키지: {item.packageCode} +
+ )} + + {/* 보기 모드만 유지 */} +
+
수량: {item.quantity} | 단가: {item.unitPrice || 0}
+ {item.description && ( +
설명: {item.description}
+ )} +
+
+ + {/* 삭제 버튼만 유지 */} +
+ +
+
+
+ ))} +
+
+ )} + + {selectedContract && contractItems.length === 0 && !loading && ( +
+ 선택된 계약에 아이템이 없습니다. +
+ )} + + {loading && ( +
+ 로딩 중... +
+ )} +
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/components/contract-items-form.tsx b/app/[lng]/admin/edp/components/contract-items-form.tsx new file mode 100644 index 00000000..ef8f09ef --- /dev/null +++ b/app/[lng]/admin/edp/components/contract-items-form.tsx @@ -0,0 +1,230 @@ +'use client' + +import { useState } from 'react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' + +import { toast } from 'sonner' +import { createMultipleContractItems } from '../actions/contract-actions' +import { ContractSelector } from './contract-selector' +import { ItemSelector } from './item-selector' +import { Item } from '../types/item' +import { X } from 'lucide-react' + +interface Contract { + id: number + contractNo: string + contractName: string + status: string + projectId: number + vendorId: number + projectCode: string | null + projectName: string | null + vendorName: string | null + vendorCode: string | null +} + + + +interface SelectedItem { + itemId: number + ProjectNo: string + itemName: string + packageCode: string + itemCode: string | null + description?: string + quantity?: number + unitPrice?: number +} + +interface ContractItemsFormProps { + preselectedContractId?: number +} + +export function ContractItemsForm({ preselectedContractId }: ContractItemsFormProps) { + const [loading, setLoading] = useState(false) + const [selectedContract, setSelectedContract] = useState() + const [selectedItems, setSelectedItems] = useState([]) + const [selectedItemIds, setSelectedItemIds] = useState([]) + + // 아이템 선택 처리 + const handleItemsSelect = (itemIds: number[], itemsData?: Item[]) => { + setSelectedItemIds(itemIds) + + if (itemsData) { + // 새로운 아이템 데이터로 선택된 아이템 목록 업데이트 + const newSelectedItems: SelectedItem[] = itemsData.map(item => { + // 기존 선택된 아이템에서 수량, 단가, 설명 정보 유지 + const existing = selectedItems.find(selected => selected.itemId === item.id) + return { + itemId: item.id, + ProjectNo: item.ProjectNo, + itemName: item.itemName, + packageCode: item.packageCode, + itemCode: item.itemCode, + description: existing?.description || item.description || '', + quantity: existing?.quantity || 1, + unitPrice: existing?.unitPrice || 0 + } + }) + setSelectedItems(newSelectedItems) + } else { + // 기존 선택된 아이템들 중에서 새로 선택되지 않은 것들은 제거 + setSelectedItems(prev => prev.filter(item => itemIds.includes(item.itemId))) + } + } + + // 선택된 아이템 정보 반환 (이제 실제 데이터가 있음) + const getSelectedItemsWithDetails = (): SelectedItem[] => { + return selectedItems + } + + // 선택된 아이템 정보 업데이트 + const updateSelectedItem = (itemId: number, field: keyof SelectedItem, value: string | number) => { + setSelectedItems(prev => prev.map(item => + item.itemId === itemId ? { ...item, [field]: value } : item + )) + } + + // 선택된 아이템 제거 + const removeSelectedItem = (itemId: number) => { + setSelectedItemIds(prev => prev.filter(id => id !== itemId)) + setSelectedItems(prev => prev.filter(item => item.itemId !== itemId)) + } + + // 폼 제출 + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (!selectedContract) { + toast.error('계약을 선택해주세요.') + return + } + + if (selectedItemIds.length === 0) { + toast.error('최소 하나의 아이템을 선택해주세요.') + return + } + + setLoading(true) + try { + const currentSelectedItems = getSelectedItemsWithDetails() + const itemsData = currentSelectedItems.map(item => ({ + itemId: item.itemId, + description: item.description, + quantity: item.quantity || 1, + unitPrice: item.unitPrice || 0 + })) + + const result = await createMultipleContractItems(selectedContract.id, itemsData) + + if (result.success) { + toast.success(result.message) + setSelectedItems([]) + setSelectedItemIds([]) + } else { + toast.error(result.error) + } + } catch { + toast.error('계약 아이템 생성 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + return ( + + + 계약 아이템 생성 + + 계약에 아이템들을 추가합니다. + + + +
+ {/* 계약 선택 */} +
+ + +
+ + {/* 선택된 아이템 목록 */} + {selectedItemIds.length > 0 && ( +
+ +
+ {getSelectedItemsWithDetails().map(item => ( +
+
+
+ {item.itemName} {item.itemCode && `(${item.itemCode})`} +
+
+ 프로젝트: {item.ProjectNo} | 패키지: {item.packageCode} +
+
+ updateSelectedItem(item.itemId, 'quantity', parseInt(e.target.value) || 1)} + className="w-20 h-8 text-xs" + /> + updateSelectedItem(item.itemId, 'unitPrice', parseFloat(e.target.value) || 0)} + className="w-24 h-8 text-xs" + /> + updateSelectedItem(item.itemId, 'description', e.target.value)} + className="flex-1 h-8 text-xs" + /> +
+
+ +
+ ))} +
+
+ )} + + {/* 아이템 선택 */} +
+ +
+ +
+
+ + +
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/components/contract-selector.tsx b/app/[lng]/admin/edp/components/contract-selector.tsx new file mode 100644 index 00000000..88986a88 --- /dev/null +++ b/app/[lng]/admin/edp/components/contract-selector.tsx @@ -0,0 +1,334 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Search, Check } from 'lucide-react' +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, + SortingState, + ColumnFiltersState, + VisibilityState, + RowSelectionState, +} from '@tanstack/react-table' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { getContracts } from '../actions/data-actions' +import { toast } from 'sonner' + +interface Contract { + id: number + contractNo: string + contractName: string + status: string + projectId: number + vendorId: number + projectCode: string | null + projectName: string | null + vendorName: string | null + vendorCode: string | null +} + +interface ContractSelectorProps { + selectedContract?: Contract + onContractSelect: (contract: Contract) => void + disabled?: boolean + preselectedContractId?: number +} + +export function ContractSelector({ selectedContract, onContractSelect, disabled, preselectedContractId }: ContractSelectorProps) { + const [open, setOpen] = useState(false) + const [contracts, setContracts] = useState([]) + const [loading, setLoading] = useState(false) + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + const [columnVisibility, setColumnVisibility] = useState({}) + const [rowSelection, setRowSelection] = useState({}) + const [globalFilter, setGlobalFilter] = useState('') + + const columns: ColumnDef[] = [ + { + accessorKey: 'contractNo', + header: '계약번호', + cell: ({ row }) => ( +
{row.getValue('contractNo')}
+ ), + }, + { + accessorKey: 'contractName', + header: '계약명', + cell: ({ row }) => ( +
{row.getValue('contractName')}
+ ), + }, + { + accessorKey: 'status', + header: '상태', + cell: ({ row }) => { + const status = row.getValue('status') as string + const getStatusColor = (status: string) => { + switch (status) { + case 'ACTIVE': return 'bg-green-100 text-green-800' + case 'TEST': return 'bg-blue-100 text-blue-800' + case 'DRAFT': return 'bg-yellow-100 text-yellow-800' + case 'PENDING': return 'bg-orange-100 text-orange-800' + default: return 'bg-gray-100 text-gray-800' + } + } + return ( + + {status} + + ) + }, + }, + { + accessorKey: 'projectCode', + header: '프로젝트', + cell: ({ row }) => { + const projectCode = row.getValue('projectCode') as string | null + const projectName = row.original.projectName + return projectCode ? ( +
+
{projectCode}
+ {projectName && ( +
+ {projectName} +
+ )} +
+ ) : ( + - + ) + }, + }, + { + accessorKey: 'vendorName', + header: '벤더', + cell: ({ row }) => { + const vendorName = row.getValue('vendorName') as string | null + const vendorCode = row.original.vendorCode + return vendorName ? ( +
+
{vendorName}
+ {vendorCode && ( +
+ {vendorCode} +
+ )} +
+ ) : ( + - + ) + }, + }, + { + id: 'actions', + header: '선택', + cell: ({ row }) => ( + + ), + }, + ] + + const table = useReactTable({ + data: contracts, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + globalFilter, + }, + }) + + const loadContracts = async () => { + setLoading(true) + try { + const result = await getContracts() + if (result.success) { + setContracts(result.data) + + // preselectedContractId가 있으면 자동 선택 + if (preselectedContractId && !selectedContract) { + const preselectedContract = result.data.find(c => c.id === preselectedContractId) + if (preselectedContract) { + onContractSelect(preselectedContract) + } + } + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('계약을 불러오는 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + const handleContractSelect = (contract: Contract) => { + onContractSelect(contract) + setOpen(false) + } + + useEffect(() => { + if (open && contracts.length === 0) { + loadContracts() + } + }, [open]) + + // preselectedContractId가 변경되면 계약 목록 다시 로드 + useEffect(() => { + if (preselectedContractId && contracts.length === 0) { + loadContracts() + } + }, [preselectedContractId]) + + return ( + + + + + + + 계약 선택 + + +
+
+ + setGlobalFilter(e.target.value)} + className="flex-1" + /> +
+ + {loading ? ( +
+
계약을 불러오는 중...
+
+ ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + handleContractSelect(row.original)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 검색 결과가 없습니다. + + + )} + +
+
+ )} + +
+
+ 총 {table.getFilteredRowModel().rows.length}개 계약 +
+
+ +
+ {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} +
+ +
+
+
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/components/item-selector.tsx b/app/[lng]/admin/edp/components/item-selector.tsx new file mode 100644 index 00000000..a81d2ff6 --- /dev/null +++ b/app/[lng]/admin/edp/components/item-selector.tsx @@ -0,0 +1,368 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Search, Check } from 'lucide-react' +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, + SortingState, + ColumnFiltersState, + VisibilityState, + RowSelectionState, +} from '@tanstack/react-table' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { Checkbox } from '@/components/ui/checkbox' +import { getItems } from '../actions/data-actions' +import { toast } from 'sonner' +import { Item } from '../types/item' + +interface ItemSelectorProps { + selectedItems: number[] + onItemsSelect: (itemIds: number[], itemsData?: Item[]) => void + disabled?: boolean +} + +export function ItemSelector({ selectedItems, onItemsSelect, disabled }: ItemSelectorProps) { + const [open, setOpen] = useState(false) + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(false) + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + const [columnVisibility, setColumnVisibility] = useState({}) + const [rowSelection, setRowSelection] = useState({}) + const [globalFilter, setGlobalFilter] = useState('') + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + // 현업 관심 필드들 (우선 순위) + { + accessorKey: 'ProjectNo', + header: '프로젝트 번호', + cell: ({ row }) => ( +
{row.getValue('ProjectNo')}
+ ), + }, + { + accessorKey: 'itemName', + header: '아이템명', + cell: ({ row }) => ( +
{row.getValue('itemName')}
+ ), + }, + { + accessorKey: 'packageCode', + header: '패키지 코드', + cell: ({ row }) => ( +
{row.getValue('packageCode')}
+ ), + }, + // 추가 필드들 + { + accessorKey: 'itemCode', + header: '아이템 코드', + cell: ({ row }) => { + const code = row.getValue('itemCode') as string | null + return code ? ( +
{code}
+ ) : ( + - + ) + }, + }, + { + accessorKey: 'description', + header: '설명', + cell: ({ row }) => { + const description = row.getValue('description') as string | null + return description ? ( +
{description}
+ ) : ( + - + ) + }, + }, + { + accessorKey: 'unitOfMeasure', + header: '단위', + cell: ({ row }) => { + const unit = row.getValue('unitOfMeasure') as string | null + return unit ? ( + {unit} + ) : ( + - + ) + }, + }, + { + accessorKey: 'smCode', + header: 'SM 코드', + cell: ({ row }) => { + const code = row.getValue('smCode') as string | null + return code ? ( +
{code}
+ ) : ( + - + ) + }, + }, + { + accessorKey: 'steelType', + header: '강종', + cell: ({ row }) => { + const steelType = row.getValue('steelType') as string | null + return steelType ? ( + {steelType} + ) : ( + - + ) + }, + }, + { + accessorKey: 'itemLevel', + header: '레벨', + cell: ({ row }) => { + const level = row.getValue('itemLevel') as number | null + return level !== null ? ( +
{level}
+ ) : ( + - + ) + }, + }, + ] + + const table = useReactTable({ + data: items, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + globalFilter, + }, + initialState: { + pagination: { + pageSize: 10, + }, + }, + }) + + const loadItems = async () => { + setLoading(true) + try { + const result = await getItems() + if (result.success && result.data) { + setItems(result.data) + } else { + toast.error(result.error) + } + } catch { + toast.error('아이템을 불러오는 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + const handleConfirmSelection = () => { + const selectedRows = table.getFilteredSelectedRowModel().rows + const selectedItemIds = selectedRows.map(row => row.original.id) + const selectedItemsData = selectedRows.map(row => row.original) + onItemsSelect(selectedItemIds, selectedItemsData) + setOpen(false) + } + + // 선택된 아이템 수 계산 + const selectedCount = Object.keys(rowSelection).length + + useEffect(() => { + if (open && items.length === 0) { + loadItems() + } + }, [open]) + + // 기존 선택 상태를 rowSelection에 반영 + useEffect(() => { + if (items.length > 0 && selectedItems.length > 0) { + const newRowSelection: RowSelectionState = {} + items.forEach((item, index) => { + if (selectedItems.includes(item.id)) { + newRowSelection[index] = true + } + }) + setRowSelection(newRowSelection) + } + }, [items, selectedItems]) + + return ( + + + + + + + 아이템 선택 + + +
+
+
+ + setGlobalFilter(e.target.value)} + className="flex-1" + /> +
+
+ {selectedCount}개 선택됨 +
+
+ + {loading ? ( +
+
아이템을 불러오는 중...
+
+ ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + row.toggleSelected()} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 검색 결과가 없습니다. + + + )} + +
+
+ )} + +
+
+ 총 {table.getFilteredRowModel().rows.length}개 아이템 +
+
+ +
+ {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} +
+ +
+
+ +
+ + +
+
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/components/project-selector.tsx b/app/[lng]/admin/edp/components/project-selector.tsx new file mode 100644 index 00000000..68895cd1 --- /dev/null +++ b/app/[lng]/admin/edp/components/project-selector.tsx @@ -0,0 +1,258 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Search, Check } from 'lucide-react' +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, + SortingState, + ColumnFiltersState, + VisibilityState, + RowSelectionState, +} from '@tanstack/react-table' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { getProjects } from '../actions/data-actions' +import { toast } from 'sonner' + +interface Project { + id: number + code: string + name: string + type: string +} + +interface ProjectSelectorProps { + selectedProject?: Project + onProjectSelect: (project: Project) => void + disabled?: boolean +} + +export function ProjectSelector({ selectedProject, onProjectSelect, disabled }: ProjectSelectorProps) { + const [open, setOpen] = useState(false) + const [projects, setProjects] = useState([]) + const [loading, setLoading] = useState(false) + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + const [columnVisibility, setColumnVisibility] = useState({}) + const [rowSelection, setRowSelection] = useState({}) + const [globalFilter, setGlobalFilter] = useState('') + + const columns: ColumnDef[] = [ + { + accessorKey: 'code', + header: '프로젝트 코드', + cell: ({ row }) => ( +
{row.getValue('code')}
+ ), + }, + { + accessorKey: 'name', + header: '프로젝트명', + cell: ({ row }) => ( +
{row.getValue('name')}
+ ), + }, + { + accessorKey: 'type', + header: '타입', + cell: ({ row }) => ( + {row.getValue('type')} + ), + }, + { + id: 'actions', + header: '선택', + cell: ({ row }) => ( + + ), + }, + ] + + const table = useReactTable({ + data: projects, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + globalFilter, + }, + }) + + const loadProjects = async () => { + setLoading(true) + try { + const result = await getProjects() + if (result.success) { + setProjects(result.data) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('프로젝트를 불러오는 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + const handleProjectSelect = (project: Project) => { + onProjectSelect(project) + setOpen(false) + } + + useEffect(() => { + if (open && projects.length === 0) { + loadProjects() + } + }, [open]) + + return ( + + + + + + + 프로젝트 선택 + + +
+
+ + setGlobalFilter(e.target.value)} + className="flex-1" + /> +
+ + {loading ? ( +
+
프로젝트를 불러오는 중...
+
+ ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + handleProjectSelect(row.original)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 검색 결과가 없습니다. + + + )} + +
+
+ )} + +
+
+ 총 {table.getFilteredRowModel().rows.length}개 프로젝트 +
+
+ +
+ {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} +
+ +
+
+
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/components/vendor-selector.tsx b/app/[lng]/admin/edp/components/vendor-selector.tsx new file mode 100644 index 00000000..22dfad65 --- /dev/null +++ b/app/[lng]/admin/edp/components/vendor-selector.tsx @@ -0,0 +1,281 @@ +'use client' + +import { useState, useEffect } from 'react' +import { Button } from '@/components/ui/button' +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Badge } from '@/components/ui/badge' +import { Search, Check } from 'lucide-react' +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, + SortingState, + ColumnFiltersState, + VisibilityState, + RowSelectionState, +} from '@tanstack/react-table' +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table' +import { getVendors } from '../actions/data-actions' +import { toast } from 'sonner' + +interface Vendor { + id: number + vendorName: string + vendorCode: string | null + status: string +} + +interface VendorSelectorProps { + selectedVendor?: Vendor + onVendorSelect: (vendor: Vendor) => void + disabled?: boolean +} + +export function VendorSelector({ selectedVendor, onVendorSelect, disabled }: VendorSelectorProps) { + const [open, setOpen] = useState(false) + const [vendors, setVendors] = useState([]) + const [loading, setLoading] = useState(false) + const [sorting, setSorting] = useState([]) + const [columnFilters, setColumnFilters] = useState([]) + const [columnVisibility, setColumnVisibility] = useState({}) + const [rowSelection, setRowSelection] = useState({}) + const [globalFilter, setGlobalFilter] = useState('') + + const columns: ColumnDef[] = [ + { + accessorKey: 'vendorName', + header: '벤더명', + cell: ({ row }) => ( +
{row.getValue('vendorName')}
+ ), + }, + { + accessorKey: 'vendorCode', + header: '벤더 코드', + cell: ({ row }) => { + const code = row.getValue('vendorCode') as string | null + return code ? ( +
{code}
+ ) : ( + - + ) + }, + }, + { + accessorKey: 'status', + header: '상태', + cell: ({ row }) => { + const status = row.getValue('status') as string + const getStatusColor = (status: string) => { + switch (status) { + case 'ACTIVE': return 'bg-green-100 text-green-800' + case 'APPROVED': return 'bg-blue-100 text-blue-800' + case 'PENDING_REVIEW': return 'bg-yellow-100 text-yellow-800' + case 'INACTIVE': return 'bg-gray-100 text-gray-800' + default: return 'bg-gray-100 text-gray-800' + } + } + return ( + + {status} + + ) + }, + }, + { + id: 'actions', + header: '선택', + cell: ({ row }) => ( + + ), + }, + ] + + const table = useReactTable({ + data: vendors, + columns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onGlobalFilterChange: setGlobalFilter, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + globalFilter, + }, + }) + + const loadVendors = async () => { + setLoading(true) + try { + const result = await getVendors() + if (result.success) { + setVendors(result.data) + } else { + toast.error(result.error) + } + } catch (error) { + toast.error('벤더를 불러오는 중 오류가 발생했습니다.') + } finally { + setLoading(false) + } + } + + const handleVendorSelect = (vendor: Vendor) => { + onVendorSelect(vendor) + setOpen(false) + } + + useEffect(() => { + if (open && vendors.length === 0) { + loadVendors() + } + }, [open]) + + return ( + + + + + + + 벤더 선택 + + +
+
+ + setGlobalFilter(e.target.value)} + className="flex-1" + /> +
+ + {loading ? ( +
+
벤더를 불러오는 중...
+
+ ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + handleVendorSelect(row.original)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + 검색 결과가 없습니다. + + + )} + +
+
+ )} + +
+
+ 총 {table.getFilteredRowModel().rows.length}개 벤더 +
+
+ +
+ {table.getState().pagination.pageIndex + 1} / {table.getPageCount()} +
+ +
+
+
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/page.tsx b/app/[lng]/admin/edp/page.tsx new file mode 100644 index 00000000..4b72196f --- /dev/null +++ b/app/[lng]/admin/edp/page.tsx @@ -0,0 +1,69 @@ +'use client' + +import { useState } from 'react' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { ContractForm } from './components/contract-form' +import { ContractItemsForm } from './components/contract-items-form' +import { ContractEditForm } from './components/contract-edit-form' +import { ContractItemsEditForm } from './components/contract-items-edit-form' + +export default function EDPPage() { + const [activeTab, setActiveTab] = useState('contracts') + const [selectedContractId, setSelectedContractId] = useState() + + // 계약 생성 완료시 계약 아이템 탭으로 이동하고 해당 계약을 사전 선택 + const handleContractCreated = (contract: { id: number; contractNo: string; contractName: string; status: string }) => { + setSelectedContractId(contract.id) + setActiveTab('contract-items') + } + + // 계약 수정 완료시 계약 아이템 삭제 탭으로 이동하고 해당 계약을 사전 선택 + const handleContractUpdated = (contract: { id: number; contractNo: string; contractName: string; status: string }) => { + setSelectedContractId(contract.id) + setActiveTab('contract-items-edit') + } + + return ( +
+
+

EDP 테스트 데이터 관리

+

+ 현업 테스트를 위한 계약 및 계약 아이템 데이터를 수동으로 생성할 수 있습니다. +

+
+ + + + 계약 생성 + 계약 아이템 생성 + 계약 수정 + 계약 아이템 삭제 + + + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+ + +
+ +
+
+
+
+ ) +} diff --git a/app/[lng]/admin/edp/types/item.ts b/app/[lng]/admin/edp/types/item.ts new file mode 100644 index 00000000..332e3878 --- /dev/null +++ b/app/[lng]/admin/edp/types/item.ts @@ -0,0 +1,18 @@ +// 공통 Item 타입 정의 +export interface Item { + id: number + ProjectNo: string + itemCode: string | null + itemName: string + packageCode: string + smCode: string | null + description: string | null + parentItemCode: string | null + itemLevel: number | null + deleteFlag: string | null + unitOfMeasure: string | null + steelType: string | null + gradeMaterial: string | null + changeDate: string | null + baseUnitOfMeasure: string | null +} diff --git a/app/[lng]/admin/layout.tsx b/app/[lng]/admin/layout.tsx new file mode 100644 index 00000000..918a8554 --- /dev/null +++ b/app/[lng]/admin/layout.tsx @@ -0,0 +1,34 @@ +import { getServerSession } from "next-auth" +import { authOptions } from "@/app/api/auth/[...nextauth]/route" +import { redirect } from "next/navigation" + +export default async function AdminLayout({ + children, +}: { + children: React.ReactNode +}) { + const session = await getServerSession(authOptions) + + // 세션이 없거나 domain이 'evcp'가 아닌 경우 접근 거부 + if (!session?.user || session.user.domain !== 'evcp') { + redirect('/en/evcp') + } + + return ( +
+
+
+
+

임시 관리자 페이지

+
+ {session.user.name} ({session.user.email}) +
+
+
+
+
+ {children} +
+
+ ) +} -- cgit v1.2.3