From e84cf02a1cb4959a9d3bb5bbf37885c13a447f78 Mon Sep 17 00:00:00 2001
From: joonhoekim <26rote@gmail.com>
Date: Mon, 13 Oct 2025 17:29:33 +0900
Subject: (김준회) SHI/벤더 PO 구현
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../evcp/(evcp)/(master-data)/projects/page.tsx | 2 +-
.../po/[id]/contract-detail-client.tsx | 143 ++++++++++++
.../evcp/(evcp)/(procurement)/po/[id]/page.tsx | 56 +++++
.../(partners)/po/[id]/contract-detail-client.tsx | 249 +++++++++++++++++++++
app/[lng]/partners/(partners)/po/[id]/page.tsx | 55 +++++
.../(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts | 10 +-
6 files changed, 512 insertions(+), 3 deletions(-)
create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/po/[id]/contract-detail-client.tsx
create mode 100644 app/[lng]/evcp/(evcp)/(procurement)/po/[id]/page.tsx
create mode 100644 app/[lng]/partners/(partners)/po/[id]/contract-detail-client.tsx
create mode 100644 app/[lng]/partners/(partners)/po/[id]/page.tsx
(limited to 'app')
diff --git a/app/[lng]/evcp/(evcp)/(master-data)/projects/page.tsx b/app/[lng]/evcp/(evcp)/(master-data)/projects/page.tsx
index c3d429d1..c9e6b0e3 100644
--- a/app/[lng]/evcp/(evcp)/(master-data)/projects/page.tsx
+++ b/app/[lng]/evcp/(evcp)/(master-data)/projects/page.tsx
@@ -36,7 +36,7 @@ export default async function IndexPage(props: IndexPageProps) {
- 프로젝트 리스트 from S-EDP
+ 프로젝트 리스트
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/po/[id]/contract-detail-client.tsx b/app/[lng]/evcp/(evcp)/(procurement)/po/[id]/contract-detail-client.tsx
new file mode 100644
index 00000000..28a85e50
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(procurement)/po/[id]/contract-detail-client.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import { ChevronLeft } from "lucide-react"
+import Link from "next/link"
+import { toast } from "sonner"
+import { saveSHIComment } from "@/lib/po/vendor-table/service"
+import { useRouter } from "next/navigation"
+import { ContractInfoCard } from "@/components/contract/contract-info-card"
+import { ContractItemsCard } from "@/components/contract/contract-items-card"
+import { Alert, AlertDescription } from "@/components/ui/alert"
+import { AlertCircle } from "lucide-react"
+
+interface ContractDetailClientProps {
+ contract: any
+ lng: string
+}
+
+export function ContractDetailClient({ contract, lng }: ContractDetailClientProps) {
+ const router = useRouter()
+ const [shiComment, setSHIComment] = React.useState(contract.shiComment || "")
+ const [isLoading, setIsLoading] = React.useState(false)
+
+ const handleSaveComment = async () => {
+ try {
+ setIsLoading(true)
+ const result = await saveSHIComment(contract.id, shiComment)
+ if (result.success) {
+ toast.success(result.message)
+ router.refresh()
+ } else {
+ toast.error("의견 저장에 실패했습니다.")
+ }
+ } catch (error) {
+ toast.error("의견 저장 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return (
+ <>
+ {/* 헤더 */}
+
+
+
+
+
+
+
계약 상세
+
+ 계약번호:
+ {contract.contractNo}
+
+ {contract.status}
+
+
+
+
+
+
+
+
+
+ {/* 계약 거절 사유 표시 (있는 경우) */}
+ {contract.rejectionReason && (
+
+
+
+ 계약 거절 사유: {contract.rejectionReason}
+
+
+ )}
+
+ {/* 계약 조건 */}
+
+
+ {/* 코멘트 */}
+
+
+ 코멘트
+
+
+
+
+
+
+ {contract.vendorComment || "코멘트가 없습니다."}
+
+
+
+
+
+
+
+
+
+ {/* 계약문서 */}
+
+
+ 계약문서
+
+
+ {contract.contractContent ? (
+
+
+ {contract.contractContent}
+
+
+ ) : (
+ 계약문서 내용이 없습니다.
+ )}
+
+
+
+ {/* 계약 품목 */}
+
+ >
+ )
+}
+
diff --git a/app/[lng]/evcp/(evcp)/(procurement)/po/[id]/page.tsx b/app/[lng]/evcp/(evcp)/(procurement)/po/[id]/page.tsx
new file mode 100644
index 00000000..226d960d
--- /dev/null
+++ b/app/[lng]/evcp/(evcp)/(procurement)/po/[id]/page.tsx
@@ -0,0 +1,56 @@
+import * as React from "react"
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { redirect } from "next/navigation"
+import { getContractDetail } from "@/lib/po/vendor-table/service"
+import { Shell } from "@/components/shell"
+import { ContractDetailClient } from "./contract-detail-client"
+
+interface ContractDetailPageProps {
+ params: Promise<{
+ id: string
+ lng: string
+ }>
+}
+
+export default async function EVCPContractDetailPage(props: ContractDetailPageProps) {
+ const params = await props.params
+ const contractId = parseInt(params.id, 10)
+
+ // 유효하지 않은 ID 체크
+ if (isNaN(contractId) || contractId <= 0) {
+ return (
+
+
+
+ )
+ }
+
+ // 세션 체크 (EVCP 사용자만 접근 가능)
+ const session = await getServerSession(authOptions)
+ if (!session?.user) {
+ redirect("/")
+ }
+
+ // 계약 상세 정보 조회
+ const result = await getContractDetail(contractId)
+
+ if (!result.success || !result.data) {
+ return (
+
+
+
{result.error || "계약 정보를 찾을 수 없습니다."}
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
+
diff --git a/app/[lng]/partners/(partners)/po/[id]/contract-detail-client.tsx b/app/[lng]/partners/(partners)/po/[id]/contract-detail-client.tsx
new file mode 100644
index 00000000..a6f7a729
--- /dev/null
+++ b/app/[lng]/partners/(partners)/po/[id]/contract-detail-client.tsx
@@ -0,0 +1,249 @@
+"use client"
+
+import * as React from "react"
+import { Button } from "@/components/ui/button"
+import { Badge } from "@/components/ui/badge"
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog"
+import { Textarea } from "@/components/ui/textarea"
+import { ChevronLeft } from "lucide-react"
+import Link from "next/link"
+import { toast } from "sonner"
+import { saveVendorComment, acceptContract, rejectContract } from "@/lib/po/vendor-table/service"
+import { useRouter } from "next/navigation"
+import { ContractInfoCard } from "@/components/contract/contract-info-card"
+import { ContractItemsCard } from "@/components/contract/contract-items-card"
+
+interface ContractDetailClientProps {
+ contract: any
+ lng: string
+}
+
+export function ContractDetailClient({ contract, lng }: ContractDetailClientProps) {
+ const router = useRouter()
+ const [vendorComment, setVendorComment] = React.useState(contract.vendorComment || "")
+ const [isLoading, setIsLoading] = React.useState(false)
+ const [isRejectDialogOpen, setIsRejectDialogOpen] = React.useState(false)
+ const [rejectReason, setRejectReason] = React.useState("")
+
+ const handleSaveComment = async () => {
+ try {
+ setIsLoading(true)
+ const result = await saveVendorComment(contract.id, vendorComment)
+ if (result.success) {
+ toast.success(result.message)
+ router.refresh()
+ } else {
+ toast.error("의견 저장에 실패했습니다.")
+ }
+ } catch {
+ toast.error("의견 저장 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleApprove = async () => {
+ try {
+ setIsLoading(true)
+ const result = await acceptContract(contract.id)
+ if (result.success) {
+ toast.success(result.message)
+ router.refresh()
+ } else {
+ toast.error("계약 승인에 실패했습니다.")
+ }
+ } catch {
+ toast.error("계약 승인 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleRejectClick = () => {
+ setIsRejectDialogOpen(true)
+ }
+
+ const handleRejectConfirm = async () => {
+ if (!rejectReason.trim()) {
+ toast.error("계약 반려 사유를 입력해주세요.")
+ return
+ }
+
+ try {
+ setIsLoading(true)
+ const result = await rejectContract(contract.id, rejectReason)
+ if (result.success) {
+ toast.success(result.message)
+ setIsRejectDialogOpen(false)
+ setRejectReason("")
+ router.refresh()
+ } else {
+ toast.error("계약 거절에 실패했습니다.")
+ }
+ } catch {
+ toast.error("계약 거절 중 오류가 발생했습니다.")
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ const handleRejectCancel = () => {
+ setIsRejectDialogOpen(false)
+ setRejectReason("")
+ }
+
+ return (
+ <>
+ {/* 헤더 */}
+
+
+
+
+
+
+
계약 상세
+
+ 계약번호:
+ {contract.contractNo}
+
+ {contract.status}
+
+
+
+
+
+
+
+
+
+
+
+ {/* 계약 조건 */}
+
+
+ {/* 코멘트 */}
+
+
+ 코멘트
+
+
+
+
+
+
+
+
+
+ {contract.shiComment || "코멘트가 없습니다."}
+
+
+
+
+
+
+ {/* 계약문서 */}
+
+
+ 계약문서
+
+
+ {contract.contractContent ? (
+
+
+ {contract.contractContent}
+
+
+ ) : (
+ 계약문서 내용이 없습니다.
+ )}
+
+
+
+ {/* 계약 품목 */}
+
+
+ {/* 계약 반려 다이얼로그 */}
+
+ >
+ )
+}
+
diff --git a/app/[lng]/partners/(partners)/po/[id]/page.tsx b/app/[lng]/partners/(partners)/po/[id]/page.tsx
new file mode 100644
index 00000000..8df2c90e
--- /dev/null
+++ b/app/[lng]/partners/(partners)/po/[id]/page.tsx
@@ -0,0 +1,55 @@
+import * as React from "react"
+import { getServerSession } from "next-auth/next"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { redirect } from "next/navigation"
+import { getVendorContractDetail } from "@/lib/po/vendor-table/service"
+import { Shell } from "@/components/shell"
+import { ContractDetailClient } from "./contract-detail-client"
+
+interface ContractDetailPageProps {
+ params: Promise<{
+ id: string
+ lng: string
+ }>
+}
+
+export default async function ContractDetailPage(props: ContractDetailPageProps) {
+ const params = await props.params
+ const contractId = parseInt(params.id, 10)
+
+ // 유효하지 않은 ID 체크
+ if (isNaN(contractId) || contractId <= 0) {
+ return (
+
+
+
+ )
+ }
+
+ // 세션에서 벤더 정보 가져오기
+ const session = await getServerSession(authOptions)
+ if (!session?.user?.companyId) {
+ redirect("/")
+ }
+
+ // 계약 상세 정보 조회
+ const result = await getVendorContractDetail(contractId, session.user.companyId)
+
+ if (!result.success || !result.data) {
+ return (
+
+
+
{result.error || "계약 정보를 찾을 수 없습니다."}
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts
index eea16e84..c6275c68 100644
--- a/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts
+++ b/app/api/(S-ERP)/(ECC)/IF_ECC_EVCP_PO_INFORMATION/route.ts
@@ -117,9 +117,15 @@ export async function POST(request: NextRequest) {
await saveToDatabase(processedData);
// 6) PO 데이터를 비즈니스 테이블(contracts, contract_items)로 매핑
- // ProcessedPOData를 POMapperProcessedData 형식으로 변환
+ // ProcessedPOData를 POMapperProcessedData 형식으로 변환 (notes 포함)
const mappingData: POMapperProcessedData[] = processedData.map(poData => ({
- header: poData.header,
+ header: {
+ ...poData.header,
+ notes: poData.notes.map(note => ({
+ ZNOTE_SER: note.ZNOTE_SER || '',
+ ZNOTE_TXT: note.ZNOTE_TXT || ''
+ }))
+ },
details: poData.details
}));
--
cgit v1.2.3