summaryrefslogtreecommitdiff
path: root/lib/mail
diff options
context:
space:
mode:
Diffstat (limited to 'lib/mail')
-rw-r--r--lib/mail/layouts/base.hbs22
-rw-r--r--lib/mail/mailer.ts133
-rw-r--r--lib/mail/partials/footer.hbs8
-rw-r--r--lib/mail/partials/header.hbs7
-rw-r--r--lib/mail/sendEmail.ts53
-rw-r--r--lib/mail/templates/evaluation-request.hbs285
6 files changed, 444 insertions, 64 deletions
diff --git a/lib/mail/layouts/base.hbs b/lib/mail/layouts/base.hbs
deleted file mode 100644
index 2e18f035..00000000
--- a/lib/mail/layouts/base.hbs
+++ /dev/null
@@ -1,22 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8" />
- <title>{{subject}}</title>
- </head>
- <body style="margin:0; padding:20px; background-color:#f5f5f5; font-family:Arial, sans-serif; color:#111827;">
- <table width="100%" cellpadding="0" cellspacing="0" border="0" align="center">
- <tr>
- <td align="center">
- <table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color:#ffffff; border:1px solid #e5e7eb; border-radius:6px; padding:24px;">
- <tr>
- <td>
- {{{body}}}
- </td>
- </tr>
- </table>
- </td>
- </tr>
- </table>
- </body>
-</html>
diff --git a/lib/mail/mailer.ts b/lib/mail/mailer.ts
index 329e2e52..61201e99 100644
--- a/lib/mail/mailer.ts
+++ b/lib/mail/mailer.ts
@@ -15,18 +15,137 @@ const transporter = nodemailer.createTransport({
},
});
-// 템플릿 로더 함수 - 단순화된 버전
+// 헬퍼 함수들 등록
+function registerHandlebarsHelpers() {
+ // i18next 헬퍼 등록
+ handlebars.registerHelper('t', function(key: string, options: { hash?: Record<string, unknown> }) {
+ // options.hash에는 Handlebars에서 넘긴 named parameter들이 들어있음
+ return i18next.t(key, options.hash || {});
+ });
+
+ // eq 헬퍼 등록 - 두 값을 비교 (블록 헬퍼)
+ handlebars.registerHelper('eq', function(a: any, b: any, options: any) {
+ if (a === b) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ // 기타 유용한 헬퍼들
+ handlebars.registerHelper('ne', function(a: any, b: any, options: any) {
+ if (a !== b) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ handlebars.registerHelper('gt', function(a: any, b: any, options: any) {
+ if (a > b) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ handlebars.registerHelper('gte', function(a: any, b: any, options: any) {
+ if (a >= b) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ handlebars.registerHelper('lt', function(a: any, b: any, options: any) {
+ if (a < b) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ handlebars.registerHelper('lte', function(a: any, b: any, options: any) {
+ if (a <= b) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ // and 헬퍼 - 모든 조건이 true인지 확인 (블록 헬퍼)
+ handlebars.registerHelper('and', function(...args: any[]) {
+ // 마지막 인자는 Handlebars 옵션
+ const options = args[args.length - 1];
+ const values = args.slice(0, -1);
+
+ if (values.every(Boolean)) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ // or 헬퍼 - 하나라도 true인지 확인 (블록 헬퍼)
+ handlebars.registerHelper('or', function(...args: any[]) {
+ // 마지막 인자는 Handlebars 옵션
+ const options = args[args.length - 1];
+ const values = args.slice(0, -1);
+
+ if (values.some(Boolean)) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ // not 헬퍼 - 값 반전 (블록 헬퍼)
+ handlebars.registerHelper('not', function(value: any, options: any) {
+ if (!value) {
+ return options.fn(this);
+ } else {
+ return options.inverse(this);
+ }
+ });
+
+ // formatDate 헬퍼 - 날짜 포맷팅
+ handlebars.registerHelper('formatDate', function(date: string | Date, format: string = 'YYYY-MM-DD') {
+ if (!date) return '';
+ const dateObj = new Date(date);
+ if (isNaN(dateObj.getTime())) return '';
+
+ // 간단한 날짜 포맷팅 (더 복잡한 경우 moment.js나 date-fns 사용)
+ const year = dateObj.getFullYear();
+ const month = String(dateObj.getMonth() + 1).padStart(2, '0');
+ const day = String(dateObj.getDate()).padStart(2, '0');
+
+ return format
+ .replace('YYYY', String(year))
+ .replace('MM', month)
+ .replace('DD', day);
+ });
+
+ // formatNumber 헬퍼 - 숫자 포맷팅
+ handlebars.registerHelper('formatNumber', function(number: number, locale: string = 'ko-KR') {
+ if (typeof number !== 'number') return number;
+ return new Intl.NumberFormat(locale).format(number);
+ });
+}
+
+// 헬퍼 등록 실행
+registerHandlebarsHelpers();
+
+// 템플릿 로더 함수
function loadTemplate(templateName: string, data: Record<string, unknown>) {
const templatePath = path.join(process.cwd(), 'lib', 'mail', 'templates', `${templateName}.hbs`);
+
+ if (!fs.existsSync(templatePath)) {
+ throw new Error(`Template not found: ${templatePath}`);
+ }
+
const source = fs.readFileSync(templatePath, 'utf8');
const template = handlebars.compile(source);
return template(data);
}
-// i18next 헬퍼 등록
-handlebars.registerHelper('t', function(key: string, options: { hash?: Record<string, unknown> }) {
- // options.hash에는 Handlebars에서 넘긴 named parameter들이 들어있음
- return i18next.t(key, options.hash || {});
-});
-
export { transporter, loadTemplate }; \ No newline at end of file
diff --git a/lib/mail/partials/footer.hbs b/lib/mail/partials/footer.hbs
deleted file mode 100644
index 06aae57d..00000000
--- a/lib/mail/partials/footer.hbs
+++ /dev/null
@@ -1,8 +0,0 @@
-<table width="100%" cellpadding="0" cellspacing="0" style="margin-top:32px; border-top:1px solid #e5e7eb; padding-top:16px;">
- <tr>
- <td align="center">
- <p style="font-size:16px; color:#6b7280; margin:4px 0;">© {{currentYear}} EVCP. {{t "email.vendor.invitation.copyright"}}</p>
- <p style="font-size:16px; color:#6b7280; margin:4px 0;">{{t "email.vendor.invitation.no_reply"}}</p>
- </td>
- </tr>
-</table>
diff --git a/lib/mail/partials/header.hbs b/lib/mail/partials/header.hbs
deleted file mode 100644
index 7898c82e..00000000
--- a/lib/mail/partials/header.hbs
+++ /dev/null
@@ -1,7 +0,0 @@
-<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px; border-bottom:1px solid #163CC4; padding-bottom:16px;">
- <tr>
- <td align="center">
- <span style="display: block; text-align: left; color: #163CC4; font-weight: bold; font-size: 32px;">eVCP</span>
- </td>
- </tr>
-</table>
diff --git a/lib/mail/sendEmail.ts b/lib/mail/sendEmail.ts
index 97617e7a..3f88cb04 100644
--- a/lib/mail/sendEmail.ts
+++ b/lib/mail/sendEmail.ts
@@ -17,29 +17,42 @@ interface SendEmailOptions {
}[]
}
-export async function sendEmail({
- to,
- subject,
- template,
- context,
+export async function sendEmail({
+ to,
+ subject,
+ template,
+ context,
cc, // cc 매개변수 추가
attachments = []
}: SendEmailOptions) {
- const { t, i18n } = await useTranslation(context.language ?? "en", "translation");
+ try {
+ // i18n 설정
+ const { t, i18n } = await useTranslation(context.language ?? "en", "translation");
+
+ // t 헬퍼만 동적으로 등록 (이미 mailer.ts에서 기본 등록되어 있지만, 언어별로 다시 등록)
+ handlebars.registerHelper("t", function (key: string, options: any) {
+ // 여기서 i18n은 로컬 인스턴스
+ return i18n.t(key, options.hash || {});
+ });
- handlebars.registerHelper("t", function (key: string, options: any) {
- // 여기서 i18n은 로컬 인스턴스
- return i18n.t(key, options.hash || {});
- });
+ // 템플릿 컴파일 및 HTML 생성
+ const html = loadTemplate(template, context);
- const html = loadTemplate(template, context);
+ // 이메일 발송
+ const result = await transporter.sendMail({
+ from: `"${process.env.Email_From_Name}" <${process.env.Email_From_Address}>`,
+ to,
+ cc, // cc 필드 추가
+ subject,
+ html,
+ attachments
+ });
- await transporter.sendMail({
- from: `"${process.env.Email_From_Name}" <${process.env.Email_From_Address}>`,
- to,
- cc, // cc 필드 추가
- subject,
- html,
- attachments
- });
-}
+ console.log(`이메일 발송 성공: ${to}`, result.messageId);
+ return result;
+
+ } catch (error) {
+ console.error(`이메일 발송 실패: ${to}`, error);
+ throw error;
+ }
+} \ No newline at end of file
diff --git a/lib/mail/templates/evaluation-request.hbs b/lib/mail/templates/evaluation-request.hbs
new file mode 100644
index 00000000..84aae0f5
--- /dev/null
+++ b/lib/mail/templates/evaluation-request.hbs
@@ -0,0 +1,285 @@
+<!DOCTYPE html>
+<html lang="ko">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>협력업체 평가 요청</title>
+ <style>
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans KR', Arial, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 20px;
+ background-color: #f9f9f9;
+ }
+ .container {
+ background-color: white;
+ border-radius: 8px;
+ padding: 30px;
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
+ }
+ .header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding-bottom: 20px;
+ border-bottom: 2px solid #e5e5e5;
+ }
+ .header h1 {
+ color: #1f2937;
+ margin: 0;
+ font-size: 24px;
+ font-weight: 600;
+ }
+ .header .subtitle {
+ color: #6b7280;
+ font-size: 14px;
+ margin-top: 5px;
+ }
+ .greeting {
+ margin-bottom: 25px;
+ font-size: 16px;
+ }
+ .evaluation-info {
+ background-color: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: 6px;
+ padding: 20px;
+ margin: 20px 0;
+ }
+ .evaluation-info h3 {
+ color: #1e40af;
+ margin: 0 0 15px 0;
+ font-size: 18px;
+ }
+ .info-grid {
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 8px 15px;
+ margin-bottom: 15px;
+ }
+ .info-label {
+ font-weight: 600;
+ color: #374151;
+ }
+ .info-value {
+ color: #6b7280;
+ }
+ .status-badge {
+ display: inline-block;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ }
+ .status-domestic { background-color: #dcfce7; color: #166534; }
+ .status-foreign { background-color: #dbeafe; color: #1d4ed8; }
+ .status-equipment { background-color: #fef3c7; color: #92400e; }
+ .status-bulk { background-color: #e0e7ff; color: #3730a3; }
+ .department-badge {
+ background-color: #1f2937;
+ color: white;
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 500;
+ display: inline-block;
+ margin-bottom: 15px;
+ }
+ .reviewers-section {
+ margin: 25px 0;
+ }
+ .reviewers-section h4 {
+ color: #374151;
+ margin-bottom: 15px;
+ font-size: 16px;
+ }
+ .reviewer-list {
+ background-color: #f1f5f9;
+ border-radius: 6px;
+ padding: 15px;
+ }
+ .reviewer-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 0;
+ border-bottom: 1px solid #e2e8f0;
+ }
+ .reviewer-item:last-child {
+ border-bottom: none;
+ }
+ .reviewer-name {
+ font-weight: 500;
+ color: #1f2937;
+ }
+ .reviewer-dept {
+ font-size: 12px;
+ color: #6b7280;
+ background-color: #e5e7eb;
+ padding: 2px 6px;
+ border-radius: 3px;
+ }
+ .message-section {
+ background-color: #fffbeb;
+ border-left: 4px solid #f59e0b;
+ padding: 15px;
+ margin: 20px 0;
+ }
+ .message-section h4 {
+ color: #92400e;
+ margin: 0 0 10px 0;
+ font-size: 14px;
+ }
+ .message-text {
+ color: #78350f;
+ font-style: italic;
+ }
+ .action-button {
+ display: inline-block;
+ background-color: #1f2937;
+ color: white;
+ padding: 12px 24px;
+ text-decoration: none;
+ border-radius: 6px;
+ font-weight: 500;
+ text-align: center;
+ margin: 25px 0;
+ }
+ .action-button:hover {
+ background-color: #374151;
+ }
+ .footer {
+ margin-top: 30px;
+ padding-top: 20px;
+ border-top: 1px solid #e5e5e5;
+ text-align: center;
+ color: #6b7280;
+ font-size: 14px;
+ }
+ .deadline-notice {
+ background-color: #fef2f2;
+ border: 1px solid #fecaca;
+ border-radius: 6px;
+ padding: 15px;
+ margin: 20px 0;
+ }
+ .deadline-notice .deadline-label {
+ color: #dc2626;
+ font-weight: 600;
+ font-size: 14px;
+ }
+ .deadline-notice .deadline-date {
+ color: #991b1b;
+ font-size: 16px;
+ font-weight: 500;
+ }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <div class="header">
+ <h1>🏢 협력업체 정기평가 요청</h1>
+ <div class="subtitle">Vendor Performance Evaluation Request</div>
+ </div>
+
+ <div class="greeting">
+ 안녕하세요, <strong>{{reviewerName}}</strong>님
+ </div>
+
+ <p>
+ {{departmentLabel}}으로 지정되어 아래 협력업체에 대한 정기평가를 요청드립니다.
+ </p>
+
+ <div class="department-badge">
+ 📋 {{departmentLabel}}
+ </div>
+
+ <!-- 평가 대상 정보 -->
+ <div class="evaluation-info">
+ <h3>📊 평가 대상 정보</h3>
+
+ <div class="info-grid">
+ <span class="info-label">업체명:</span>
+ <span class="info-value"><strong>{{evaluation.vendorName}}</strong></span>
+
+ <span class="info-label">업체코드:</span>
+ <span class="info-value">{{evaluation.vendorCode}}</span>
+
+ <span class="info-label">평가년도:</span>
+ <span class="info-value">{{evaluation.evaluationYear}}년</span>
+
+
+ <span class="info-label">구분:</span>
+ <span class="info-value">
+ {{#eq evaluation.division "SHIP"}}조선{{else}}{{#eq evaluation.division "PLANT"}}해양{{/eq}}{{/eq}}
+ </span>
+
+ <span class="info-label">내외자:</span>
+ <span class="info-value">
+ <span class="status-badge {{#eq evaluation.domesticForeign 'DOMESTIC'}}status-domestic{{else}}status-foreign{{/eq}}">
+ {{#eq evaluation.domesticForeign "DOMESTIC"}}국내{{else}}해외{{/eq}}
+ </span>
+ </span>
+
+ <span class="info-label">자재구분:</span>
+ <span class="info-value">
+ <span class="status-badge {{#eq evaluation.materialType 'EQUIPMENT'}}status-equipment{{else}}{{#eq evaluation.materialType 'BULK'}}status-bulk{{else}}status-equipment{{/eq}}{{/eq}}">
+ {{#eq evaluation.materialType "EQUIPMENT"}}기자재{{else}}{{#eq evaluation.materialType "BULK"}}벌크{{else}}{{#eq evaluation.materialType "EQUIPMENT_BULK"}}기자재+벌크{{else}}{{evaluation.materialType}}{{/eq}}{{/eq}}{{/eq}}
+ </span>
+ </span>
+ </div>
+
+ </div>
+
+ <!-- 함께 평가하는 다른 담당자들 -->
+ {{#if otherReviewers}}
+ <div class="reviewers-section">
+ <h4>👥 함께 평가하는 다른 담당자</h4>
+ <div class="reviewer-list">
+ {{#each otherReviewers}}
+ <div class="reviewer-item">
+ <div>
+ <div class="reviewer-name">{{this.name}}</div>
+ <div style="font-size: 12px; color: #6b7280;">{{this.email}}</div>
+ </div>
+ <div class="reviewer-dept">{{this.department}}</div>
+ </div>
+ {{/each}}
+ </div>
+ </div>
+ {{/if}}
+
+ <!-- 요청 메시지 -->
+ {{#if message}}
+ <div class="message-section">
+ <h4>💬 요청 메시지</h4>
+ <div class="message-text">"{{message}}"</div>
+ </div>
+ {{/if}}
+
+ <!-- 평가 시작 버튼 -->
+ <div style="text-align: center;">
+ <a href="{{evaluationUrl}}" class="action-button">
+ 🚀 평가 시작하기
+ </a>
+ </div>
+
+ <div style="margin-top: 25px; padding: 15px; background-color: #f8fafc; border-radius: 6px;">
+ <p style="margin: 0; font-size: 14px; color: #6b7280;">
+ <strong>📋 평가 진행 안내:</strong><br>
+ • 위 버튼을 클릭하여 온라인 평가 시스템에 접속하실 수 있습니다<br>
+ • 평가 기준에 따라 각 항목별로 점수를 입력해 주세요<br>
+ • 모든 평가가 완료되면 자동으로 최종 집계됩니다<br>
+ • 문의사항이 있으시면 시스템 관리자에게 연락해 주세요
+ </p>
+ </div>
+
+ <div class="footer">
+ <p>본 메일은 협력업체 평가 시스템에서 자동 발송된 메일입니다.</p>
+ <p style="margin: 5px 0 0 0;">Samsung Heavy Industries Vendor Evaluation System</p>
+ </div>
+ </div>
+</body>
+</html> \ No newline at end of file