diff options
| -rw-r--r-- | lib/po/table/po-table-columns.tsx | 10 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 81 | ||||
| -rw-r--r-- | lib/rfq-last/shared/rfq-items-dialog.tsx | 37 | ||||
| -rw-r--r-- | lib/rfq-last/vendor-response/editor/quotation-items-table.tsx | 2 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/vendor-detail-dialog.tsx | 4 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/rfq-and-pr-mapper.ts | 17 | ||||
| -rw-r--r-- | lib/vendor-candidates/service.ts | 45 |
7 files changed, 126 insertions, 70 deletions
diff --git a/lib/po/table/po-table-columns.tsx b/lib/po/table/po-table-columns.tsx index 7edd2435..20b7c64b 100644 --- a/lib/po/table/po-table-columns.tsx +++ b/lib/po/table/po-table-columns.tsx @@ -26,6 +26,7 @@ import { Badge } from "@/components/ui/badge" import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { poColumnsConfig } from "@/config/poColumnsConfig" import { ContractDetailParsed } from "@/db/schema/contract" +import { formatNumber } from "@/lib/utils" interface GetColumnsProps { setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<ContractDetailParsed> | null>> @@ -251,8 +252,15 @@ export function getColumns({ setRowAction }: GetColumnsProps): ColumnDef<Contrac if (cfg.type === "date") { const dateVal = cell.getValue() as Date return formatDate(dateVal, "KR") + } + if (cfg.id === "totalAmount") { + const value = cell.getValue() as number | string | null | undefined + return ( + <div className="text-sm text-right font-mono"> + {formatNumber(value)} + </div> + ) } - // ... return row.getValue(cfg.id) ?? "" }, } diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index 462b5604..52d67280 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -669,41 +669,48 @@ export async function getRfqItemsAction(rfqId: number) { .where(eq(prItemsLastView.rfqsLastId, rfqId)) .orderBy(prItemsLastView.majorYn, prItemsLastView.rfqItem, prItemsLastView.materialCode) - const formattedItems = items.map(item => ({ - id: item.id, - rfqsLastId: item.rfqsLastId, - rfqItem: item.rfqItem, - prItem: item.prItem, - prNo: item.prNo, - materialCode: item.materialCode, - materialCategory: item.materialCategory, - acc: item.acc, - materialDescription: item.materialDescription, - size: item.size, - deliveryDate: item.deliveryDate, - quantity: Number(item.quantity) || 0, // 여기서 숫자로 변환 - uom: item.uom, - grossWeight: Number(item.grossWeight) || 0, // 여기서 숫자로 변환 - gwUom: item.gwUom, - specNo: item.specNo, - specUrl: item.specUrl, - trackingNo: item.trackingNo, - majorYn: item.majorYn, - remark: item.remark, - projectDef: item.projectDef, - projectSc: item.projectSc, - projectKl: item.projectKl, - projectLc: item.projectLc, - projectDl: item.projectDl, - // RFQ 관련 정보 - rfqCode: item.rfqCode, - rfqType: item.rfqType, - rfqTitle: item.rfqTitle, - itemCode: item.itemCode, - itemName: item.itemName, - projectCode: item.projectCode, - projectName: item.projectName, - })) + const formattedItems = items.map(item => { + const specification = + (item as { specification?: string | null }).specification ?? null + + return { + id: item.id, + rfqsLastId: item.rfqsLastId, + rfqItem: item.rfqItem, + prItem: item.prItem, + prNo: item.prNo, + materialCode: item.materialCode, + materialCategory: item.materialCategory, + acc: item.acc, + materialDescription: item.materialDescription, + size: item.size, + deliveryDate: item.deliveryDate, + quantity: Number(item.quantity) || 0, // 여기서 숫자로 변환 + uom: item.uom, + grossWeight: Number(item.grossWeight) || 0, // 여기서 숫자로 변환 + gwUom: item.gwUom, + specNo: item.specNo, + specUrl: item.specUrl, + trackingNo: item.trackingNo, + specification, + majorYn: item.majorYn, + remark: item.remark, + projectDef: item.projectDef, + projectSc: item.projectSc, + projectKl: item.projectKl, + projectLc: item.projectLc, + projectDl: item.projectDl, + prIssueDate: item.prIssueDate ? new Date(item.prIssueDate) : null, + // RFQ 관련 정보 + rfqCode: item.rfqCode, + rfqType: item.rfqType, + rfqTitle: item.rfqTitle, + itemCode: item.itemCode, + itemName: item.itemName, + projectCode: item.projectCode, + projectName: item.projectName, + } + }) // 주요 품목과 일반 품목 분리 및 통계 const majorItems = formattedItems.filter(item => item.majorYn) @@ -5068,11 +5075,11 @@ export async function updateShortList( }) ); - // 2-3. RFQ 상태를 "Short List 확정"으로 업데이트 + // 2-3. RFQ 상태를 "TBE 요청"으로 업데이트 await tx .update(rfqsLast) .set({ - status: "Short List 확정" as RfqStatus, + status: "TBE 요청" as RfqStatus, updatedBy: Number(session.user.id), updatedAt: new Date() }) diff --git a/lib/rfq-last/shared/rfq-items-dialog.tsx b/lib/rfq-last/shared/rfq-items-dialog.tsx index 4b41897b..f3095c98 100644 --- a/lib/rfq-last/shared/rfq-items-dialog.tsx +++ b/lib/rfq-last/shared/rfq-items-dialog.tsx @@ -20,7 +20,6 @@ import { } from "@/components/ui/table" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { ScrollArea } from "@/components/ui/scroll-area" import { Skeleton } from "@/components/ui/skeleton" import { Separator } from "@/components/ui/separator" import { toast } from "sonner" @@ -49,6 +48,7 @@ interface RfqItem { specNo: string | null specUrl: string | null trackingNo: string | null + specification: string | null majorYn: boolean | null remark: string | null projectDef: string | null @@ -56,6 +56,7 @@ interface RfqItem { projectKl: string | null projectLc: string | null projectDl: string | null + prIssueDate: Date | null // RFQ 관련 정보 rfqCode: string | null rfqType: string | null @@ -258,22 +259,23 @@ export function RfqItemsDialog({ </> )} - <ScrollArea className="flex-1"> + <div className="flex-1 overflow-auto"> {isLoading ? ( <Table> - <TableHeader> + <TableHeader className="sticky top-0 z-10 bg-background"> <TableRow> <TableHead className="w-[60px]">아이템</TableHead> <TableHead className="w-[120px]">자재코드</TableHead> <TableHead>자재명</TableHead> + <TableHead className="w-[140px]">사양</TableHead> <TableHead className="w-[80px]">수량</TableHead> <TableHead className="w-[60px]">수량단위</TableHead> <TableHead className="w-[80px]">중량</TableHead> <TableHead className="w-[60px]">중량단위</TableHead> - <TableHead className="w-[100px]">납기일</TableHead> + <TableHead className="w-[110px]">PR 발행일</TableHead> + <TableHead className="w-[100px]">PR납기 요청일</TableHead> <TableHead className="w-[100px]">PR번호</TableHead> <TableHead className="w-[120px]">사양/설계문서</TableHead> - <TableHead>비고</TableHead> </TableRow> </TableHeader> <TableBody> @@ -290,6 +292,7 @@ export function RfqItemsDialog({ <TableCell><Skeleton className="h-8 w-full" /></TableCell> <TableCell><Skeleton className="h-8 w-full" /></TableCell> <TableCell><Skeleton className="h-8 w-full" /></TableCell> + <TableCell><Skeleton className="h-8 w-full" /></TableCell> </TableRow> ))} </TableBody> @@ -301,21 +304,22 @@ export function RfqItemsDialog({ </div> ) : ( <Table> - <TableHeader> + <TableHeader className="sticky top-0 z-10 bg-background"> <TableRow> <TableHead className="w-[60px]">아이템</TableHead> <TableHead className="w-[120px]">자재코드</TableHead> <TableHead>자재명</TableHead> + <TableHead className="w-[140px]">사양</TableHead> <TableHead className="w-[80px]">수량</TableHead> <TableHead className="w-[60px]">수량단위</TableHead> <TableHead className="w-[80px]">중량</TableHead> <TableHead className="w-[60px]">중량단위</TableHead> - <TableHead className="w-[100px]">납기일</TableHead> + <TableHead className="w-[110px]">PR 발행일</TableHead> + <TableHead className="w-[100px]">PR납기 요청일</TableHead> <TableHead className="w-[100px]">PR번호</TableHead> <TableHead className="w-[100px]">PR 아이템 번호</TableHead> <TableHead className="w-[120px]">사양/설계문서</TableHead> <TableHead className="w-[100px]">프로젝트</TableHead> - <TableHead>비고</TableHead> </TableRow> </TableHeader> <TableBody> @@ -360,6 +364,11 @@ export function RfqItemsDialog({ </TableCell> <TableCell> <span className="text-sm font-medium"> + {item.specification?.trim() ? item.specification : "-"} + </span> + </TableCell> + <TableCell> + <span className="text-sm font-medium"> {item.quantity ? item.quantity.toLocaleString() : "-"} </span> </TableCell> @@ -379,6 +388,11 @@ export function RfqItemsDialog({ </span> </TableCell> <TableCell> + <span className="text-sm font-medium"> + {item.prIssueDate ? format(new Date(item.prIssueDate), "yyyy-MM-dd") : "-"} + </span> + </TableCell> + <TableCell> <span className="text-sm"> {item.deliveryDate ? format(new Date(item.deliveryDate), "yyyy-MM-dd") : "-"} </span> @@ -446,17 +460,12 @@ export function RfqItemsDialog({ ].filter(Boolean).join(" | ") || "-"} </div> </TableCell> - <TableCell> - <span className="text-xs" title={item.remark || ""}> - {item.remark ? (item.remark.length > 30 ? `${item.remark.slice(0, 30)}...` : item.remark) : "-"} - </span> - </TableCell> </TableRow> ))} </TableBody> </Table> )} - </ScrollArea> + </div> {/* 하단 통계 정보 */} {statistics && !isLoading && ( diff --git a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx index b0c1488a..d2e0ff0b 100644 --- a/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx +++ b/lib/rfq-last/vendor-response/editor/quotation-items-table.tsx @@ -483,7 +483,7 @@ export default function QuotationItemsTable({ prItems }: QuotationItemsTableProp <TableHead className="w-[60px]">중량단위</TableHead> <TableHead className="w-[150px]">단가</TableHead> <TableHead className="text-right w-[150px]">총액</TableHead> - <TableHead className="w-[150px]">납기일</TableHead> + <TableHead className="w-[150px]">PR납기 요청일</TableHead> <TableHead className="w-[180px]">사양/POS</TableHead> <TableHead className="w-[120px]">프로젝트</TableHead> <TableHead className="w-[80px]">상세</TableHead> diff --git a/lib/rfq-last/vendor/vendor-detail-dialog.tsx b/lib/rfq-last/vendor/vendor-detail-dialog.tsx index 0eee1b8b..f379b032 100644 --- a/lib/rfq-last/vendor/vendor-detail-dialog.tsx +++ b/lib/rfq-last/vendor/vendor-detail-dialog.tsx @@ -398,7 +398,7 @@ export function VendorResponseDetailDialog({ </div> <div className="space-y-3"> <div className="flex items-center justify-between"> - <span className="text-sm text-muted-foreground">납기일</span> + <span className="text-sm text-muted-foreground">PR납기 요청일</span> <span className="text-sm"> {data.deliveryDate ? format(new Date(data.deliveryDate), "yyyy-MM-dd") @@ -596,7 +596,7 @@ export function VendorResponseDetailDialog({ <TableHead className="text-right">단가</TableHead> <TableHead className="text-right">금액</TableHead> <TableHead>통화</TableHead> - <TableHead>납기일</TableHead> + <TableHead>PR납기 요청일</TableHead> </TableRow> </TableHeader> <TableBody> diff --git a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts index cc241aa6..85fbb918 100644 --- a/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/rfq-and-pr-mapper.ts @@ -240,9 +240,20 @@ export function mapECCRfqItemToRfqPrItem( // 날짜 파싱 let deliveryDate: Date | null = null; - if (eccItem.LFDAT) { - const dateStr = parseSAPDateTime(eccItem.LFDAT, '000000'); - deliveryDate = dateStr; + if (eccItem.LFDAT && eccItem.LFDAT.length === 8) { + try { + // YYYYMMDD 형식을 YYYY-MM-DD로 변환 후 한국 시간 기준으로 설정 + const year = eccItem.LFDAT.substring(0, 4); + const month = eccItem.LFDAT.substring(4, 6); + const day = eccItem.LFDAT.substring(6, 8); + // 한국 시간(KST, UTC+9) 기준으로 날짜 생성 + deliveryDate = new Date(`${year}-${month}-${day}T00:00:00+09:00`); + } catch (error) { + debugError('SAP 날짜 파싱 오류', { + lfdat: eccItem.LFDAT, + error, + }); + } } const mappedData: RfqPrItemData = { diff --git a/lib/vendor-candidates/service.ts b/lib/vendor-candidates/service.ts index bfeb3090..2b6421f5 100644 --- a/lib/vendor-candidates/service.ts +++ b/lib/vendor-candidates/service.ts @@ -252,20 +252,41 @@ export async function updateVendorCandidate(input: UpdateVendorCandidateSchema, .returning(); // 로그 작성 - const statusChanged = - updateData.status && + const statusChanged = + updateData.status !== undefined && existingCandidate.status !== updateData.status; - await tx.insert(vendorCandidateLogs).values({ - vendorCandidateId: id, - userId: userId, - action: statusChanged ? "status_change" : "update", - oldStatus: statusChanged ? existingCandidate.status : undefined, - newStatus: statusChanged ? updateData.status : undefined, - comment: statusChanged - ? `Status changed from ${existingCandidate.status} to ${updateData.status}` - : `Updated vendor candidate: ${existingCandidate.companyName}` - }); + // 상태가 변경된 경우에만 상태 변경 로그 기록 + if (statusChanged) { + await tx.insert(vendorCandidateLogs).values({ + vendorCandidateId: id, + userId: userId, + action: "status_change", + oldStatus: existingCandidate.status, + newStatus: updateData.status, + comment: `Status changed from ${existingCandidate.status} to ${updateData.status}` + }); + } + + // 상태가 변경되지 않았지만 다른 필드가 변경된 경우에만 일반 업데이트 로그 기록 + // (실제로 변경된 필드가 있는지 확인하는 로직은 복잡하므로, 상태 변경이 아닌 경우에만 로그 기록) + // 참고: 모든 필드가 동일한 경우도 있지만, 사용자가 저장 버튼을 눌렀다는 것은 변경 의도가 있다는 의미 + if (!statusChanged) { + // 다른 필드 변경 여부를 간단히 확인 (실제로는 더 정교한 비교가 필요할 수 있음) + const hasOtherChanges = Object.keys(updateData).some(key => { + if (key === 'status' || key === 'updatedAt') return false; + return (existingCandidate as any)[key] !== (updateData as any)[key]; + }); + + if (hasOtherChanges) { + await tx.insert(vendorCandidateLogs).values({ + vendorCandidateId: id, + userId: userId, + action: "update", + comment: `Updated vendor candidate: ${existingCandidate.companyName}` + }); + } + } |
