From 79cfa7ea8f21ae227dbb2843ae536fe876ba7c55 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 27 Nov 2025 03:08:50 +0000 Subject: (최겸) 구매 입찰 수정 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bidding/create/bidding-create-dialog.tsx | 112 +++++---------------- .../bidding/manage/bidding-basic-info-editor.tsx | 44 ++++---- .../bidding/manage/bidding-companies-editor.tsx | 11 +- components/bidding/manage/bidding-items-editor.tsx | 61 +++++++++-- .../bidding/manage/bidding-schedule-editor.tsx | 26 ++++- .../selectors/cost-center/cost-center-selector.tsx | 4 - 6 files changed, 132 insertions(+), 126 deletions(-) (limited to 'components') diff --git a/components/bidding/create/bidding-create-dialog.tsx b/components/bidding/create/bidding-create-dialog.tsx index cf662cd1..f298721b 100644 --- a/components/bidding/create/bidding-create-dialog.tsx +++ b/components/bidding/create/bidding-create-dialog.tsx @@ -36,10 +36,6 @@ import { getPlaceOfDestinationForSelection, } from '@/lib/procurement-select/service' import { TAX_CONDITIONS } from '@/lib/tax-conditions/types' -import { getBiddingNoticeTemplate } from '@/lib/bidding/service' -import TiptapEditor from '@/components/qna/tiptap-editor' - -// Dropzone components import { Dropzone, DropzoneDescription, @@ -101,10 +97,6 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp const [selectedBidPic, setSelectedBidPic] = React.useState(undefined) const [selectedSupplyPic, setSelectedSupplyPic] = React.useState(undefined) - // 입찰공고 템플릿 관련 상태 - const [noticeTemplate, setNoticeTemplate] = React.useState('') - const [isLoadingTemplate, setIsLoadingTemplate] = React.useState(false) - // -- 데이터 로딩 및 상태 동기화 로직 const loadPaymentTerms = React.useCallback(async () => { try { @@ -174,59 +166,9 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp setBiddingConditions((prev) => ({ ...prev, taxConditions: 'V1' })) form.setValue('biddingConditions.taxConditions', 'V1') } - - // 초기 표준 템플릿 로드 - const loadInitialTemplate = async () => { - try { - const standardTemplate = await getBiddingNoticeTemplate('standard') - if (standardTemplate) { - console.log('standardTemplate', standardTemplate) - setNoticeTemplate(standardTemplate.content) - form.setValue('content', standardTemplate.content) - } - } catch (error) { - console.error('Failed to load initial template:', error) - toast.error('기본 템플릿을 불러오는데 실패했습니다.') - } - } - loadInitialTemplate() } }, [isOpen, loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces, form]) - // 입찰공고 템플릿 로딩 - const noticeTypeValue = form.watch('noticeType') - const selectedNoticeType = React.useMemo(() => noticeTypeValue, [noticeTypeValue]) - - React.useEffect(() => { - const loadNoticeTemplate = async () => { - setIsLoadingTemplate(true) - try { - // 처음 로드할 때는 무조건 standard 템플릿 사용 - const templateType = selectedNoticeType || 'standard' - const template = await getBiddingNoticeTemplate(templateType) - if (template) { - setNoticeTemplate(template.content) - // 폼의 content 필드도 업데이트 - form.setValue('content', template.content) - } else { - // 템플릿이 없으면 표준 템플릿 사용 - const defaultTemplate = await getBiddingNoticeTemplate('standard') - if (defaultTemplate) { - setNoticeTemplate(defaultTemplate.content) - form.setValue('content', defaultTemplate.content) - } - } - } catch (error) { - console.error('Failed to load notice template:', error) - toast.error('입찰공고 템플릿을 불러오는데 실패했습니다.') - } finally { - setIsLoadingTemplate(false) - } - } - - loadNoticeTemplate() - }, [selectedNoticeType, form]) - // SHI용 파일 첨부 핸들러 const handleShiFileUpload = (event: React.ChangeEvent) => { const files = Array.from(event.target.files || []) @@ -313,7 +255,6 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp setVendorAttachmentFiles([]) setSelectedBidPic(undefined) setSelectedSupplyPic(undefined) - setNoticeTemplate('') if (onSuccess) { onSuccess() } @@ -337,7 +278,6 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp setVendorAttachmentFiles([]) setSelectedBidPic(undefined) setSelectedSupplyPic(undefined) - setNoticeTemplate('') setBiddingConditions({ paymentTerms: '', taxConditions: 'V1', @@ -1097,7 +1037,7 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp {/* 입찰공고 내용 */} - + {/* 입찰공고 내용

@@ -1127,14 +1067,14 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp )} /> - {/* {isLoadingTemplate && ( + {isLoadingTemplate && (

입찰공고 템플릿을 불러오는 중...
- )} */} + )} -
+
*/} {/* SHI용 첨부파일 */} @@ -1162,18 +1102,16 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp }) }} > - {() => ( - - -
- -
- 파일을 드래그하여 업로드 -
-
-
-
- )} + + + + 파일을 드래그하거나 클릭하여 업로드 + + + PDF, Word, Excel, 이미지 파일 (최대 600MB) + + + {shiAttachmentFiles.length > 0 && ( @@ -1236,18 +1174,16 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp }) }} > - {() => ( - - -
- -
- 파일을 드래그하여 업로드 -
-
-
-
- )} + + + + 파일을 드래그하거나 클릭하여 업로드 + + + 협력업체용 문서나 파일을 업로드 하세요. (최대 600MB) + + + {vendorAttachmentFiles.length > 0 && ( diff --git a/components/bidding/manage/bidding-basic-info-editor.tsx b/components/bidding/manage/bidding-basic-info-editor.tsx index e2d888ff..90923825 100644 --- a/components/bidding/manage/bidding-basic-info-editor.tsx +++ b/components/bidding/manage/bidding-basic-info-editor.tsx @@ -1141,18 +1141,16 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB }) }} > - {() => ( - - -
- -
- 파일을 드래그하여 업로드 -
-
-
-
- )} + + + + 파일을 드래그하거나 클릭하여 업로드 + + + PDF, Word, Excel, 이미지 파일 (최대 600MB) + + + @@ -1235,18 +1233,16 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB }) }} > - {() => ( - - -
- -
- 파일을 드래그하여 업로드 -
-
-
-
- )} + + + + 파일을 드래그하거나 클릭하여 업로드 + + + 협력업체용 문서나 파일을 업로드 하세요. (최대 600MB) + + + diff --git a/components/bidding/manage/bidding-companies-editor.tsx b/components/bidding/manage/bidding-companies-editor.tsx index da566c82..f6b3a3f0 100644 --- a/components/bidding/manage/bidding-companies-editor.tsx +++ b/components/bidding/manage/bidding-companies-editor.tsx @@ -271,6 +271,12 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC return } + // 전화번호 형식 검증 (국제 표준) + if (newContact.contactNumber && !newContact.contactNumber.startsWith('+')) { + toast.error('전화번호는 국제 표준 형식(+)으로 시작해야 합니다. (예: +821012345678)') + return + } + try { const result = await createBiddingCompanyContact( biddingId, @@ -703,8 +709,11 @@ export function BiddingCompaniesEditor({ biddingId, readonly = false }: BiddingC id="contactNumber" value={newContact.contactNumber} onChange={(e) => setNewContact(prev => ({ ...prev, contactNumber: e.target.value }))} - placeholder="010-1234-5678" + placeholder="+821012345678" /> +

+ * SMS 발송을 위해 국제 표준 형식으로 입력해주세요. (예: +821012345678) +

diff --git a/components/bidding/manage/bidding-items-editor.tsx b/components/bidding/manage/bidding-items-editor.tsx index 208cf040..f61b3960 100644 --- a/components/bidding/manage/bidding-items-editor.tsx +++ b/components/bidding/manage/bidding-items-editor.tsx @@ -227,6 +227,57 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems const userId = session?.user?.id?.toString() || '1' let hasError = false + // 필수값 검증 + for (let i = 0; i < items.length; i++) { + const item = items[i]; + + // 필수값: 자재그룹코드, 자재그룹명 + if (!item.materialGroupNumber || !item.materialGroupInfo) { + toast.error(`${i + 1}번 품목의 자재그룹 정보를 입력해주세요.`); + setIsSubmitting(false); + return; + } + + // 필수값: 수량 또는 중량 + if (quantityWeightMode === 'quantity') { + if (!item.quantity || parseFloat(item.quantity) <= 0) { + toast.error(`${i + 1}번 품목의 수량을 입력해주세요.`); + setIsSubmitting(false); + return; + } + if (!item.quantityUnit) { + toast.error(`${i + 1}번 품목의 수량 단위를 선택해주세요.`); + setIsSubmitting(false); + return; + } + } else { + if (!item.totalWeight || parseFloat(item.totalWeight) <= 0) { + toast.error(`${i + 1}번 품목의 중량을 입력해주세요.`); + setIsSubmitting(false); + return; + } + if (!item.weightUnit) { + toast.error(`${i + 1}번 품목의 중량 단위를 선택해주세요.`); + setIsSubmitting(false); + return; + } + } + + // 필수값: 납품요청일 + if (!item.requestedDeliveryDate) { + toast.error(`${i + 1}번 품목의 납품요청일을 입력해주세요.`); + setIsSubmitting(false); + return; + } + + // 필수값: 내정단가 (사용자 요청) + if (!item.targetUnitPrice || parseFloat(item.targetUnitPrice.replace(/,/g, '')) <= 0) { + toast.error(`${i + 1}번 품목의 내정단가를 입력해주세요.`); + setIsSubmitting(false); + return; + } + } + // 모든 아이템을 upsert 처리 (id가 있으면 update, 없으면 insert) for (const item of items) { const targetAmount = calculateTargetAmount(item) @@ -1111,10 +1162,8 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems if (!open) setSelectedItemForWbs(null) }} selectedCode={item.wbsCode ? { - PROJ_NO: '', WBS_ELMT: item.wbsCode, WBS_ELMT_NM: item.wbsName || '', - WBS_LVL: '' } : undefined} onCodeSelect={(wbsCode) => { updatePRItem(item.id, { @@ -1167,15 +1216,12 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems }} selectedCode={item.costCenterCode ? { KOSTL: item.costCenterCode, - KTEXT: '', - LTEXT: item.costCenterName || '', - DATAB: '', - DATBI: '' + KTEXT: item.costCenterName || '', } : undefined} onCodeSelect={(costCenter) => { updatePRItem(item.id, { costCenterCode: costCenter.KOSTL, - costCenterName: costCenter.LTEXT + costCenterName: costCenter.KTEXT }) setCostCenterDialogOpen(false) setSelectedItemForCostCenter(null) @@ -1223,7 +1269,6 @@ export function BiddingItemsEditor({ biddingId, readonly = false }: BiddingItems }} selectedCode={item.glAccountCode ? { SAKNR: item.glAccountCode, - FIPEX: '', TEXT1: item.glAccountName || '' } : undefined} onCodeSelect={(glAccount) => { diff --git a/components/bidding/manage/bidding-schedule-editor.tsx b/components/bidding/manage/bidding-schedule-editor.tsx index b5f4aaf0..ca4643ff 100644 --- a/components/bidding/manage/bidding-schedule-editor.tsx +++ b/components/bidding/manage/bidding-schedule-editor.tsx @@ -324,11 +324,23 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc vendors: selectedVendors, message: invitationData.message || '', currentUser: { - id: session.user.id, + id: Number(session.user.id), epId: session.user.epId, email: session.user.email || undefined, }, approvers, + specificationMeeting: schedule.hasSpecificationMeeting ? { + meetingDate: specMeetingInfo.meetingDate, + meetingTime: specMeetingInfo.meetingTime, + location: specMeetingInfo.location, + address: specMeetingInfo.address, + contactPerson: specMeetingInfo.contactPerson, + contactPhone: specMeetingInfo.contactPhone, + contactEmail: specMeetingInfo.contactEmail, + agenda: specMeetingInfo.agenda, + materials: specMeetingInfo.materials, + notes: specMeetingInfo.notes, + } : undefined, }) if (result.status === 'pending_approval') { @@ -428,6 +440,18 @@ export function BiddingScheduleEditor({ biddingId, readonly = false }: BiddingSc biddingId, vendors, message: data.message || '', + specificationMeeting: schedule.hasSpecificationMeeting ? { + meetingDate: specMeetingInfo.meetingDate, + meetingTime: specMeetingInfo.meetingTime, + location: specMeetingInfo.location, + address: specMeetingInfo.address, + contactPerson: specMeetingInfo.contactPerson, + contactPhone: specMeetingInfo.contactPhone, + contactEmail: specMeetingInfo.contactEmail, + agenda: specMeetingInfo.agenda, + materials: specMeetingInfo.materials, + notes: specMeetingInfo.notes, + } : undefined, }) // 결재 준비 완료 - invitationData와 결재 데이터 저장 및 결재 다이얼로그 열기 diff --git a/components/common/selectors/cost-center/cost-center-selector.tsx b/components/common/selectors/cost-center/cost-center-selector.tsx index f87b6928..8c733cd0 100644 --- a/components/common/selectors/cost-center/cost-center-selector.tsx +++ b/components/common/selectors/cost-center/cost-center-selector.tsx @@ -55,10 +55,6 @@ export interface CostCenterSelectorProps { export interface CostCenterItem { kostl: string // Cost Center ktext: string // 단축명 - ltext: string // 설명 - datab: string // 시작일 - datbi: string // 종료일 - displayText: string // 표시용 텍스트 } export function CostCenterSelector({ -- cgit v1.2.3