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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
|
"use server"
import { oracleKnex } from '@/lib/oracle-db/db'
import db from '@/db/db'
import { basicContract, agreementComments } from '@/db/schema'
import { eq, inArray, and } from 'drizzle-orm'
import { revalidateTag } from 'next/cache'
import { sendEmail } from '@/lib/mail/sendEmail'
// SSLVW_PUR_INQ_REQ 테이블 데이터 타입 (실제 테이블 구조에 맞게 조정 필요)
export interface SSLVWPurInqReq {
[key: string]: string | number | Date | null | undefined
}
// 테스트 환경용 폴백 데이터
const FALLBACK_TEST_DATA: SSLVWPurInqReq[] = [
{
id: 1,
request_number: 'REQ001',
status: 'PENDING',
created_date: new Date('2025-01-01'),
description: '테스트 요청 1'
},
{
id: 2,
request_number: 'REQ002',
status: 'APPROVED',
created_date: new Date('2025-01-02'),
description: '테스트 요청 2'
}
]
/**
* SSLVW_PUR_INQ_REQ 테이블 전체 조회
* @returns 테이블 데이터 배열
*/
export async function getSSLVWPurInqReqData(): Promise<{
success: boolean
data: SSLVWPurInqReq[]
error?: string
isUsingFallback?: boolean
}> {
try {
console.log('📋 [getSSLVWPurInqReqData] SSLVW_PUR_INQ_REQ 테이블 조회 시작...')
const result = await oracleKnex.raw(`
SELECT
ID,
PRGS_STAT_DSC,
REQ_DT,
REQ_NO,
REQ_TIT,
REQ_CONT,
VEND_CD,
VEND_NM,
CNTR_CTGR_DSC,
CNTR_AMT,
CNTR_STRT_DT,
CNTR_END_DT,
RPLY_DT,
RPLY_CONT,
RPLY_USER_NM,
RPLY_USER_ID,
CREATED_AT,
UPDATED_AT
FROM SSLVW_PUR_INQ_REQ
WHERE ROWNUM < 100
ORDER BY REQ_DT DESC
`)
// Oracle raw query의 결과는 rows 배열에 들어있음
const rows = (result.rows || result) as Array<Record<string, unknown>>
console.log(`✅ [getSSLVWPurInqReqData] 조회 성공 - ${rows.length}건`)
// 데이터 타입 변환 (필요에 따라 조정)
const cleanedResult = rows.map((item) => {
const convertedItem: SSLVWPurInqReq = {}
for (const [key, value] of Object.entries(item)) {
if (value instanceof Date) {
convertedItem[key] = value
} else if (value === null) {
convertedItem[key] = null
} else {
convertedItem[key] = String(value)
}
}
return convertedItem
})
return {
success: true,
data: cleanedResult,
isUsingFallback: false
}
} catch (error) {
console.error('❌ [getSSLVWPurInqReqData] 오류:', error)
console.log('🔄 [getSSLVWPurInqReqData] 폴백 테스트 데이터 사용')
return {
success: true,
data: FALLBACK_TEST_DATA,
isUsingFallback: true
}
}
}
/**
* 법무검토 요청
* @param contractIds 계약서 ID 배열
* @returns 성공 여부 및 메시지
*/
export async function requestLegalReview(contractIds: number[]): Promise<{
success: boolean
message: string
requested: number
skipped: number
errors: string[]
}> {
console.log(`📋 [requestLegalReview] 법무검토 요청 시작: ${contractIds.length}건`)
if (!contractIds || contractIds.length === 0) {
return {
success: false,
message: '선택된 계약서가 없습니다.',
requested: 0,
skipped: 0,
errors: []
}
}
try {
// 계약서 정보 조회
const contracts = await db
.select()
.from(basicContract)
.where(inArray(basicContract.id, contractIds))
if (!contracts || contracts.length === 0) {
return {
success: false,
message: '계약서를 찾을 수 없습니다.',
requested: 0,
skipped: 0,
errors: []
}
}
let requestedCount = 0
let skippedCount = 0
const errors: string[] = []
for (const contract of contracts) {
try {
// 유효성 검사
if (contract.legalReviewRequestedAt) {
console.log(`⚠️ [requestLegalReview] 계약서 ${contract.id}: 이미 법무검토 요청됨`)
skippedCount++
errors.push(`${contract.id}: 이미 법무검토 요청됨`)
continue
}
// 협의 완료 여부 확인
// 1. 협의 완료됨 (negotiationCompletedAt 있음) → 가능
// 2. 협의 없음 (코멘트 없음) → 가능
// 3. 협의 중 (negotiationCompletedAt 없고 코멘트 있음) → 불가
if (!contract.negotiationCompletedAt) {
// 협의 완료되지 않은 경우, 코멘트 존재 여부 확인
// 삭제되지 않은 코멘트가 있으면 협의 중이므로 불가
const comments = await db
.select()
.from(agreementComments)
.where(
and(
eq(agreementComments.basicContractId, contract.id),
eq(agreementComments.isDeleted, false)
)
)
.limit(1);
// 삭제되지 않은 코멘트가 있으면 협의 중이므로 불가
if (comments.length > 0) {
console.log(`⚠️ [requestLegalReview] 계약서 ${contract.id}: 협의 진행 중`)
skippedCount++
errors.push(`${contract.id}: 협의가 진행 중입니다`)
continue
}
// 코멘트가 없으면 협의 없음으로 간주하고 가능
console.log(`ℹ️ [requestLegalReview] 계약서 ${contract.id}: 협의 없음, 법무검토 요청 가능`)
}
// 법무검토 요청 상태로 업데이트
await db
.update(basicContract)
.set({
legalReviewRequestedAt: new Date(),
updatedAt: new Date(),
} as any)
.where(eq(basicContract.id, contract.id))
requestedCount++
console.log(`✅ [requestLegalReview] 계약서 ${contract.id}: 법무검토 요청 완료`)
// 법무팀에 이메일 알림 발송 (선택사항)
try {
// TODO: 법무팀 이메일 주소를 설정에서 가져오기
const legalTeamEmail = process.env.LEGAL_TEAM_EMAIL || 'legal@example.com'
await sendEmail({
to: legalTeamEmail,
subject: `[eVCP] 기본계약서 법무검토 요청 - ${contract.id}`,
template: 'legal-review-request',
context: {
language: 'ko',
contractId: contract.id,
vendorName: contract.vendorId || '업체명 없음',
contractUrl: `${process.env.NEXT_PUBLIC_APP_URL}/evcp/basic-contract/${contract.id}`,
systemUrl: process.env.NEXT_PUBLIC_APP_URL || 'https://evcp.com',
currentYear: new Date().getFullYear(),
},
})
} catch (emailError) {
console.error(`⚠️ [requestLegalReview] 이메일 발송 실패 (계약서 ${contract.id}):`, emailError)
// 이메일 실패는 법무검토 요청 성공에 영향을 주지 않음
}
} catch (error) {
console.error(`❌ [requestLegalReview] 계약서 ${contract.id} 처리 실패:`, error)
errors.push(`${contract.id}: 처리 중 오류 발생`)
skippedCount++
}
}
// 캐시 무효화
revalidateTag('basic-contracts')
const totalProcessed = requestedCount + skippedCount
let message = ''
if (requestedCount === contracts.length) {
message = `${requestedCount}건의 계약서에 대한 법무검토가 요청되었습니다.`
} else if (requestedCount > 0) {
message = `${requestedCount}건 요청 완료, ${skippedCount}건 건너뜀`
} else {
message = `모든 계약서를 건너뛰었습니다. (${skippedCount}건)`
}
console.log(`✅ [requestLegalReview] 법무검토 요청 완료: ${message}`)
return {
success: requestedCount > 0,
message,
requested: requestedCount,
skipped: skippedCount,
errors
}
} catch (error) {
console.error('❌ [requestLegalReview] 법무검토 요청 실패:', error)
return {
success: false,
message: '법무검토 요청 중 오류가 발생했습니다.',
requested: 0,
skipped: contractIds.length,
errors: [error instanceof Error ? error.message : '알 수 없는 오류']
}
}
}
|