From 6b1ef9a409d47c1456a4b5d67e74ebcc6d8f89d9 Mon Sep 17 00:00:00 2001 From: Minji6 <105301353+Minji6@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:40:34 +0900 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=EC=9E=84=EC=8B=9C=20=EB=B9=84?= =?UTF-8?q?=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20HTM?= =?UTF-8?q?L=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/service/FindPasswordService.java | 71 +++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java b/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java index 1760842..1f55197 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java @@ -76,12 +76,71 @@ private void sendPasswordResetEmail(String email, String temporaryPassword) { helper.setTo(email); helper.setSubject("[Sequence] 임시 비밀번호 발급 안내"); - String content = "" - + "

임시 비밀번호 발급 안내

" - + "

안녕하세요. 임시 비밀번호가 발급되었습니다.

" - + "

아래의 임시 비밀번호로 로그인 후, 보안을 위해 마이페이지에서 비밀번호를 변경해주세요.

" - + "

임시 비밀번호: " + temporaryPassword + "

" - + ""; + String content = "" + + "" + + "" + + "" + + "" + + "Sequence 임시비밀번호 발송" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "" + + + // 컨텐츠 섹션 + "" + + "" + + "" + + + // 구분선 + "" + + "" + + "" + + + // 푸터 섹션 + "" + + "" + + "" + + "
" + + "

안녕하세요. 임시 비밀번호가 발급되었습니다.

" + + "

아래의 임시 비밀번호로 로그인 후, 보안을 위해 마이페이지에서 비밀번호를 변경해주세요.

" + + + // 비밀번호 섹션 + "" + + "" + + "" + + "" + + "
" + + "

임시 비밀번호

" + + "
" + temporaryPassword + "
" + + "
" + + + // 경고 섹션 + "" + + "" + + "" + + "" + + "
" + + "

보안 알림 및 이용 안내

" + + "

• 로그인 즉시 마이페이지에서 새로운 비밀번호로 변경하세요

" + + "

• 비밀번호를 타인과 공유하지 마세요

" + + "

• Sequence 로그인 페이지에 접속합니다

" + + "

• 이메일과 임시 비밀번호를 입력합니다

" + + "

• 로그인 후 마이페이지에서 비밀번호 변경을 완료합니다

" + + "
" + + "
" + + "
" + + "
" + + "

Sequence

" + + "

이 메시지는 발신 전용입니다.

" + + "
" + + "
" + + "" + + ""; helper.setText(content, true); mailSender.send(message); From fc3839d0dcc592839061beb0c7bf08a582a11742 Mon Sep 17 00:00:00 2001 From: Minji6 <105301353+Minji6@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:43:50 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=B4=88=EB=8C=80=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EB=B0=9C=EC=86=A1=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/ProjectInviteEmailService.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java diff --git a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java new file mode 100644 index 0000000..482034c --- /dev/null +++ b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java @@ -0,0 +1,125 @@ +package sequence.sequence_member.project.service; + + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import sequence.sequence_member.global.exception.BaseException; +import sequence.sequence_member.global.response.Code; +import sequence.sequence_member.member.entity.MemberEntity; +import sequence.sequence_member.project.entity.Project; + +@Service +@Slf4j +@RequiredArgsConstructor +public class ProjectInviteEmailService { + private final JavaMailSender mailSender; + + @Value("${NAVER_MAIL_USERNAME:dev_mj_@naver.com}") + private String fromEmail; + +// 프로젝트 초대 이메일 발송 + public void sendInviteEmail(Project project, MemberEntity invitedMember) { + try{ + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + // 이메일 기본 설정 + helper.setFrom(fromEmail); + helper.setTo(invitedMember.getEmail()); + helper.setSubject(String.format("[%s] 프로젝트 초대장이 도착했습니다!", project.getProjectName())); + + // HTML 이메일 내용 작성 + String htmlContent = createInvitationHtmlContent(project, invitedMember); + helper.setText(htmlContent, true); + + // 이메일 발송 + mailSender.send(message); + log.info("프로젝트 초대 이메일 발송 완료 - 수신자: {}, 프로젝트: {}", + invitedMember.getEmail(), project.getTitle()); + + } catch (MessagingException e) { + throw new BaseException(Code.INTERNAL_SERVER_ERROR, "프로젝트 초대 이메일 발송에 실패했습니다."); + } + } + + // 초대 이메일 HTML + private String createInvitationHtmlContent(Project project, MemberEntity invitedMember) { + return String.format(""" +
+
+

🎉 프로젝트 초대장

+

+ 새로운 프로젝트에 초대되었습니다! +

+
+ +
+

+ 안녕하세요 %s님! 👋 +

+ +

+ %s님이 회원님을 다음 프로젝트에 초대했습니다. +

+ +
+

+ 📋 %s +

+

+ 프로젝트명: %s +

+

+ 카테고리: %s +

+

+ 모집인원: %d명 +

+

+ 프로젝트 기간: %s ~ %s +

+
+ +
+ + ✨ 프로젝트 확인하기 + +
+ +
+

+ 💡 참고사항:
+ 이 초대를 수락하시려면 위 버튼을 클릭하여 프로젝트 페이지에서 참가 승인을 해주세요. +

+
+
+ +
+

이 이메일은 자동으로 발송된 메일입니다.

+

프로젝트 협업 플랫폼 © 2025

+
+
+ """, + invitedMember.getNickname(), + project.getWriter().getNickname(), + project.getTitle(), + project.getProjectName(), + project.getCategory().toString(), + project.getPersonnel(), + project.getStartDate(), + project.getEndDate(), + project.getId() + ); + } +} From 4c3bb345f1006d208c969cfe9cf496d008660fc6 Mon Sep 17 00:00:00 2001 From: Minji6 <105301353+Minji6@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:45:30 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=A4=EB=B2=84=20=EC=A0=80=EC=9E=A5=20=EC=8B=9C?= =?UTF-8?q?=20=EC=B4=88=EB=8C=80=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ProjectInviteEmailService 의존성 주입 - 프로젝트 초대 멤버 저장 후 이메일 발송 기능 연동 --- .../sequence_member/project/service/ProjectMemberService.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java index bf2284e..aedbdf9 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java @@ -23,6 +23,7 @@ public class ProjectMemberService { private final ProjectMemberRepository projectMemberRepository; private final MemberRepository memberRepository; private final ProjectInvitedMemberRepository projectInvitedMemberRepository; + private final ProjectInviteEmailService projectInviteEmailService; // 프로젝트 초대 멤버를 저장하는 함수 @Transactional @@ -40,6 +41,9 @@ public void saveProjectInvitedMemberEntities(Project project, List for(MemberEntity member : invitedMembers){ ProjectInvitedMember entity = ProjectInvitedMember.fromProjectAndMember(project,member); projectInvitedMemberRepository.save(entity); + + // 이메일 초대장 발송 추가 + projectInviteEmailService.sendInviteEmail(project, member); } } From 8177ea53b2c9eeae9b674ff53a0bdca428d6fba9 Mon Sep 17 00:00:00 2001 From: Minji6 <105301353+Minji6@users.noreply.github.com> Date: Mon, 23 Jun 2025 21:47:02 +0900 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=95=8C=EB=A6=BC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AsyncConfig: 이메일 발송용 비동기 처리 설정 추가 - ProjectInviteEmailService: 프로젝트 초대 및 수정 알림 이메일 발송 서비스 구현 - ProjectMemberService: 멤버 초대 시 이메일 자동 발송 로직 추가 - ProjectService: 프로젝트 수정 시 변경사항 추적 및 알림 이메일 발송 기능 추가 --- .../global/config/AsyncConfig.java | 17 ++ .../service/ProjectInviteEmailService.java | 153 +++++++++++++++--- .../project/service/ProjectMemberService.java | 32 +++- .../project/service/ProjectService.java | 44 +++++ 4 files changed, 219 insertions(+), 27 deletions(-) diff --git a/sequence_member/src/main/java/sequence/sequence_member/global/config/AsyncConfig.java b/sequence_member/src/main/java/sequence/sequence_member/global/config/AsyncConfig.java index d56d2a1..2ee4783 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/global/config/AsyncConfig.java +++ b/sequence_member/src/main/java/sequence/sequence_member/global/config/AsyncConfig.java @@ -1,9 +1,26 @@ package sequence.sequence_member.global.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; @Configuration @EnableAsync public class AsyncConfig { + + @Bean(name = "emailTaskExecutor") + public Executor emailTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("Email-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(30); + executor.initialize(); + return executor; + } } \ No newline at end of file diff --git a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java index 482034c..e5f4625 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java @@ -8,12 +8,15 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import sequence.sequence_member.global.exception.BaseException; import sequence.sequence_member.global.response.Code; import sequence.sequence_member.member.entity.MemberEntity; import sequence.sequence_member.project.entity.Project; +import java.util.List; + @Service @Slf4j @RequiredArgsConstructor @@ -23,9 +26,13 @@ public class ProjectInviteEmailService { @Value("${NAVER_MAIL_USERNAME:dev_mj_@naver.com}") private String fromEmail; -// 프로젝트 초대 이메일 발송 + // 프로젝트 초대 이메일 발송 - 비동기 + @Async public void sendInviteEmail(Project project, MemberEntity invitedMember) { - try{ + try { + log.info("이메일 발송 시작 - 수신자: {}, 프로젝트: {}", + invitedMember.getEmail(), project.getProjectName()); + MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); @@ -41,15 +48,78 @@ public void sendInviteEmail(Project project, MemberEntity invitedMember) { // 이메일 발송 mailSender.send(message); log.info("프로젝트 초대 이메일 발송 완료 - 수신자: {}, 프로젝트: {}", - invitedMember.getEmail(), project.getTitle()); + invitedMember.getEmail(), project.getProjectName()); } catch (MessagingException e) { - throw new BaseException(Code.INTERNAL_SERVER_ERROR, "프로젝트 초대 이메일 발송에 실패했습니다."); + log.error("이메일 발송 실패 - 수신자: {}, 프로젝트: {}, 오류: {}", + invitedMember.getEmail(), project.getProjectName(), e.getMessage(), e); + } catch (Exception e) { + log.error("예상치 못한 이메일 발송 오류 - 수신자: {}, 프로젝트: {}, 오류: {}", + invitedMember.getEmail(), project.getProjectName(), e.getMessage(), e); } } - // 초대 이메일 HTML + // 프로젝트 수정 알림 이메일 발송 + @Async + public void sendProjectUpdateEmail(Project project, List members, String updateDetails) { + if (project == null) { + log.error("프로젝트 정보가 없습니다."); + return; + } + + if (members == null || members.isEmpty()) { + log.warn("알림을 받을 멤버가 없습니다 - 프로젝트: {}", project.getProjectName()); + return; + } + + for (MemberEntity member : members) { + try { + if (member == null || member.getEmail() == null || member.getEmail().trim().isEmpty()) { + log.warn("유효하지 않은 멤버 정보 - 프로젝트: {}", project.getProjectName()); + continue; + } + + log.info("프로젝트 수정 알림 이메일 발송 시작 - 수신자: {}, 프로젝트: {}", + member.getEmail(), project.getProjectName()); + + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom(fromEmail); + helper.setTo(member.getEmail()); + helper.setSubject(String.format("[%s] 프로젝트 정보가 변경되었습니다", + project.getProjectName())); + + String htmlContent = createUpdateHtmlContent(project, member, updateDetails); + helper.setText(htmlContent, true); + + mailSender.send(message); + log.info("프로젝트 수정 알림 이메일 발송 완료 - 수신자: {}, 프로젝트: {}", + member.getEmail(), project.getProjectName()); + + } catch (MessagingException e) { + log.error("프로젝트 수정 알림 이메일 발송 실패 - 수신자: {}, 프로젝트: {}, 오류: {}", + member.getEmail(), project.getProjectName(), e.getMessage(), e); + } catch (Exception e) { + log.error("프로젝트 수정 알림 예상치 못한 오류 - 수신자: {}, 프로젝트: {}, 오류: {}", + member.getEmail(), project.getProjectName(), e.getMessage(), e); + } + } + } + + // 초대 이메일 HTML 템플릿 private String createInvitationHtmlContent(Project project, MemberEntity invitedMember) { + // Null 체크 및 기본값 설정 + String nickname = invitedMember.getNickname() != null ? invitedMember.getNickname() : "회원"; + String writerNickname = project.getWriter() != null && project.getWriter().getNickname() != null + ? project.getWriter().getNickname() : "관리자"; + String title = project.getTitle() != null ? project.getTitle() : "프로젝트"; + String projectName = project.getProjectName() != null ? project.getProjectName() : "프로젝트"; + String category = project.getCategory() != null ? project.getCategory().toString() : "미정"; + int personnel = project.getPersonnel(); + String startDate = project.getStartDate() != null ? project.getStartDate().toString() : "미정"; + String endDate = project.getEndDate() != null ? project.getEndDate().toString() : "미정"; + return String.format("""
- - ✨ 프로젝트 확인하기 - +
+

+ 💡 안내:
+ 프로젝트 초대 알림입니다. 참여를 원하시면 로그인하여 확인해주세요. +

+
+
+ +
+

이 이메일은 자동으로 발송된 메일입니다.

+

Sequence © 2025

+
+
+ """, + nickname, writerNickname, title, projectName, category, personnel, + startDate, endDate + ); + } + + // 프로젝트 수정 알림 이메일 HTML 템플릿 + private String createUpdateHtmlContent(Project project, MemberEntity member, String updateDetails) { + String nickname = member.getNickname() != null ? member.getNickname() : "회원"; + String projectName = project.getProjectName() != null ? project.getProjectName() : "프로젝트"; + String details = updateDetails != null && !updateDetails.isEmpty() ? updateDetails : "프로젝트 정보가 변경되었습니다."; + + return String.format(""" +
+
+

📝 프로젝트 정보 변경

+

+ 프로젝트 정보가 업데이트되었습니다! +

+
+ +
+

+ 안녕하세요 %s님! 👋 +

+ +

+ 참여 중인 %s 프로젝트의 정보가 변경되었습니다. +

+ +
+

변경 내용

+

%s

-

- 💡 참고사항:
- 이 초대를 수락하시려면 위 버튼을 클릭하여 프로젝트 페이지에서 참가 승인을 해주세요. +

+ 💡 안내:
+ 프로젝트 변경 알림입니다. 자세한 내용은 로그인하여 확인해주세요.

이 이메일은 자동으로 발송된 메일입니다.

-

프로젝트 협업 플랫폼 © 2025

+

Sequence © 2025

""", - invitedMember.getNickname(), - project.getWriter().getNickname(), - project.getTitle(), - project.getProjectName(), - project.getCategory().toString(), - project.getPersonnel(), - project.getStartDate(), - project.getEndDate(), - project.getId() + nickname, projectName, details ); } } diff --git a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java index aedbdf9..bd80e5d 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -18,6 +19,7 @@ @Service @RequiredArgsConstructor +@Slf4j public class ProjectMemberService { private final ProjectMemberRepository projectMemberRepository; @@ -38,12 +40,14 @@ public void saveProjectInvitedMember(ProjectInputDTO projectInputDTO, MemberEnti // 초대된 멤버들을 저장하는 함수 public void saveProjectInvitedMemberEntities(Project project, List invitedMembers){ + for(MemberEntity member : invitedMembers){ ProjectInvitedMember entity = ProjectInvitedMember.fromProjectAndMember(project,member); projectInvitedMemberRepository.save(entity); - // 이메일 초대장 발송 추가 + // 초대 이메일 발송 projectInviteEmailService.sendInviteEmail(project, member); + log.info("프로젝트 초대 이메일 발송 - 프로젝트: {}, 멤버: {}", project.getProjectName(), member.getNickname()); } } @@ -69,4 +73,30 @@ public List getProjectMemberOutputDTOS(Project project) return projectMemberOutputDTOS; } + // 프로젝트 수정 시 멤버들에게 알림 이메일 발송 + @Transactional(readOnly = true) + public void notifyProjectUpdate(Project project, String updateDetails) { + try { + List projectMembers = project.getMembers(); + + if (projectMembers != null && !projectMembers.isEmpty()) { + // MemberEntity 리스트로 변환 + List memberList = projectMembers.stream() + .map(ProjectMember::getMember) + .filter(member -> member != null) + .toList(); + + // 리스트로 전달 (현재 메서드 시그니처에 맞게) + projectInviteEmailService.sendProjectUpdateEmail(project, memberList, updateDetails); + + log.info("프로젝트 수정 알림 발송 완료 - 프로젝트: {}, 대상자: {}명", + project.getProjectName(), memberList.size()); + } + + } catch (Exception e) { + log.error("프로젝트 수정 알림 실패 - 프로젝트: {}, 오류: {}", + project.getProjectName(), e.getMessage()); + } + } + } diff --git a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectService.java b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectService.java index 2914376..7a8f67d 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectService.java @@ -3,6 +3,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -61,6 +62,13 @@ public ProjectOutputDTO updateProject(Long projectId, CustomUserDetails customUs if (!project.getWriter().equals(writer)) { throw new AuthException("작성자만 수정할 수 있습니다."); } + + // 수정 전 원본 정보 저장 + String originalTitle = project.getTitle(); + String originalProjectName = project.getProjectName(); + Category originalCategory = project.getCategory(); + int originalPersonnel = project.getPersonnel(); + // Project Entity에 ProjectInputDTO의 정보를 업데이트 project.updateProject(projectUpdateDTO); @@ -98,6 +106,17 @@ public ProjectOutputDTO updateProject(Long projectId, CustomUserDetails customUs projectMemberService.saveProjectInvitedMemberEntities(project, invitedMembers); + // 원본 정보 사용해서 변경사항 비교 + String updateDetails = buildUpdateDetailsWithOriginal( + originalTitle, originalProjectName, originalCategory, originalPersonnel, + projectUpdateDTO + ); + + // 멤버들에게 알림 이메일 발송 + if (!updateDetails.isEmpty()) { + projectMemberService.notifyProjectUpdate(project, updateDetails); + } + return projectGetService.getProject(projectId,request, customUserDetails); } @@ -214,7 +233,32 @@ public List getAllProjects(){ } + // 변경사항 요약 헬퍼 메서드 + private String buildUpdateDetailsWithOriginal(String originalTitle, String originalProjectName, + Category originalCategory, int originalPersonnel, + ProjectUpdateDTO request) { + List changes = new ArrayList<>(); + if (!Objects.equals(originalTitle, request.getTitle())) { + changes.add("프로젝트 제목 변경"); + } + if (!Objects.equals(originalProjectName, request.getProjectName())) { + changes.add("프로젝트명 변경"); + } + if (!Objects.equals(originalCategory, request.getCategory())) { + changes.add("카테고리 변경"); + } + if (originalPersonnel != request.getPersonnel()) { + changes.add("모집인원 변경"); + } + if (request.getDeletedMembersNicknames() != null && !request.getDeletedMembersNicknames().isEmpty()) { + changes.add("멤버 제외"); + } + if (request.getInvitedMembersNicknames() != null && !request.getInvitedMembersNicknames().isEmpty()) { + changes.add("새 멤버 초대"); + } + return String.join(", ", changes); + } } From f25bb237ae30dda8ceb7b1f18af0aaecb323a6d7 Mon Sep 17 00:00:00 2001 From: Minji6 <105301353+Minji6@users.noreply.github.com> Date: Mon, 30 Jun 2025 02:42:48 +0900 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=EB=B0=98=EC=9D=91=ED=98=95=20?= =?UTF-8?q?=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 프로젝트 초대/수정 이메일 템플릿 추가 - 모바일/태블릿/데스크탑 반응형 지원 --- .../member/service/FindPasswordService.java | 138 +++++-- .../service/ProjectInviteEmailService.java | 372 +++++++++++++----- 2 files changed, 388 insertions(+), 122 deletions(-) diff --git a/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java b/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java index 1f55197..dd3247d 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java @@ -82,59 +82,148 @@ private void sendPasswordResetEmail(String email, String temporaryPassword) { "" + "" + "Sequence 임시비밀번호 발송" + + "" + "" + - "" + - "" + + "" + + + // 외부 컨테이너 + "
" + + "" + + "
" + + + // 메인 컨테이너 (반응형) + "" + + + // 헤더 섹션 "" + - "" + "" + @@ -148,4 +237,5 @@ private void sendPasswordResetEmail(String email, String temporaryPassword) { throw new RuntimeException("이메일 발송 중 오류가 발생했습니다.",e); } } + } diff --git a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java index e5f4625..10e51d9 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectInviteEmailService.java @@ -10,8 +10,6 @@ import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import sequence.sequence_member.global.exception.BaseException; -import sequence.sequence_member.global.response.Code; import sequence.sequence_member.member.entity.MemberEntity; import sequence.sequence_member.project.entity.Project; @@ -107,7 +105,7 @@ public void sendProjectUpdateEmail(Project project, List members, } } - // 초대 이메일 HTML 템플릿 + // 프로젝트 초대 이메일 HTML 템플릿 (반응형) private String createInvitationHtmlContent(Project project, MemberEntity invitedMember) { // Null 체크 및 기본값 설정 String nickname = invitedMember.getNickname() != null ? invitedMember.getNickname() : "회원"; @@ -120,107 +118,285 @@ private String createInvitationHtmlContent(Project project, MemberEntity invited String startDate = project.getStartDate() != null ? project.getStartDate().toString() : "미정"; String endDate = project.getEndDate() != null ? project.getEndDate().toString() : "미정"; - return String.format(""" -
-
-

🎉 프로젝트 초대장

-

- 새로운 프로젝트에 초대되었습니다! -

-
- -
-

- 안녕하세요 %s님! 👋 -

- -

- %s님이 회원님을 다음 프로젝트에 초대했습니다. -

- -
-

- 📋 %s -

-

- 프로젝트명: %s -

-

- 카테고리: %s -

-

- 모집인원: %d명 -

-

- 프로젝트 기간: %s ~ %s -

-
- -
-

- 💡 안내:
- 프로젝트 초대 알림입니다. 참여를 원하시면 로그인하여 확인해주세요. -

-
-
- -
-

이 이메일은 자동으로 발송된 메일입니다.

-

Sequence © 2025

-
-
- """, - nickname, writerNickname, title, projectName, category, personnel, - startDate, endDate - ); + return "" + + "" + + "" + + "" + + "" + + "Sequence 프로젝트 초대" + + "" + + "" + + "" + + + // 외부 컨테이너 + "
" + - "" + + "" + + "" + // 컨텐츠 섹션 "" + - "" + "" + // 구분선 "" + "" + "" + // 푸터 섹션 "" + - "" + "" + + "
" + + "

Sequence

" + + "

🔑 임시 비밀번호 발급

" + + "

계정 복구를 위한 임시 비밀번호를 발급했습니다

" + + "
" + - "

안녕하세요. 임시 비밀번호가 발급되었습니다.

" + - "

아래의 임시 비밀번호로 로그인 후, 보안을 위해 마이페이지에서 비밀번호를 변경해주세요.

" + + "
" + - // 비밀번호 섹션 - "" + + // 안내 메시지 + "
" + "" + - "" + "" + "
" + - "

임시 비밀번호

" + - "
" + temporaryPassword + "
" + + "
" + + "

" + + "안녕하세요," + + "

" + + "

" + + "요청하신 임시 비밀번호가 발급되었습니다. 아래의 임시 비밀번호로 로그인 후, 보안을 위해 마이페이지에서 비밀번호를 변경해주세요." + + "

" + "
" + - // 경고 섹션 + // 임시 비밀번호 섹션 "" + "" + - "" + + "" + + "
" + - "

보안 알림 및 이용 안내

" + - "

• 로그인 즉시 마이페이지에서 새로운 비밀번호로 변경하세요

" + - "

• 비밀번호를 타인과 공유하지 마세요

" + - "

• Sequence 로그인 페이지에 접속합니다

" + - "

• 이메일과 임시 비밀번호를 입력합니다

" + - "

• 로그인 후 마이페이지에서 비밀번호 변경을 완료합니다

" + + "
" + + + // 비밀번호 라벨 + "" + + "" + + "" + + "" + + "
" + + "

임시 비밀번호

" + + "
" + + + // 비밀번호 표시 + "" + + "" + + "" + "" + "
" + + "
" + + temporaryPassword + + "
" + "
" + + + "
" + + + // 보안 안내 섹션 + "" + + "" + + "" + + "" + + "
" + + "

보안 알림 및 이용 안내

" + + + // 안내사항 목록 + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "

• 로그인 즉시 마이페이지에서 새로운 비밀번호로 변경하세요

" + + "
" + + "

• 비밀번호를 타인과 공유하지 마세요

" + + "
" + + "

• Sequence 로그인 페이지에 접속합니다

" + + "
" + + "

• 이메일과 임시 비밀번호를 입력합니다

" + + "
" + + "

• 로그인 후 마이페이지에서 비밀번호 변경을 완료합니다

" + + "
" + + + "
" + + "
" + - "
" + + "
" + "
" + - "

Sequence

" + - "

이 메시지는 발신 전용입니다.

" + + "
" + + "

Sequence

" + + "

이 메시지는 발신 전용입니다.

" + + "

Sequence © 2025

" + "
" + "
" + + "" + + "" + + "" + + "
" + + + // 메인 컨테이너 (반응형) + "" + + + // 헤더 섹션 + "" + + "" + + "" + + + // 컨텐츠 섹션 + "" + + "" + + "" + + + // 구분선 + "" + + "" + + "" + + + // 푸터 섹션 + "" + + "" + + "" + + + "
" + + "

Sequence

" + + "

📋 프로젝트 초대 알림

" + + "

새로운 프로젝트 참여 요청이 있습니다

" + + "
" + + + // 인사말 + "" + + "" + + "" + + "" + + "
" + + "

" + + "안녕하세요 " + nickname + "님," + + "

" + + "
" + + + // 초대 메시지 + "" + + "" + + "" + + "" + + "
" + + "

" + + "" + writerNickname + "님이 회원님을 다음 프로젝트에 초대했습니다." + + "

" + + "
" + + + // 프로젝트 정보 섹션 (반응형 테이블) + "" + + "" + + "" + + "" + + "
" + + + // 프로젝트 제목 + "" + + "" + + "" + + "" + + "
" + + "

" + + "📋 " + title + + "

" + + "
" + + + // 프로젝트 상세 정보 (모바일에서 세로 정렬) + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + "

" + + "프로젝트명: " + projectName + + "

" + + "
" + + "

" + + "카테고리: " + category + + "

" + + "
" + + "

" + + "모집인원: " + personnel + "명" + + "

" + + "
" + + "

" + + "프로젝트 기간: " + startDate + " ~ " + endDate + + "

" + + "
" + + + "
" + + + // 안내 섹션 + "" + + "" + + "" + + "" + + "
" + + "

💡 안내사항

" + + "

• 프로젝트 참여 초대 안내

" + + "

• 참여 의사를 결정하시려면 로그인 후 확인해주세요

" + + "

• 추가 문의사항은 프로젝트 관리자에게 연락해주세요

" + + "
" + + + "
" + + "
" + + "
" + + "

Sequence

" + + "

이 메시지는 발신 전용입니다.

" + + "

Sequence © 2025

" + + "
" + + "
" + + "" + + ""; } - // 프로젝트 수정 알림 이메일 HTML 템플릿 + // 프로젝트 수정 알림 이메일 HTML 템플릿 (반응형) private String createUpdateHtmlContent(Project project, MemberEntity member, String updateDetails) { String nickname = member.getNickname() != null ? member.getNickname() : "회원"; String projectName = project.getProjectName() != null ? project.getProjectName() : "프로젝트"; String details = updateDetails != null && !updateDetails.isEmpty() ? updateDetails : "프로젝트 정보가 변경되었습니다."; - return String.format(""" -
-
-

📝 프로젝트 정보 변경

-

- 프로젝트 정보가 업데이트되었습니다! -

-
- -
-

- 안녕하세요 %s님! 👋 -

- -

- 참여 중인 %s 프로젝트의 정보가 변경되었습니다. -

- -
-

변경 내용

-

%s

-
- -
-

- 💡 안내:
- 프로젝트 변경 알림입니다. 자세한 내용은 로그인하여 확인해주세요. -

-
-
- -
-

이 이메일은 자동으로 발송된 메일입니다.

-

Sequence © 2025

-
-
- """, - nickname, projectName, details - ); + return "" + + "" + + "" + + "" + + "" + + "Sequence 프로젝트 정보 변경" + + "" + + "" + + "" + + + // 외부 컨테이너 + "" + + "" + + "" + + "" + + "
" + + + // 메인 컨테이너 (반응형) + "" + + + // 헤더 섹션 + "" + + "" + + "" + + + // 컨텐츠 섹션 + "" + + "" + + "" + + + // 구분선 + "" + + "" + + "" + + + // 푸터 섹션 + "" + + "" + + "" + + + "
" + + "

Sequence

" + + "

📝 프로젝트 정보 업데이트

" + + "

참여 중인 프로젝트의 정보가 변경되었습니다

" + + "
" + + + // 인사말 + "" + + "" + + "" + + "" + + "
" + + "

" + + "안녕하세요 " + nickname + "님," + + "

" + + "
" + + + // 업데이트 메시지 + "" + + "" + + "" + + "" + + "
" + + "

" + + "참여 중인 " + projectName + " 프로젝트의 정보가 변경되었습니다." + + "

" + + "
" + + + // 변경 내용 섹션 + "" + + "" + + "" + + "" + + "
" + + "

변경 내용

" + + "
" + + details + + "
" + + "
" + + + // 안내 섹션 + "" + + "" + + "" + + "" + + "
" + + "

💡 안내사항

" + + "

• 프로젝트 정보 변경 알림

" + + "

• 상세 내용은 로그인하여 프로젝트 페이지에서 확인 가능합니다

" + + "

• 변경사항에 대한 문의는 프로젝트 관리자에게 연락해주세요

" + + "
" + + + "
" + + "
" + + "
" + + "

Sequence

" + + "

이 메시지는 발신 전용입니다.

" + + "

Sequence © 2025

" + + "
" + + "
" + + "" + + ""; } } From 1c27a272c241a0a1a2280a60be00f0c2e2d91665 Mon Sep 17 00:00:00 2001 From: Minji6 <105301353+Minji6@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:56:46 +0900 Subject: [PATCH 6/7] Update FindPasswordService.java --- .../sequence_member/member/service/FindPasswordService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java b/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java index dd3247d..d88fe75 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/member/service/FindPasswordService.java @@ -237,5 +237,5 @@ private void sendPasswordResetEmail(String email, String temporaryPassword) { throw new RuntimeException("이메일 발송 중 오류가 발생했습니다.",e); } } - + } From 40a1e977e6f66588a9f0111b916fddf651d9328b Mon Sep 17 00:00:00 2001 From: Minji6 <105301353+Minji6@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:37:06 +0900 Subject: [PATCH 7/7] Update ProjectMemberService.java MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 중복 어노테이션 삭제 --- .../sequence_member/project/service/ProjectMemberService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java index 453dc6c..01ba584 100644 --- a/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java +++ b/sequence_member/src/main/java/sequence/sequence_member/project/service/ProjectMemberService.java @@ -20,7 +20,6 @@ @Slf4j @Service @RequiredArgsConstructor -@Slf4j public class ProjectMemberService { private final ProjectMemberRepository projectMemberRepository;