1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
|
"use client"
import * as React from "react"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Download, FileText } from "lucide-react"
import { toast } from "sonner"
import { formatCurrency, formatNumber } from "@/lib/utils"
import { getDownloadUrlByMaterialCode, checkPosFileExists } from "@/lib/pos"
import { PosFileSelectionDialog } from "@/lib/pos/components/pos-file-selection-dialog"
interface ContractItem {
materialNo?: string
itemDescription?: string
specification?: string
quantity?: number
quantityUnit?: string
ZPO_UNIT?: string
unitPrice?: number | string
contractAmount?: number | string
// SAP ECC 금액 필드
NETWR?: number
BRTWR?: number
ZPDT_EXDS_AMT?: number
// SAP 날짜 필드
ZPO_DLV_DT?: string
ZPLN_ST_DT?: string
ZPLN_ED_DT?: string
}
interface ContractItemsCardProps {
items: ContractItem[]
currency?: string | null
/**
* 뷰어 타입
* - 'evcp': EVCP 사용자 (암호화된 파일 직접 다운로드)
* - 'partners': 협력사 사용자 (복호화된 파일 다운로드)
*/
viewerType?: 'evcp' | 'partners'
}
export function ContractItemsCard({ items, currency, viewerType = 'partners' }: ContractItemsCardProps) {
// POS 파일 선택 다이얼로그 상태
const [posDialogOpen, setPosDialogOpen] = React.useState(false)
const [selectedMaterialCode, setSelectedMaterialCode] = React.useState<string>("")
const [posFiles, setPosFiles] = React.useState<Array<{
fileName: string
dcmtmId: string
projNo: string
posNo: string
posRevNo: string
fileSer: string
}>>([])
const [loadingPosFiles, setLoadingPosFiles] = React.useState(false)
const [downloadingFileIndex, setDownloadingFileIndex] = React.useState<number | null>(null)
// POS 파일 목록 조회 및 다이얼로그 열기
const handleOpenPosDialog = async (materialCode: string) => {
if (!materialCode) {
toast.error("자재코드가 없습니다")
return
}
setLoadingPosFiles(true)
setSelectedMaterialCode(materialCode)
try {
toast.loading(`POS 파일 목록 조회 중... (${materialCode})`, { id: `pos-check-${materialCode}` })
const result = await checkPosFileExists(materialCode)
if (result.exists && result.files && result.files.length > 0) {
// 파일 정보를 상세하게 가져오기 위해 getDownloadUrlByMaterialCode 사용
const detailResult = await getDownloadUrlByMaterialCode(materialCode)
if (detailResult.success && detailResult.availableFiles) {
setPosFiles(detailResult.availableFiles)
setPosDialogOpen(true)
toast.success(`${result.fileCount}개의 POS 파일을 찾았습니다`, { id: `pos-check-${materialCode}` })
} else {
toast.error('POS 파일 정보를 가져올 수 없습니다', { id: `pos-check-${materialCode}` })
}
} else {
toast.error(result.error || 'POS 파일을 찾을 수 없습니다', { id: `pos-check-${materialCode}` })
}
} catch (error) {
console.error("POS 파일 조회 오류:", error)
toast.error("POS 파일 조회에 실패했습니다", { id: `pos-check-${materialCode}` })
} finally {
setLoadingPosFiles(false)
}
}
// POS 파일 다운로드 실행
const handleDownloadPosFile = async (fileIndex: number, fileName: string) => {
if (!selectedMaterialCode) return
setDownloadingFileIndex(fileIndex)
try {
toast.loading(`POS 파일 다운로드 준비 중...`, { id: `download-${fileIndex}` })
// viewerType에 따라 다른 엔드포인트 사용
const endpoint = viewerType === 'partners'
? `/api/pos/download-on-demand-partners` // 복호화 포함
: `/api/pos/download-on-demand` // 암호화 파일 그대로
const downloadUrl = `${endpoint}?materialCode=${encodeURIComponent(selectedMaterialCode)}&fileIndex=${fileIndex}`
toast.success(`POS 파일 다운로드 시작: ${fileName}`, { id: `download-${fileIndex}` })
window.open(downloadUrl, '_blank', 'noopener,noreferrer')
// 다운로드 시작 후 잠시 대기 후 상태 초기화
setTimeout(() => {
setDownloadingFileIndex(null)
}, 1000)
} catch (error) {
console.error("POS 파일 다운로드 오류:", error)
toast.error("POS 파일 다운로드에 실패했습니다", { id: `download-${fileIndex}` })
setDownloadingFileIndex(null)
}
}
// POS 다이얼로그 닫기
const handleClosePosDialog = () => {
setPosDialogOpen(false)
setSelectedMaterialCode("")
setPosFiles([])
setDownloadingFileIndex(null)
}
if (!items || items.length === 0) {
return null
}
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">계약 품목</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead className="border-b bg-muted/50">
<tr>
<th className="px-4 py-3 text-left font-medium">자재번호</th>
<th className="px-4 py-3 text-left font-medium">품목/자재내역</th>
<th className="px-4 py-3 text-left font-medium">규격</th>
<th className="px-4 py-3 text-right font-medium">수량</th>
<th className="px-4 py-3 text-right font-medium">단가</th>
<th className="px-4 py-3 text-right font-medium">기본총액(BRTWR)</th>
<th className="px-4 py-3 text-right font-medium">조정금액(ZPDT)</th>
<th className="px-4 py-3 text-right font-medium">최종정가(NETWR)</th>
<th className="px-4 py-3 text-center font-medium">납기일자</th>
<th className="px-4 py-3 text-center font-medium">POS</th>
</tr>
</thead>
<tbody>
{items.map((item, idx) => (
<tr key={idx} className="border-b last:border-0">
<td className="px-4 py-3">{item.materialNo || "-"}</td>
<td className="px-4 py-3">{item.itemDescription || "-"}</td>
<td className="px-4 py-3">{item.specification || "-"}</td>
<td className="px-4 py-3 text-right">
{item.quantity} {item.ZPO_UNIT || item.quantityUnit || ""}
</td>
<td className="px-4 py-3 text-right font-mono">
{item.unitPrice
? currency
? formatCurrency(
parseFloat(item.unitPrice.toString()),
currency
)
: formatNumber(parseFloat(item.unitPrice.toString()))
: "-"}
</td>
<td className="px-4 py-3 text-right font-mono">
{item.BRTWR
? currency
? formatCurrency(item.BRTWR, currency)
: formatNumber(item.BRTWR)
: "-"}
</td>
<td className="px-4 py-3 text-right font-mono">
{item.ZPDT_EXDS_AMT
? currency
? formatCurrency(item.ZPDT_EXDS_AMT, currency)
: formatNumber(item.ZPDT_EXDS_AMT)
: "-"}
</td>
<td className="px-4 py-3 text-right font-mono font-medium">
{item.NETWR
? currency
? formatCurrency(item.NETWR, currency)
: formatNumber(item.NETWR)
: "-"}
</td>
<td className="px-4 py-3 text-center">
{item.ZPO_DLV_DT || "-"}
</td>
<td className="px-4 py-3 text-center">
{item.materialNo ? (
<Button
variant="ghost"
size="sm"
className="h-8 px-2 text-xs text-green-600 hover:text-green-800"
onClick={() => handleOpenPosDialog(item.materialNo!)}
disabled={loadingPosFiles && selectedMaterialCode === item.materialNo}
title={`POS 파일 다운로드 (자재코드: ${item.materialNo})`}
>
<FileText className="h-3 w-3 mr-1" />
<Download className="h-3 w-3 mr-1" />
{loadingPosFiles && selectedMaterialCode === item.materialNo ? '조회중...' : 'POS'}
</Button>
) : (
<span className="text-muted-foreground">-</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
{/* POS 파일 선택 다이얼로그 */}
<PosFileSelectionDialog
isOpen={posDialogOpen}
onClose={handleClosePosDialog}
materialCode={selectedMaterialCode}
files={posFiles}
onDownload={handleDownloadPosFile}
downloadingIndex={downloadingFileIndex}
/>
</Card>
)
}
|