diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-13 08:56:27 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-13 08:56:27 +0000 |
| commit | b9a2081a76e669688d5884f20482b37cc8acca22 (patch) | |
| tree | 385e78c05d193a54daaced836f1e1152696153a8 /lib | |
| parent | e84cf02a1cb4959a9d3bb5bbf37885c13a447f78 (diff) | |
(최겸, 임수민) 구매 입찰, 견적(그룹코드, tbe에러) 수정, data-room 수정
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/bidding/list/biddings-stats-cards.tsx | 4 | ||||
| -rw-r--r-- | lib/bidding/list/biddings-table-columns.tsx | 34 | ||||
| -rw-r--r-- | lib/bidding/list/biddings-table.tsx | 5 | ||||
| -rw-r--r-- | lib/bidding/list/create-bidding-dialog.tsx | 650 | ||||
| -rw-r--r-- | lib/bidding/list/edit-bidding-sheet.tsx | 4 | ||||
| -rw-r--r-- | lib/bidding/service.ts | 16 | ||||
| -rw-r--r-- | lib/bidding/validation.ts | 17 | ||||
| -rw-r--r-- | lib/rfq-last/attachment/rfq-attachments-table.tsx | 6 | ||||
| -rw-r--r-- | lib/rfq-last/service.ts | 29 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/batch-update-conditions-dialog.tsx | 69 | ||||
| -rw-r--r-- | lib/rfq-last/vendor/vendor-detail-dialog.tsx | 10 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/bidding-and-pr-mapper.ts | 12 | ||||
| -rw-r--r-- | lib/soap/ecc/mapper/common-mapper-utils.ts | 7 |
13 files changed, 473 insertions, 390 deletions
diff --git a/lib/bidding/list/biddings-stats-cards.tsx b/lib/bidding/list/biddings-stats-cards.tsx index 2926adac..14e29c16 100644 --- a/lib/bidding/list/biddings-stats-cards.tsx +++ b/lib/bidding/list/biddings-stats-cards.tsx @@ -60,9 +60,9 @@ export function BiddingsStatsCards({ </CardHeader> <CardContent> <div className="text-2xl font-bold">{total.toLocaleString()}</div> - <p className="text-xs text-muted-foreground"> + {/* <p className="text-xs text-muted-foreground"> 이번 달 <span className="font-medium text-green-600">+{thisMonthCount}</span>건 - </p> + </p> */} </CardContent> </Card> diff --git a/lib/bidding/list/biddings-table-columns.tsx b/lib/bidding/list/biddings-table-columns.tsx index 7f0b8e40..4900d18a 100644 --- a/lib/bidding/list/biddings-table-columns.tsx +++ b/lib/bidding/list/biddings-table-columns.tsx @@ -5,6 +5,7 @@ import { type ColumnDef } from "@tanstack/react-table" import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" +import { getUserCodeByEmail } from "@/lib/bidding/service" import { Eye, Edit, MoreHorizontal, FileText, Users, Calendar, Building, Package, DollarSign, Clock, CheckCircle, XCircle, @@ -26,6 +27,12 @@ import { import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" import { BiddingListItem } from "@/db/schema" import { DataTableRowAction } from "@/types/table" + +// BiddingListItem에 manager 정보 추가 +type BiddingListItemWithManagerCode = BiddingListItem & { + managerName?: string | null + managerCode?: string | null +} import { biddingStatusLabels, contractTypeLabels, @@ -35,7 +42,7 @@ import { import { formatDate } from "@/lib/utils" interface GetColumnsProps { - setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BiddingListItem> | null>> + setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<BiddingListItemWithManagerCode> | null>> } // 상태별 배지 색상 @@ -78,7 +85,8 @@ const formatCurrency = (amount: string | number | null, currency = 'KRW') => { -export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef<BiddingListItem>[] { +export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef<BiddingListItemWithManagerCode>[] { + return [ // ═══════════════════════════════════════════════════════════════ // 선택 및 기본 정보 @@ -191,11 +199,11 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef { accessorKey: "managerName", header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="입찰담당자" />, - cell: ({ row }) => ( - <div className="truncate max-w-[100px]" title={row.original.managerName || ''}> - {row.original.managerName || '-'} - </div> - ), + cell: ({ row }) => { + const name = row.original.managerName || "-"; + const managerCode = row.original.managerCode || ""; + return name === "-" ? "-" : `${name}(${managerCode})`; + }, size: 100, meta: { excelHeader: "입찰담당자" }, }, @@ -237,10 +245,12 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef <div className="truncate max-w-[200px]" title={row.original.title}> <Button variant="link" - className="p-0 h-auto text-left justify-start" + className="p-0 h-auto text-left justify-start font-bold underline" onClick={() => setRowAction({ row, type: "view" })} > - {row.original.title} + <div className="whitespace-pre-line"> + {row.original.title} + </div> </Button> </div> ), @@ -394,7 +404,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="예산" />, cell: ({ row }) => ( <span className="text-sm font-medium"> - {formatCurrency(row.original.budget, row.original.currency)} + {row.original.budget} </span> ), size: 120, @@ -406,7 +416,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="내정가" />, cell: ({ row }) => ( <span className="text-sm font-medium text-orange-600"> - {formatCurrency(row.original.targetPrice, row.original.currency)} + {row.original.targetPrice} </span> ), size: 120, @@ -418,7 +428,7 @@ export function getBiddingsColumns({ setRowAction }: GetColumnsProps): ColumnDef header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="최종입찰가" />, cell: ({ row }) => ( <span className="text-sm font-medium text-green-600"> - {formatCurrency(row.original.finalBidPrice, row.original.currency)} + {row.original.finalBidPrice} </span> ), size: 120, diff --git a/lib/bidding/list/biddings-table.tsx b/lib/bidding/list/biddings-table.tsx index 2ecfaa73..8920d9db 100644 --- a/lib/bidding/list/biddings-table.tsx +++ b/lib/bidding/list/biddings-table.tsx @@ -14,6 +14,7 @@ import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-adv import { getBiddingsColumns } from "./biddings-table-columns" import { getBiddings, getBiddingStatusCounts } from "@/lib/bidding/service" import { BiddingListItem } from "@/db/schema" +import { BiddingListItemWithManagerCode } from "./biddings-table-columns" import { BiddingsTableToolbarActions } from "./biddings-table-toolbar-actions" import { biddingStatusLabels, @@ -42,11 +43,11 @@ export function BiddingsTable({ promises }: BiddingsTableProps) { const [isCompact, setIsCompact] = React.useState<boolean>(false) const [specMeetingDialogOpen, setSpecMeetingDialogOpen] = React.useState(false) const [prDocumentsDialogOpen, setPrDocumentsDialogOpen] = React.useState(false) - const [selectedBidding, setSelectedBidding] = React.useState<BiddingListItem | null>(null) + const [selectedBidding, setSelectedBidding] = React.useState<BiddingListItemWithManagerCode | null>(null) console.log(data,"data") - const [rowAction, setRowAction] = React.useState<DataTableRowAction<BiddingListItem> | null>(null) + const [rowAction, setRowAction] = React.useState<DataTableRowAction<BiddingListItemWithManagerCode> | null>(null) const router = useRouter() diff --git a/lib/bidding/list/create-bidding-dialog.tsx b/lib/bidding/list/create-bidding-dialog.tsx index e99ac06f..50246f58 100644 --- a/lib/bidding/list/create-bidding-dialog.tsx +++ b/lib/bidding/list/create-bidding-dialog.tsx @@ -126,7 +126,7 @@ interface PRItemInfo { } // 탭 순서 정의 -const TAB_ORDER = ["basic", "contract", "schedule", "conditions", "details", "manager"] as const +const TAB_ORDER = ["basic", "schedule", "details", "manager"] as const type TabType = typeof TAB_ORDER[number] export function CreateBiddingDialog() { @@ -184,11 +184,11 @@ export function CreateBiddingDialog() { // 파일 첨부를 위해 선택된 아이템 ID const [selectedItemForFile, setSelectedItemForFile] = React.useState<string | null>(null) - // 입찰 조건 상태 + // 입찰 조건 상태 (기본값 설정 포함) const [biddingConditions, setBiddingConditions] = React.useState({ - paymentTerms: "", - taxConditions: "", - incoterms: "", + paymentTerms: "", // 초기값 빈값, 데이터 로드 후 설정 + taxConditions: "", // 초기값 빈값, 데이터 로드 후 설정 + incoterms: "", // 초기값 빈값, 데이터 로드 후 설정 contractDeliveryDate: "", shippingPort: "", destinationPort: "", @@ -202,26 +202,49 @@ export function CreateBiddingDialog() { try { const data = await getPaymentTermsForSelection(); setPaymentTermsOptions(data); + // 기본값 설정 로직: P008이 있으면 P008로, 없으면 첫 번째 항목으로 설정 + const setDefaultPaymentTerms = () => { + const p008Exists = data.some(item => item.code === "P008"); + if (p008Exists) { + setBiddingConditions(prev => ({ ...prev, paymentTerms: "P008" })); + } + }; + + setDefaultPaymentTerms(); } catch (error) { console.error("Failed to load payment terms:", error); toast.error("결제조건 목록을 불러오는데 실패했습니다."); + // 에러 시 기본값 초기화 + if (biddingConditions.paymentTerms === "P008") { + setBiddingConditions(prev => ({ ...prev, paymentTerms: "" })); + } } finally { setProcurementLoading(false); } - }, []); + }, [biddingConditions.paymentTerms]); const loadIncoterms = React.useCallback(async () => { setProcurementLoading(true); try { const data = await getIncotermsForSelection(); setIncotermsOptions(data); + + // 기본값 설정 로직: DAP가 있으면 DAP로, 없으면 첫 번째 항목으로 설정 + const setDefaultIncoterms = () => { + const dapExists = data.some(item => item.code === "DAP"); + if (dapExists) { + setBiddingConditions(prev => ({ ...prev, incoterms: "DAP" })); + } + }; + + setDefaultIncoterms(); } catch (error) { console.error("Failed to load incoterms:", error); toast.error("운송조건 목록을 불러오는데 실패했습니다."); } finally { setProcurementLoading(false); } - }, []); + }, [biddingConditions.incoterms]); const loadShippingPlaces = React.useCallback(async () => { setProcurementLoading(true); @@ -249,13 +272,19 @@ export function CreateBiddingDialog() { } }, []); - // 다이얼로그 열릴 때 procurement 데이터 로드 + // 다이얼로그 열릴 때 procurement 데이터 로드 및 기본값 설정 React.useEffect(() => { if (open) { loadPaymentTerms(); loadIncoterms(); loadShippingPlaces(); loadDestinationPlaces(); + + // 세금조건 기본값 설정 (V1이 있는지 확인하고 설정) + const v1Exists = TAX_CONDITIONS.some(item => item.code === "V1"); + if (v1Exists) { + setBiddingConditions(prev => ({ ...prev, taxConditions: "V1" })); + } } }, [open, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces]) @@ -294,6 +323,7 @@ export function CreateBiddingDialog() { contractType: "general", biddingType: "equipment", + biddingTypeCustom: "", awardCount: "single", contractStartDate: "", contractEndDate: "", @@ -344,10 +374,8 @@ export function CreateBiddingDialog() { return { basic: { - isValid: formValues.projectId > 0 && - formValues.itemName.trim() !== "" && - formValues.title.trim() !== "", - hasErrors: !!(formErrors.projectId || formErrors.itemName || formErrors.title) + isValid: formValues.title.trim() !== "", + hasErrors: !!(formErrors.title) }, contract: { isValid: formValues.contractType && @@ -399,11 +427,18 @@ export function CreateBiddingDialog() { return representativeItem?.prNumber || "" }, [prItems]) - // hasPrDocument 필드와 prNumber를 자동으로 업데이트 + // 대표 품목명 자동 계산 (첫 번째 PR 아이템의 itemInfo) + const representativeItemName = React.useMemo(() => { + const representativeItem = prItems.find(item => item.isRepresentative) + return representativeItem?.itemInfo || "" + }, [prItems]) + + // hasPrDocument 필드와 prNumber, itemName을 자동으로 업데이트 React.useEffect(() => { form.setValue("hasPrDocument", hasPrDocuments) form.setValue("prNumber", representativePrNumber) - }, [hasPrDocuments, representativePrNumber, form]) + form.setValue("itemName", representativeItemName) + }, [hasPrDocuments, representativePrNumber, representativeItemName, form]) @@ -525,7 +560,7 @@ export function CreateBiddingDialog() { if (!isCurrentTabValid()) { // 특정 탭별 에러 메시지 if (activeTab === "basic") { - toast.error("기본 정보를 모두 입력해주세요 (프로젝트, 품목명, 입찰명)") + toast.error("기본 정보를 모두 입력해주세요 (품목명, 입찰명)") } else if (activeTab === "contract") { toast.error("계약 정보를 모두 입력해주세요") } else if (activeTab === "schedule") { @@ -535,7 +570,7 @@ export function CreateBiddingDialog() { toast.error("제출 시작일시와 마감일시를 입력해주세요") } } else if (activeTab === "conditions") { - toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건, 계약납품일, 선적지, 하역지)") + toast.error("입찰 조건을 모두 입력해주세요 (지급조건, 세금조건, 운송조건, 계약납품일)") } else if (activeTab === "details") { toast.error("품목정보, 수량/단위 또는 중량/중량단위를 입력해주세요") } @@ -617,6 +652,7 @@ export function CreateBiddingDialog() { content: "", contractType: "general", biddingType: "equipment", + biddingTypeCustom: "", awardCount: "single", contractStartDate: "", contractEndDate: "", @@ -625,9 +661,6 @@ export function CreateBiddingDialog() { hasSpecificationMeeting: false, prNumber: "", currency: "KRW", - budget: "", - targetPrice: "", - finalBidPrice: "", status: "bidding_generated", isPublic: false, managerName: "", @@ -780,20 +813,6 @@ export function CreateBiddingDialog() { </button> <button type="button" - onClick={() => setActiveTab("contract")} - className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ - activeTab === "contract" - ? "bg-background text-foreground shadow-sm" - : "text-muted-foreground hover:text-foreground" - }`} - > - 계약정보 - {!tabValidation.contract.isValid && ( - <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span> - )} - </button> - <button - type="button" onClick={() => setActiveTab("schedule")} className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ activeTab === "schedule" @@ -801,27 +820,13 @@ export function CreateBiddingDialog() { : "text-muted-foreground hover:text-foreground" }`} > - 일정회의 + 입찰계획 {!tabValidation.schedule.isValid && ( <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span> )} </button> <button type="button" - onClick={() => setActiveTab("conditions")} - className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ - activeTab === "conditions" - ? "bg-background text-foreground shadow-sm" - : "text-muted-foreground hover:text-foreground" - }`} - > - 입찰조건 - {!tabValidation.conditions.isValid && ( - <span className="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full"></span> - )} - </button> - <button - type="button" onClick={() => setActiveTab("details")} className={`relative px-3 py-2 text-sm font-medium rounded-md transition-all whitespace-nowrap flex-shrink-0 ${ activeTab === "details" @@ -853,7 +858,7 @@ export function CreateBiddingDialog() { <TabsContent value="basic" className="mt-0 space-y-6"> <Card> <CardHeader> - <CardTitle>기본 정보</CardTitle> + <CardTitle>기본 정보 및 계약 정보</CardTitle> </CardHeader> <CardContent className="space-y-6"> {/* 프로젝트 선택 */} @@ -863,7 +868,7 @@ export function CreateBiddingDialog() { render={({ field }) => ( <FormItem> <FormLabel> - 프로젝트 <span className="text-red-500">*</span> + 프로젝트 </FormLabel> <FormControl> <ProjectSelector @@ -877,9 +882,9 @@ export function CreateBiddingDialog() { )} /> - <div className="grid grid-cols-2 gap-6"> + {/* <div className="grid grid-cols-2 gap-6"> */} {/* 품목명 */} - <FormField + {/* <FormField control={form.control} name="itemName" render={({ field }) => ( @@ -896,10 +901,10 @@ export function CreateBiddingDialog() { <FormMessage /> </FormItem> )} - /> + /> */} {/* 리비전 */} - <FormField + {/* <FormField control={form.control} name="revision" render={({ field }) => ( @@ -916,8 +921,8 @@ export function CreateBiddingDialog() { <FormMessage /> </FormItem> )} - /> - </div> + /> */} + {/* </div> */} {/* 입찰명 */} <FormField @@ -957,17 +962,8 @@ export function CreateBiddingDialog() { </FormItem> )} /> - </CardContent> - </Card> - </TabsContent> - {/* 계약 정보 탭 */} - <TabsContent value="contract" className="mt-0 space-y-6"> - <Card> - <CardHeader> - <CardTitle>계약 정보</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> + {/* 계약 정보 섹션 */} <div className="grid grid-cols-2 gap-6"> {/* 계약구분 */} <FormField @@ -1024,6 +1020,28 @@ export function CreateBiddingDialog() { </FormItem> )} /> + + {/* 기타 입찰유형 직접입력 */} + {form.watch("biddingType") === "other" && ( + <FormField + control={form.control} + name="biddingTypeCustom" + render={({ field }) => ( + <FormItem> + <FormLabel> + 기타 입찰유형 <span className="text-red-500">*</span> + </FormLabel> + <FormControl> + <Input + placeholder="직접 입력하세요" + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} </div> <div className="grid grid-cols-2 gap-6"> @@ -1091,15 +1109,8 @@ export function CreateBiddingDialog() { )} /> </div> - </CardContent> - </Card> - <Card> - <CardHeader> - <CardTitle>가격 정보</CardTitle> - </CardHeader> - <CardContent className="space-y-6"> - {/* 통화 */} + {/* 통화 선택만 유지 */} <FormField control={form.control} name="currency" @@ -1126,71 +1137,204 @@ export function CreateBiddingDialog() { )} /> - <div className="grid grid-cols-3 gap-6"> - {/* 예산 */} - <FormField - control={form.control} - name="budget" - render={({ field }) => ( - <FormItem> - <FormLabel>예산</FormLabel> - <FormControl> - <Input - type="number" - step="0.01" - placeholder="0" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + {/* 입찰 조건 섹션 */} + <Card> + <CardHeader> + <CardTitle>입찰 조건</CardTitle> + <p className="text-sm text-muted-foreground"> + 벤더가 사전견적 시 참고할 입찰 조건을 설정하세요 + </p> + </CardHeader> + <CardContent className="space-y-6"> + <div className="grid grid-cols-2 gap-6"> + <div className="space-y-2"> + <label className="text-sm font-medium"> + 지급조건 <span className="text-red-500">*</span> + </label> + <Select + value={biddingConditions.paymentTerms} + onValueChange={(value) => setBiddingConditions(prev => ({ + ...prev, + paymentTerms: value + }))} + > + <SelectTrigger> + <SelectValue placeholder="지급조건 선택" /> + </SelectTrigger> + <SelectContent> + {paymentTermsOptions.length > 0 ? ( + paymentTermsOptions.map((option) => ( + <SelectItem key={option.code} value={option.code}> + {option.code} {option.description && `(${option.description})`} + </SelectItem> + )) + ) : ( + <SelectItem value="loading" disabled> + 데이터를 불러오는 중... + </SelectItem> + )} + </SelectContent> + </Select> + </div> - {/* 내정가 */} - <FormField - control={form.control} - name="targetPrice" - render={({ field }) => ( - <FormItem> - <FormLabel>내정가</FormLabel> - <FormControl> - <Input - type="number" - step="0.01" - placeholder="0" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> + <div className="space-y-2"> + <label className="text-sm font-medium"> + 세금조건 <span className="text-red-500">*</span> + </label> + <Select + value={biddingConditions.taxConditions} + onValueChange={(value) => setBiddingConditions(prev => ({ + ...prev, + taxConditions: value + }))} + > + <SelectTrigger> + <SelectValue placeholder="세금조건 선택" /> + </SelectTrigger> + <SelectContent> + {TAX_CONDITIONS.map((condition) => ( + <SelectItem key={condition.code} value={condition.code}> + {condition.name} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> - {/* 최종입찰가 */} - <FormField - control={form.control} - name="finalBidPrice" - render={({ field }) => ( - <FormItem> - <FormLabel>최종입찰가</FormLabel> - <FormControl> - <Input - type="number" - step="0.01" - placeholder="0" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> + <div className="space-y-2"> + <label className="text-sm font-medium"> + 운송조건(인코텀즈) <span className="text-red-500">*</span> + </label> + <Select + value={biddingConditions.incoterms} + onValueChange={(value) => setBiddingConditions(prev => ({ + ...prev, + incoterms: value + }))} + > + <SelectTrigger> + <SelectValue placeholder="인코텀즈 선택" /> + </SelectTrigger> + <SelectContent> + {incotermsOptions.length > 0 ? ( + incotermsOptions.map((option) => ( + <SelectItem key={option.code} value={option.code}> + {option.code} {option.description && `(${option.description})`} + </SelectItem> + )) + ) : ( + <SelectItem value="loading" disabled> + 데이터를 불러오는 중... + </SelectItem> + )} + </SelectContent> + </Select> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium"> + 계약 납품일 <span className="text-red-500">*</span> + </label> + <Input + type="date" + value={biddingConditions.contractDeliveryDate} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + contractDeliveryDate: e.target.value + }))} + /> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium">선적지 (선택사항)</label> + <Select + value={biddingConditions.shippingPort} + onValueChange={(value) => setBiddingConditions(prev => ({ + ...prev, + shippingPort: value + }))} + > + <SelectTrigger> + <SelectValue placeholder="선적지 선택" /> + </SelectTrigger> + <SelectContent> + {shippingPlaces.length > 0 ? ( + shippingPlaces.map((place) => ( + <SelectItem key={place.code} value={place.code}> + {place.code} {place.description && `(${place.description})`} + </SelectItem> + )) + ) : ( + <SelectItem value="loading" disabled> + 데이터를 불러오는 중... + </SelectItem> + )} + </SelectContent> + </Select> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium">하역지 (선택사항)</label> + <Select + value={biddingConditions.destinationPort} + onValueChange={(value) => setBiddingConditions(prev => ({ + ...prev, + destinationPort: value + }))} + > + <SelectTrigger> + <SelectValue placeholder="하역지 선택" /> + </SelectTrigger> + <SelectContent> + {destinationPlaces.length > 0 ? ( + destinationPlaces.map((place) => ( + <SelectItem key={place.code} value={place.code}> + {place.code} {place.description && `(${place.description})`} + </SelectItem> + )) + ) : ( + <SelectItem value="loading" disabled> + 데이터를 불러오는 중... + </SelectItem> + )} + </SelectContent> + </Select> + </div> + </div> + + <div className="flex items-center space-x-2"> + <Switch + id="price-adjustment" + checked={biddingConditions.isPriceAdjustmentApplicable} + onCheckedChange={(checked) => setBiddingConditions(prev => ({ + ...prev, + isPriceAdjustmentApplicable: checked + }))} + /> + <label htmlFor="price-adjustment" className="text-sm font-medium"> + 연동제 적용 요건 문의 + </label> + </div> + + <div className="space-y-2"> + <label className="text-sm font-medium">스페어파트 옵션</label> + <Textarea + placeholder="스페어파트 관련 옵션을 입력하세요" + value={biddingConditions.sparePartOptions} + onChange={(e) => setBiddingConditions(prev => ({ + ...prev, + sparePartOptions: e.target.value + }))} + rows={3} + /> + </div> + </CardContent> + </Card> </CardContent> </Card> </TabsContent> + {/* 일정 & 회의 탭 */} <TabsContent value="schedule" className="mt-0 space-y-6"> <Card> @@ -1444,204 +1588,40 @@ export function CreateBiddingDialog() { )} </CardContent> </Card> - </TabsContent> - {/* 입찰 조건 탭 */} - <TabsContent value="conditions" className="mt-0 space-y-6"> + {/* 긴급 입찰 설정 */} <Card> <CardHeader> - <CardTitle>입찰 조건</CardTitle> - <p className="text-sm text-muted-foreground"> - 벤더가 사전견적 시 참고할 입찰 조건을 설정하세요 - </p> + <CardTitle>긴급 입찰 설정</CardTitle> </CardHeader> <CardContent className="space-y-6"> - <div className="grid grid-cols-2 gap-6"> - <div className="space-y-2"> - <label className="text-sm font-medium"> - 지급조건 <span className="text-red-500">*</span> - </label> - <Select - value={biddingConditions.paymentTerms} - onValueChange={(value) => setBiddingConditions(prev => ({ - ...prev, - paymentTerms: value - }))} - > - <SelectTrigger> - <SelectValue placeholder="지급조건 선택" /> - </SelectTrigger> - <SelectContent> - {paymentTermsOptions.length > 0 ? ( - paymentTermsOptions.map((option) => ( - <SelectItem key={option.code} value={option.code}> - {option.code} {option.description && `(${option.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> - </div> - - <div className="space-y-2"> - <label className="text-sm font-medium"> - 세금조건 <span className="text-red-500">*</span> - </label> - <Select - value={biddingConditions.taxConditions} - onValueChange={(value) => setBiddingConditions(prev => ({ - ...prev, - taxConditions: value - }))} - > - <SelectTrigger> - <SelectValue placeholder="세금조건 선택" /> - </SelectTrigger> - <SelectContent> - {TAX_CONDITIONS.map((condition) => ( - <SelectItem key={condition.code} value={condition.code}> - {condition.name} - </SelectItem> - ))} - </SelectContent> - </Select> - </div> - - <div className="space-y-2"> - <label className="text-sm font-medium"> - 운송조건(인코텀즈) <span className="text-red-500">*</span> - </label> - <Select - value={biddingConditions.incoterms} - onValueChange={(value) => setBiddingConditions(prev => ({ - ...prev, - incoterms: value - }))} - > - <SelectTrigger> - <SelectValue placeholder="인코텀즈 선택" /> - </SelectTrigger> - <SelectContent> - {incotermsOptions.length > 0 ? ( - incotermsOptions.map((option) => ( - <SelectItem key={option.code} value={option.code}> - {option.code} {option.description && `(${option.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> - </div> - - <div className="space-y-2"> - <label className="text-sm font-medium"> - 계약 납품일 <span className="text-red-500">*</span> - </label> - <Input - type="date" - value={biddingConditions.contractDeliveryDate} - onChange={(e) => setBiddingConditions(prev => ({ - ...prev, - contractDeliveryDate: e.target.value - }))} - /> - </div> - - <div className="space-y-2"> - <label className="text-sm font-medium">선적지 <span className="text-red-500">*</span></label> - <Select - value={biddingConditions.shippingPort} - onValueChange={(value) => setBiddingConditions(prev => ({ - ...prev, - shippingPort: value - }))} - > - <SelectTrigger> - <SelectValue placeholder="선적지 선택" /> - </SelectTrigger> - <SelectContent> - {shippingPlaces.length > 0 ? ( - shippingPlaces.map((place) => ( - <SelectItem key={place.code} value={place.code}> - {place.code} {place.description && `(${place.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> - </div> - - <div className="space-y-2"> - <label className="text-sm font-medium">하역지 <span className="text-red-500">*</span></label> - <Select - value={biddingConditions.destinationPort} - onValueChange={(value) => setBiddingConditions(prev => ({ - ...prev, - destinationPort: value - }))} - > - <SelectTrigger> - <SelectValue placeholder="하역지 선택" /> - </SelectTrigger> - <SelectContent> - {destinationPlaces.length > 0 ? ( - destinationPlaces.map((place) => ( - <SelectItem key={place.code} value={place.code}> - {place.code} {place.description && `(${place.description})`} - </SelectItem> - )) - ) : ( - <SelectItem value="loading" disabled> - 데이터를 불러오는 중... - </SelectItem> - )} - </SelectContent> - </Select> - </div> - </div> - - <div className="flex items-center space-x-2"> - <Switch - id="price-adjustment" - checked={biddingConditions.isPriceAdjustmentApplicable} - onCheckedChange={(checked) => setBiddingConditions(prev => ({ - ...prev, - isPriceAdjustmentApplicable: checked - }))} - /> - <label htmlFor="price-adjustment" className="text-sm font-medium"> - 연동제 적용 가능 - </label> - </div> - - <div className="space-y-2"> - <label className="text-sm font-medium">스페어파트 옵션</label> - <Textarea - placeholder="스페어파트 관련 옵션을 입력하세요" - value={biddingConditions.sparePartOptions} - onChange={(e) => setBiddingConditions(prev => ({ - ...prev, - sparePartOptions: e.target.value - }))} - rows={3} - /> - </div> + <FormField + control={form.control} + name="isUrgent" + render={({ field }) => ( + <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4"> + <div className="space-y-0.5"> + <FormLabel className="text-base"> + 긴급 입찰 + </FormLabel> + <FormDescription> + 긴급 입찰 여부를 설정합니다 + </FormDescription> + </div> + <FormControl> + <Switch + checked={field.value} + onCheckedChange={field.onChange} + /> + </FormControl> + </FormItem> + )} + /> </CardContent> </Card> </TabsContent> + {/* 세부내역 탭 */} <TabsContent value="details" className="mt-0 space-y-6"> <Card> @@ -2029,28 +2009,6 @@ export function CreateBiddingDialog() { )} /> - <FormField - control={form.control} - name="isUrgent" - render={({ field }) => ( - <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4"> - <div className="space-y-0.5"> - <FormLabel className="text-base"> - 긴급 입찰 - </FormLabel> - <FormDescription> - 긴급 입찰 여부를 설정합니다 - </FormDescription> - </div> - <FormControl> - <Switch - checked={field.value} - onCheckedChange={field.onChange} - /> - </FormControl> - </FormItem> - )} - /> <FormField control={form.control} @@ -2073,7 +2031,7 @@ export function CreateBiddingDialog() { </Card> {/* 입찰 생성 요약 */} - <Card> + {/* <Card> <CardHeader> <CardTitle>입찰 생성 요약</CardTitle> </CardHeader> @@ -2129,7 +2087,7 @@ export function CreateBiddingDialog() { </div> </div> </CardContent> - </Card> + </Card> */} </TabsContent> </div> diff --git a/lib/bidding/list/edit-bidding-sheet.tsx b/lib/bidding/list/edit-bidding-sheet.tsx index dc24d0cf..ed3d3f41 100644 --- a/lib/bidding/list/edit-bidding-sheet.tsx +++ b/lib/bidding/list/edit-bidding-sheet.tsx @@ -389,7 +389,7 @@ export function EditBiddingSheet({ /> </div> - <FormField + {/* <FormField control={form.control} name="status" render={({ field }) => ( @@ -412,7 +412,7 @@ export function EditBiddingSheet({ <FormMessage /> </FormItem> )} - /> + /> */} <div className="space-y-3"> <FormField diff --git a/lib/bidding/service.ts b/lib/bidding/service.ts index 8cbe2a2b..5ab18ef1 100644 --- a/lib/bidding/service.ts +++ b/lib/bidding/service.ts @@ -31,6 +31,22 @@ import { like, notInArray } from 'drizzle-orm' + +// 사용자 이메일로 사용자 코드 조회 +export async function getUserCodeByEmail(email: string): Promise<string | null> { + try { + const user = await db + .select({ userCode: users.userCode }) + .from(users) + .where(and(eq(users.email, email), eq(users.isActive, true))) + .limit(1) + + return user[0]?.userCode || null + } catch (error) { + console.error('Failed to get user code by email:', error) + return null + } +} import { revalidatePath } from 'next/cache' import { filterColumns } from '@/lib/filter-columns' import { CreateBiddingSchema, GetBiddingsSchema, UpdateBiddingSchema } from './validation' diff --git a/lib/bidding/validation.ts b/lib/bidding/validation.ts index 2011cd27..8476be1c 100644 --- a/lib/bidding/validation.ts +++ b/lib/bidding/validation.ts @@ -71,6 +71,7 @@ export const createBiddingSchema = z.object({ biddingType: z.enum(biddings.biddingType.enumValues, { required_error: "입찰유형을 선택해주세요" }), + biddingTypeCustom: z.string().optional(), awardCount: z.enum(biddings.awardCount.enumValues, { required_error: "낙찰수를 선택해주세요" }), @@ -89,9 +90,6 @@ export const createBiddingSchema = z.object({ // ✅ 가격 정보 (통화 필수) currency: z.string().min(1, "통화를 선택해주세요").default("KRW"), - budget: z.string().optional(), - targetPrice: z.string().optional(), - finalBidPrice: z.string().optional(), // 상태 및 담당자 status: z.enum(biddings.status.enumValues).default("bidding_generated"), @@ -110,8 +108,8 @@ export const createBiddingSchema = z.object({ taxConditions: z.string().min(1, "세금조건은 필수입니다"), incoterms: z.string().min(1, "운송조건은 필수입니다"), contractDeliveryDate: z.string().min(1, "계약납품일은 필수입니다"), - shippingPort: z.string().min(1, "선적지는 필수입니다"), - destinationPort: z.string().min(1, "하역지는 필수입니다"), + shippingPort: z.string().optional(), + destinationPort: z.string().optional(), isPriceAdjustmentApplicable: z.boolean().default(false), sparePartOptions: z.string().optional(), }).optional(), @@ -126,6 +124,15 @@ export const createBiddingSchema = z.object({ }, { message: "제출시작일시가 제출마감일시보다 늦을 수 없습니다", path: ["submissionEndDate"] + }).refine((data) => { + // 기타 입찰유형 선택 시 직접입력 필드 검증 + if (data.biddingType === "other") { + return data.biddingTypeCustom && data.biddingTypeCustom.trim().length > 0 + } + return true + }, { + message: "기타 입찰유형을 선택한 경우 직접 입력해주세요", + path: ["biddingTypeCustom"] }) export const updateBiddingSchema = z.object({ diff --git a/lib/rfq-last/attachment/rfq-attachments-table.tsx b/lib/rfq-last/attachment/rfq-attachments-table.tsx index 3098f8f5..d97d32fd 100644 --- a/lib/rfq-last/attachment/rfq-attachments-table.tsx +++ b/lib/rfq-last/attachment/rfq-attachments-table.tsx @@ -416,7 +416,7 @@ export function RfqAttachmentsTable({ if (activeTab !== '구매') { return <span className="text-muted-foreground text-sm">-</span>; } - + return ( <DropdownMenu> <DropdownMenuTrigger asChild> @@ -442,7 +442,7 @@ export function RfqAttachmentsTable({ 새 버전 업로드 </DropdownMenuItem> <DropdownMenuSeparator /> - <DropdownMenuItem + <DropdownMenuItem onClick={() => handleAction({ type: "delete", row })} className="text-red-600" > @@ -455,7 +455,7 @@ export function RfqAttachmentsTable({ size: 60, enablePinning: true, }, - ], [handleAction]); + ], [handleAction, activeTab]); const advancedFilterFields: DataTableAdvancedFilterField<RfqAttachment>[] = [ { id: "serialNo", label: "일련번호", type: "text" }, diff --git a/lib/rfq-last/service.ts b/lib/rfq-last/service.ts index f536a142..f600d04b 100644 --- a/lib/rfq-last/service.ts +++ b/lib/rfq-last/service.ts @@ -2574,6 +2574,35 @@ export async function getRfqAttachments(rfqId: number) { return fullInfo.attachments; } +/** + * 특정 벤더의 현재 조건 조회 + */ +export async function getVendorConditions(rfqId: number, vendorId: number) { + const fullInfo = await getRfqFullInfo(rfqId); + const vendor = fullInfo.vendors?.find(v => v.vendorId === vendorId); + + if (!vendor) { + throw new Error('벤더 정보를 찾을 수 없습니다.'); + } + + return { + currency: vendor.currency, + paymentTermsCode: vendor.paymentTermsCode, + incotermsCode: vendor.incotermsCode, + incotermsDetail: vendor.incotermsDetail, + deliveryDate: vendor.deliveryDate, + contractDuration: vendor.contractDuration, + taxCode: vendor.taxCode, + placeOfShipping: vendor.placeOfShipping, + placeOfDestination: vendor.placeOfDestination, + materialPriceRelatedYn: vendor.materialPriceRelatedYn, + sparepartYn: vendor.sparepartYn, + firstYn: vendor.firstYn, + firstDescription: vendor.firstDescription, + sparepartDescription: vendor.sparepartDescription, + }; +} + // RFQ 발송용 데이터 타입 export interface RfqSendData { diff --git a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx index 7eae48db..70d5569f 100644 --- a/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx +++ b/lib/rfq-last/vendor/batch-update-conditions-dialog.tsx @@ -44,7 +44,7 @@ import { format } from "date-fns"; import { ko } from "date-fns/locale"; import { cn } from "@/lib/utils"; import { toast } from "sonner"; -import { updateVendorConditionsBatch } from "../service"; +import { updateVendorConditionsBatch, getVendorConditions } from "../service"; import { Badge } from "@/components/ui/badge"; import { TAX_CONDITIONS } from "@/lib/tax-conditions/types"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -72,8 +72,8 @@ interface BatchUpdateConditionsDialogProps { } // 타입 정의 -interface SelectOption { - id: number; +type SelectOption = { + id?: number; code: string; description: string; } @@ -169,7 +169,7 @@ export function BatchUpdateConditionsDialog({ setIncotermsLoading(true); try { const data = await getIncotermsForSelection(); - setIncoterms(data); + setIncoterms(data as unknown as SelectOption[]); } catch (error) { console.error("Failed to load incoterms:", error); toast.error("Incoterms 목록을 불러오는데 실패했습니다."); @@ -182,7 +182,7 @@ export function BatchUpdateConditionsDialog({ setPaymentTermsLoading(true); try { const data = await getPaymentTermsForSelection(); - setPaymentTerms(data); + setPaymentTerms(data as unknown as SelectOption[]); } catch (error) { console.error("Failed to load payment terms:", error); toast.error("결제조건 목록을 불러오는데 실패했습니다."); @@ -195,7 +195,7 @@ export function BatchUpdateConditionsDialog({ setShippingLoading(true); try { const data = await getPlaceOfShippingForSelection(); - setShippingPlaces(data); + setShippingPlaces(data as unknown as SelectOption[]); } catch (error) { console.error("Failed to load shipping places:", error); toast.error("선적지 목록을 불러오는데 실패했습니다."); @@ -208,7 +208,7 @@ export function BatchUpdateConditionsDialog({ setDestinationLoading(true); try { const data = await getPlaceOfDestinationForSelection(); - setDestinationPlaces(data); + setDestinationPlaces(data as unknown as SelectOption[]); } catch (error) { console.error("Failed to load destination places:", error); toast.error("도착지 목록을 불러오는데 실패했습니다."); @@ -217,6 +217,33 @@ export function BatchUpdateConditionsDialog({ } }, []); + // 벤더별 조건 로드 함수 + const loadVendorConditions = React.useCallback(async (vendorId: number) => { + try { + const conditions = await getVendorConditions(rfqId, vendorId); + // 가져온 조건으로 폼 초기화 + form.reset({ + currency: conditions.currency || "", + paymentTermsCode: conditions.paymentTermsCode || "", + incotermsCode: conditions.incotermsCode || "", + incotermsDetail: conditions.incotermsDetail || "", + deliveryDate: conditions.deliveryDate || undefined, + contractDuration: conditions.contractDuration || "", + taxCode: conditions.taxCode || "", + placeOfShipping: conditions.placeOfShipping || "", + placeOfDestination: conditions.placeOfDestination || "", + materialPriceRelatedYn: conditions.materialPriceRelatedYn || false, + sparepartYn: conditions.sparepartYn || false, + firstYn: conditions.firstYn || false, + firstDescription: conditions.firstDescription || "", + sparepartDescription: conditions.sparepartDescription || "", + }); + } catch (error) { + console.error("Failed to load vendor conditions:", error); + toast.error("벤더 조건을 불러오는데 실패했습니다."); + } + }, [rfqId, form]); + // 초기 데이터 로드 React.useEffect(() => { if (open) { @@ -224,13 +251,35 @@ export function BatchUpdateConditionsDialog({ loadPaymentTerms(); loadShippingPlaces(); loadDestinationPlaces(); + + // 선택된 벤더가 1개일 때만 해당 벤더의 조건을 가져옴 + if (selectedVendors.length === 1) { + loadVendorConditions(selectedVendors[0].id); + } } - }, [open, loadIncoterms, loadPaymentTerms, loadShippingPlaces, loadDestinationPlaces]); + }, [open, loadIncoterms, loadPaymentTerms, loadShippingPlaces, loadDestinationPlaces, selectedVendors, loadVendorConditions]); // 다이얼로그 닫힐 때 초기화 React.useEffect(() => { if (!open) { - form.reset(); + // 선택된 벤더가 2개 이상이거나 없다면 기본값으로 초기화 + if (selectedVendors.length !== 1) { + form.reset({ + currency: "", + paymentTermsCode: "", + incotermsCode: "", + incotermsDetail: "", + contractDuration: "", + taxCode: "", + placeOfShipping: "", + placeOfDestination: "", + materialPriceRelatedYn: false, + sparepartYn: false, + firstYn: false, + firstDescription: "", + sparepartDescription: "", + }); + } setFieldsToUpdate({ currency: false, paymentTermsCode: false, @@ -244,7 +293,7 @@ export function BatchUpdateConditionsDialog({ first: false, }); } - }, [open, form]); + }, [open, form, selectedVendors]); // 제출 처리 const onSubmit = async (data: FormValues) => { diff --git a/lib/rfq-last/vendor/vendor-detail-dialog.tsx b/lib/rfq-last/vendor/vendor-detail-dialog.tsx index 074924eb..08288dd6 100644 --- a/lib/rfq-last/vendor/vendor-detail-dialog.tsx +++ b/lib/rfq-last/vendor/vendor-detail-dialog.tsx @@ -36,6 +36,7 @@ import { Paperclip, Info, Edit, + X, } from "lucide-react"; import { format } from "date-fns"; import { ko } from "date-fns/locale"; @@ -160,6 +161,15 @@ export function VendorResponseDetailDialog({ </DialogDescription> </div> <div className="flex items-center gap-2"> + <Button + variant="ghost" + size="sm" + onClick={() => onOpenChange(false)} + className="h-8 w-8 p-0" + > + <X className="h-4 w-4" /> + <span className="sr-only">창 닫기</span> + </Button> {/* {onEdit && ( <Button variant="outline" size="sm" onClick={onEdit}> <Edit className="h-4 w-4 mr-2" /> diff --git a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts index 99373555..a02ef9bf 100644 --- a/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts +++ b/lib/soap/ecc/mapper/bidding-and-pr-mapper.ts @@ -209,11 +209,11 @@ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<str maxBiddingNumber: max(biddings.biddingNumber) }) .from(biddings) - .where(sql`${biddings.biddingNumber} LIKE ${`BID${ekgrp}%`}`); + .where(sql`${biddings.biddingNumber} LIKE ${`B${ekgrp}%`}`); let nextSeq = 1; if (maxResult[0]?.maxBiddingNumber) { - const prefix = `BID${ekgrp}`; + const prefix = `B${ekgrp}`; const currentCode = maxResult[0].maxBiddingNumber; if (currentCode.startsWith(prefix)) { const seqPart = currentCode.substring(prefix.length); @@ -227,7 +227,7 @@ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<str // 동일 EKGRP 내에서 순차적으로 새 코드 생성 for (const header of headers) { const seqString = nextSeq.toString().padStart(5, '0'); - const biddingCode = `BID${ekgrp}${seqString}`; + const biddingCode = `B${ekgrp}${seqString}`; biddingCodeMap.set(header.ANFNR || '', biddingCode); nextSeq++; // 다음 시퀀스로 증가 } @@ -247,7 +247,7 @@ async function generateBiddingCodes(eccHeaders: ECCBidHeader[]): Promise<Map<str eccHeaders.forEach((header, index) => { const ekgrp = header.EKGRP || 'UNKNOWN'; const seqString = (index + 1).toString().padStart(5, '0'); - fallbackMap.set(header.ANFNR, `BID${ekgrp}${seqString}`); + fallbackMap.set(header.ANFNR, `B${ekgrp}${seqString}`); }); return fallbackMap; } @@ -275,7 +275,7 @@ export async function mapECCBiddingHeaderToBidding( // 담당자 찾기 const inChargeUserInfo = await findUserInfoByEKGRP(eccHeader.EKGRP || null); - + // 첫번째 PR Item 기반으로 projectId, projectName, itemName 설정 let projectId: number | null = null; let projectName: string | null = null; @@ -342,7 +342,7 @@ export async function mapECCBiddingHeaderToBidding( // 담당자 정보 - EKGRP 기반으로 설정 managerName: inChargeUserInfo?.userName || null, - managerEmail: null, + managerEmail: inChargeUserInfo?.userEmail || null, managerPhone: inChargeUserInfo?.userPhone || null, // 메타 정보 diff --git a/lib/soap/ecc/mapper/common-mapper-utils.ts b/lib/soap/ecc/mapper/common-mapper-utils.ts index 526decb5..8558f058 100644 --- a/lib/soap/ecc/mapper/common-mapper-utils.ts +++ b/lib/soap/ecc/mapper/common-mapper-utils.ts @@ -24,11 +24,12 @@ import { eq } from 'drizzle-orm'; export async function findUserInfoByEKGRP(EKGRP: string | null): Promise<{ userId: number; userName: string; + userEmail: string | null; userPhone: string | null; } | null> { try { debugLog('담당자 찾기 시작', { EKGRP }); - + if (!EKGRP) { debugError('EKGRP가 null 또는 undefined', { EKGRP }); return null; @@ -36,9 +37,10 @@ export async function findUserInfoByEKGRP(EKGRP: string | null): Promise<{ // users 테이블에서 userCode로 직접 조회 const userResult = await db - .select({ + .select({ id: users.id, name: users.name, + email: users.email, phone: users.phone }) .from(users) @@ -53,6 +55,7 @@ export async function findUserInfoByEKGRP(EKGRP: string | null): Promise<{ const userInfo = { userId: userResult[0].id, userName: userResult[0].name, + userEmail: userResult[0].email, userPhone: userResult[0].phone }; debugSuccess('담당자 찾음', { EKGRP, userInfo }); |
