diff options
Diffstat (limited to 'lib/site-visit/service.ts')
| -rw-r--r-- | lib/site-visit/service.ts | 239 |
1 files changed, 193 insertions, 46 deletions
diff --git a/lib/site-visit/service.ts b/lib/site-visit/service.ts index 1dc07c77..d78682b5 100644 --- a/lib/site-visit/service.ts +++ b/lib/site-visit/service.ts @@ -1,9 +1,9 @@ "use server"
import db from "@/db/db"
-import { and, eq, isNull, desc, sql} from "drizzle-orm";
+import { and, eq, isNull, desc, sql, ne, or, ilike} from "drizzle-orm";
import { revalidatePath} from "next/cache";
-import { format } from "date-fns"
+import { format, addDays } from "date-fns"
import { vendorInvestigations, vendorPQSubmissions, siteVisitRequests, vendorSiteVisitInfo, siteVisitRequestAttachments } from "@/db/schema/pq"
import { sendEmail } from "../mail/sendEmail";
import { decryptWithServerAction } from '@/components/drm/drmUtils'
@@ -19,9 +19,10 @@ import { users } from "@/db/schema" // 실사 ID로 모든 siteVisitRequests 조회 (복수 확정정보 지원)
+// 협력업체 제출 정보(vendorSiteVisitInfo) 포함
export async function getAllSiteVisitRequestsForInvestigationAction(investigationId: number) {
try {
- const confirmations = await db
+ const siteVisitRequestsList = await db
.select({
id: siteVisitRequests.id,
status: siteVisitRequests.status,
@@ -36,7 +37,35 @@ export async function getAllSiteVisitRequestsForInvestigationAction(investigatio .where(eq(siteVisitRequests.investigationId, investigationId))
.orderBy(desc(siteVisitRequests.createdAt))
- return { success: true, confirmations }
+ // 각 siteVisitRequest에 대해 협력업체 제출 정보 조회
+ const requestsWithVendorInfo = await Promise.all(
+ siteVisitRequestsList.map(async (request) => {
+ const vendorInfoResult = await db
+ .select()
+ .from(vendorSiteVisitInfo)
+ .where(eq(vendorSiteVisitInfo.siteVisitRequestId, request.id))
+ .limit(1)
+
+ const vendorInfo = vendorInfoResult.length > 0 ? vendorInfoResult[0] : null
+
+ // 첨부파일 조회 (vendorSiteVisitInfo가 있는 경우)
+ let attachments: any[] = []
+ if (vendorInfo) {
+ attachments = await db
+ .select()
+ .from(siteVisitRequestAttachments)
+ .where(eq(siteVisitRequestAttachments.vendorSiteVisitInfoId, vendorInfo.id))
+ }
+
+ return {
+ ...request,
+ vendorInfo,
+ attachments,
+ }
+ })
+ )
+
+ return { success: true, requests: requestsWithVendorInfo }
} catch (error) {
console.error("실사 확정정보 조회 오류:", error)
return { success: false, error: "실사 확정정보 조회에 실패했습니다." }
@@ -155,7 +184,16 @@ export async function createSiteVisitRequestAction(input: { inspectionDuration: number;
requestedStartDate: Date;
requestedEndDate: Date;
- shiAttendees: Record<string, boolean>;
+ shiAttendees: {
+ [key: string]: {
+ checked: boolean;
+ attendees: Array<{
+ name: string;
+ department?: string;
+ email?: string;
+ }>;
+ };
+ };
shiAttendeeDetails?: string;
vendorRequests: Record<string, boolean>;
otherVendorRequests?: string;
@@ -176,12 +214,12 @@ export async function createSiteVisitRequestAction(input: { .where(eq(siteVisitRequests.investigationId, investigationId))
.limit(1);
- if (existingRequest.length > 0) {
- return {
- success: false,
- error: "이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다."
- };
- }
+ // if (existingRequest.length > 0) {
+ // return {
+ // success: false,
+ // error: "이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다."
+ // };
+ // }
// 방문실사 요청 생성
const [siteVisitRequest] = await db
@@ -287,63 +325,118 @@ export async function createSiteVisitRequestAction(input: { throw new Error('발송자 정보를 찾을 수 없습니다.');
}
- // 마감일 계산 (발송일 + 7일)
- const deadlineDate = format(new Date(), 'yyyy.MM.dd');
+ // 마감일 계산 (발송일 + 7일 또는 실사 예정일 중 먼저 도래하는 날)
+ const deadlineDate = (() => {
+ const deadlineFromToday = addDays(new Date(), 7);
+ if (investigation.forecastedAt) {
+ const forecastedDate = new Date(investigation.forecastedAt);
+ return forecastedDate < deadlineFromToday ? format(forecastedDate, 'yyyy.MM.dd') : format(deadlineFromToday, 'yyyy.MM.dd');
+ }
+ return format(deadlineFromToday, 'yyyy.MM.dd');
+ })();
+
+ // 실사 방법 한글 매핑
+ const investigationMethodMap: Record<string, string> = {
+ 'PURCHASE_SELF_EVAL': '구매자체평가',
+ 'DOCUMENT_EVAL': '서류평가',
+ 'PRODUCT_INSPECTION': '제품검사평가',
+ 'SITE_VISIT_EVAL': '방문실사평가'
+ };
+ const investigationMethodKorean = investigation.investigationMethod
+ ? (investigationMethodMap[investigation.investigationMethod] || investigation.investigationMethod)
+ : null;
// SHI 참석자 정보 파싱 (새로운 구조에 맞게)
const shiAttendees = input.shiAttendees as any;
+ // 실사 주소 및 기간/일정은 QM이 입력한 값 사용
+ const investigationAddress = investigation.investigationAddress || '';
+ const scheduledStartDate = investigation.scheduledStartAt
+ ? format(new Date(investigation.scheduledStartAt), 'yyyy.MM.dd')
+ : format(siteVisitRequest.requestedStartDate!, 'yyyy.MM.dd');
+ const scheduledEndDate = investigation.scheduledEndAt
+ ? format(new Date(investigation.scheduledEndAt), 'yyyy.MM.dd')
+ : format(siteVisitRequest.requestedEndDate!, 'yyyy.MM.dd');
+ const scheduledDuration = investigation.scheduledStartAt && investigation.scheduledEndAt
+ ? Math.ceil((new Date(investigation.scheduledEndAt).getTime() - new Date(investigation.scheduledStartAt).getTime()) / (1000 * 60 * 60 * 24))
+ : siteVisitRequest.inspectionDuration;
+
+ // SHI 참석자 정보 (새로운 구조: attendees 배열)
+ const shiAttendeesList: string[] = [];
+ const attendeeEmails: string[] = [];
+
+ Object.entries(shiAttendees).forEach(([key, value]: [string, any]) => {
+ if (value?.checked && value?.attendees && Array.isArray(value.attendees) && value.attendees.length > 0) {
+ const departmentLabels: Record<string, string> = {
+ technicalSales: "기술영업",
+ design: "설계",
+ procurement: "구매",
+ quality: "품질",
+ production: "생산",
+ commissioning: "시운전",
+ other: "기타"
+ };
+ const departmentName = departmentLabels[key] || key;
+
+ // 참석자 목록 생성
+ const attendeeCount = value.attendees.length;
+ const attendeeDetails = value.attendees
+ .map((attendee: any) => {
+ const parts: string[] = [];
+ if (attendee.name) parts.push(attendee.name);
+ if (attendee.department) parts.push(attendee.department);
+ return parts.join(' / ');
+ })
+ .filter(Boolean)
+ .join(', ');
+
+ const details = attendeeDetails ? ` (${attendeeDetails})` : '';
+ shiAttendeesList.push(`${departmentName} ${attendeeCount}명${details}`);
+
+ // 이메일 수집
+ value.attendees.forEach((attendee: any) => {
+ if (attendee?.email && attendee.email.trim() && attendee.email.includes('@')) {
+ attendeeEmails.push(attendee.email.trim());
+ }
+ });
+ }
+ });
+
+ // 중복 제거 및 유효성 검증
+ const uniqueAttendeeEmails = Array.from(new Set(attendeeEmails.filter(email => email && email.includes('@'))));
+
// 메일 제목
- const subject = `[SHI Audit] 방문실사 시행 안내 및 실사 관련 추가정보 요청 _ ${vendor.vendorName} (${vendor.vendorCode}, 사업자번호: ${vendor.taxId})`;
+ const subject = `[SHI Audit] 방문실사 시행 안내 및 실사 관련 추가정보 요청 _ ${vendor.vendorName}`;
// 메일 컨텍스트
const context = {
// 기본 정보
vendorName: vendor.vendorName,
- vendorContactName: vendor.vendorName || '',
+ vendorEmail: vendor.email || '',
requesterName: sender.name,
requesterTitle: 'Procurement Manager',
requesterEmail: sender.email,
// 실사 정보
- investigationMethod: investigation.investigationMethod,
- // investigationMethodDescription: investigation.investigationMethodDescription,
- requestedStartDate: format(siteVisitRequest.requestedStartDate!, 'yyyy.MM.dd'),
- requestedEndDate: format(siteVisitRequest.requestedEndDate!, 'yyyy.MM.dd'),
- inspectionDuration: siteVisitRequest.inspectionDuration,
+ investigationMethod: investigationMethodKorean,
+ investigationAddress: investigationAddress,
+ requestedStartDate: scheduledStartDate,
+ requestedEndDate: scheduledEndDate,
+ inspectionDuration: scheduledDuration,
// 마감일
deadlineDate,
// SHI 참석자 정보 (새로운 구조)
- shiAttendees: Object.entries(shiAttendees)
- .filter(([, value]) => value.checked)
- .map(([key, value]) => {
- const departmentLabels: Record<string, string> = {
- technicalSales: "기술영업",
- design: "설계",
- procurement: "구매",
- quality: "품질",
- production: "생산",
- commissioning: "시운전",
- other: "기타"
- };
- const departmentName = departmentLabels[key] || key;
- const details = value.details ? ` (${value.details})` : '';
- return `${departmentName} ${value.count}명${details}`;
- }),
+ shiAttendees: shiAttendeesList,
shiAttendeeDetails: input.shiAttendeeDetails || null,
// 협력업체 요청 정보 (default 값으로 고정)
vendorRequests: [
- ' 실사공장명',
- ' 실사공장 주소',
- ' 실사공장 가는 방법',
- ' 실사공장 Contact Point',
- ' 실사공장 연락처',
- ' 실사공장 이메일',
- ' 실사 참석 예정인력',
- ' 공장 출입절차 및 준비물'
+ '실사 공장 정보(공장명, 주소, 접근 방법 등)',
+ '실사 일정 확인',
+ '협력업체 실사 참석자 정보',
+ '사전 조치 필요 사항(출입증 등)'
],
otherVendorRequests: input.otherVendorRequests,
@@ -358,13 +451,24 @@ export async function createSiteVisitRequestAction(input: { };
// 메일 발송 (벤더 이메일로 직접 발송)
+ // cc에는 요청자 및 SHI 참석자 이메일 모두 포함
+ const ccEmails: string[] = [];
+ if (sender.email) {
+ ccEmails.push(sender.email);
+ }
+ // 참석자 이메일 추가 (요청자 이메일과 중복 제거)
+ uniqueAttendeeEmails.forEach(email => {
+ if (email && email !== sender.email && !ccEmails.includes(email)) {
+ ccEmails.push(email);
+ }
+ });
+
await sendEmail({
to: vendor.email || '',
- cc: sender.email,
+ cc: ccEmails.length > 0 ? ccEmails : undefined,
subject,
template: 'site-visit-request' as string,
context,
- // cc: vendor.email !== sender.email ? sender.email : undefined
});
console.log('방문실사 요청 메일 발송 완료:', {
@@ -770,4 +874,47 @@ export async function getSiteVisitRequestAction(investigationId: number) { error: "협력업체 방문실사 정보 조회 중 오류가 발생했습니다."
};
}
+ }
+
+ // domain이 'partners'가 아닌 사용자 목록 가져오기
+ export async function getUsersForSiteVisitAction(searchQuery?: string) {
+ try {
+ let whereCondition = ne(users.domain, "partners");
+
+ // 검색 쿼리가 있으면 이름 또는 이메일로 필터링
+ if (searchQuery && searchQuery.trim()) {
+ const searchPattern = `%${searchQuery.trim()}%`;
+ whereCondition = and(
+ ne(users.domain, "partners"),
+ or(
+ ilike(users.name, searchPattern),
+ ilike(users.email, searchPattern)
+ )
+ ) as any;
+ }
+
+ const userList = await db
+ .select({
+ id: users.id,
+ name: users.name,
+ email: users.email,
+ deptName: users.deptName,
+ })
+ .from(users)
+ .where(whereCondition)
+ .orderBy(users.name)
+ .limit(100); // 최대 100명까지
+
+ return {
+ success: true,
+ data: userList,
+ };
+ } catch (error) {
+ console.error("사용자 목록 조회 오류:", error);
+ return {
+ success: false,
+ error: "사용자 목록 조회 중 오류가 발생했습니다.",
+ data: [],
+ };
+ }
}
\ No newline at end of file |
