summaryrefslogtreecommitdiff
path: root/components/bidding
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-27 03:08:50 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-27 03:08:50 +0000
commit79cfa7ea8f21ae227dbb2843ae536fe876ba7c55 (patch)
treef12efae72c62286c1a2e9a3f31d695ca22d83b6e /components/bidding
parente1da84ac863989b9f63b089c09aaa2bbcdc3d6cd (diff)
(최겸) 구매 입찰 수정
Diffstat (limited to 'components/bidding')
-rw-r--r--components/bidding/create/bidding-create-dialog.tsx112
-rw-r--r--components/bidding/manage/bidding-basic-info-editor.tsx44
-rw-r--r--components/bidding/manage/bidding-companies-editor.tsx11
-rw-r--r--components/bidding/manage/bidding-items-editor.tsx61
-rw-r--r--components/bidding/manage/bidding-schedule-editor.tsx26
5 files changed, 132 insertions, 122 deletions
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<PurchaseGroupCodeWithUser | undefined>(undefined)
const [selectedSupplyPic, setSelectedSupplyPic] = React.useState<ProcurementManagerWithUser | undefined>(undefined)
- // 입찰공고 템플릿 관련 상태
- const [noticeTemplate, setNoticeTemplate] = React.useState<string>('')
- 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<HTMLInputElement>) => {
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
</Card>
{/* 입찰공고 내용 */}
- <Card>
+ {/* <Card>
<CardHeader>
<CardTitle>입찰공고 내용</CardTitle>
<p className="text-sm text-muted-foreground">
@@ -1127,14 +1067,14 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp
)}
/>
- {/* {isLoadingTemplate && (
+ {isLoadingTemplate && (
<div className="flex items-center justify-center p-4 text-sm text-muted-foreground">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-900 mr-2"></div>
입찰공고 템플릿을 불러오는 중...
</div>
- )} */}
+ )}
</CardContent>
- </Card>
+ </Card> */}
{/* SHI용 첨부파일 */}
<Card>
@@ -1162,18 +1102,16 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp
})
}}
>
- {() => (
- <DropzoneTrigger asChild>
- <DropzoneZone className="flex justify-center h-32">
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>파일을 드래그하여 업로드</DropzoneTitle>
- </div>
- </div>
- </DropzoneZone>
- </DropzoneTrigger>
- )}
+ <DropzoneZone>
+ <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
+ <DropzoneTitle className="text-lg font-medium">
+ 파일을 드래그하거나 클릭하여 업로드
+ </DropzoneTitle>
+ <DropzoneDescription className="text-sm text-muted-foreground">
+ PDF, Word, Excel, 이미지 파일 (최대 600MB)
+ </DropzoneDescription>
+ </DropzoneZone>
+ <DropzoneInput />
</Dropzone>
{shiAttachmentFiles.length > 0 && (
@@ -1236,18 +1174,16 @@ export function BiddingCreateDialog({ form, onSuccess }: BiddingCreateDialogProp
})
}}
>
- {() => (
- <DropzoneTrigger asChild>
- <DropzoneZone className="flex justify-center h-32">
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>파일을 드래그하여 업로드</DropzoneTitle>
- </div>
- </div>
- </DropzoneZone>
- </DropzoneTrigger>
- )}
+ <DropzoneZone>
+ <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
+ <DropzoneTitle className="text-lg font-medium">
+ 파일을 드래그하거나 클릭하여 업로드
+ </DropzoneTitle>
+ <DropzoneDescription className="text-sm text-muted-foreground">
+ 협력업체용 문서나 파일을 업로드 하세요. (최대 600MB)
+ </DropzoneDescription>
+ </DropzoneZone>
+ <DropzoneInput />
</Dropzone>
{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
})
}}
>
- {() => (
- <DropzoneTrigger asChild>
- <DropzoneZone className="flex justify-center h-32">
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>파일을 드래그하여 업로드</DropzoneTitle>
- </div>
- </div>
- </DropzoneZone>
- </DropzoneTrigger>
- )}
+ <DropzoneZone>
+ <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
+ <DropzoneTitle className="text-lg font-medium">
+ 파일을 드래그하거나 클릭하여 업로드
+ </DropzoneTitle>
+ <DropzoneDescription className="text-sm text-muted-foreground">
+ PDF, Word, Excel, 이미지 파일 (최대 600MB)
+ </DropzoneDescription>
+ </DropzoneZone>
+ <DropzoneInput />
</Dropzone>
@@ -1235,18 +1233,16 @@ export function BiddingBasicInfoEditor({ biddingId, readonly = false }: BiddingB
})
}}
>
- {() => (
- <DropzoneTrigger asChild>
- <DropzoneZone className="flex justify-center h-32">
- <div className="flex items-center gap-6">
- <DropzoneUploadIcon />
- <div className="grid gap-0.5">
- <DropzoneTitle>파일을 드래그하여 업로드</DropzoneTitle>
- </div>
- </div>
- </DropzoneZone>
- </DropzoneTrigger>
- )}
+ <DropzoneZone>
+ <DropzoneUploadIcon className="mx-auto h-12 w-12 text-muted-foreground" />
+ <DropzoneTitle className="text-lg font-medium">
+ 파일을 드래그하거나 클릭하여 업로드
+ </DropzoneTitle>
+ <DropzoneDescription className="text-sm text-muted-foreground">
+ 협력업체용 문서나 파일을 업로드 하세요. (최대 600MB)
+ </DropzoneDescription>
+ </DropzoneZone>
+ <DropzoneInput />
</Dropzone>
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"
/>
+ <p className="text-[0.8rem] text-muted-foreground">
+ * SMS 발송을 위해 국제 표준 형식으로 입력해주세요. (예: +821012345678)
+ </p>
</div>
</div>
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와 결재 데이터 저장 및 결재 다이얼로그 열기