diff --git a/ServerlessFunction/build.gradle b/ServerlessFunction/build.gradle index 34615a92..b8f86d93 100644 --- a/ServerlessFunction/build.gradle +++ b/ServerlessFunction/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation 'software.amazon.awssdk:ssm' implementation 'software.amazon.awssdk:scheduler' implementation 'software.amazon.awssdk:sqs' + implementation 'software.amazon.awssdk:ses' // AWS X-Ray SDK (다운스트림 서비스 추적용) implementation 'com.amazonaws:aws-xray-recorder-sdk-core:2.15.0' diff --git a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/common/config/AwsClients.java b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/common/config/AwsClients.java index a1d2286b..0b7416ba 100644 --- a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/common/config/AwsClients.java +++ b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/common/config/AwsClients.java @@ -10,6 +10,7 @@ import software.amazon.awssdk.services.polly.PollyClient; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.ses.SesClient; import software.amazon.awssdk.services.sns.SnsClient; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.ssm.SsmClient; @@ -67,6 +68,11 @@ public final class AwsClients { .overrideConfiguration(XRAY_CONFIG) .build(); + // SES + private static final SesClient SES_CLIENT = SesClient.builder() + .overrideConfiguration(XRAY_CONFIG) + .build(); + private AwsClients() { // 인스턴스화 방지 } @@ -114,4 +120,6 @@ public static SsmClient ssm() { public static SqsClient sqs() { return SQS_CLIENT; } + + public static SesClient ses() { return SES_CLIENT; } } diff --git a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/opic/handler/OPIcSessionHandler.java b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/opic/handler/OPIcSessionHandler.java index cd7335d4..659ba648 100644 --- a/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/opic/handler/OPIcSessionHandler.java +++ b/ServerlessFunction/src/main/java/com/mzc/secondproject/serverless/domain/opic/handler/OPIcSessionHandler.java @@ -7,6 +7,7 @@ import com.google.gson.*; import com.mzc.secondproject.serverless.common.config.AwsClients; import com.mzc.secondproject.serverless.common.service.PollyService; +import com.mzc.secondproject.serverless.common.util.CognitoUtil; import com.mzc.secondproject.serverless.common.util.JwtUtil; import com.mzc.secondproject.serverless.common.util.ResponseGenerator; import com.mzc.secondproject.serverless.domain.opic.dto.request.CreateSessionRequest; @@ -18,6 +19,7 @@ import com.mzc.secondproject.serverless.domain.opic.model.OPIcQuestion; import com.mzc.secondproject.serverless.domain.opic.model.OPIcSession; import com.mzc.secondproject.serverless.domain.opic.repository.OPIcRepository; +import com.mzc.secondproject.serverless.domain.opic.service.EmailService; import com.mzc.secondproject.serverless.domain.opic.service.FeedbackService; import com.mzc.secondproject.serverless.domain.opic.service.TranscribeProxyService; import org.slf4j.Logger; @@ -52,12 +54,15 @@ public class OPIcSessionHandler implements RequestHandler"); + html.append(""); + html.append(""); + + // Header + html.append("
"); + html.append("

🎯 OPIc 스피킹 테스트 결과

"); + html.append("
"); + + // Main Content + html.append("
"); + + // Greeting + html.append("

안녕하세요, ") + .append(userName != null ? userName : "학습자") + .append("님!

"); + html.append("

OPIc 스피킹 테스트 결과를 알려드립니다.

"); + + // Score Cards + html.append("
"); + + // Estimated Level + html.append("
"); + html.append("

예상 등급

"); + html.append("

") + .append(report.estimatedLevel()).append("

"); + html.append("
"); + + // Overall Score + html.append("
"); + html.append("

종합 점수

"); + html.append("

") + .append(report.overallScore()).append("

"); + html.append("
"); + + html.append("
"); + + // Feedback + html.append("
"); + html.append("

📝 종합 피드백

"); + html.append("

") + .append(report.feedback()).append("

"); + html.append("
"); + + // Strengths + html.append("
"); + html.append("

💪 잘한 점

"); + html.append("
    "); + for (String strength : report.strengths()) { + html.append("
  • ").append(strength).append("
  • "); + } + html.append("
"); + html.append("
"); + + // Weaknesses + html.append("
"); + html.append("

📈 개선할 점

"); + html.append("
    "); + for (String weakness : report.weaknesses()) { + html.append("
  • ").append(weakness).append("
  • "); + } + html.append("
"); + html.append("
"); + + // Recommendations + html.append("
"); + html.append("

💡 학습 추천

"); + html.append("
    "); + for (String rec : report.recommendations()) { + html.append("
  1. ").append(rec).append("
  2. "); + } + html.append("
"); + html.append("
"); + + // CTA Button + html.append("
"); + html.append(""); + html.append("전체 리포트 보기"); + html.append("
"); + + // Footer + html.append("
"); + html.append("

"); + html.append("본 이메일은 English Study 서비스에서 자동으로 발송되었습니다.
"); + html.append("© 2025 English Study. All rights reserved.

"); + + html.append("
"); + html.append(""); + + return html.toString(); + } + + /** + * OPIc 리포트 텍스트 버전 (HTML 미지원 이메일 클라이언트용) + */ + private String buildOPIcReportText(String userName, SessionReportResponse report) { + StringBuilder text = new StringBuilder(); + + text.append("OPIc 스피킹 테스트 결과\n"); + text.append("================================\n\n"); + + text.append("안녕하세요, ").append(userName != null ? userName : "학습자").append("님!\n"); + text.append("OPIc 스피킹 테스트 결과를 알려드립니다.\n\n"); + + text.append("결과 요약\n"); + text.append("------------\n"); + text.append("예상 등급: ").append(report.estimatedLevel()).append("\n"); + text.append("종합 점수: ").append(report.overallScore()).append("점\n\n"); + + text.append("종합 피드백\n"); + text.append("------------\n"); + text.append(report.feedback()).append("\n\n"); + + text.append("잘한 점\n"); + text.append("------------\n"); + for (String strength : report.strengths()) { + text.append("• ").append(strength).append("\n"); + } + text.append("\n"); + + text.append("개선할 점\n"); + text.append("------------\n"); + for (String weakness : report.weaknesses()) { + text.append("• ").append(weakness).append("\n"); + } + text.append("\n"); + + text.append("학습 추천\n"); + text.append("------------\n"); + int i = 1; + for (String rec : report.recommendations()) { + text.append(i++).append(". ").append(rec).append("\n"); + } + text.append("\n"); + + text.append("================================\n"); + text.append("© 2025 English Study\n"); + + return text.toString(); + } + + /** + * 레벨별 색상 반환 + */ + private String getLevelColor(String level) { + return switch (level) { + case "NL", "NM", "NH" -> "#6b7280"; + case "IL" -> "#22c55e"; + case "IM1" -> "#10b981"; + case "IM2" -> "#3b82f6"; + case "IM3" -> "#8b5cf6"; + case "IH" -> "#f97316"; + case "AL" -> "#ef4444"; + default -> "#3b82f6"; + }; + } + + /** + * 점수별 색상 반환 + */ + private String getScoreColor(int score) { + if (score >= 90) return "#059669"; + if (score >= 70) return "#3b82f6"; + if (score >= 50) return "#f97316"; + return "#ef4444"; + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ServerlessFunction/template.yaml b/ServerlessFunction/template.yaml index ed741aeb..7b0e54bb 100644 --- a/ServerlessFunction/template.yaml +++ b/ServerlessFunction/template.yaml @@ -48,6 +48,7 @@ Globals: AWS_REGION_NAME: !Ref AWS::Region ROOM_TOKEN_TTL_SECONDS: "300" TRANSCRIBE_PROXY_URL: "https://tfo1zm7vec.execute-api.ap-northeast-2.amazonaws.com/prod/transcribe" + SES_SENDER_EMAIL: "hye.ina0130@gmail.com" Api: TracingEnabled: true @@ -1553,6 +1554,12 @@ Resources: Action: - ssm:GetParameter Resource: !Sub "arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/opic/*" + - Statement: + - Effect: Allow + Action: + - ses:SendEmail + - ses:SendRawEmail + Resource: "*" - SNSPublishMessagePolicy: TopicName: !GetAtt NotificationTopic.TopicName Events: