Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.permitseoul.permitserver.domain.admin.guestticket.api.service;

import com.permitseoul.permitserver.domain.admin.guestticket.core.domain.GuestTicketStatus;
import com.permitseoul.permitserver.domain.admin.guestticket.core.exception.GuestEmailSendException;
import com.permitseoul.permitserver.domain.admin.guestticket.core.exception.GuestTicketNotFoundException;
import com.permitseoul.permitserver.domain.admin.guestticket.core.facade.AdminGuestTicketFacade;
import com.permitseoul.permitserver.domain.admin.property.QrCodeProperties;
Expand Down Expand Up @@ -55,9 +56,9 @@ public void issueGuestTickets(final long eventId, final List<GuestTicketIssueReq
throw new AdminGuestTicketApiException(ErrorCode.NOT_FOUND_EVENT);
} catch (GuestTicketNotFoundException e) {
throw new AdminGuestTicketApiException(ErrorCode.NOT_FOUND_GUEST_TICKET);
} catch (Exception e) {
} catch (GuestEmailSendException e) {
log.error("[Guest Ticket Email] 발송 실패 - 이벤트아이디:{}, 수신자 정보 : {}", eventId, guestTicketList, e);
throw new AdminGuestTicketApiException(ErrorCode.INTERNAL_SERVER_ERROR);
throw new AdminGuestTicketApiException(ErrorCode.INTERNAL_EMAIL_SEND_ERROR);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.permitseoul.permitserver.domain.admin.guestticket.core.exception;

public class GuestEmailSendException extends GuestTicketCoreException {
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.permitseoul.permitserver.domain.admin.util;

import com.permitseoul.permitserver.domain.admin.guestticket.core.exception.GuestEmailSendException;
import com.permitseoul.permitserver.domain.admin.property.EmailProperties;
import com.permitseoul.permitserver.domain.admin.util.exception.EmailSendException;
import com.permitseoul.permitserver.domain.event.core.domain.EventType;
Expand All @@ -9,6 +10,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.InputStreamSource;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
Expand Down Expand Up @@ -37,6 +39,7 @@ public class GuestTicketEmailSender {
private final static String INLINE_CONTENT_ID = "qr-";
private final static String INLINE_CONTENT_TYPE = "image/png";


public void sendGuestTicketsEmail(
final String toEmail,
final String guestName,
Expand All @@ -53,23 +56,39 @@ public void sendGuestTicketsEmail(
context.setVariable(CONTEXT_TICKET_CODES, ticketCodes);
final String html = templateEngine.process(TEMPLATE_NAME, context);


final String subject = "[" + eventType.getDisplayName() + "] Guest Ticket Info";
final String plain =
subject + "\n" +
"안녕하세요 " + guestName + "님\n" +
"Event: " + eventName + "\n" +
"Ticket Code: " + String.join(", ", ticketCodes) + "\n";


final MimeMessage mimeMessage = mailSender.createMimeMessage();
final MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, StandardCharsets.UTF_8.name()); //multipart:true는 인라인 이미지 추가
helper.setFrom(emailProperties.sender(), SENDER_NAME);
helper.setTo(toEmail);
helper.setSubject("​[" + eventType.getDisplayName() + "] Guest Ticket Info"); //Gmail thread subject가 맨앞 "[]" 태그를 무시/정규화하는 경우가 있어, Zero-width space(U+200B)로 패턴 인식을 회피
helper.setText(html, true);
mimeMessage.setSubject(subject, StandardCharsets.UTF_8.name());

// helper.setSubject("​ [" + eventType.getDisplayName() + "] Guest Ticket Info"); //Gmail thread subject가 맨앞 "[]" 태그를 무시/정규화하는 경우가 있어, Zero-width space(U+200B)로 패턴 인식을 회피
helper.setText(plain, html);

for (int i = 0; i < qrPngs.size(); i++) {
final InputStreamSource src = new ByteArrayResource(qrPngs.get(i));
helper.addInline(INLINE_CONTENT_ID + i, src, INLINE_CONTENT_TYPE);

String fileName = "guest ticket-" + ticketCodes.get(i) + ".png";
helper.addAttachment(fileName, src, "image/png");
}

mailSender.send(mimeMessage);
log.info("[Guest Ticket Email] 발송 완료 - 수신자: {} ({}), 이벤트: {}, 티켓 수: {}",
guestName, toEmail, eventName, ticketCodes.size());
} catch (MessagingException | UnsupportedEncodingException e) {
throw new EmailSendException(ErrorCode.INTERNAL_EMAIL_SEND_ERROR);
} catch (Exception e) {
log.error("[Guest Ticket Email] 발송 실패 - to={}, guestName={}, event={}, ticketCount={}",
toEmail, guestName, eventName, ticketCodes.size(), e);
throw new GuestEmailSendException();
Comment on lines +88 to +91
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

에러 로그에 이메일(PII) 포함 — 개인정보 유출 위험

Line 89-90에서 toEmailguestName을 에러 로그에 직접 기록하고 있습니다. 이메일 주소와 이름은 개인식별정보(PII)이며, 로그 시스템에 저장될 경우 GDPR/개인정보보호법 위반 가능성이 있습니다.

이메일은 마스킹 처리하거나, 식별 가능한 내부 ID로 대체하는 것을 권장합니다. 또한, new GuestEmailSendException()에 원인 예외(e)를 전달하면 디버깅이 용이합니다.

🛡️ PII 마스킹 및 예외 원인 전달 제안
         } catch (Exception e) {
-            log.error("[Guest Ticket Email] 발송 실패 - to={}, guestName={}, event={}, ticketCount={}",
-                    toEmail, guestName, eventName, ticketCodes.size(), e);
-            throw new GuestEmailSendException();
+            log.error("[Guest Ticket Email] 발송 실패 - event={}, ticketCount={}",
+                    eventName, ticketCodes.size(), e);
+            throw new GuestEmailSendException(e);
         }
🤖 Prompt for AI Agents
In
`@src/main/java/com/permitseoul/permitserver/domain/admin/util/GuestTicketEmailSender.java`
around lines 88 - 91, The catch block in GuestTicketEmailSender that logs
toEmail and guestName must avoid writing PII; replace those values in the log
with either masked versions (e.g., maskLocalPart(toEmail)) or a non-identifying
internal id, and update the log statement in the catch of send (references:
toEmail, guestName, log.error call) to only include non-PII fields and the
exception message/stack; also throw the GuestEmailSendException with the
original exception as the cause (new GuestEmailSendException(e)) so debugging
retains the root cause while keeping logs PII-free.

}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.permitseoul.permitserver.domain.admin.util;

import com.permitseoul.permitserver.domain.admin.base.AdminBaseException;
import com.permitseoul.permitserver.domain.admin.timetable.base.core.exception.NotionPublicUrlNotFoundException;
import com.permitseoul.permitserver.domain.admin.timetable.base.core.exception.NotionUrlMalformedException;
import com.permitseoul.permitserver.domain.admin.util.exception.PermitListSizeNotMatchException;
Expand All @@ -9,9 +8,7 @@
import com.permitseoul.permitserver.domain.eventtimetable.blockmedia.domain.entity.TimetableBlockMediaEntity;
import com.permitseoul.permitserver.domain.eventtimetable.category.core.domain.entity.TimetableCategoryEntity;
import com.permitseoul.permitserver.domain.eventtimetable.stage.core.domain.entity.TimetableStageEntity;
import com.permitseoul.permitserver.global.exception.DateFormatException;
import com.permitseoul.permitserver.global.exception.PermitIllegalStateException;
import com.permitseoul.permitserver.global.exception.UrlSecureException;
import com.permitseoul.permitserver.global.external.notion.dto.NotionCategoryDatasourceResponse;
import com.permitseoul.permitserver.global.external.notion.dto.NotionStageDatasourceResponse;
import com.permitseoul.permitserver.global.external.notion.dto.NotionTimetableDatasourceResponse;
Expand All @@ -21,8 +18,6 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.permitseoul.permitserver.domain.admin.util;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;

import java.awt.*;
import java.io.InputStream;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j
public final class PngFontUtil {
private static final Font BASE = loadBase();
private static final Font SIZE_12 = BASE.deriveFont(Font.PLAIN, 12f);


private static Font loadBase() {
try (InputStream is = new ClassPathResource("fonts/NotoSansKR-Regular.ttf").getInputStream()) {
return Font.createFont(Font.TRUETYPE_FONT, is);
} catch (Exception e) {
log.warn("Failed to load font", e);
return new Font("SansSerif", Font.PLAIN, 12);
}
}

public static Font size12() {
return SIZE_12;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static BufferedImage addEventNameOnTop(final BufferedImage qrImage, fina
g.drawImage(qrImage, 0, TEXT_AREA_HEIGHT, null);

g.setColor(Color.BLACK);
g.setFont(new Font("SansSerif", Font.PLAIN, 15));
g.setFont(PngFontUtil.size12());
final FontMetrics fm = g.getFontMetrics();

final String text = ellipsize(label, fm, width - (TEXT_PADDING_X * 2));
Expand Down
Binary file added src/main/resources/fonts/NotoSansKR-Regular.ttf
Binary file not shown.