diff options
Diffstat (limited to 'components')
18 files changed, 439 insertions, 314 deletions
diff --git a/components/bidding/ProjectSelectorBid.tsx b/components/bidding/ProjectSelectorBid.tsx index de9e435e..0fc567b3 100644 --- a/components/bidding/ProjectSelectorBid.tsx +++ b/components/bidding/ProjectSelectorBid.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import { Check, ChevronsUpDown, X } from "lucide-react" +import { Check, ChevronsUpDown } from "lucide-react" import { Button } from "@/components/ui/button" import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover" import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from "@/components/ui/command" @@ -10,7 +10,7 @@ import { getProjects, type Project } from "@/lib/rfqs/service" interface ProjectSelectorProps { selectedProjectId?: number | null; - onProjectSelect: (project: Project) => void; + onProjectSelect: (project: Project | null) => void; placeholder?: string; filterType?: string; // 옵션으로 필터 타입 지정 가능 } @@ -76,7 +76,7 @@ export function ProjectSelector({ // 이미 선택된 프로젝트를 다시 선택하면 선택 해제 if (selectedProject?.id === project.id) { setSelectedProject(null); - onProjectSelect(null as any); // 선택 해제를 위해 null 전달 + onProjectSelect(null); // 선택 해제를 위해 null 전달 setOpen(false); return; } @@ -86,12 +86,6 @@ export function ProjectSelector({ setOpen(false); }; - // 프로젝트 선택 해제 - const handleClearSelection = (e: React.MouseEvent) => { - e.stopPropagation(); // Popover가 열리지 않도록 방지 - setSelectedProject(null); - onProjectSelect(null as any); // 선택 해제를 위해 null 전달 - }; return ( <Popover open={open} onOpenChange={setOpen}> @@ -106,20 +100,7 @@ export function ProjectSelector({ {isLoading ? ( "프로젝트 로딩 중..." ) : selectedProject ? ( - <div className="flex items-center justify-between w-full"> - <span>{selectedProject.projectCode}</span> - <div className="flex items-center gap-1"> - <Button - variant="ghost" - size="sm" - className="h-4 w-4 p-0 hover:bg-destructive hover:text-destructive-foreground" - onClick={handleClearSelection} - > - <X className="h-3 w-3" /> - </Button> - <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> - </div> - </div> + <span>{selectedProject.projectCode}</span> ) : ( <div className="flex items-center justify-between w-full"> <span>{placeholder}</span> diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx index 9b0a6f66..498d8d1c 100644 --- a/components/bidding/create/bidding-create-dialog.tsx +++ b/components/bidding/create/bidding-create-dialog.tsx @@ -618,7 +618,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp {/* 4행: 하도급법적용여부, SHI 지급조건 */}
<div className="grid grid-cols-2 gap-4">
- {/* <FormField
+ <FormField
control={form.control}
name="biddingConditions.isPriceAdjustmentApplicable"
render={({ field }) => (
@@ -637,13 +637,13 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp }}
/>
<FormLabel htmlFor="price-adjustment" className="text-sm">
- 연동제 적용 요건
+ 하도급법 적용여부
</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
- /> */}
+ />
<FormField
control={form.control}
@@ -957,7 +957,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp <FormItem>
<FormLabel className="flex items-center gap-1">
<Building className="h-3 w-3" />
- 구매조직
+ 구매조직 <span className="text-red-500">*</span>
</FormLabel>
<Select onValueChange={field.onChange} value={field.value || ''}>
<FormControl>
diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index c2c668a4..3c450065 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -721,7 +721,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB <FormItem> <FormLabel className="flex items-center gap-1"> <Building className="h-3 w-3" /> - 구매조직 + 구매조직 <span className="text-red-500">*</span> </FormLabel> <Select onValueChange={field.onChange} value={field.value}> <FormControl> @@ -1019,7 +1019,7 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB </div> </div> - {/* 4행: 계약 납품일, 연동제 적용 가능 */} + {/* 4행: 계약 납품일, 하도급법 적용여부 */} <div className="grid grid-cols-2 gap-4 mb-4"> <div> <FormLabel>계약 납품일</FormLabel> @@ -1034,6 +1034,24 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB }} /> </div> + <div> + <FormLabel>하도급법 적용여부</FormLabel> + <div className="flex items-center space-x-2"> + <Switch + checked={biddingConditions.isPriceAdjustmentApplicable} + onCheckedChange={(checked) => { + setBiddingConditions(prev => ({ + ...prev, + isPriceAdjustmentApplicable: checked + })) + }} + id="price-adjustment" + /> + <FormLabel htmlFor="price-adjustment" className="text-sm"> + {biddingConditions.isPriceAdjustmentApplicable ? "적용" : "미적용"} + </FormLabel> + </div> + </div> </div> {/* 5행: 스페어파트 옵션 */} diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index a5ce1349..da566c82 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -49,6 +49,7 @@ interface QuotationVendor { currency: string invitationStatus: string isPriceAdjustmentApplicableQuestion?: boolean + businessSize?: string | null } interface BiddingCompaniesEditorProps { @@ -106,17 +107,18 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC try { const result = await getBiddingVendors(biddingId) if (result.success && result.data) { - const vendorsList = result.data.map(v => ({ - ...v, - companyId: v.companyId || undefined, - vendorName: v.vendorName || '', - vendorCode: v.vendorCode || '', - contactPerson: v.contactPerson ?? undefined, - contactEmail: v.contactEmail ?? undefined, - contactPhone: v.contactPhone ?? undefined, - quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined, - isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false, - })) + const vendorsList = result.data.map(v => ({ + ...v, + companyId: v.companyId || undefined, + vendorName: v.vendorName || '', + vendorCode: v.vendorCode || '', + contactPerson: v.contactPerson ?? undefined, + contactEmail: v.contactEmail ?? undefined, + contactPhone: v.contactPhone ?? undefined, + quotationAmount: v.quotationAmount ? parseFloat(v.quotationAmount) : undefined, + isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false, + businessSize: v.businessSize ?? undefined, + })) setVendors(vendorsList) // 각 업체별 첫 번째 담당자 정보 로드 @@ -161,6 +163,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC currency: v.currency || 'KRW', invitationStatus: v.invitationStatus, isPriceAdjustmentApplicableQuestion: v.isPriceAdjustmentApplicableQuestion ?? false, + businessSize: v.businessSize ?? undefined, })) setVendors(vendorsList) @@ -503,6 +506,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC <TableHead className="w-[50px]">선택</TableHead> <TableHead>업체명</TableHead> <TableHead>업체코드</TableHead> + <TableHead>기업규모</TableHead> <TableHead>담당자 이름</TableHead> <TableHead>담당자 이메일</TableHead> <TableHead>담당자 연락처</TableHead> @@ -526,6 +530,7 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC </TableCell> <TableCell className="font-medium">{vendor.vendorName}</TableCell> <TableCell>{vendor.vendorCode}</TableCell> + <TableCell>{vendor.businessSize || '-'}</TableCell> <TableCell> {vendor.companyId && vendorFirstContacts.has(vendor.companyId) ? vendorFirstContacts.get(vendor.companyId)!.contactName diff --git a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx index de813121..0dd9f0eb 100644 --- a/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx +++ b/components/bidding/manage/bidding-detail-vendor-create-dialog.tsx @@ -28,7 +28,7 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { ChevronsUpDown, Loader2, X, Plus } from 'lucide-react' import { createBiddingDetailVendor } from '@/lib/bidding/detail/service' -import { searchVendorsForBidding } from '@/lib/bidding/service' +import { searchVendorsForBidding, getVendorsBusinessSize } from '@/lib/bidding/service' import { useToast } from '@/hooks/use-toast' import { useTransition } from 'react' import { Badge } from '@/components/ui/badge' @@ -68,6 +68,9 @@ export function BiddingDetailVendorCreateDialog({ const [selectedVendorsWithQuestion, setSelectedVendorsWithQuestion] = React.useState<SelectedVendorWithQuestion[]>([]) const [vendorOpen, setVendorOpen] = React.useState(false) + // Business size 정보 캐싱 + const [businessSizeMap, setBusinessSizeMap] = React.useState<Record<number, string | null>>({}) + // 벤더 로드 const loadVendors = React.useCallback(async () => { try { @@ -204,6 +207,28 @@ export function BiddingDetailVendorCreateDialog({ const selectedVendors = selectedVendorsWithQuestion.map(item => item.vendor) + // 선택된 vendor들의 businessSize 정보를 useMemo로 캐싱 + const loadBusinessSize = React.useMemo(() => { + const selectedVendorIds = selectedVendors.map(v => v.id) + if (selectedVendorIds.length === 0) return + + // 이미 로드된 vendor들은 제외 + const newVendorIds = selectedVendorIds.filter(id => !(id in businessSizeMap)) + + if (newVendorIds.length > 0) { + getVendorsBusinessSize(newVendorIds).then(result => { + setBusinessSizeMap(prev => ({ ...prev, ...result })) + }).catch(error => { + console.error('Failed to load business size:', error) + }) + } + }, [selectedVendors, businessSizeMap]) + + // selectedVendors가 변경될 때마다 businessSize 로드 + React.useEffect(() => { + loadBusinessSize + }, [loadBusinessSize]) + return ( <Dialog open={open} onOpenChange={onOpenChange}> <DialogContent className="max-w-4xl max-h-[90vh] p-0 flex flex-col"> @@ -382,6 +407,9 @@ export function BiddingDetailVendorCreateDialog({ > 연동제 적용요건 문의 </Label> + <span className="text-xs text-muted-foreground"> + 기업규모: {businessSizeMap[item.vendor.id] || '미정'} + </span> </div> </div> ))} diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx index f0287ae4..208cf040 100644 --- a/components/bidding/manage/bidding-items-editor.tsx +++ b/components/bidding/manage/bidding-items-editor.tsx @@ -441,8 +441,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems materialGroupInfo: null, materialNumber: null, materialInfo: null, - priceUnit: null, - purchaseUnit: '1', + priceUnit: 1, + purchaseUnit: 'EA', materialWeight: null, wbsCode: null, wbsName: null, @@ -495,8 +495,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems prev.map((item) => { if (item.id === id) { const updatedItem = { ...item, ...updates } - // 내정단가, 수량, 중량, 구매단위가 변경되면 내정금액 재계산 - if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.purchaseUnit) { + // 내정단가, 수량, 중량, 가격단위가 변경되면 내정금액 재계산 + if (updates.targetUnitPrice || updates.quantity || updates.totalWeight || updates.priceUnit) { updatedItem.targetAmount = calculateTargetAmount(updatedItem) } return updatedItem @@ -533,20 +533,66 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems const calculateTargetAmount = (item: PRItemInfo): string => { const unitPrice = parseFloat(item.targetUnitPrice?.replace(/,/g, '') || '0') || 0 - const purchaseUnit = parseFloat(item.purchaseUnit || '1') || 1 + const priceUnit = parseFloat(item.priceUnit || '1') || 1 let amount = 0 if (quantityWeightMode === 'quantity') { const quantity = parseFloat(item.quantity || '0') || 0 - amount = (quantity / purchaseUnit) * unitPrice + amount = (quantity / priceUnit) * unitPrice } else { const weight = parseFloat(item.totalWeight || '0') || 0 - amount = (weight / purchaseUnit) * unitPrice + amount = (weight / priceUnit) * unitPrice } return Math.floor(amount).toString() } + // 합계 계산 함수들 + const calculateTotals = () => { + let quantityTotal = 0 + let weightTotal = 0 + let targetAmountTotal = 0 + let budgetAmountTotal = 0 + let actualAmountTotal = 0 + + items.forEach((item) => { + // 수량 합계 + if (item.quantity) { + quantityTotal += parseFloat(item.quantity) || 0 + } + + // 중량 합계 + if (item.totalWeight) { + weightTotal += parseFloat(item.totalWeight) || 0 + } + + // 내정금액 합계 + if (item.targetAmount) { + targetAmountTotal += parseFloat(item.targetAmount.replace(/,/g, '')) || 0 + } + + // 예산금액 합계 + if (item.budgetAmount) { + budgetAmountTotal += parseFloat(item.budgetAmount.replace(/,/g, '')) || 0 + } + + // 실적금액 합계 + if (item.actualAmount) { + actualAmountTotal += parseFloat(item.actualAmount.replace(/,/g, '')) || 0 + } + }) + + return { + quantityTotal, + weightTotal, + targetAmountTotal, + budgetAmountTotal, + actualAmountTotal, + } + } + + const totals = calculateTotals() + if (isLoading) { return ( <div className="flex items-center justify-center p-8"> @@ -572,14 +618,16 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">프로젝트코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">프로젝트명</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재그룹코드 <span className="text-red-500">*</span></th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재그룹명 <span className="text-red-500">*</span></th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">자재코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">자재명</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">수량(중량) <span className="text-red-500">*</span></th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">단위 <span className="text-red-500">*</span></th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">가격단위</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">구매단위</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[100px]">자재순중량</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정단가</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">내정금액</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">내정통화</th> @@ -587,19 +635,120 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">예산통화</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">실적금액</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[80px]">실적통화</th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[300px]">WBS코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">WBS명</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">코스트센터코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">코스트센터명</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">GL계정코드</th> <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[150px]">GL계정명</th> - <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">납품요청일 <span className="text-red-500">*</span></th> + <th className="border-r px-3 py-3 text-left text-xs font-medium min-w-[120px]">PR 번호</th> <th className="sticky right-0 z-10 bg-muted/50 border-l px-3 py-3 text-center text-xs font-medium min-w-[100px]"> 액션 </th> </tr> </thead> <tbody> + {/* 합계 행 */} + <tr className="bg-blue-50 border-y-2 border-blue-200 font-semibold"> + <td className="sticky left-0 z-10 bg-blue-50 border-r px-2 py-3 text-center"> + <span className="text-xs">합계</span> + </td> + <td className="sticky left-[50px] z-10 bg-blue-50 border-r px-3 py-3 text-center"> + <span className="text-xs">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs"> + {quantityWeightMode === 'quantity' + ? `${formatNumberWithCommas(totals.quantityTotal.toString())}` + : `${formatNumberWithCommas(totals.weightTotal.toString())}` + } + </span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs"> + {quantityWeightMode === 'quantity' + ? `${items[0]?.quantityUnit || 'EA'}` + : `${items[0]?.weightUnit || 'KG'}` + } + </span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs">{formatNumberWithCommas(totals.targetAmountTotal.toString())}</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs">{formatNumberWithCommas(totals.budgetAmountTotal.toString())}</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs">{formatNumberWithCommas(totals.actualAmountTotal.toString())}</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="border-r px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + <td className="sticky right-0 z-10 bg-blue-50 border-l px-3 py-3 text-center"> + <span className="text-xs text-muted-foreground">-</span> + </td> + </tr> {items.map((item, index) => ( <tr key={item.id} className="border-t hover:bg-muted/30"> <td className="sticky left-0 z-10 bg-background border-r px-2 py-2 text-center"> @@ -617,10 +766,17 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <ProjectSelector selectedProjectId={item.projectId || null} onProjectSelect={(project) => { - updatePRItem(item.id, { - projectId: project.id, - projectInfo: project.projectName - }) + if (project) { + updatePRItem(item.id, { + projectId: project.id, + projectInfo: project.projectName + }) + } else { + updatePRItem(item.id, { + projectId: null, + projectInfo: null + }) + } }} placeholder="프로젝트 선택" /> @@ -633,14 +789,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems className="h-8 text-xs bg-muted/50" /> </td> - <td className="border-r px-3 py-2"> - <Input - placeholder="PR 번호" - value={item.prNumber || ''} - readOnly - className="h-8 text-xs bg-muted/50" - /> - </td> + <td className="border-r px-3 py-2"> {biddingType !== 'equipment' ? ( <ProcurementItemSelectorDialogSingle @@ -745,6 +894,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.quantity || ''} onChange={(e) => updatePRItem(item.id, { quantity: e.target.value })} className="h-8 text-xs" + required /> ) : ( <Input @@ -754,6 +904,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems value={item.totalWeight || ''} onChange={(e) => updatePRItem(item.id, { totalWeight: e.target.value })} className="h-8 text-xs" + required /> )} </td> @@ -762,6 +913,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Select value={item.quantityUnit || 'EA'} onValueChange={(value) => updatePRItem(item.id, { quantityUnit: value })} + required > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -779,6 +931,7 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Select value={item.weightUnit || 'KG'} onValueChange={(value) => updatePRItem(item.id, { weightUnit: value })} + required > <SelectTrigger className="h-8 text-xs"> <SelectValue /> @@ -797,9 +950,42 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems type="number" min="1" step="1" - placeholder="구매단위" - value={item.purchaseUnit || ''} - onChange={(e) => updatePRItem(item.id, { purchaseUnit: e.target.value })} + placeholder="가격단위" + value={item.priceUnit || ''} + onChange={(e) => updatePRItem(item.id, { priceUnit: e.target.value })} + className="h-8 text-xs" + /> + </td> + <td className="border-r px-3 py-2"> + <Select + value={item.purchaseUnit || 'EA'} + onValueChange={(value) => updatePRItem(item.id, { purchaseUnit: value })} + > + <SelectTrigger className="h-8 text-xs"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="EA">EA</SelectItem> + <SelectItem value="SET">SET</SelectItem> + <SelectItem value="LOT">LOT</SelectItem> + <SelectItem value="M">M</SelectItem> + <SelectItem value="M2">M²</SelectItem> + <SelectItem value="M3">M³</SelectItem> + <SelectItem value="KG">KG</SelectItem> + <SelectItem value="TON">TON</SelectItem> + <SelectItem value="G">G</SelectItem> + <SelectItem value="LB">LB</SelectItem> + </SelectContent> + </Select> + </td> + <td className="border-r px-3 py-2"> + <Input + type="number" + min="0" + step="0.01" + placeholder="자재순중량" + value={item.materialWeight || ''} + onChange={(e) => updatePRItem(item.id, { materialWeight: e.target.value })} className="h-8 text-xs" /> </td> @@ -888,9 +1074,23 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </Select> </td> <td className="border-r px-3 py-2"> + <Input + type="date" + value={item.requestedDeliveryDate || ''} + onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })} + className="h-8 text-xs" + required + /> + </td> + <td className="border-r px-3 py-2"> <Button variant="outline" onClick={() => { + // 재클릭 시 기존 데이터 클리어 + updatePRItem(item.id, { + wbsCode: null, + wbsName: null + }) setSelectedItemForWbs(item.id) setWbsCodeDialogOpen(true) }} @@ -941,6 +1141,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Button variant="outline" onClick={() => { + // 재클릭 시 기존 데이터 클리어 + updatePRItem(item.id, { + costCenterCode: null, + costCenterName: null + }) setSelectedItemForCostCenter(item.id) setCostCenterDialogOpen(true) }} @@ -992,6 +1197,11 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems <Button variant="outline" onClick={() => { + // 재클릭 시 기존 데이터 클리어 + updatePRItem(item.id, { + glAccountCode: null, + glAccountName: null + }) setSelectedItemForGlAccount(item.id) setGlAccountDialogOpen(true) }} @@ -1039,11 +1249,10 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems </td> <td className="border-r px-3 py-2"> <Input - type="date" - value={item.requestedDeliveryDate || ''} - onChange={(e) => updatePRItem(item.id, { requestedDeliveryDate: e.target.value })} - className="h-8 text-xs" - required + placeholder="PR 번호" + value={item.prNumber || ''} + readOnly + className="h-8 text-xs bg-muted/50" /> </td> <td className="sticky right-0 z-10 bg-background border-l px-3 py-2"> diff --git a/components/bidding/manage/create-pre-quote-rfq-dialog.tsx b/components/bidding/manage/create-pre-quote-rfq-dialog.tsx index c49f6232..cdcf1ef1 100644 --- a/components/bidding/manage/create-pre-quote-rfq-dialog.tsx +++ b/components/bidding/manage/create-pre-quote-rfq-dialog.tsx @@ -51,8 +51,8 @@ import { MaterialGroupSelectorDialogSingle } from "@/components/common/material/ import { MaterialSearchItem } from "@/lib/material/material-group-service"
import { MaterialSelectorDialogSingle } from "@/components/common/selectors/material/material-selector-dialog-single"
import { MaterialSearchItem as SAPMaterialSearchItem } from "@/components/common/selectors/material/material-service"
-import { ProcurementManagerSelector } from "@/components/common/selectors/procurement-manager"
-import type { ProcurementManagerWithUser } from "@/components/common/selectors/procurement-manager/procurement-manager-service"
+import { PurchaseGroupCodeSelector } from "@/components/common/selectors/purchase-group-code/purchase-group-code-selector"
+import { getBiddingById } from "@/lib/bidding/service"
// 아이템 스키마
const itemSchema = z.object({
@@ -122,7 +122,7 @@ export function CreatePreQuoteRfqDialog({ const [isLoading, setIsLoading] = React.useState(false)
const [previewCode, setPreviewCode] = React.useState("")
const [isLoadingPreview, setIsLoadingPreview] = React.useState(false)
- const [selectedManager, setSelectedManager] = React.useState<ProcurementManagerWithUser | undefined>(undefined)
+ const [selectedBidPic, setSelectedBidPic] = React.useState<any | undefined>(undefined)
const { data: session } = useSession()
const userId = React.useMemo(() => {
@@ -170,6 +170,29 @@ export function CreatePreQuoteRfqDialog({ name: "items",
})
+ // 입찰담당자 정보 로드
+ React.useEffect(() => {
+ const loadBiddingInfo = async () => {
+ if (!biddingId || !open) return
+
+ try {
+ const bidding = await getBiddingById(biddingId)
+ if (bidding && bidding.bidPicId) {
+ // 입찰담당자 정보를 로드하는 로직 추가 필요
+ // 현재는 임시로 bidPicId를 사용
+ setSelectedBidPic({
+ USER_ID: bidding.bidPicId,
+ DISPLAY_NAME: bidding.bidPicName || '입찰담당자'
+ })
+ }
+ } catch (error) {
+ console.error('Failed to load bidding info:', error)
+ }
+ }
+
+ loadBiddingInfo()
+ }, [biddingId, open])
+
// 다이얼로그가 열릴 때 폼 초기화
React.useEffect(() => {
if (open) {
@@ -177,7 +200,7 @@ export function CreatePreQuoteRfqDialog({ rfqType: "",
rfqTitle: "",
dueDate: undefined,
- picUserId: undefined,
+ picUserId: selectedBidPic?.USER_ID,
projectId: undefined,
remark: "",
items: initialItems.length > 0 ? initialItems : [
@@ -192,14 +215,13 @@ export function CreatePreQuoteRfqDialog({ },
],
})
- setSelectedManager(undefined)
setPreviewCode("")
}
- }, [open, initialItems, form])
+ }, [open, initialItems, form, selectedBidPic])
// 견적담당자 선택 시 RFQ 코드 미리보기 생성
React.useEffect(() => {
- if (!selectedManager?.user?.id) {
+ if (!selectedBidPic?.USER_ID) {
setPreviewCode("")
return
}
@@ -208,7 +230,7 @@ export function CreatePreQuoteRfqDialog({ (async () => {
setIsLoadingPreview(true)
try {
- const code = await previewGeneralRfqCode(selectedManager.user!.id!)
+ const code = await previewGeneralRfqCode(selectedBidPic.USER_ID)
setPreviewCode(code)
} catch (error) {
console.error("코드 미리보기 오류:", error)
@@ -217,7 +239,7 @@ export function CreatePreQuoteRfqDialog({ setIsLoadingPreview(false)
}
})()
- }, [selectedManager])
+ }, [selectedBidPic])
// 견적 종류 변경
const handleRfqTypeChange = (value: string) => {
@@ -244,8 +266,8 @@ export function CreatePreQuoteRfqDialog({ },
],
})
- setSelectedManager(undefined)
- setPreviewCode("")
+ setSelectedBidPic(undefined)
+ setPreviewCode("")
onOpenChange(false)
}
@@ -255,12 +277,12 @@ export function CreatePreQuoteRfqDialog({ return
}
- if (!selectedManager?.user?.id) {
- toast.error("견적담당자를 선택해주세요")
+ if (!selectedBidPic?.USER_ID) {
+ toast.error("입찰담당자를 선택해주세요")
return
}
- const picUserId = selectedManager.user.id
+ const picUserId = selectedBidPic.USER_ID
setIsLoading(true)
@@ -476,13 +498,13 @@ export function CreatePreQuoteRfqDialog({ 견적담당자 <span className="text-red-500">*</span>
</FormLabel>
<FormControl>
- <ProcurementManagerSelector
- selectedManager={selectedManager}
- onManagerSelect={(manager) => {
- setSelectedManager(manager)
- field.onChange(manager.user?.id)
+ <PurchaseGroupCodeSelector
+ selectedCode={selectedBidPic}
+ onCodeSelect={(code) => {
+ setSelectedBidPic(code)
+ field.onChange(code.USER_ID)
}}
- placeholder="견적담당자를 선택하세요"
+ placeholder="입찰담당자 선택"
/>
</FormControl>
<FormDescription>
diff --git a/components/bidding/price-adjustment-dialog.tsx b/components/bidding/price-adjustment-dialog.tsx index 149b8e9a..6a520eac 100644 --- a/components/bidding/price-adjustment-dialog.tsx +++ b/components/bidding/price-adjustment-dialog.tsx @@ -67,7 +67,7 @@ export function PriceAdjustmentDialog({ <h3 className="text-sm font-medium text-gray-900 mb-3">기본 정보</h3> <div className="grid grid-cols-2 gap-4"> <div> - <label className="text-xs text-gray-500">품목등의 명칭</label> + <label className="text-xs text-gray-500">물품등의 명칭</label> <p className="text-sm font-medium">{data.itemName || '-'}</p> </div> <div> @@ -126,16 +126,16 @@ export function PriceAdjustmentDialog({ </div> <div className="grid grid-cols-2 gap-4"> <div> - <label className="text-xs text-gray-500">기준시점</label> + <label className="text-xs text-gray-500">원재료 기준 가격의 변동률 산정을 위한 기준시점</label> <p className="text-sm font-medium">{data.referenceDate ? formatDate(data.referenceDate, "kr") : '-'}</p> </div> <div> - <label className="text-xs text-gray-500">비교시점</label> + <label className="text-xs text-gray-500">원재료 기준 가격의 변동률 산정을 위한 비교시점</label> <p className="text-sm font-medium">{data.comparisonDate ? formatDate(data.comparisonDate, "kr") : '-'}</p> </div> </div> <div> - <label className="text-xs text-gray-500">연동 비율</label> + <label className="text-xs text-gray-500">반영비율</label> <p className="text-sm font-medium"> {data.adjustmentRatio ? `${data.adjustmentRatio}%` : '-'} </p> @@ -166,11 +166,11 @@ export function PriceAdjustmentDialog({ </div> </div> <div> - <label className="text-xs text-gray-500">수탁기업(협력사) 작성자</label> + <label className="text-xs text-gray-500">수탁기업(협력사)작성자</label> <p className="text-sm font-medium">{data.contractorWriter || '-'}</p> </div> <div> - <label className="text-xs text-gray-500">기타 사항</label> + <label className="text-xs text-gray-500">기타사항</label> <p className="text-sm font-medium whitespace-pre-wrap"> {data.notes || '-'} </p> @@ -185,6 +185,17 @@ export function PriceAdjustmentDialog({ <p>작성일: {formatDate(data.createdAt, "kr")}</p> <p>수정일: {formatDate(data.updatedAt, "kr")}</p> </div> + + <Separator /> + + {/* 참고 경고문 */} + <div className="text-xs text-red-600 space-y-2 bg-red-50 p-3 rounded-md border border-red-200"> + <p className="font-medium">※ 참고사항</p> + <div className="space-y-1"> + <p>• 납품대금의 10% 이상을 차지하는 주요 원재료가 있는 경우 모든 주요 원재료에 대해서 적용 또는 미적용에 대한 연동표를 작성해야 한다.</p> + <p>• 납품대급연동표를 허위로 작성하거나 근거자료를 허위로 제출할 경우 본 계약이 체결되지 않을 수 있으며, 본 계약이 체결되었더라도 계약의 전부 또는 일부를 해제 또는 해지할 수 있다.</p> + </div> + </div> </div> </DialogContent> </Dialog> diff --git a/components/common/selectors/cost-center/cost-center-selector.tsx b/components/common/selectors/cost-center/cost-center-selector.tsx index 3aad5787..f87b6928 100644 --- a/components/common/selectors/cost-center/cost-center-selector.tsx +++ b/components/common/selectors/cost-center/cost-center-selector.tsx @@ -77,11 +77,6 @@ export function CostCenterSelector({ const [globalFilter, setGlobalFilter] = useState('')
const [isPending, startTransition] = useTransition()
- // 날짜 포맷 함수 (YYYYMMDD -> YYYY-MM-DD)
- const formatDate = (dateStr: string) => {
- if (!dateStr || dateStr.length !== 8) return dateStr
- return `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`
- }
// Cost Center 선택 핸들러
const handleCodeSelect = useCallback(async (code: CostCenter) => {
@@ -118,27 +113,6 @@ export function CostCenterSelector({ ),
},
{
- accessorKey: 'LTEXT',
- header: '설명',
- cell: ({ row }) => (
- <div>{row.getValue('LTEXT')}</div>
- ),
- },
- {
- accessorKey: 'DATAB',
- header: '시작일',
- cell: ({ row }) => (
- <div className="text-sm">{formatDate(row.getValue('DATAB'))}</div>
- ),
- },
- {
- accessorKey: 'DATBI',
- header: '종료일',
- cell: ({ row }) => (
- <div className="text-sm">{formatDate(row.getValue('DATBI'))}</div>
- ),
- },
- {
id: 'actions',
header: '선택',
cell: ({ row }) => (
diff --git a/components/common/selectors/cost-center/cost-center-service.ts b/components/common/selectors/cost-center/cost-center-service.ts index 844215f0..1e7e43f5 100644 --- a/components/common/selectors/cost-center/cost-center-service.ts +++ b/components/common/selectors/cost-center/cost-center-service.ts @@ -5,19 +5,16 @@ import { oracleKnex } from '@/lib/oracle-db/db' // Cost Center 타입 정의
export interface CostCenter {
KOSTL: string // Cost Center 코드
- DATAB: string // 시작일
- DATBI: string // 종료일
KTEXT: string // 단축 텍스트
- LTEXT: string // 긴 텍스트
}
// 테스트 환경용 폴백 데이터
const FALLBACK_TEST_DATA: CostCenter[] = [
- { KOSTL: 'D6023930', DATAB: '20230101', DATBI: '99991231', KTEXT: '구매팀', LTEXT: '구매팀 Cost Center(테스트데이터 - 오라클 페칭 실패시)' },
- { KOSTL: 'D6023931', DATAB: '20230101', DATBI: '99991231', KTEXT: '자재팀', LTEXT: '자재팀 Cost Center(테스트데이터 - 오라클 페칭 실패시)' },
- { KOSTL: 'D6023932', DATAB: '20230101', DATBI: '99991231', KTEXT: '조달팀', LTEXT: '조달팀 Cost Center(테스트데이터 - 오라클 페칭 실패시)' },
- { KOSTL: 'D6023933', DATAB: '20230101', DATBI: '99991231', KTEXT: '구매1팀', LTEXT: '구매1팀 Cost Center(테스트데이터 - 오라클 페칭 실패시)' },
- { KOSTL: 'D6023934', DATAB: '20230101', DATBI: '99991231', KTEXT: '구매2팀', LTEXT: '구매2팀 Cost Center(테스트데이터 - 오라클 페칭 실패시)' },
+ { KOSTL: 'D6023930', KTEXT: '구매팀(테스트데이터 - 오라클 페칭 실패시)' },
+ { KOSTL: 'D6023931', KTEXT: '자재팀(테스트데이터 - 오라클 페칭 실패시)' },
+ { KOSTL: 'D6023932', KTEXT: '조달팀(테스트데이터 - 오라클 페칭 실패시)' },
+ { KOSTL: 'D6023933', KTEXT: '구매1팀(테스트데이터 - 오라클 페칭 실패시)' },
+ { KOSTL: 'D6023934', KTEXT: '구매2팀(테스트데이터 - 오라클 페칭 실패시)' },
]
/**
@@ -35,19 +32,11 @@ export async function getCostCenters(): Promise<{ console.log('📋 [getCostCenters] Oracle 쿼리 시작...')
const result = await oracleKnex.raw(`
- SELECT
- KOSTL,
- DATAB,
- DATBI,
- KTEXT,
- LTEXT
- FROM CMCTB_COSTCENTER
- WHERE ROWNUM < 100
- AND NVL(BKZKP,' ') = ' '
- AND TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN DATAB AND DATBI
- AND KOKRS = 'H100'
- ORDER BY KOSTL
+ SELECT KOSTL , KTEXT FROM CMCTB_COSTCENTER
+ WHERE TO_CHAR(SYSDATE,'YYYYMMDD') BETWEEN DATAB AND DATBI AND ROWNUM < 10
`)
+ // KOSTL -> CONTCENTER
+ // KTEXT -> CONTCENTER명
// Oracle raw query의 결과는 rows 배열에 들어있음
const rows = (result.rows || result) as Array<Record<string, unknown>>
@@ -56,17 +45,12 @@ export async function getCostCenters(): Promise<{ // null 값 필터링
const cleanedResult = rows
- .filter((item) =>
- item.KOSTL &&
- item.DATAB &&
- item.DATBI
+ .filter((item) =>
+ item.KOSTL
)
.map((item) => ({
KOSTL: String(item.KOSTL),
- DATAB: String(item.DATAB),
- DATBI: String(item.DATBI),
- KTEXT: String(item.KTEXT || ''),
- LTEXT: String(item.LTEXT || '')
+ KTEXT: String(item.KTEXT || '')
}))
console.log(`✅ [getCostCenters] 필터링 후 ${cleanedResult.length}건`)
diff --git a/components/common/selectors/cost-center/cost-center-single-selector.tsx b/components/common/selectors/cost-center/cost-center-single-selector.tsx index e09f782b..952fab3f 100644 --- a/components/common/selectors/cost-center/cost-center-single-selector.tsx +++ b/components/common/selectors/cost-center/cost-center-single-selector.tsx @@ -134,27 +134,6 @@ export function CostCenterSingleSelector({ ), }, { - accessorKey: 'LTEXT', - header: '설명', - cell: ({ row }) => ( - <div>{row.getValue('LTEXT')}</div> - ), - }, - { - accessorKey: 'DATAB', - header: '시작일', - cell: ({ row }) => ( - <div className="text-sm">{formatDate(row.getValue('DATAB'))}</div> - ), - }, - { - accessorKey: 'DATBI', - header: '종료일', - cell: ({ row }) => ( - <div className="text-sm">{formatDate(row.getValue('DATBI'))}</div> - ), - }, - { id: 'actions', header: '선택', cell: ({ row }) => { @@ -283,7 +262,6 @@ export function CostCenterSingleSelector({ <div className="flex items-center gap-2 mt-1"> <span className="font-mono text-sm">[{currentSelectedCode.KOSTL}]</span> <span>{currentSelectedCode.KTEXT}</span> - <span className="text-muted-foreground">- {currentSelectedCode.LTEXT}</span> </div> </div> )} @@ -291,7 +269,7 @@ export function CostCenterSingleSelector({ <div className="flex items-center space-x-2"> <Search className="h-4 w-4" /> <Input - placeholder="코스트센터 코드, 단축명, 설명으로 검색..." + placeholder="코스트센터 코드, 단축명으로 검색..." value={globalFilter} onChange={(e) => handleSearchChange(e.target.value)} className="flex-1" diff --git a/components/common/selectors/gl-account/gl-account-selector.tsx b/components/common/selectors/gl-account/gl-account-selector.tsx index 7e47a072..63eeede8 100644 --- a/components/common/selectors/gl-account/gl-account-selector.tsx +++ b/components/common/selectors/gl-account/gl-account-selector.tsx @@ -5,9 +5,8 @@ * * @description * - 오라클에서 GL 계정들을 조회 - * - SAKNR: 계정(G/L) - * - FIPEX: 세부계정 - * - TEXT1: 계정명 + * - GL: 계정 + * - GL명: 계정명 */ import { useState, useCallback, useMemo, useTransition } from 'react' @@ -52,7 +51,6 @@ export interface GlAccountSelectorProps { export interface GlAccountItem { saknr: string // 계정(G/L) - fipex: string // 세부계정 text1: string // 계정명 displayText: string // 표시용 텍스트 } @@ -76,7 +74,7 @@ export function GlAccountSelector({ // GL 계정 선택 핸들러 const handleCodeSelect = useCallback(async (code: GlAccount) => { // 이미 선택된 계정을 다시 선택하면 선택 해제 - if (selectedCode && selectedCode.SAKNR === code.SAKNR && selectedCode.FIPEX === code.FIPEX) { + if (selectedCode && selectedCode.SAKNR === code.SAKNR) { onCodeSelect(undefined as any) // 선택 해제를 위해 undefined 전달 setOpen(false) return @@ -101,13 +99,6 @@ export function GlAccountSelector({ ), }, { - accessorKey: 'FIPEX', - header: '세부계정', - cell: ({ row }) => ( - <div className="font-mono text-sm">{row.getValue('FIPEX')}</div> - ), - }, - { accessorKey: 'TEXT1', header: '계정명', cell: ({ row }) => ( @@ -206,7 +197,6 @@ export function GlAccountSelector({ {selectedCode ? ( <div className="flex items-center gap-2 w-full"> <span className="font-mono text-sm">[{selectedCode.SAKNR}]</span> - <span className="font-mono text-sm">{selectedCode.FIPEX}</span> <span className="truncate flex-1 text-left">{selectedCode.TEXT1}</span> <Button variant="ghost" @@ -237,7 +227,7 @@ export function GlAccountSelector({ <div className="flex items-center space-x-2"> <Search className="h-4 w-4" /> <Input - placeholder="계정, 세부계정, 계정명으로 검색..." + placeholder="계정, 계정명으로 검색..." value={globalFilter} onChange={(e) => handleSearchChange(e.target.value)} className="flex-1" @@ -284,7 +274,7 @@ export function GlAccountSelector({ )} </TableCell> ))} - {selectedCode && selectedCode.SAKNR === row.original.SAKNR && selectedCode.FIPEX === row.original.FIPEX && ( + {selectedCode && selectedCode.SAKNR === row.original.SAKNR && ( <TableCell className="text-right"> <span className="text-xs text-muted-foreground">(선택됨)</span> </TableCell> diff --git a/components/common/selectors/gl-account/gl-account-service.ts b/components/common/selectors/gl-account/gl-account-service.ts index 75c82c95..3feaf24a 100644 --- a/components/common/selectors/gl-account/gl-account-service.ts +++ b/components/common/selectors/gl-account/gl-account-service.ts @@ -5,17 +5,16 @@ import { oracleKnex } from '@/lib/oracle-db/db' // GL 계정 타입 정의
export interface GlAccount {
SAKNR: string // 계정 (G/L)
- FIPEX: string // 세부계정
TEXT1: string // 계정명
}
// 테스트 환경용 폴백 데이터
const FALLBACK_TEST_DATA: GlAccount[] = [
- { SAKNR: '53351977', FIPEX: 'FIP001', TEXT1: '원재료 구매(테스트데이터 - 오라클 페칭 실패시)' },
- { SAKNR: '53351978', FIPEX: 'FIP002', TEXT1: '소모품 구매(테스트데이터 - 오라클 페칭 실패시)' },
- { SAKNR: '53351979', FIPEX: 'FIP003', TEXT1: '부품 구매(테스트데이터 - 오라클 페칭 실패시)' },
- { SAKNR: '53351980', FIPEX: 'FIP004', TEXT1: '자재 구매(테스트데이터 - 오라클 페칭 실패시)' },
- { SAKNR: '53351981', FIPEX: 'FIP005', TEXT1: '외주 가공비(테스트데이터 - 오라클 페칭 실패시)' },
+ { SAKNR: '53351977', TEXT1: '원재료 구매(테스트데이터 - 오라클 페칭 실패시)' },
+ { SAKNR: '53351978', TEXT1: '소모품 구매(테스트데이터 - 오라클 페칭 실패시)' },
+ { SAKNR: '53351979', TEXT1: '부품 구매(테스트데이터 - 오라클 페칭 실패시)' },
+ { SAKNR: '53351980', TEXT1: '자재 구매(테스트데이터 - 오라클 페칭 실패시)' },
+ { SAKNR: '53351981', TEXT1: '외주 가공비(테스트데이터 - 오라클 페칭 실패시)' },
]
/**
@@ -32,15 +31,10 @@ export async function getGlAccounts(): Promise<{ console.log('📋 [getGlAccounts] Oracle 쿼리 시작...')
const result = await oracleKnex.raw(`
- SELECT
- SAKNR,
- FIPEX,
- TEXT1"
- FROM CMCTB_BGT_MNG_ITM
- WHERE ROWNUM < 100
- AND BUKRS = 'H100'
- ORDER BY SAKNR
+ SELECT SAKNR , TEXT1 FROM CMCTB_BGT_MNG_ITM WHERE ROWNUM < 10
`)
+ // SAKNR -> GL
+ // TEXT1 -> GL명
// Oracle raw query의 결과는 rows 배열에 들어있음
const rows = (result.rows || result) as Array<Record<string, unknown>>
@@ -49,14 +43,12 @@ export async function getGlAccounts(): Promise<{ // null 값 필터링
const cleanedResult = rows
- .filter((item) =>
- item['계정(G/L)'] &&
- item['세부계정']
+ .filter((item) =>
+ item.SAKNR
)
.map((item) => ({
- SAKNR: String(item['계정(G/L)']),
- FIPEX: String(item['세부계정']),
- TEXT1: String(item['계정명'] || '')
+ SAKNR: String(item.SAKNR),
+ TEXT1: String(item.TEXT1 || '')
}))
console.log(`✅ [getGlAccounts] 필터링 후 ${cleanedResult.length}건`)
diff --git a/components/common/selectors/gl-account/gl-account-single-selector.tsx b/components/common/selectors/gl-account/gl-account-single-selector.tsx index 55a58a1f..c3237043 100644 --- a/components/common/selectors/gl-account/gl-account-single-selector.tsx +++ b/components/common/selectors/gl-account/gl-account-single-selector.tsx @@ -77,7 +77,7 @@ export function GlAccountSingleSelector({ const handleCodeSelect = useCallback((code: GlAccount) => { // 이미 선택된 계정을 다시 선택하면 선택 해제 const currentSelected = showConfirmButtons ? tempSelectedCode : selectedCode - if (currentSelected && currentSelected.SAKNR === code.SAKNR && currentSelected.FIPEX === code.FIPEX) { + if (currentSelected && currentSelected.SAKNR === code.SAKNR) { if (showConfirmButtons) { setTempSelectedCode(undefined) } else { @@ -121,13 +121,6 @@ export function GlAccountSingleSelector({ ), }, { - accessorKey: 'FIPEX', - header: '세부계정', - cell: ({ row }) => ( - <div className="font-mono text-sm">{row.getValue('FIPEX')}</div> - ), - }, - { accessorKey: 'TEXT1', header: '계정명', cell: ({ row }) => ( @@ -262,8 +255,7 @@ export function GlAccountSingleSelector({ </div> <div className="flex items-center gap-2 mt-1"> <span className="font-mono text-sm">[{currentSelectedCode.SAKNR}]</span> - <span className="font-mono text-sm">{currentSelectedCode.FIPEX}</span> - <span>- {currentSelectedCode.TEXT1}</span> + <span>{currentSelectedCode.TEXT1}</span> </div> </div> )} @@ -271,7 +263,7 @@ export function GlAccountSingleSelector({ <div className="flex items-center space-x-2"> <Search className="h-4 w-4" /> <Input - placeholder="계정, 세부계정, 계정명으로 검색..." + placeholder="계정, 계정명으로 검색..." value={globalFilter} onChange={(e) => handleSearchChange(e.target.value)} className="flex-1" diff --git a/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx b/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx index 90d4975b..84fd85ff 100644 --- a/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx +++ b/components/common/selectors/procurement-item/procurement-item-selector-dialog-single.tsx @@ -131,7 +131,7 @@ export function ProcurementItemSelectorDialogSingle({ <Button variant={triggerVariant} size={triggerSize}> {selectedProcurementItem ? ( <span className="truncate"> - {`${selectedProcurementItem.itemCode} - ${selectedProcurementItem.itemName}`} + {`${selectedProcurementItem.itemCode}`} </span> ) : ( <span className="text-muted-foreground">{triggerLabel}</span> diff --git a/components/common/selectors/wbs-code/wbs-code-selector.tsx b/components/common/selectors/wbs-code/wbs-code-selector.tsx index aa5a6a64..5356c723 100644 --- a/components/common/selectors/wbs-code/wbs-code-selector.tsx +++ b/components/common/selectors/wbs-code/wbs-code-selector.tsx @@ -5,10 +5,8 @@ *
* @description
* - 오라클에서 WBS 코드들을 조회
- * - PROJ_NO: 프로젝트 번호
* - WBS_ELMT: WBS 요소
* - WBS_ELMT_NM: WBS 요소명
- * - WBS_LVL: WBS 레벨
*/
import { useState, useCallback, useMemo, useTransition } from 'react'
@@ -49,7 +47,6 @@ export interface WbsCodeSelectorProps { disabled?: boolean
placeholder?: string
className?: string
- projNo?: string // 프로젝트 번호 필터
}
export interface WbsCodeItem {
@@ -66,8 +63,7 @@ export function WbsCodeSelector({ onCodeSelect,
disabled,
placeholder = "WBS 코드를 선택하세요",
- className,
- projNo
+ className
}: WbsCodeSelectorProps) {
const [open, setOpen] = useState(false)
const [codes, setCodes] = useState<WbsCode[]>([])
@@ -81,7 +77,7 @@ export function WbsCodeSelector({ // WBS 코드 선택 핸들러
const handleCodeSelect = useCallback(async (code: WbsCode) => {
// 이미 선택된 코드를 다시 선택하면 선택 해제
- if (selectedCode && selectedCode.PROJ_NO === code.PROJ_NO && selectedCode.WBS_ELMT === code.WBS_ELMT) {
+ if (selectedCode && selectedCode.WBS_ELMT === code.WBS_ELMT) {
onCodeSelect(undefined as any) // 선택 해제를 위해 undefined 전달
setOpen(false)
return
@@ -99,34 +95,20 @@ export function WbsCodeSelector({ // 테이블 컬럼 정의
const columns: ColumnDef<WbsCode>[] = useMemo(() => [
{
- accessorKey: 'PROJ_NO',
- header: '프로젝트 번호',
- cell: ({ row }) => (
- <div className="font-mono text-sm">{row.getValue('PROJ_NO')}</div>
- ),
- },
- {
accessorKey: 'WBS_ELMT',
- header: 'WBS 요소',
+ header: 'WBS_ELMT',
cell: ({ row }) => (
<div className="font-mono text-sm">{row.getValue('WBS_ELMT')}</div>
),
},
{
accessorKey: 'WBS_ELMT_NM',
- header: 'WBS 요소명',
+ header: 'WBS_ELMT_NM',
cell: ({ row }) => (
<div>{row.getValue('WBS_ELMT_NM')}</div>
),
},
{
- accessorKey: 'WBS_LVL',
- header: '레벨',
- cell: ({ row }) => (
- <div className="text-center">{row.getValue('WBS_LVL')}</div>
- ),
- },
- {
id: 'actions',
header: '선택',
cell: ({ row }) => (
@@ -170,7 +152,7 @@ export function WbsCodeSelector({ const loadCodes = useCallback(async () => {
startTransition(async () => {
try {
- const result = await getWbsCodes(projNo)
+ const result = await getWbsCodes()
if (result.success) {
setCodes(result.data)
@@ -192,7 +174,7 @@ export function WbsCodeSelector({ setCodes([])
}
})
- }, [projNo])
+ }, [])
// 다이얼로그 열기 핸들러
const handleDialogOpenChange = useCallback((newOpen: boolean) => {
@@ -217,8 +199,7 @@ export function WbsCodeSelector({ >
{selectedCode ? (
<div className="flex items-center gap-2 w-full">
- <span className="font-mono text-sm">[{selectedCode.PROJ_NO}]</span>
- <span className="font-mono text-sm">{selectedCode.WBS_ELMT}</span>
+ <span className="font-mono text-sm">[{selectedCode.WBS_ELMT}]</span>
<span className="truncate flex-1 text-left">{selectedCode.WBS_ELMT_NM}</span>
<Button
variant="ghost"
@@ -249,7 +230,7 @@ export function WbsCodeSelector({ <div className="flex items-center space-x-2">
<Search className="h-4 w-4" />
<Input
- placeholder="프로젝트 번호, WBS 요소, WBS 요소명으로 검색..."
+ placeholder="WBS 요소, WBS 요소명으로 검색..."
value={globalFilter}
onChange={(e) => handleSearchChange(e.target.value)}
className="flex-1"
@@ -296,7 +277,7 @@ export function WbsCodeSelector({ )}
</TableCell>
))}
- {selectedCode && selectedCode.PROJ_NO === row.original.PROJ_NO && selectedCode.WBS_ELMT === row.original.WBS_ELMT && (
+ {selectedCode && selectedCode.WBS_ELMT === row.original.WBS_ELMT && (
<TableCell className="text-right">
<span className="text-xs text-muted-foreground">(선택됨)</span>
</TableCell>
diff --git a/components/common/selectors/wbs-code/wbs-code-service.ts b/components/common/selectors/wbs-code/wbs-code-service.ts index 7d9c17b1..6ceedcb7 100644 --- a/components/common/selectors/wbs-code/wbs-code-service.ts +++ b/components/common/selectors/wbs-code/wbs-code-service.ts @@ -4,54 +4,35 @@ import { oracleKnex } from '@/lib/oracle-db/db' // WBS 코드 타입 정의
export interface WbsCode {
- PROJ_NO: string // 프로젝트 번호
WBS_ELMT: string // WBS 요소
WBS_ELMT_NM: string // WBS 요소명
- WBS_LVL: string // WBS 레벨
}
// 테스트 환경용 폴백 데이터
const FALLBACK_TEST_DATA: WbsCode[] = [
- { PROJ_NO: 'SN2661', WBS_ELMT: 'WBS001', WBS_ELMT_NM: 'WBS 항목 1(테스트데이터 - 오라클 페칭 실패시)', WBS_LVL: '1' },
- { PROJ_NO: 'SN2661', WBS_ELMT: 'WBS002', WBS_ELMT_NM: 'WBS 항목 2(테스트데이터 - 오라클 페칭 실패시)', WBS_LVL: '2' },
- { PROJ_NO: 'SN2661', WBS_ELMT: 'WBS003', WBS_ELMT_NM: 'WBS 항목 3(테스트데이터 - 오라클 페칭 실패시)', WBS_LVL: '1' },
- { PROJ_NO: 'SN2661', WBS_ELMT: 'WBS004', WBS_ELMT_NM: 'WBS 항목 4(테스트데이터 - 오라클 페칭 실패시)', WBS_LVL: '2' },
- { PROJ_NO: 'SN2661', WBS_ELMT: 'WBS005', WBS_ELMT_NM: 'WBS 항목 5(테스트데이터 - 오라클 페칭 실패시)', WBS_LVL: '3' },
+ { WBS_ELMT: 'WBS001', WBS_ELMT_NM: 'WBS 항목 1' },
+ { WBS_ELMT: 'WBS002', WBS_ELMT_NM: 'WBS 항목 2' },
+ { WBS_ELMT: 'WBS003', WBS_ELMT_NM: 'WBS 항목 3' },
+ { WBS_ELMT: 'WBS004', WBS_ELMT_NM: 'WBS 항목 4' },
+ { WBS_ELMT: 'WBS005', WBS_ELMT_NM: 'WBS 항목 5' },
]
/**
* WBS 코드 목록 조회 (Oracle에서 전체 조회, 실패 시 폴백 데이터 사용)
* CMCTB_PROJ_WBS 테이블에서 조회
- * @param projNo - 프로젝트 번호 (선택적, 없으면 전체 조회)
*/
-export async function getWbsCodes(projNo?: string): Promise<{
+export async function getWbsCodes(): Promise<{
success: boolean
data: WbsCode[]
error?: string
isUsingFallback?: boolean
}> {
try {
- console.log('📋 [getWbsCodes] Oracle 쿼리 시작...', projNo ? `프로젝트: ${projNo}` : '전체')
+ console.log('📋 [getWbsCodes] Oracle 쿼리 시작...')
- let query = `
- SELECT
- PROJ_NO,
- WBS_ELMT,
- WBS_ELMT_NM,
- WBS_LVL
- FROM CMCTB_PROJ_WBS
- WHERE ROWNUM < 100
- `
-
- if (projNo) {
- query += ` AND PROJ_NO = :projNo`
- }
-
- query += ` ORDER BY PROJ_NO, WBS_ELMT`
-
- const result = projNo
- ? await oracleKnex.raw(query, { projNo })
- : await oracleKnex.raw(query)
+ const result = await oracleKnex.raw(`
+ SELECT WBS_ELMT,WBS_ELMT_NM FROM CMCTB_PROJ_WBS WHERE ROWNUM < 30 AND PROJ_NO = 'SN2673'
+ `)
// Oracle raw query의 결과는 rows 배열에 들어있음
const rows = (result.rows || result) as Array<Record<string, unknown>>
@@ -60,16 +41,13 @@ export async function getWbsCodes(projNo?: string): Promise<{ // null 값 필터링
const cleanedResult = rows
- .filter((item) =>
- item.PROJ_NO &&
- item.WBS_ELMT &&
+ .filter((item) =>
+ item.WBS_ELMT &&
item.WBS_ELMT_NM
)
.map((item) => ({
- PROJ_NO: String(item.PROJ_NO),
WBS_ELMT: String(item.WBS_ELMT),
- WBS_ELMT_NM: String(item.WBS_ELMT_NM),
- WBS_LVL: String(item.WBS_LVL || '')
+ WBS_ELMT_NM: String(item.WBS_ELMT_NM)
}))
console.log(`✅ [getWbsCodes] 필터링 후 ${cleanedResult.length}건`)
diff --git a/components/common/selectors/wbs-code/wbs-code-single-selector.tsx b/components/common/selectors/wbs-code/wbs-code-single-selector.tsx index 77a32afe..5c84b8c2 100644 --- a/components/common/selectors/wbs-code/wbs-code-single-selector.tsx +++ b/components/common/selectors/wbs-code/wbs-code-single-selector.tsx @@ -49,7 +49,6 @@ export interface WbsCodeSingleSelectorProps { title?: string description?: string showConfirmButtons?: boolean - projNo?: string // 프로젝트 번호 필터 } export function WbsCodeSingleSelector({ @@ -61,8 +60,7 @@ export function WbsCodeSingleSelector({ onCancel, title = "WBS 코드 선택", description = "WBS 코드를 선택하세요", - showConfirmButtons = false, - projNo + showConfirmButtons = false }: WbsCodeSingleSelectorProps) { const [codes, setCodes] = useState<WbsCode[]>([]) const [sorting, setSorting] = useState<SortingState>([]) @@ -77,7 +75,7 @@ export function WbsCodeSingleSelector({ const handleCodeSelect = useCallback((code: WbsCode) => { // 이미 선택된 코드를 다시 선택하면 선택 해제 const currentSelected = showConfirmButtons ? tempSelectedCode : selectedCode - if (currentSelected && currentSelected.WBS_ELMT === code.WBS_ELMT && currentSelected.PROJ_NO === code.PROJ_NO) { + if (currentSelected && currentSelected.WBS_ELMT === code.WBS_ELMT) { if (showConfirmButtons) { setTempSelectedCode(undefined) } else { @@ -114,40 +112,26 @@ export function WbsCodeSingleSelector({ // 테이블 컬럼 정의 const columns: ColumnDef<WbsCode>[] = useMemo(() => [ { - accessorKey: 'PROJ_NO', - header: '프로젝트 번호', - cell: ({ row }) => ( - <div className="font-mono text-sm">{row.getValue('PROJ_NO')}</div> - ), - }, - { accessorKey: 'WBS_ELMT', - header: 'WBS 요소', + header: 'WBS_ELMT', cell: ({ row }) => ( <div className="font-mono text-sm">{row.getValue('WBS_ELMT')}</div> ), }, { accessorKey: 'WBS_ELMT_NM', - header: 'WBS 요소명', + header: 'WBS_ELMT_NM', cell: ({ row }) => ( <div>{row.getValue('WBS_ELMT_NM')}</div> ), }, { - accessorKey: 'WBS_LVL', - header: '레벨', - cell: ({ row }) => ( - <div className="text-center">{row.getValue('WBS_LVL')}</div> - ), - }, - { id: 'actions', header: '선택', cell: ({ row }) => { const isSelected = showConfirmButtons - ? tempSelectedCode?.WBS_ELMT === row.original.WBS_ELMT && tempSelectedCode?.PROJ_NO === row.original.PROJ_NO - : selectedCode?.WBS_ELMT === row.original.WBS_ELMT && selectedCode?.PROJ_NO === row.original.PROJ_NO + ? tempSelectedCode?.WBS_ELMT === row.original.WBS_ELMT + : selectedCode?.WBS_ELMT === row.original.WBS_ELMT return ( <Button @@ -191,7 +175,7 @@ export function WbsCodeSingleSelector({ const loadCodes = useCallback(async () => { startTransition(async () => { try { - const result = await getWbsCodes(projNo) + const result = await getWbsCodes() if (result.success) { setCodes(result.data) @@ -213,7 +197,7 @@ export function WbsCodeSingleSelector({ setCodes([]) } }) - }, [projNo]) + }, []) // 다이얼로그 열릴 때 코드 로드 (open prop 변화 감지) useEffect(() => { @@ -268,8 +252,7 @@ export function WbsCodeSingleSelector({ </Button> </div> <div className="flex items-center gap-2 mt-1"> - <span className="font-mono text-sm">[{currentSelectedCode.PROJ_NO}]</span> - <span className="font-mono text-sm">{currentSelectedCode.WBS_ELMT}</span> + <span className="font-mono text-sm">[{currentSelectedCode.WBS_ELMT}]</span> <span>{currentSelectedCode.WBS_ELMT_NM}</span> </div> </div> @@ -278,7 +261,7 @@ export function WbsCodeSingleSelector({ <div className="flex items-center space-x-2"> <Search className="h-4 w-4" /> <Input - placeholder="프로젝트 번호, WBS 요소, WBS 요소명으로 검색..." + placeholder="WBS 요소, WBS 요소명으로 검색..." value={globalFilter} onChange={(e) => handleSearchChange(e.target.value)} className="flex-1" @@ -311,8 +294,7 @@ export function WbsCodeSingleSelector({ <TableBody> {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => { - const isRowSelected = currentSelectedCode?.WBS_ELMT === row.original.WBS_ELMT && - currentSelectedCode?.PROJ_NO === row.original.PROJ_NO + const isRowSelected = currentSelectedCode?.WBS_ELMT === row.original.WBS_ELMT return ( <TableRow key={row.id} |
