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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
|
// AVL 기반 RFQ/ITB 생성 서비스
'use server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
import db from '@/db/db'
import { users, rfqsLast, rfqPrItems } from '@/db/schema'
import { eq, desc, sql, and } from 'drizzle-orm'
import type { AvlDetailItem } from './types'
// RFQ/ITB 코드 생성 헬퍼 함수
async function generateAvlRfqItbCode(userCode: string, type: 'RFQ' | 'ITB'): Promise<string> {
try {
// 동일한 userCode를 가진 마지막 RFQ/ITB 번호 조회
const lastRfq = await db
.select({ rfqCode: rfqsLast.rfqCode })
.from(rfqsLast)
.where(
and(
eq(rfqsLast.picCode, userCode),
type === 'RFQ'
? sql`${rfqsLast.prNumber} IS NOT NULL AND ${rfqsLast.prNumber} != ''`
: sql`${rfqsLast.projectCompany} IS NOT NULL AND ${rfqsLast.projectCompany} != ''`
)
)
.orderBy(desc(rfqsLast.createdAt))
.limit(1);
let nextNumber = 1;
if (lastRfq.length > 0 && lastRfq[0].rfqCode) {
// 마지막 코드에서 숫자 부분 추출 (ex: "RFQ001001" -> "001")
const codeMatch = lastRfq[0].rfqCode.match(/([A-Z]{3})(\d{3})(\d{3})/);
if (codeMatch) {
const currentNumber = parseInt(codeMatch[3]);
nextNumber = currentNumber + 1;
}
}
// 코드 형식: RFQ/ITB + userCode(3자리) + 일련번호(3자리)
const prefix = type === 'RFQ' ? 'RFQ' : 'ITB';
const paddedNumber = nextNumber.toString().padStart(3, '0');
return `${prefix}${userCode}${paddedNumber}`;
} catch (error) {
console.error('RFQ/ITB 코드 생성 오류:', error);
// 오류 발생 시 기본 코드 생성
const prefix = type === 'RFQ' ? 'RFQ' : 'ITB';
return `${prefix}${userCode}001`;
}
}
// AVL 기반 RFQ/ITB 생성을 위한 입력 타입
export interface CreateAvlRfqItbInput {
// AVL 정보
avlItems: AvlDetailItem[]
businessType: '조선' | '해양' // 조선: RFQ, 해양: ITB
// RFQ/ITB 공통 정보
rfqTitle: string
dueDate: Date
remark?: string
// 담당자 정보
picUserId: number
// 추가 정보 (ITB용)
projectCompany?: string
projectFlag?: string
projectSite?: string
smCode?: string
// PR 정보 (RFQ용)
prNumber?: string
prIssueDate?: Date
series?: string
}
// RFQ/ITB 생성 결과 타입
export interface CreateAvlRfqItbResult {
success: boolean
message?: string
data?: {
id: number
rfqCode: string
type: 'RFQ' | 'ITB'
}
error?: string
}
/**
* AVL 기반 RFQ/ITB 생성 서비스
* - 조선 사업: RFQ 생성
* - 해양 사업: ITB 생성
* - rfqLast 테이블에 직접 데이터 삽입
*/
export async function createAvlRfqItbAction(input: CreateAvlRfqItbInput): Promise<CreateAvlRfqItbResult> {
try {
// 세션 확인
const session = await getServerSession(authOptions)
if (!session?.user?.id) {
return {
success: false,
error: '로그인이 필요합니다.'
}
}
// 입력 검증
if (!input.avlItems || input.avlItems.length === 0) {
return {
success: false,
error: '견적 요청할 AVL 아이템이 없습니다.'
}
}
if (!input.businessType || !['조선', '해양'].includes(input.businessType)) {
return {
success: false,
error: '올바른 사업 유형을 선택해주세요.'
}
}
// 담당자 정보 확인
const picUser = await db
.select({
id: users.id,
name: users.name,
userCode: users.userCode
})
.from(users)
.where(eq(users.id, input.picUserId))
.limit(1)
if (!picUser || picUser.length === 0) {
return {
success: false,
error: '담당자를 찾을 수 없습니다.'
}
}
const userCode = picUser[0].userCode;
if (!userCode || userCode.length !== 3) {
return {
success: false,
error: '담당자의 userCode가 올바르지 않습니다 (3자리 필요)'
}
}
// 사업 유형에 따른 RFQ/ITB 구분 및 데이터 준비
const rfqType = input.businessType === '조선' ? 'RFQ' : 'ITB'
const rfqTypeLabel = rfqType
// RFQ/ITB 코드 생성
const rfqCode = await generateAvlRfqItbCode(userCode, rfqType)
// 대표 아이템 정보 추출 (첫 번째 아이템)
const representativeItem = input.avlItems[0]
// 트랜잭션으로 RFQ/ITB 생성
const result = await db.transaction(async (tx) => {
// 1. rfqsLast 테이블에 기본 정보 삽입
const [newRfq] = await tx
.insert(rfqsLast)
.values({
rfqCode,
status: "RFQ 생성",
dueDate: input.dueDate,
// 대표 아이템 정보
itemCode: representativeItem.materialGroupCode || `AVL-${representativeItem.id}`,
itemName: representativeItem.materialNameCustomerSide || representativeItem.materialGroupName || 'AVL 아이템',
// 담당자 정보
pic: input.picUserId,
picCode: userCode,
picName: picUser[0].name || '',
// 기타 정보
remark: input.remark || null,
createdBy: Number(session.user.id),
updatedBy: Number(session.user.id),
createdAt: new Date(),
updatedAt: new Date(),
// 사업 유형별 추가 필드
...(input.businessType === '조선' && {
// RFQ 필드
prNumber: input.prNumber || rfqCode, // PR 번호가 없으면 RFQ 코드 사용
prIssueDate: input.prIssueDate || new Date(),
series: input.series || null
}),
...(input.businessType === '해양' && {
// ITB 필드
projectCompany: input.projectCompany || 'AVL 기반 프로젝트',
projectFlag: input.projectFlag || null,
projectSite: input.projectSite || null,
smCode: input.smCode || null
})
})
.returning()
// 2. rfqPrItems 테이블에 AVL 아이템들 삽입
const prItemsData = input.avlItems.map((item, index) => ({
rfqsLastId: newRfq.id,
rfqItem: `${index + 1}`.padStart(3, '0'), // 001, 002, ...
prItem: `${index + 1}`.padStart(3, '0'),
prNo: rfqCode,
materialCode: item.materialGroupCode || `AVL-${item.id}`,
materialDescription: item.materialNameCustomerSide || item.materialGroupName || `AVL 아이템 ${index + 1}`,
materialCategory: item.materialGroupCode || null,
quantity: 1, // AVL에서는 수량 정보가 없으므로 1로 설정
uom: 'EA', // 기본 단위
majorYn: index === 0, // 첫 번째 아이템을 주요 아이템으로 설정
deliveryDate: input.dueDate, // 납기일은 RFQ 마감일과 동일하게 설정
}))
await tx.insert(rfqPrItems).values(prItemsData)
return newRfq
})
// 성공 결과 반환
return {
success: true,
message: `${rfqTypeLabel}가 성공적으로 생성되었습니다.`,
data: {
id: result.id,
rfqCode: result.rfqCode!,
type: rfqTypeLabel as 'RFQ' | 'ITB'
}
}
} catch (error) {
console.error('AVL RFQ/ITB 생성 오류:', error)
if (error instanceof Error) {
return {
success: false,
error: error.message
}
}
return {
success: false,
error: '알 수 없는 오류가 발생했습니다.'
}
}
}
/**
* AVL 데이터에서 RFQ/ITB 생성을 위한 기본값 설정 헬퍼 함수
*/
export async function prepareAvlRfqItbInput(
selectedItems: AvlDetailItem[],
businessType: '조선' | '해양',
defaultValues?: Partial<CreateAvlRfqItbInput>
): Promise<CreateAvlRfqItbInput> {
const now = new Date()
const dueDate = new Date(now.getTime() + (30 * 24 * 60 * 60 * 1000)) // 30일 후
// 선택된 아이템들의 대표 정보를 추출하여 제목 생성
const representativeItem = selectedItems[0]
const itemCount = selectedItems.length
const titleSuffix = itemCount > 1 ? ` 외 ${itemCount - 1}건` : ''
const defaultTitle = `${representativeItem?.materialNameCustomerSide || 'AVL 자재'}${titleSuffix}`
return {
avlItems: selectedItems,
businessType,
rfqTitle: defaultValues?.rfqTitle || `${businessType} - ${defaultTitle}`,
dueDate: defaultValues?.dueDate || dueDate,
remark: defaultValues?.remark || `AVL 기반 ${businessType} 견적 요청`,
picUserId: defaultValues?.picUserId || 0, // 호출 측에서 설정 필요
// ITB용 필드들
projectCompany: defaultValues?.projectCompany,
projectFlag: defaultValues?.projectFlag,
projectSite: defaultValues?.projectSite,
smCode: defaultValues?.smCode,
// RFQ용 필드들
prNumber: defaultValues?.prNumber,
prIssueDate: defaultValues?.prIssueDate,
series: defaultValues?.series
}
}
|