summaryrefslogtreecommitdiff
path: root/lib/general-contracts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/general-contracts')
-rw-r--r--lib/general-contracts/detail/general-contract-detail.tsx1
-rw-r--r--lib/general-contracts/detail/general-contract-items-table.tsx139
-rw-r--r--lib/general-contracts/service.ts88
3 files changed, 151 insertions, 77 deletions
diff --git a/lib/general-contracts/detail/general-contract-detail.tsx b/lib/general-contracts/detail/general-contract-detail.tsx
index 9d9f35bd..8e7a7aff 100644
--- a/lib/general-contracts/detail/general-contract-detail.tsx
+++ b/lib/general-contracts/detail/general-contract-detail.tsx
@@ -149,7 +149,6 @@ export default function ContractDetailPage() {
items={[]}
onItemsChange={() => {}}
onTotalAmountChange={() => {}}
- currency="USD"
availableBudget={0}
readOnly={contract?.contractScope === '단가' || contract?.contractScope === '물량(실적)'}
/>
diff --git a/lib/general-contracts/detail/general-contract-items-table.tsx b/lib/general-contracts/detail/general-contract-items-table.tsx
index 5176c6ce..1b9a1a06 100644
--- a/lib/general-contracts/detail/general-contract-items-table.tsx
+++ b/lib/general-contracts/detail/general-contract-items-table.tsx
@@ -7,6 +7,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import {
Table,
TableBody,
@@ -26,12 +27,13 @@ import { Save, LoaderIcon } from 'lucide-react'
interface ContractItem {
id?: number
- project: string
itemCode: string
itemInfo: string
specification: string
quantity: number
quantityUnit: string
+ totalWeight: number
+ weightUnit: string
contractDeliveryDate: string
contractUnitPrice: number
contractAmount: number
@@ -45,22 +47,27 @@ interface ContractItemsTableProps {
items: ContractItem[]
onItemsChange: (items: ContractItem[]) => void
onTotalAmountChange: (total: number) => void
- currency?: string
availableBudget?: number
readOnly?: boolean
}
+// 통화 목록
+const CURRENCIES = ["USD", "EUR", "KRW", "JPY", "CNY"];
+
+// 수량 단위 목록
+const QUANTITY_UNITS = ["KG", "TON", "EA", "M", "M2", "M3", "L", "ML", "G", "SET", "PCS"];
+
+// 중량 단위 목록
+const WEIGHT_UNITS = ["KG", "TON", "G", "LB", "OZ"];
+
export function ContractItemsTable({
contractId,
items,
onItemsChange,
onTotalAmountChange,
- currency = 'USD',
availableBudget = 0,
readOnly = false
}: ContractItemsTableProps) {
- // 통화 코드가 null이거나 undefined일 때 기본값 설정
- const safeCurrency = currency || 'USD'
const [localItems, setLocalItems] = React.useState<ContractItem[]>(items)
const [isSaving, setIsSaving] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(false)
@@ -74,16 +81,17 @@ export function ContractItemsTable({
const fetchedItems = await getContractItems(contractId)
const formattedItems = fetchedItems.map(item => ({
id: item.id,
- project: item.project || '',
itemCode: item.itemCode || '',
itemInfo: item.itemInfo || '',
specification: item.specification || '',
quantity: Number(item.quantity) || 0,
- quantityUnit: item.quantityUnit || 'KG',
+ quantityUnit: item.quantityUnit || 'EA',
+ totalWeight: Number(item.totalWeight) || 0,
+ weightUnit: item.weightUnit || 'KG',
contractDeliveryDate: item.contractDeliveryDate || '',
contractUnitPrice: Number(item.contractUnitPrice) || 0,
contractAmount: Number(item.contractAmount) || 0,
- contractCurrency: item.contractCurrency || safeCurrency,
+ contractCurrency: item.contractCurrency || 'KRW',
isSelected: false
})) as ContractItem[]
setLocalItems(formattedItems as ContractItem[])
@@ -99,7 +107,7 @@ export function ContractItemsTable({
}
loadItems()
- }, [contractId, currency, onItemsChange])
+ }, [contractId, onItemsChange])
// 로컬 상태와 부모 상태 동기화 (초기 로드 후에는 부모 상태 우선)
React.useEffect(() => {
@@ -116,10 +124,8 @@ export function ContractItemsTable({
const errors: string[] = []
for (let index = 0; index < localItems.length; index++) {
const item = localItems[index]
- if (!item.project) errors.push(`${index + 1}번째 품목의 프로젝트`)
if (!item.itemCode) errors.push(`${index + 1}번째 품목의 품목코드`)
if (!item.itemInfo) errors.push(`${index + 1}번째 품목의 Item 정보`)
- if (!item.specification) errors.push(`${index + 1}번째 품목의 사양`)
if (!item.quantity || item.quantity <= 0) errors.push(`${index + 1}번째 품목의 수량`)
if (!item.contractUnitPrice || item.contractUnitPrice <= 0) errors.push(`${index + 1}번째 품목의 단가`)
if (!item.contractDeliveryDate) errors.push(`${index + 1}번째 품목의 납기일`)
@@ -170,16 +176,17 @@ export function ContractItemsTable({
// 행 추가
const addRow = () => {
const newItem: ContractItem = {
- project: '',
itemCode: '',
itemInfo: '',
specification: '',
quantity: 0,
- quantityUnit: 'KG',
+ quantityUnit: 'EA', // 기본 수량 단위
+ totalWeight: 0,
+ weightUnit: 'KG', // 기본 중량 단위
contractDeliveryDate: '',
contractUnitPrice: 0,
contractAmount: 0,
- contractCurrency: safeCurrency,
+ contractCurrency: 'KRW', // 기본 통화
isSelected: false
}
const updatedItems = [...localItems, newItem]
@@ -213,10 +220,10 @@ export function ContractItemsTable({
// 통화 포맷팅
- const formatCurrency = (amount: number) => {
+ const formatCurrency = (amount: number, currency: string = 'KRW') => {
return new Intl.NumberFormat('ko-KR', {
style: 'currency',
- currency: safeCurrency,
+ currency: currency,
}).format(amount)
}
@@ -270,7 +277,7 @@ export function ContractItemsTable({
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
- <span className="text-sm text-gray-600">총 금액: {totalAmount.toLocaleString()} {currency}</span>
+ <span className="text-sm text-gray-600">총 금액: {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}</span>
<span className="text-sm text-gray-600">총 수량: {totalQuantity.toLocaleString()}</span>
</div>
{!readOnly && (
@@ -316,19 +323,19 @@ export function ContractItemsTable({
<div className="space-y-1">
<Label className="text-sm font-medium">총 계약금액</Label>
<div className="text-lg font-bold text-primary">
- {formatCurrency(totalAmount)}
+ {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
</div>
</div>
<div className="space-y-1">
<Label className="text-sm font-medium">가용예산</Label>
<div className="text-lg font-bold">
- {formatCurrency(availableBudget)}
+ {formatCurrency(availableBudget, localItems[0]?.contractCurrency || 'KRW')}
</div>
</div>
<div className="space-y-1">
<Label className="text-sm font-medium">가용예산 比 (금액차)</Label>
<div className={`text-lg font-bold ${amountDifference >= 0 ? 'text-green-600' : 'text-red-600'}`}>
- {formatCurrency(amountDifference)}
+ {formatCurrency(amountDifference, localItems[0]?.contractCurrency || 'KRW')}
</div>
</div>
<div className="space-y-1">
@@ -357,12 +364,13 @@ export function ContractItemsTable({
/>
)}
</TableHead>
- <TableHead className="px-3 py-3 font-semibold">프로젝트</TableHead>
<TableHead className="px-3 py-3 font-semibold">품목코드 (PKG No.)</TableHead>
<TableHead className="px-3 py-3 font-semibold">Item 정보 (자재그룹 / 자재코드)</TableHead>
<TableHead className="px-3 py-3 font-semibold">규격</TableHead>
<TableHead className="px-3 py-3 font-semibold text-right">수량</TableHead>
<TableHead className="px-3 py-3 font-semibold">수량단위</TableHead>
+ <TableHead className="px-3 py-3 font-semibold text-right">총 중량</TableHead>
+ <TableHead className="px-3 py-3 font-semibold">중량단위</TableHead>
<TableHead className="px-3 py-3 font-semibold">계약납기일</TableHead>
<TableHead className="px-3 py-3 font-semibold text-right">계약단가</TableHead>
<TableHead className="px-3 py-3 font-semibold text-right">계약금액</TableHead>
@@ -385,19 +393,6 @@ export function ContractItemsTable({
</TableCell>
<TableCell className="px-3 py-3">
{readOnly ? (
- <span className="text-sm">{item.project || '-'}</span>
- ) : (
- <Input
- value={item.project}
- onChange={(e) => updateItem(index, 'project', e.target.value)}
- placeholder="프로젝트"
- className="h-8 text-sm"
- disabled={!isEnabled}
- />
- )}
- </TableCell>
- <TableCell className="px-3 py-3">
- {readOnly ? (
<span className="text-sm">{item.itemCode || '-'}</span>
) : (
<Input
@@ -453,17 +448,62 @@ export function ContractItemsTable({
{readOnly ? (
<span className="text-sm">{item.quantityUnit || '-'}</span>
) : (
- <Input
+ <Select
value={item.quantityUnit}
- onChange={(e) => updateItem(index, 'quantityUnit', e.target.value)}
- placeholder="단위"
- className="h-8 text-sm w-16"
+ onValueChange={(value) => updateItem(index, 'quantityUnit', value)}
+ disabled={!isEnabled}
+ >
+ <SelectTrigger className="h-8 text-sm w-20">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {QUANTITY_UNITS.map((unit) => (
+ <SelectItem key={unit} value={unit}>
+ {unit}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ )}
+ </TableCell>
+ <TableCell className="px-3 py-3">
+ {readOnly ? (
+ <span className="text-sm text-right">{item.totalWeight.toLocaleString()}</span>
+ ) : (
+ <Input
+ type="number"
+ value={item.totalWeight}
+ onChange={(e) => updateItem(index, 'totalWeight', parseFloat(e.target.value) || 0)}
+ className="h-8 text-sm text-right"
+ placeholder="0"
disabled={!isEnabled}
/>
)}
</TableCell>
<TableCell className="px-3 py-3">
{readOnly ? (
+ <span className="text-sm">{item.weightUnit || '-'}</span>
+ ) : (
+ <Select
+ value={item.weightUnit}
+ onValueChange={(value) => updateItem(index, 'weightUnit', value)}
+ disabled={!isEnabled}
+ >
+ <SelectTrigger className="h-8 text-sm w-20">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {WEIGHT_UNITS.map((unit) => (
+ <SelectItem key={unit} value={unit}>
+ {unit}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ )}
+ </TableCell>
+ <TableCell className="px-3 py-3">
+ {readOnly ? (
<span className="text-sm">{item.contractDeliveryDate || '-'}</span>
) : (
<Input
@@ -498,13 +538,22 @@ export function ContractItemsTable({
{readOnly ? (
<span className="text-sm">{item.contractCurrency || '-'}</span>
) : (
- <Input
+ <Select
value={item.contractCurrency}
- onChange={(e) => updateItem(index, 'contractCurrency', e.target.value)}
- placeholder="통화"
- className="h-8 text-sm w-16"
+ onValueChange={(value) => updateItem(index, 'contractCurrency', value)}
disabled={!isEnabled}
- />
+ >
+ <SelectTrigger className="h-8 text-sm w-20">
+ <SelectValue />
+ </SelectTrigger>
+ <SelectContent>
+ {CURRENCIES.map((currency) => (
+ <SelectItem key={currency} value={currency}>
+ {currency}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
)}
</TableCell>
</TableRow>
@@ -528,14 +577,14 @@ export function ContractItemsTable({
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-muted-foreground">총 단가</span>
<span className="text-lg font-semibold">
- {totalUnitPrice.toLocaleString()} {currency}
+ {formatCurrency(totalUnitPrice, localItems[0]?.contractCurrency || 'KRW')}
</span>
</div>
<div className="border-t pt-4">
<div className="flex items-center justify-between">
<span className="text-xl font-bold text-primary">합계 금액</span>
<span className="text-2xl font-bold text-primary">
- {formatCurrency(totalAmount)}
+ {formatCurrency(totalAmount, localItems[0]?.contractCurrency || 'KRW')}
</span>
</div>
</div>
diff --git a/lib/general-contracts/service.ts b/lib/general-contracts/service.ts
index 8c74c616..52301dae 100644
--- a/lib/general-contracts/service.ts
+++ b/lib/general-contracts/service.ts
@@ -372,7 +372,8 @@ export async function createContract(data: Record<string, unknown>) {
try {
// 계약번호 자동 생성
// TODO: 구매 발주담당자 코드 필요 - 파라미터 추가
- const userId = data.registeredById as string
+ const rawUserId = data.registeredById
+ const userId = (rawUserId && !isNaN(Number(rawUserId))) ? String(rawUserId) : undefined
const contractNumber = await generateContractNumber(
userId,
data.type as string
@@ -676,6 +677,8 @@ export async function updateContractItems(contractId: number, items: Record<stri
specification: item.specification as string,
quantity: item.quantity as number,
quantityUnit: item.quantityUnit as string,
+ totalWeight: item.totalWeight as number,
+ weightUnit: item.weightUnit as string,
contractDeliveryDate: item.contractDeliveryDate as string,
contractUnitPrice: item.contractUnitPrice as number,
contractAmount: item.contractAmount as number,
@@ -1554,8 +1557,8 @@ async function mapContractSummaryToDb(contractSummary: any) {
// 계약번호 생성
const contractNumber = await generateContractNumber(
- basicInfo.contractType || basicInfo.type || 'UP',
- basicInfo.userId
+ basicInfo.userId,
+ basicInfo.contractType || basicInfo.type || 'UP'
)
return {
@@ -1584,38 +1587,38 @@ async function mapContractSummaryToDb(contractSummary: any) {
currency: basicInfo.currency || basicInfo.contractCurrency || 'USD',
totalAmount: Number(basicInfo.totalAmount || basicInfo.contractAmount || 0),
- // SAP ECC 관련 필드들
- poVersion: basicInfo.revision || 1,
- purchaseDocType: basicInfo.type || 'UP',
- purchaseOrg: basicInfo.purchaseOrg || '',
- purchaseGroup: basicInfo.purchaseGroup || '',
- exchangeRate: Number(basicInfo.exchangeRate || 1),
+ // // SAP ECC 관련 필드들
+ // poVersion: basicInfo.revision || 1,
+ // purchaseDocType: basicInfo.type || 'UP',
+ // purchaseOrg: basicInfo.purchaseOrg || '',
+ // purchaseGroup: basicInfo.purchaseGroup || '',
+ // exchangeRate: Number(basicInfo.exchangeRate || 1),
- // 계약/보증 관련
- contractGuaranteeCode: basicInfo.contractGuaranteeCode || '',
- defectGuaranteeCode: basicInfo.defectGuaranteeCode || '',
- guaranteePeriodCode: basicInfo.guaranteePeriodCode || '',
- advancePaymentYn: basicInfo.advancePaymentYn || 'N',
+ // // 계약/보증 관련
+ // contractGuaranteeCode: basicInfo.contractGuaranteeCode || '',
+ // defectGuaranteeCode: basicInfo.defectGuaranteeCode || '',
+ // guaranteePeriodCode: basicInfo.guaranteePeriodCode || '',
+ // advancePaymentYn: basicInfo.advancePaymentYn || 'N',
- // 전자계약/승인 관련
- electronicContractYn: basicInfo.electronicContractYn || 'Y',
- electronicApprovalDate: basicInfo.electronicApprovalDate || null,
- electronicApprovalTime: basicInfo.electronicApprovalTime || '',
- ownerApprovalYn: basicInfo.ownerApprovalYn || 'N',
+ // // 전자계약/승인 관련
+ // electronicContractYn: basicInfo.electronicContractYn || 'Y',
+ // electronicApprovalDate: basicInfo.electronicApprovalDate || null,
+ // electronicApprovalTime: basicInfo.electronicApprovalTime || '',
+ // ownerApprovalYn: basicInfo.ownerApprovalYn || 'N',
- // 기타
- plannedInOutFlag: basicInfo.plannedInOutFlag || 'I',
- settlementStandard: basicInfo.settlementStandard || 'A',
- weightSettlementFlag: basicInfo.weightSettlementFlag || 'N',
+ // // 기타
+ // plannedInOutFlag: basicInfo.plannedInOutFlag || 'I',
+ // settlementStandard: basicInfo.settlementStandard || 'A',
+ // weightSettlementFlag: basicInfo.weightSettlementFlag || 'N',
// 연동제 관련
priceIndexYn: basicInfo.priceIndexYn || 'N',
writtenContractNo: basicInfo.contractNumber || '',
contractVersion: basicInfo.revision || 1,
- // 부분 납품/결제
- partialShippingAllowed: basicInfo.partialShippingAllowed || false,
- partialPaymentAllowed: basicInfo.partialPaymentAllowed || false,
+ // // 부분 납품/결제
+ // partialShippingAllowed: basicInfo.partialShippingAllowed || false,
+ // partialPaymentAllowed: basicInfo.partialPaymentAllowed || false,
// 메모
remarks: basicInfo.notes || basicInfo.remarks || '',
@@ -1748,7 +1751,7 @@ export async function generateContractNumber(
const user = await db
.select({ userCode: users.userCode })
.from(users)
- .where(eq(users.id, userId))
+ .where(eq(users.id, parseInt(userId || '0')))
.limit(1);
if (user[0]?.userCode && user[0].userCode.length >= 3) {
purchaseManagerCode = user[0].userCode.substring(0, 3).toUpperCase();
@@ -1774,8 +1777,20 @@ export async function generateContractNumber(
let sequenceNumber = 1
if (existingContracts.length > 0) {
const lastContractNumber = existingContracts[0].contractNumber
- const lastSequence = parseInt(lastContractNumber.slice(-3))
- sequenceNumber = lastSequence + 1
+ const lastSequenceStr = lastContractNumber.slice(-3)
+
+ // contractNumber에서 숫자만 추출하여 sequence 찾기
+ const numericParts = lastContractNumber.match(/\d+/g)
+ if (numericParts && numericParts.length > 0) {
+ // 마지막 숫자 부분을 시퀀스로 사용 (일반적으로 마지막 3자리)
+ const potentialSequence = numericParts[numericParts.length - 1]
+ const lastSequence = parseInt(potentialSequence)
+
+ if (!isNaN(lastSequence)) {
+ sequenceNumber = lastSequence + 1
+ }
+ }
+ // 숫자를 찾지 못했거나 파싱 실패 시 sequenceNumber = 1 유지
}
// 일련번호를 3자리로 포맷팅
@@ -1797,8 +1812,19 @@ export async function generateContractNumber(
let sequenceNumber = 1
if (existingContracts.length > 0) {
const lastContractNumber = existingContracts[0].contractNumber
- const lastSequence = parseInt(lastContractNumber.slice(-3))
- sequenceNumber = lastSequence + 1
+
+ // contractNumber에서 숫자만 추출하여 sequence 찾기
+ const numericParts = lastContractNumber.match(/\d+/g)
+ if (numericParts && numericParts.length > 0) {
+ // 마지막 숫자 부분을 시퀀스로 사용
+ const potentialSequence = numericParts[numericParts.length - 1]
+ const lastSequence = parseInt(potentialSequence)
+
+ if (!isNaN(lastSequence)) {
+ sequenceNumber = lastSequence + 1
+ }
+ }
+ // 숫자를 찾지 못했거나 파싱 실패 시 sequenceNumber = 1 유지
}
// 최종 계약번호 생성: C + 발주담당자코드(3자리) + 계약종류(2자리) + 연도(2자리) + 일련번호(3자리)