summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-11-04 10:03:32 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-11-04 10:03:32 +0000
commit47fb72704161b4b58a27c7f5c679fc44618de9a1 (patch)
treeaf4fe1517352784d1876c164171f6dba2e40403a /app
parent1a034c7f6f50e443bc9f97c3d84bfb0a819af6ce (diff)
(최겸) 구매 견적 내 RFQ Cancel/Delete, 연동제 적용, MRC Type 개발
Diffstat (limited to 'app')
-rw-r--r--app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/layout.tsx23
-rw-r--r--app/[lng]/partners/(partners)/rfq-last/[id]/page.tsx37
-rw-r--r--app/api/partners/rfq-last/[id]/response/route.ts144
3 files changed, 193 insertions, 11 deletions
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/layout.tsx b/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/layout.tsx
index 6dcbf018..8fffb221 100644
--- a/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/layout.tsx
+++ b/app/[lng]/evcp/(evcp)/(procurement)/rfq-last/[id]/layout.tsx
@@ -4,7 +4,6 @@ import { Separator } from "@/components/ui/separator"
import { SidebarNav } from "@/components/layout/sidebar-nav"
import { formatDate } from "@/lib/utils"
import { Button } from "@/components/ui/button"
-import { Badge } from "@/components/ui/badge"
import { ArrowLeft, Clock, AlertTriangle, CheckCircle, XCircle, AlertCircle, Calendar, CalendarDays } from "lucide-react"
import { RfqsLastView } from "@/db/schema"
import { findRfqLastById } from "@/lib/rfq-last/service"
@@ -47,7 +46,27 @@ export default async function RfqLayout({
// 2) DB에서 해당 협력업체 정보 조회
const rfq: RfqsLastView | null = await findRfqLastById(rfqId)
- // 3) 사이드바 메뉴
+ // 3) 취소된 RFQ 접근 제어
+ if (rfq?.status === "RFQ 삭제") {
+ return (
+ <div className="p-4 space-y-4">
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertTitle>접근 불가</AlertTitle>
+ <AlertDescription>
+ 이 RFQ는 삭제되어 접근할 수 없습니다.
+ </AlertDescription>
+ </Alert>
+ <div className="p-4 bg-muted rounded-lg">
+ <p className="text-sm font-medium mb-2">삭제 사유:</p>
+ <p className="text-sm text-muted-foreground whitespace-pre-wrap">{rfq.deleteReason}</p>
+ </div>
+
+ </div>
+ );
+ }
+
+ // 4) 사이드바 메뉴
const sidebarNavItems = [
{
title: "견적 문서관리",
diff --git a/app/[lng]/partners/(partners)/rfq-last/[id]/page.tsx b/app/[lng]/partners/(partners)/rfq-last/[id]/page.tsx
index 7a68e3a2..9052de6f 100644
--- a/app/[lng]/partners/(partners)/rfq-last/[id]/page.tsx
+++ b/app/[lng]/partners/(partners)/rfq-last/[id]/page.tsx
@@ -86,6 +86,23 @@ export default async function VendorResponsePage({ params }: PageProps) {
if (!rfq || !rfq.rfqDetails[0]) {
notFound()
}
+
+ // 취소된 RFQ 접근 제어
+ if (rfq.status === "RFQ 삭제") {
+ return (
+ <div className="flex h-full items-center justify-center">
+ <div className="text-center max-w-md">
+ <h2 className="text-xl font-bold">접근 불가</h2>
+ <p className="mt-2 text-muted-foreground">이 RFQ는 삭제되어 접근할 수 없습니다.</p>
+ <div className="mt-4 p-4 bg-muted rounded-lg text-left">
+ <p className="text-sm font-medium mb-2">삭제 사유:</p>
+ <p className="text-sm text-muted-foreground whitespace-pre-wrap">{rfq.deleteReason}</p>
+ </div>
+
+ </div>
+ </div>
+ )
+ }
const rfqDetail = rfq.rfqDetails[0]
@@ -99,8 +116,28 @@ export default async function VendorResponsePage({ params }: PageProps) {
with: {
quotationItems: true,
attachments: true,
+ priceAdjustmentForm: true,
}
})
+
+ // 취소된 RFQ 접근 제어 (vendor response가 취소 상태인 경우)
+ if (existingResponse?.status === "취소" || rfqDetail.cancelReason) {
+ return (
+ <div className="flex h-full items-center justify-center">
+ <div className="text-center">
+ <h2 className="text-xl font-bold">RFQ 취소됨</h2>
+ <p className="mt-2 text-muted-foreground">
+ 이 RFQ는 취소되어 더 이상 견적을 제출할 수 없습니다.
+ </p>
+ {rfqDetail.cancelReason && (
+ <p className="mt-4 text-sm text-muted-foreground">
+ 취소 사유: {rfqDetail.cancelReason}
+ </p>
+ )}
+ </div>
+ </div>
+ )
+ }
// PR Items 가져오기
const prItems = await db.query.rfqPrItems.findMany({
diff --git a/app/api/partners/rfq-last/[id]/response/route.ts b/app/api/partners/rfq-last/[id]/response/route.ts
index 5d05db50..06ace9a0 100644
--- a/app/api/partners/rfq-last/[id]/response/route.ts
+++ b/app/api/partners/rfq-last/[id]/response/route.ts
@@ -7,9 +7,10 @@ import {
rfqLastVendorResponses,
rfqLastVendorQuotationItems,
rfqLastVendorAttachments,
- rfqLastVendorResponseHistory
+ rfqLastVendorResponseHistory,
+ rfqLastPriceAdjustmentForms
} from "@/db/schema"
-import { eq, and, inArray } from "drizzle-orm"
+import { eq, and } from "drizzle-orm"
import { writeFile, mkdir } from "fs/promises"
import { createWriteStream } from "fs"
import { pipeline } from "stream/promises"
@@ -139,17 +140,77 @@ export async function POST(
await tx.insert(rfqLastVendorQuotationItems).values(quotationItemsData)
}
- // 이력 기록
+ // 3. 연동제 정보 저장 (연동제 적용이 true이고 연동제 정보가 있는 경우)
+ if (data.vendorMaterialPriceRelatedYn && data.priceAdjustmentForm && vendorResponse.id) {
+ const priceAdjustmentData: {
+ rfqLastVendorResponsesId: number;
+ itemName?: string | null;
+ adjustmentReflectionPoint?: string | null;
+ majorApplicableRawMaterial?: string | null;
+ adjustmentFormula?: string | null;
+ rawMaterialPriceIndex?: string | null;
+ referenceDate?: string | null;
+ comparisonDate?: string | null;
+ adjustmentRatio?: number | null;
+ notes?: string | null;
+ adjustmentConditions?: string | null;
+ majorNonApplicableRawMaterial?: string | null;
+ adjustmentPeriod?: string | null;
+ contractorWriter?: string | null;
+ adjustmentDate?: string | null;
+ nonApplicableReason?: string | null;
+ } = {
+ rfqLastVendorResponsesId: vendorResponse.id,
+ itemName: data.priceAdjustmentForm.itemName || null,
+ adjustmentReflectionPoint: data.priceAdjustmentForm.adjustmentReflectionPoint || null,
+ majorApplicableRawMaterial: data.priceAdjustmentForm.majorApplicableRawMaterial || null,
+ adjustmentFormula: data.priceAdjustmentForm.adjustmentFormula || null,
+ rawMaterialPriceIndex: data.priceAdjustmentForm.rawMaterialPriceIndex || null,
+ referenceDate: data.priceAdjustmentForm.referenceDate || null,
+ comparisonDate: data.priceAdjustmentForm.comparisonDate || null,
+ adjustmentRatio: data.priceAdjustmentForm.adjustmentRatio || null,
+ notes: data.priceAdjustmentForm.notes || null,
+ adjustmentConditions: data.priceAdjustmentForm.adjustmentConditions || null,
+ majorNonApplicableRawMaterial: data.priceAdjustmentForm.majorNonApplicableRawMaterial || null,
+ adjustmentPeriod: data.priceAdjustmentForm.adjustmentPeriod || null,
+ contractorWriter: data.priceAdjustmentForm.contractorWriter || null,
+ adjustmentDate: data.priceAdjustmentForm.adjustmentDate || null,
+ nonApplicableReason: data.priceAdjustmentForm.nonApplicableReason || null,
+ }
+
+ // 기존 연동제 정보가 있는지 확인
+ const existingPriceAdjustment = await tx
+ .select()
+ .from(rfqLastPriceAdjustmentForms)
+ .where(eq(rfqLastPriceAdjustmentForms.rfqLastVendorResponsesId, vendorResponse.id))
+ .limit(1)
+
+ if (existingPriceAdjustment.length > 0) {
+ // 업데이트
+ await tx
+ .update(rfqLastPriceAdjustmentForms)
+ .set({
+ ...priceAdjustmentData,
+ updatedAt: new Date(),
+ })
+ .where(eq(rfqLastPriceAdjustmentForms.rfqLastVendorResponsesId, vendorResponse.id))
+ } else {
+ // 새로 생성
+ await tx.insert(rfqLastPriceAdjustmentForms).values(priceAdjustmentData)
+ }
+ }
+
+ // 4. 이력 기록
await tx.insert(rfqLastVendorResponseHistory).values({
- vendorResponseId: vendorResponseId,
- action: isNewResponse ? "생성" : (data.status === "제출완료" ? "제출" : "수정"),
- previousStatus: existingResponse?.status || null,
+ vendorResponseId: vendorResponse.id,
+ action: "생성",
+ previousStatus: null,
newStatus: data.status || "작성중",
changeDetails: data,
performedBy: session.user.id,
})
- return { id: vendorResponseId, isNew: isNewResponse }
+ return { id: vendorResponse.id, isNew: true }
})
// 파일 저장 (트랜잭션 밖에서 처리)
@@ -262,7 +323,7 @@ export async function PUT(
const previousStatus = existingResponse.status
// 2. 새 버전 생성 (제출 시) 또는 기존 버전 업데이트
- let responseId = existingResponse.id
+ const responseId = existingResponse.id
// if (data.status === "제출완료" && previousStatus !== "제출완료") {
// // 기존 버전을 비활성화
@@ -362,7 +423,72 @@ export async function PUT(
await tx.insert(rfqLastVendorQuotationItems).values(quotationItemsData)
}
- // 4. 이력 기록
+ // 4. 연동제 정보 저장/업데이트 (연동제 적용이 true이고 연동제 정보가 있는 경우)
+ if (data.vendorMaterialPriceRelatedYn && data.priceAdjustmentForm && responseId) {
+ const priceAdjustmentData: {
+ rfqLastVendorResponsesId: number;
+ itemName?: string | null;
+ adjustmentReflectionPoint?: string | null;
+ majorApplicableRawMaterial?: string | null;
+ adjustmentFormula?: string | null;
+ rawMaterialPriceIndex?: string | null;
+ referenceDate?: string | null;
+ comparisonDate?: string | null;
+ adjustmentRatio?: number | null;
+ notes?: string | null;
+ adjustmentConditions?: string | null;
+ majorNonApplicableRawMaterial?: string | null;
+ adjustmentPeriod?: string | null;
+ contractorWriter?: string | null;
+ adjustmentDate?: string | null;
+ nonApplicableReason?: string | null;
+ } = {
+ rfqLastVendorResponsesId: responseId,
+ itemName: data.priceAdjustmentForm.itemName || null,
+ adjustmentReflectionPoint: data.priceAdjustmentForm.adjustmentReflectionPoint || null,
+ majorApplicableRawMaterial: data.priceAdjustmentForm.majorApplicableRawMaterial || null,
+ adjustmentFormula: data.priceAdjustmentForm.adjustmentFormula || null,
+ rawMaterialPriceIndex: data.priceAdjustmentForm.rawMaterialPriceIndex || null,
+ referenceDate: data.priceAdjustmentForm.referenceDate || null,
+ comparisonDate: data.priceAdjustmentForm.comparisonDate || null,
+ adjustmentRatio: data.priceAdjustmentForm.adjustmentRatio || null,
+ notes: data.priceAdjustmentForm.notes || null,
+ adjustmentConditions: data.priceAdjustmentForm.adjustmentConditions || null,
+ majorNonApplicableRawMaterial: data.priceAdjustmentForm.majorNonApplicableRawMaterial || null,
+ adjustmentPeriod: data.priceAdjustmentForm.adjustmentPeriod || null,
+ contractorWriter: data.priceAdjustmentForm.contractorWriter || null,
+ adjustmentDate: data.priceAdjustmentForm.adjustmentDate || null,
+ nonApplicableReason: data.priceAdjustmentForm.nonApplicableReason || null,
+ }
+
+ // 기존 연동제 정보가 있는지 확인
+ const existingPriceAdjustment = await tx
+ .select()
+ .from(rfqLastPriceAdjustmentForms)
+ .where(eq(rfqLastPriceAdjustmentForms.rfqLastVendorResponsesId, responseId))
+ .limit(1)
+
+ if (existingPriceAdjustment.length > 0) {
+ // 업데이트
+ await tx
+ .update(rfqLastPriceAdjustmentForms)
+ .set({
+ ...priceAdjustmentData,
+ updatedAt: new Date(),
+ })
+ .where(eq(rfqLastPriceAdjustmentForms.rfqLastVendorResponsesId, responseId))
+ } else {
+ // 새로 생성
+ await tx.insert(rfqLastPriceAdjustmentForms).values(priceAdjustmentData)
+ }
+ } else if (data.vendorMaterialPriceRelatedYn === false && responseId) {
+ // 연동제 미적용 시 기존 데이터 삭제
+ await tx
+ .delete(rfqLastPriceAdjustmentForms)
+ .where(eq(rfqLastPriceAdjustmentForms.rfqLastVendorResponsesId, responseId))
+ }
+
+ // 5. 이력 기록
await tx.insert(rfqLastVendorResponseHistory).values({
vendorResponseId: responseId,
action: data.status === "제출완료" ? "제출" : "수정",