summaryrefslogtreecommitdiff
path: root/lib/general-contracts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/general-contracts')
-rw-r--r--lib/general-contracts/detail/general-contract-approval-request-dialog.tsx2
-rw-r--r--lib/general-contracts/detail/general-contract-basic-info.tsx241
-rw-r--r--lib/general-contracts/main/create-general-contract-dialog.tsx156
-rw-r--r--lib/general-contracts/main/general-contract-update-sheet.tsx98
-rw-r--r--lib/general-contracts/main/general-contracts-table-columns.tsx45
-rw-r--r--lib/general-contracts/main/general-contracts-table.tsx17
-rw-r--r--lib/general-contracts/service.ts84
-rw-r--r--lib/general-contracts/types.ts19
-rw-r--r--lib/general-contracts/validation.ts3
9 files changed, 387 insertions, 278 deletions
diff --git a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx
index e4aa022a..f05fe9ef 100644
--- a/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx
+++ b/lib/general-contracts/detail/general-contract-approval-request-dialog.tsx
@@ -863,7 +863,7 @@ export function ContractApprovalRequestDialog({
<span className="font-medium">선적지:</span> {String(contractSummary?.basicInfo?.shippingLocation || '')}
</div>
<div>
- <span className="font-medium">도착지:</span> {String(contractSummary?.basicInfo?.dischargeLocation || '')}
+ <span className="font-medium">하역지:</span> {String(contractSummary?.basicInfo?.dischargeLocation || '')}
</div>
<div>
<span className="font-medium">계약납기:</span> {String(contractSummary?.basicInfo?.contractDeliveryDate || '')}
diff --git a/lib/general-contracts/detail/general-contract-basic-info.tsx b/lib/general-contracts/detail/general-contract-basic-info.tsx
index ac1315bb..882ed8b2 100644
--- a/lib/general-contracts/detail/general-contract-basic-info.tsx
+++ b/lib/general-contracts/detail/general-contract-basic-info.tsx
@@ -14,6 +14,7 @@ import { toast } from 'sonner'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { GeneralContract } from '@/db/schema'
import { ContractDocuments } from './general-contract-documents'
+import { getPaymentTermsForSelection, getIncotermsForSelection, getPlaceOfShippingForSelection, getPlaceOfDestinationForSelection } from '@/lib/procurement-select/service'
interface ContractBasicInfoProps {
contractId: number
@@ -27,6 +28,13 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
// 독립적인 상태 관리
const [paymentDeliveryPercent, setPaymentDeliveryPercent] = useState('')
+
+ // Procurement 데이터 상태들
+ const [paymentTermsOptions, setPaymentTermsOptions] = useState<Array<{code: string, description: string}>>([])
+ const [incotermsOptions, setIncotermsOptions] = useState<Array<{code: string, description: string}>>([])
+ const [shippingPlaces, setShippingPlaces] = useState<Array<{code: string, description: string}>>([])
+ const [destinationPlaces, setDestinationPlaces] = useState<Array<{code: string, description: string}>>([])
+ const [procurementLoading, setProcurementLoading] = useState(false)
const [formData, setFormData] = useState({
specificationType: '',
@@ -169,6 +177,67 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
loadContract()
}
}, [contractId])
+
+ // Procurement 데이터 로드 함수들
+ const loadPaymentTerms = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getPaymentTermsForSelection();
+ setPaymentTermsOptions(data);
+ } catch (error) {
+ console.error("Failed to load payment terms:", error);
+ toast.error("결제조건 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ const loadIncoterms = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getIncotermsForSelection();
+ setIncotermsOptions(data);
+ } catch (error) {
+ console.error("Failed to load incoterms:", error);
+ toast.error("운송조건 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ const loadShippingPlaces = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getPlaceOfShippingForSelection();
+ setShippingPlaces(data);
+ } catch (error) {
+ console.error("Failed to load shipping places:", error);
+ toast.error("선적지 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ const loadDestinationPlaces = React.useCallback(async () => {
+ setProcurementLoading(true);
+ try {
+ const data = await getPlaceOfDestinationForSelection();
+ setDestinationPlaces(data);
+ } catch (error) {
+ console.error("Failed to load destination places:", error);
+ toast.error("하역지 목록을 불러오는데 실패했습니다.");
+ } finally {
+ setProcurementLoading(false);
+ }
+ }, []);
+
+ // 컴포넌트 마운트 시 procurement 데이터 로드
+ React.useEffect(() => {
+ loadPaymentTerms();
+ loadIncoterms();
+ loadShippingPlaces();
+ loadDestinationPlaces();
+ }, [loadPaymentTerms, loadIncoterms, loadShippingPlaces, loadDestinationPlaces]);
const handleSaveContractInfo = async () => {
if (!userId) {
toast.error('사용자 정보를 찾을 수 없습니다.')
@@ -518,15 +587,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<Label htmlFor="apBond" className="text-sm">AP Bond & Performance Bond</Label>
<Input
type="number"
+ min="0"
placeholder="%"
className="w-16"
value={formData.paymentBeforeDelivery.apBondPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- apBondPercent: e.target.value
- }
+ onChange={(e) => setFormData(prev => ({
+ ...prev,
+ paymentBeforeDelivery: {
+ ...prev.paymentBeforeDelivery,
+ apBondPercent: e.target.value
+ }
}))}
disabled={!formData.paymentBeforeDelivery.apBond}
/>
@@ -548,15 +618,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<Label htmlFor="drawingSubmission" className="text-sm">도면제출</Label>
<Input
type="number"
+ min="0"
placeholder="%"
className="w-16"
value={formData.paymentBeforeDelivery.drawingSubmissionPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- drawingSubmissionPercent: e.target.value
- }
+ onChange={(e) => setFormData(prev => ({
+ ...prev,
+ paymentBeforeDelivery: {
+ ...prev.paymentBeforeDelivery,
+ drawingSubmissionPercent: e.target.value
+ }
}))}
disabled={!formData.paymentBeforeDelivery.drawingSubmission}
/>
@@ -578,15 +649,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<Label htmlFor="materialPurchase" className="text-sm">소재구매 문서</Label>
<Input
type="number"
+ min="0"
placeholder="%"
className="w-16"
value={formData.paymentBeforeDelivery.materialPurchasePercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentBeforeDelivery: {
- ...prev.paymentBeforeDelivery,
- materialPurchasePercent: e.target.value
- }
+ onChange={(e) => setFormData(prev => ({
+ ...prev,
+ paymentBeforeDelivery: {
+ ...prev.paymentBeforeDelivery,
+ materialPurchasePercent: e.target.value
+ }
}))}
disabled={!formData.paymentBeforeDelivery.materialPurchase}
/>
@@ -618,6 +690,7 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<div className="flex items-center gap-2 mt-2">
<Input
type="number"
+ min="0"
value={paymentDeliveryPercent}
onChange={(e) => setPaymentDeliveryPercent(e.target.value)}
placeholder="퍼센트"
@@ -654,15 +727,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<Label htmlFor="commissioning" className="text-sm">Commissioning 완료</Label>
<Input
type="number"
+ min="0"
placeholder="%"
className="w-16"
value={formData.paymentAfterDelivery.commissioningPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- commissioningPercent: e.target.value
- }
+ onChange={(e) => setFormData(prev => ({
+ ...prev,
+ paymentAfterDelivery: {
+ ...prev.paymentAfterDelivery,
+ commissioningPercent: e.target.value
+ }
}))}
disabled={!formData.paymentAfterDelivery.commissioning}
/>
@@ -684,15 +758,16 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<Label htmlFor="finalDocument" className="text-sm">최종문서 승인</Label>
<Input
type="number"
+ min="0"
placeholder="%"
className="w-16"
value={formData.paymentAfterDelivery.finalDocumentPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- paymentAfterDelivery: {
- ...prev.paymentAfterDelivery,
- finalDocumentPercent: e.target.value
- }
+ onChange={(e) => setFormData(prev => ({
+ ...prev,
+ paymentAfterDelivery: {
+ ...prev.paymentAfterDelivery,
+ finalDocumentPercent: e.target.value
+ }
}))}
disabled={!formData.paymentAfterDelivery.finalDocument}
/>
@@ -736,13 +811,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="paymentTerm">지불조건 <span className="text-red-600">*</span></Label>
- <Input
- type="text"
+ <Select
value={formData.paymentTerm}
- onChange={(e) => setFormData(prev => ({ ...prev, paymentTerm: e.target.value }))}
- placeholder="지불조건을 입력하세요"
- className={errors.paymentTerm ? 'border-red-500' : ''}
- />
+ onValueChange={(value) => setFormData(prev => ({ ...prev, paymentTerm: value }))}
+ >
+ <SelectTrigger className={errors.paymentTerm ? 'border-red-500' : ''}>
+ <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>
{errors.paymentTerm && (
<p className="text-sm text-red-600">지불조건은 필수값입니다.</p>
)}
@@ -781,12 +870,13 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<Label htmlFor="liquidatedDamages" className="text-sm">지체상금</Label>
<Input
type="number"
+ min="0"
placeholder="%"
className="w-16"
value={formData.liquidatedDamagesPercent || ''}
- onChange={(e) => setFormData(prev => ({
- ...prev,
- liquidatedDamagesPercent: e.target.value
+ onChange={(e) => setFormData(prev => ({
+ ...prev,
+ liquidatedDamagesPercent: e.target.value
}))}
disabled={!formData.liquidatedDamages}
/>
@@ -823,12 +913,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="deliveryTerm">인도조건</Label>
- <Input
- type="text"
+ <Select
value={formData.deliveryTerm}
- onChange={(e) => setFormData(prev => ({ ...prev, deliveryTerm: e.target.value }))}
- placeholder="인도조건을 입력하세요"
- />
+ onValueChange={(value) => setFormData(prev => ({ ...prev, deliveryTerm: 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>
</div>
@@ -838,11 +943,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="shippingLocation">선적지</Label>
- <Input
+ <Select
value={formData.shippingLocation}
- onChange={(e) => setFormData(prev => ({ ...prev, shippingLocation: e.target.value }))}
- placeholder="선적지를 입력하세요"
- />
+ onValueChange={(value) => setFormData(prev => ({ ...prev, shippingLocation: 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>
</div>
@@ -852,11 +973,27 @@ export function ContractBasicInfo({ contractId }: ContractBasicInfoProps) {
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="dischargeLocation">하역지</Label>
- <Input
+ <Select
value={formData.dischargeLocation}
- onChange={(e) => setFormData(prev => ({ ...prev, dischargeLocation: e.target.value }))}
- placeholder="하역지를 입력하세요"
- />
+ onValueChange={(value) => setFormData(prev => ({ ...prev, dischargeLocation: 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>
diff --git a/lib/general-contracts/main/create-general-contract-dialog.tsx b/lib/general-contracts/main/create-general-contract-dialog.tsx
index 3eb8b11c..2c3fc8bc 100644
--- a/lib/general-contracts/main/create-general-contract-dialog.tsx
+++ b/lib/general-contracts/main/create-general-contract-dialog.tsx
@@ -24,7 +24,7 @@ import { CalendarIcon } from "lucide-react"
import { format } from "date-fns"
import { ko } from "date-fns/locale"
import { cn } from "@/lib/utils"
-import { createContract, getVendors } from "@/lib/general-contracts/service"
+import { createContract, getVendors, getProjects } from "@/lib/general-contracts/service"
import {
GENERAL_CONTRACT_CATEGORIES,
GENERAL_CONTRACT_TYPES,
@@ -38,17 +38,12 @@ interface CreateContractForm {
category: string
type: string
executionMethod: string
- selectionMethod: string
vendorId: number | null
+ projectId: number | null
startDate: Date | undefined
endDate: Date | undefined
validityEndDate: Date | undefined
- // contractScope: string
- // specificationType: string
notes: string
- linkedRfqOrItb: string
- linkedBidNumber: string
- linkedPoNumber: string
}
export function CreateGeneralContractDialog() {
@@ -57,24 +52,20 @@ export function CreateGeneralContractDialog() {
const [open, setOpen] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(false)
const [vendors, setVendors] = React.useState<Array<{ id: number; vendorName: string; vendorCode: string | null }>>([])
-
+ const [projects, setProjects] = React.useState<Array<{ id: number; code: string; name: string; type: string }>>([])
+
const [form, setForm] = React.useState<CreateContractForm>({
contractNumber: '',
name: '',
category: '',
type: '',
executionMethod: '',
- selectionMethod: '',
vendorId: null,
+ projectId: null,
startDate: undefined,
endDate: undefined,
validityEndDate: undefined,
- // contractScope: '',
- // specificationType: '',
notes: '',
- linkedRfqOrItb: '',
- linkedBidNumber: '',
- linkedPoNumber: '',
})
// 업체 목록 조회
@@ -90,6 +81,20 @@ export function CreateGeneralContractDialog() {
fetchVendors()
}, [])
+ // 프로젝트 목록 조회
+ React.useEffect(() => {
+ const fetchProjects = async () => {
+ try {
+ const projectList = await getProjects()
+ console.log(projectList)
+ setProjects(projectList)
+ } catch (error) {
+ console.error('Error fetching projects:', error)
+ }
+ }
+ fetchProjects()
+ }, [])
+
const handleSubmit = async () => {
// 필수 필드 검증
if (!form.name || !form.category || !form.type || !form.executionMethod ||
@@ -111,23 +116,19 @@ export function CreateGeneralContractDialog() {
category: form.category,
type: form.type,
executionMethod: form.executionMethod,
- selectionMethod: form.selectionMethod,
+ projectId: form.projectId,
+ contractSourceType: 'manual',
vendorId: form.vendorId!,
startDate: form.startDate!.toISOString().split('T')[0],
endDate: form.endDate!.toISOString().split('T')[0],
validityEndDate: (form.validityEndDate || form.endDate!).toISOString().split('T')[0],
- // contractScope: form.contractScope,
- // specificationType: form.specificationType,
status: 'Draft',
registeredById: session?.user?.id || 1,
lastUpdatedById: session?.user?.id || 1,
notes: form.notes,
- linkedRfqOrItb: form.linkedRfqOrItb,
- linkedBidNumber: form.linkedBidNumber,
- linkedPoNumber: form.linkedPoNumber,
}
- const newContract = await createContract(contractData)
+ await createContract(contractData)
toast.success("새 계약이 생성되었습니다.")
setOpen(false)
@@ -150,17 +151,12 @@ export function CreateGeneralContractDialog() {
category: '',
type: '',
executionMethod: '',
- selectionMethod: '',
vendorId: null,
+ projectId: null,
startDate: undefined,
endDate: undefined,
validityEndDate: undefined,
- // contractScope: '',
- // specificationType: '',
notes: '',
- linkedRfqOrItb: '',
- linkedBidNumber: '',
- linkedPoNumber: '',
})
}
@@ -185,16 +181,6 @@ export function CreateGeneralContractDialog() {
<div className="grid gap-4 py-4">
<div className="grid grid-cols-1 gap-4">
- {/* <div className="grid gap-2">
- <Label htmlFor="contractNumber">계약번호</Label>
- <Input
- id="contractNumber"
- value={form.contractNumber}
- onChange={(e) => setForm(prev => ({ ...prev, contractNumber: e.target.value }))}
- placeholder="자동 생성됩니다"
- />
- </div> */}
-
<div className="grid gap-2">
<Label htmlFor="name">계약명 *</Label>
<Input
@@ -214,11 +200,18 @@ export function CreateGeneralContractDialog() {
<SelectValue placeholder="계약구분 선택" />
</SelectTrigger>
<SelectContent>
- {GENERAL_CONTRACT_CATEGORIES.map((category) => (
+ {GENERAL_CONTRACT_CATEGORIES.map((category) => {
+ const categoryLabels = {
+ 'unit_price': '단가계약',
+ 'general': '일반계약',
+ 'sale': '매각계약'
+ }
+ return (
<SelectItem key={category} value={category}>
- {category}
- </SelectItem>
- ))}
+ {category} - {categoryLabels[category as keyof typeof categoryLabels]}
+ </SelectItem>
+ )
+ })}
</SelectContent>
</Select>
</div>
@@ -276,6 +269,22 @@ export function CreateGeneralContractDialog() {
</div>
<div className="grid gap-2">
+ <Label htmlFor="project">프로젝트</Label>
+ <Select value={form.projectId?.toString()} onValueChange={(value) => setForm(prev => ({ ...prev, projectId: parseInt(value) }))}>
+ <SelectTrigger>
+ <SelectValue placeholder="프로젝트 선택 (선택사항)" />
+ </SelectTrigger>
+ <SelectContent>
+ {projects.map((project) => (
+ <SelectItem key={project.id} value={project.id.toString()}>
+ {project.name} ({project.code})
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+
+ <div className="grid gap-2">
<Label htmlFor="vendor">협력업체 *</Label>
<Select value={form.vendorId?.toString()} onValueChange={(value) => setForm(prev => ({ ...prev, vendorId: parseInt(value) }))}>
<SelectTrigger>
@@ -370,71 +379,6 @@ export function CreateGeneralContractDialog() {
</Popover>
</div>
</div>
-
- {/* <div className="grid grid-cols-2 gap-4">
- <div className="grid gap-2">
- <Label htmlFor="contractScope">계약확정범위 *</Label>
- <Select value={form.contractScope} onValueChange={(value) => setForm(prev => ({ ...prev, contractScope: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="계약확정범위 선택" />
- </SelectTrigger>
- <SelectContent>
- {GENERAL_CONTRACT_SCOPES.map((scope) => (
- <SelectItem key={scope} value={scope}>
- {scope}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
- </div>
-
- <div className="grid gap-2">
- <Label htmlFor="specificationType">사양 *</Label>
- <Select value={form.specificationType} onValueChange={(value) => setForm(prev => ({ ...prev, specificationType: value }))}>
- <SelectTrigger>
- <SelectValue placeholder="사양 선택" />
- </SelectTrigger>
- <SelectContent>
- <SelectItem value="첨부파일">첨부파일</SelectItem>
- <SelectItem value="직접입력">직접입력</SelectItem>
- <SelectItem value="기타">기타</SelectItem>
- </SelectContent>
- </Select>
- </div>
- </div> */}
-
- <div className="grid grid-cols-2 gap-4">
- <div className="grid gap-2">
- <Label htmlFor="linkedRfqOrItb">연계 견적/입찰번호</Label>
- <Input
- id="linkedRfqOrItb"
- value={form.linkedRfqOrItb}
- onChange={(e) => setForm(prev => ({ ...prev, linkedRfqOrItb: e.target.value }))}
- placeholder="연계 견적/입찰번호를 입력하세요"
- />
- </div>
-
- <div className="grid gap-2">
- <Label htmlFor="linkedBidNumber">연계 BID번호</Label>
- <Input
- id="linkedBidNumber"
- value={form.linkedBidNumber}
- onChange={(e) => setForm(prev => ({ ...prev, linkedBidNumber: e.target.value }))}
- placeholder="연계 BID번호를 입력하세요"
- />
- </div>
- </div>
-
- <div className="grid gap-2">
- <Label htmlFor="linkedPoNumber">연계PO번호</Label>
- <Input
- id="linkedPoNumber"
- value={form.linkedPoNumber}
- onChange={(e) => setForm(prev => ({ ...prev, linkedPoNumber: e.target.value }))}
- placeholder="연계PO번호를 입력하세요"
- />
- </div>
-
<div className="grid gap-2">
<Label htmlFor="notes">비고</Label>
<Textarea
diff --git a/lib/general-contracts/main/general-contract-update-sheet.tsx b/lib/general-contracts/main/general-contract-update-sheet.tsx
index 064fded3..54f4ae4e 100644
--- a/lib/general-contracts/main/general-contract-update-sheet.tsx
+++ b/lib/general-contracts/main/general-contract-update-sheet.tsx
@@ -83,8 +83,6 @@ export function GeneralContractUpdateSheet({
endDate: "",
validityEndDate: "",
contractScope: "",
- specificationType: "",
- specificationManualText: "",
notes: "",
linkedRfqOrItb: "",
linkedPoNumber: "",
@@ -109,8 +107,6 @@ export function GeneralContractUpdateSheet({
endDate: contract.endDate || "",
validityEndDate: contract.validityEndDate || "",
contractScope: contract.contractScope || "",
- specificationType: contract.specificationType || "",
- specificationManualText: contract.specificationManualText || "",
notes: contract.notes || "",
linkedRfqOrItb: contract.linkedRfqOrItb || "",
linkedPoNumber: contract.linkedPoNumber || "",
@@ -136,8 +132,6 @@ export function GeneralContractUpdateSheet({
endDate: data.endDate,
validityEndDate: data.validityEndDate,
contractScope: data.contractScope,
- specificationType: data.specificationType,
- specificationManualText: data.specificationManualText,
notes: data.notes,
linkedRfqOrItb: data.linkedRfqOrItb,
linkedPoNumber: data.linkedPoNumber,
@@ -185,11 +179,17 @@ export function GeneralContractUpdateSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {GENERAL_CONTRACT_CATEGORIES.map((category) => (
+ {GENERAL_CONTRACT_CATEGORIES.map((category) => {
+ const categoryLabels = {
+ 'unit_price': '단가계약',
+ 'general': '일반계약',
+ 'sale': '매각계약'
+ }
+ return (
<SelectItem key={category} value={category}>
- {category}
+ {category} - {categoryLabels[category as keyof typeof categoryLabels]}
</SelectItem>
- ))}
+ )})}
</SelectContent>
</Select>
<FormMessage />
@@ -211,11 +211,29 @@ export function GeneralContractUpdateSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {GENERAL_CONTRACT_TYPES.map((type) => (
+ {GENERAL_CONTRACT_TYPES.map((type) => {
+ const typeLabels = {
+ 'UP': '자재단가계약',
+ 'LE': '임대차계약',
+ 'IL': '개별운송계약',
+ 'AL': '연간운송계약',
+ 'OS': '외주용역계약',
+ 'OW': '도급계약',
+ 'IS': '검사계약',
+ 'LO': 'LOI',
+ 'FA': 'FA',
+ 'SC': '납품합의계약',
+ 'OF': '클레임상계계약',
+ 'AW': '사전작업합의',
+ 'AD': '사전납품합의',
+ 'AM': '설계계약',
+ 'SC_SELL': '폐기물매각계약'
+ }
+ return (
<SelectItem key={type} value={type}>
- {type}
+ {type} - {typeLabels[type as keyof typeof typeLabels]}
</SelectItem>
- ))}
+ )})}
</SelectContent>
</Select>
<FormMessage />
@@ -237,11 +255,16 @@ export function GeneralContractUpdateSheet({
</SelectTrigger>
</FormControl>
<SelectContent>
- {GENERAL_EXECUTION_METHODS.map((method) => (
+ {GENERAL_EXECUTION_METHODS.map((method) => {
+ const methodLabels = {
+ '전자계약': '전자계약',
+ '오프라인계약': '오프라인계약'
+ }
+ return (
<SelectItem key={method} value={method}>
- {method}
+ {method} - {methodLabels[method as keyof typeof methodLabels]}
</SelectItem>
- ))}
+ )})}
</SelectContent>
</Select>
<FormMessage />
@@ -336,51 +359,6 @@ export function GeneralContractUpdateSheet({
)}
/>
- {/* 연계 견적/입찰번호 */}
- <FormField
- control={form.control}
- name="linkedRfqOrItb"
- render={({ field }) => (
- <FormItem>
- <FormLabel>연계 견적/입찰번호</FormLabel>
- <FormControl>
- <Input placeholder="연계 견적/입찰번호를 입력하세요" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 연계 PO번호 */}
- <FormField
- control={form.control}
- name="linkedPoNumber"
- render={({ field }) => (
- <FormItem>
- <FormLabel>연계 PO번호</FormLabel>
- <FormControl>
- <Input placeholder="연계 PO번호를 입력하세요" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
- {/* 연계 BID번호 */}
- <FormField
- control={form.control}
- name="linkedBidNumber"
- render={({ field }) => (
- <FormItem>
- <FormLabel>연계 BID번호</FormLabel>
- <FormControl>
- <Input placeholder="연계 BID번호를 입력하세요" {...field} />
- </FormControl>
- <FormMessage />
- </FormItem>
- )}
- />
-
{/* 비고 */}
<FormField
control={form.control}
diff --git a/lib/general-contracts/main/general-contracts-table-columns.tsx b/lib/general-contracts/main/general-contracts-table-columns.tsx
index d7854ee6..a08d8b81 100644
--- a/lib/general-contracts/main/general-contracts-table-columns.tsx
+++ b/lib/general-contracts/main/general-contracts-table-columns.tsx
@@ -12,7 +12,6 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
- DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header"
@@ -29,7 +28,7 @@ export interface GeneralContractListItem {
type: string
executionMethod: string
name: string
- selectionMethod?: string
+ contractSourceType?: string
startDate: string
endDate: string
validityEndDate?: string
@@ -49,6 +48,9 @@ export interface GeneralContractListItem {
vendorId?: number
vendorName?: string
vendorCode?: string
+ projectId?: number
+ projectName?: string
+ projectCode?: string
managerName?: string
lastUpdatedByName?: string
}
@@ -110,11 +112,11 @@ const getStatusText = (status: string) => {
// 계약구분 텍스트 변환
const getCategoryText = (category: string) => {
switch (category) {
- case '단가계약':
+ case 'unit_price':
return '단가계약'
- case '일반계약':
+ case 'general':
return '일반계약'
- case '매각계약':
+ case 'sale':
return '매각계약'
default:
return category
@@ -172,15 +174,15 @@ const getExecutionMethodText = (method: string) => {
}
// 업체선정방법 텍스트 변환
-const getSelectionMethodText = (method?: string) => {
+const getcontractSourceTypeText = (method?: string) => {
if (!method) return '-'
switch (method) {
- case '견적':
+ case 'estimate':
return '견적'
- case '입찰':
+ case 'bid':
return '입찰'
- case '기타':
- return '기타'
+ case 'manual':
+ return '자체생성'
default:
return method
}
@@ -325,12 +327,12 @@ export function getGeneralContractsColumns({ setRowAction }: GetColumnsProps): C
},
{
- accessorKey: "selectionMethod",
+ accessorKey: "contractSourceType",
header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="업체선정방법" />,
cell: ({ row }) => (
- <span className="text-sm">
- {getSelectionMethodText(row.original.selectionMethod)}
- </span>
+ <Badge variant="outline">
+ {getcontractSourceTypeText(row.original.contractSourceType)}
+ </Badge>
),
size: 200,
meta: { excelHeader: "업체선정방법" },
@@ -359,6 +361,21 @@ export function getGeneralContractsColumns({ setRowAction }: GetColumnsProps): C
meta: { excelHeader: "협력업체명" },
},
+ {
+ accessorKey: "projectName",
+ header: ({ column }) => <DataTableColumnHeaderSimple column={column} title="프로젝트명" />,
+ cell: ({ row }) => (
+ <div className="flex flex-col">
+ <span className="font-medium">{row.original.projectName || '-'}</span>
+ <span className="text-xs text-muted-foreground">
+ {row.original.projectCode ? row.original.projectCode : "-"}
+ </span>
+ </div>
+ ),
+ size: 150,
+ meta: { excelHeader: "프로젝트명" },
+ },
+
]
},
diff --git a/lib/general-contracts/main/general-contracts-table.tsx b/lib/general-contracts/main/general-contracts-table.tsx
index 962bb61e..e4c96ee3 100644
--- a/lib/general-contracts/main/general-contracts-table.tsx
+++ b/lib/general-contracts/main/general-contracts-table.tsx
@@ -16,11 +16,7 @@ import { getGeneralContracts, getGeneralContractStatusCounts } from "@/lib/gener
import { GeneralContractsTableToolbarActions } from "./general-contracts-table-toolbar-actions"
import { GeneralContractUpdateSheet } from "./general-contract-update-sheet"
import {
- GENERAL_CONTRACT_STATUSES,
- GENERAL_CONTRACT_CATEGORIES,
- GENERAL_CONTRACT_TYPES,
- GENERAL_EXECUTION_METHODS,
- GENERAL_SELECTION_METHODS
+ GENERAL_EXECUTION_METHODS
} from "@/lib/general-contracts/types"
// 상태 라벨 매핑
@@ -155,13 +151,14 @@ export function GeneralContractsTable({ promises }: GeneralContractsTableProps)
})),
},
{
- id: "selectionMethod",
+ id: "contractSourceType",
label: "업체선정방법",
type: "select",
- options: GENERAL_SELECTION_METHODS.map(value => ({
- label: value,
- value: value,
- })),
+ options: [
+ { label: "estimate", value: "견적" },
+ { label: "bid", value: "입찰" },
+ { label: "manual", value: "자체생성" },
+ ],
},
{ id: "registeredAt", label: "계약등록일", type: "date" },
{ id: "signedAt", label: "계약체결일", type: "date" },
diff --git a/lib/general-contracts/service.ts b/lib/general-contracts/service.ts
index 2079a0be..8c74c616 100644
--- a/lib/general-contracts/service.ts
+++ b/lib/general-contracts/service.ts
@@ -10,6 +10,7 @@ import { contracts, contractItems, contractEnvelopes, contractSigners } from '@/
import { basicContract, basicContractTemplates } from '@/db/schema/basicContractDocumnet'
import { vendors } from '@/db/schema/vendors'
import { users } from '@/db/schema/users'
+import { projects } from '@/db/schema/projects'
import { filterColumns } from '@/lib/filter-columns'
import { saveDRMFile } from '@/lib/file-stroage'
import { decryptWithServerAction } from '@/components/drm/drmUtils'
@@ -70,9 +71,9 @@ export async function getGeneralContracts(input: GetGeneralContractsSchema) {
)
}
- if (input.selectionMethod && input.selectionMethod.length > 0) {
+ if (input.contractSourceType && input.contractSourceType.length > 0) {
basicConditions.push(
- or(...input.selectionMethod.map(method => eq(generalContracts.selectionMethod, method)))!
+ or(...input.contractSourceType.map(method => eq(generalContracts.contractSourceType, method)))!
)
}
@@ -202,7 +203,7 @@ export async function getGeneralContracts(input: GetGeneralContractsSchema) {
type: generalContracts.type,
executionMethod: generalContracts.executionMethod,
name: generalContracts.name,
- selectionMethod: generalContracts.selectionMethod,
+ contractSourceType: generalContracts.contractSourceType,
startDate: generalContracts.startDate,
endDate: generalContracts.endDate,
validityEndDate: generalContracts.validityEndDate,
@@ -223,6 +224,10 @@ export async function getGeneralContracts(input: GetGeneralContractsSchema) {
vendorId: generalContracts.vendorId,
vendorName: vendors.vendorName,
vendorCode: vendors.vendorCode,
+ // Project info
+ projectId: generalContracts.projectId,
+ projectName: projects.name,
+ projectCode: projects.code,
// User info
managerName: users.name,
lastUpdatedByName: users.name,
@@ -230,6 +235,7 @@ export async function getGeneralContracts(input: GetGeneralContractsSchema) {
.from(generalContracts)
.leftJoin(vendors, eq(generalContracts.vendorId, vendors.id))
.leftJoin(users, eq(generalContracts.registeredById, users.id))
+ .leftJoin(projects, eq(generalContracts.projectId, projects.id))
.where(finalWhere)
.orderBy(...orderByColumns)
.limit(input.perPage)
@@ -281,6 +287,13 @@ export async function getContractById(id: number) {
.where(eq(vendors.id, contract[0].vendorId))
.limit(1)
+ // Get project info
+ const project = contract[0].projectId ? await db
+ .select()
+ .from(projects)
+ .where(eq(projects.id, contract[0].projectId))
+ .limit(1) : null
+
// Get manager info
const manager = await db
.select()
@@ -293,6 +306,11 @@ export async function getContractById(id: number) {
contractItems: items,
attachments,
vendor: vendor[0] || null,
+ vendorCode: vendor[0]?.vendorCode || null,
+ vendorName: vendor[0]?.vendorName || null,
+ project: project ? project[0] : null,
+ projectName: project ? project[0].name : null,
+ projectCode: project ? project[0].code : null,
manager: manager[0] || null
}
} catch (error) {
@@ -354,7 +372,9 @@ export async function createContract(data: Record<string, unknown>) {
try {
// 계약번호 자동 생성
// TODO: 구매 발주담당자 코드 필요 - 파라미터 추가
+ const userId = data.registeredById as string
const contractNumber = await generateContractNumber(
+ userId,
data.type as string
)
@@ -369,8 +389,8 @@ export async function createContract(data: Record<string, unknown>) {
type: data.type as string,
executionMethod: data.executionMethod as string,
name: data.name as string,
- selectionMethod: data.selectionMethod as string,
vendorId: data.vendorId as number,
+ projectId: data.projectId as number,
startDate: data.startDate as string,
endDate: data.endDate as string,
validityEndDate: data.validityEndDate as string,
@@ -553,7 +573,6 @@ export async function createContractItem(contractId: number, itemData: Record<st
.insert(generalContractItems)
.values({
contractId,
- project: itemData.project as string,
itemCode: itemData.itemCode as string,
itemInfo: itemData.itemInfo as string,
specification: itemData.specification as string,
@@ -583,7 +602,6 @@ export async function updateContractItem(itemId: number, itemData: Record<string
const [updatedItem] = await db
.update(generalContractItems)
.set({
- project: itemData.project as string,
itemCode: itemData.itemCode as string,
itemInfo: itemData.itemInfo as string,
specification: itemData.specification as string,
@@ -653,7 +671,6 @@ export async function updateContractItems(contractId: number, items: Record<stri
.values(
items.map((item: Record<string, unknown>) => ({
contractId,
- project: item.project as string,
itemCode: item.itemCode as string,
itemInfo: item.itemInfo as string,
specification: item.specification as string,
@@ -903,11 +920,12 @@ export async function updateContract(id: number, data: Record<string, unknown>)
// 숫자 필드에서 빈 문자열을 null로 변환
const cleanedData = { ...data }
const numericFields = [
- 'vendorId',
- 'warrantyPeriodValue',
- 'warrantyPeriodMax',
- 'contractAmount',
- 'totalAmount',
+ 'vendorId',
+ 'projectId',
+ 'warrantyPeriodValue',
+ 'warrantyPeriodMax',
+ 'contractAmount',
+ 'totalAmount',
'availableBudget',
'liquidatedDamages',
'liquidatedDamagesPercent',
@@ -1537,7 +1555,7 @@ async function mapContractSummaryToDb(contractSummary: any) {
// 계약번호 생성
const contractNumber = await generateContractNumber(
basicInfo.contractType || basicInfo.type || 'UP',
- basicInfo.purchaseManagerCode
+ basicInfo.userId
)
return {
@@ -1699,8 +1717,8 @@ export async function updateOffsetDetails(
// 계약번호 생성 함수
export async function generateContractNumber(
- contractType: string,
- purchaseManagerCode?: string
+ userId?: string,
+ contractType: string
): Promise<string> {
try {
// 계약종류 매핑 (2자리) - GENERAL_CONTRACT_TYPES 상수 사용
@@ -1723,7 +1741,19 @@ export async function generateContractNumber(
}
const typeCode = contractTypeMap[contractType] || 'XX' // 기본값
- // 발주담당자 코드 처리
+ // user 테이블의 user.userCode가 있으면 발주담당자 코드로 사용
+ // userId가 주어졌을 때 user.userCode를 조회, 없으면 '000' 사용
+ let purchaseManagerCode = '000';
+ if (userId) {
+ const user = await db
+ .select({ userCode: users.userCode })
+ .from(users)
+ .where(eq(users.id, userId))
+ .limit(1);
+ if (user[0]?.userCode && user[0].userCode.length >= 3) {
+ purchaseManagerCode = user[0].userCode.substring(0, 3).toUpperCase();
+ }
+ }
let managerCode: string
if (purchaseManagerCode && purchaseManagerCode.length >= 3) {
// 발주담당자 코드가 있으면 3자리 사용
@@ -1776,9 +1806,29 @@ export async function generateContractNumber(
const contractNumber = `C${managerCode}${typeCode}${currentYear.toString().slice(-2)}${finalSequence}`
return contractNumber
-
+
} catch (error) {
console.error('계약번호 생성 오류:', error)
throw new Error('계약번호 생성에 실패했습니다.')
}
}
+
+// 프로젝트 목록 조회
+export async function getProjects() {
+ try {
+ const projectList = await db
+ .select({
+ id: projects.id,
+ code: projects.code,
+ name: projects.name,
+ type: projects.type,
+ })
+ .from(projects)
+ .orderBy(asc(projects.name))
+
+ return projectList
+ } catch (error) {
+ console.error('Error fetching projects:', error)
+ throw new Error('Failed to fetch projects')
+ }
+}
diff --git a/lib/general-contracts/types.ts b/lib/general-contracts/types.ts
index 16f35dd5..2b6731b6 100644
--- a/lib/general-contracts/types.ts
+++ b/lib/general-contracts/types.ts
@@ -2,9 +2,9 @@
// 1. 계약구분
export const GENERAL_CONTRACT_CATEGORIES = [
- '단가계약',
- '일반계약',
- '매각계약'
+ 'unit_price', // 단가계약
+ 'general', // 일반계약
+ 'sale' // 매각계약
] as const;
export type GeneralContractCategory = typeof GENERAL_CONTRACT_CATEGORIES[number];
@@ -55,15 +55,6 @@ export const GENERAL_EXECUTION_METHODS = [
export type GeneralExecutionMethod = typeof GENERAL_EXECUTION_METHODS[number];
-// 5. 업체선정방법
-export const GENERAL_SELECTION_METHODS = [
- '견적',
- '입찰',
- '기타' // 기존 정의서의 'Null'을 '기타'로 변경, 명시적 의미 부여
-] as const;
-
-export type GeneralSelectionMethod = typeof GENERAL_SELECTION_METHODS[number];
-
// 6. 계약확정범위
export const GENERAL_CONTRACT_SCOPES = [
'단가',
@@ -117,10 +108,6 @@ export const isGeneralExecutionMethod = (value: string): value is GeneralExecuti
return GENERAL_EXECUTION_METHODS.includes(value as GeneralExecutionMethod);
};
-export const isGeneralSelectionMethod = (value: string): value is GeneralSelectionMethod => {
- return GENERAL_SELECTION_METHODS.includes(value as GeneralSelectionMethod);
-};
-
export const isGeneralContractScope = (value: string): value is GeneralContractScope => {
return GENERAL_CONTRACT_SCOPES.includes(value as GeneralContractScope);
};
diff --git a/lib/general-contracts/validation.ts b/lib/general-contracts/validation.ts
index 2e3788e7..5aa516e7 100644
--- a/lib/general-contracts/validation.ts
+++ b/lib/general-contracts/validation.ts
@@ -25,7 +25,7 @@ export const searchParamsCache = createSearchParamsCache({
category: parseAsArrayOf(z.enum(generalContracts.category.enumValues)).withDefault([]),
type: parseAsArrayOf(z.enum(generalContracts.type.enumValues)).withDefault([]),
executionMethod: parseAsArrayOf(z.enum(generalContracts.executionMethod.enumValues)).withDefault([]),
- selectionMethod: parseAsArrayOf(z.enum(generalContracts.selectionMethod.enumValues)).withDefault([]),
+ contractSourceType: parseAsArrayOf(z.enum(generalContracts.contractSourceType.enumValues)).withDefault([]),
vendorId: parseAsInteger.withDefault(0),
managerName: parseAsString.withDefault(""),
@@ -57,7 +57,6 @@ export const createGeneralContractSchema = z.object({
category: z.string().min(1, "계약구분을 선택해주세요"),
type: z.string().min(1, "계약종류를 선택해주세요"),
executionMethod: z.string().min(1, "체결방식을 선택해주세요"),
- selectionMethod: z.string().optional(),
vendorId: z.number().min(1, "협력업체를 선택해주세요"),
startDate: z.string().min(1, "계약시작일을 선택해주세요"),
endDate: z.string().min(1, "계약종료일을 선택해주세요"),