Skip to content
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-mail'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/kusitms/website/WebsiteApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@EnableAsync
@SpringBootApplication
public class WebsiteApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kusitms.website.domain.admin;

import com.kusitms.website.domain.admin.dto.response.PendingMemberResponse;
import com.kusitms.website.domain.introduction.IntroService;
import com.kusitms.website.domain.introduction.dto.request.IntroRequest;
import com.kusitms.website.domain.blog.dto.request.BlogReviewRequest;
Expand All @@ -18,16 +19,20 @@
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;

@RestController
@RequiredArgsConstructor
@Tag(name = "Admin", description = "어드민 페이지 API : 테스트 단계이므로 /admin으로 요청하는 모든 데이터는 현재 배포된 사이트와 독립적임")
public class AdminController {
private final IntroService introService;
private final AdminService adminService;
private final MemberAdminService memberAdminService;
private final JwtTokenProvider jwtTokenProvider;

@GetMapping("/admin/introductions")
Expand Down Expand Up @@ -251,4 +256,31 @@ public ResponseEntity<BaseResponse> deleteBlogReview(@PathVariable("id") Long bl
adminService.deleteBlogReview(blogReviewId);
return ResponseEntity.ok(new BaseResponse());
}

@GetMapping("/admin/members/pending")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "승인 대기 회원 목록 조회", description = "가입 승인 대기 중인 회원 목록을 조회합니다.")
public ResponseEntity<BaseResponse<List<PendingMemberResponse>>> getPendingMembers() {
return ResponseEntity.ok(new BaseResponse<>(memberAdminService.getPendingMembers()));
}

@PostMapping("/admin/members/{userId}/approve")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "회원 가입 승인", description = "대기 중인 회원의 가입을 승인합니다.")
public ResponseEntity<BaseResponse> approveMember(
@PathVariable Long userId
) {
memberAdminService.approveMember(userId);
return ResponseEntity.ok(new BaseResponse());
}

@PostMapping("/admin/members/{userId}/reject")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "회원 가입 반려", description = "대기 중인 회원의 가입을 반려하고 계정을 삭제합니다.")
public ResponseEntity<BaseResponse> rejectMember(
@PathVariable Long userId
) {
memberAdminService.rejectMember(userId);
return ResponseEntity.ok(new BaseResponse());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.kusitms.website.domain.admin;

import com.kusitms.website.domain.admin.dto.response.PendingMemberResponse;
import com.kusitms.website.domain.email.service.MailService;
import com.kusitms.website.domain.user.Member;
import com.kusitms.website.domain.user.MemberRepository;
import com.kusitms.website.domain.user.MemberStatus;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
public class MemberAdminService {

private final MemberRepository memberRepository;
private final MailService mailService;

public List<PendingMemberResponse> getPendingMembers() {
return memberRepository.findAllByStatus(MemberStatus.PENDING).stream()
.map(PendingMemberResponse::from)
.collect(Collectors.toList());
}

@Transactional
public void approveMember(Long userId) {
Member member = memberRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));

if (member.getStatus() != MemberStatus.PENDING) {
throw new IllegalArgumentException("승인 대기 상태가 아닌 회원입니다.");
}

member.approve();

if (member.getEmail() != null && !member.getEmail().isBlank()) {
mailService.sendApprovalEmail(member.getEmail(), member.getName());
}
}

@Transactional
public void rejectMember(Long userId) {
Member member = memberRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 회원입니다."));

if (member.getStatus() != MemberStatus.PENDING) {
throw new IllegalArgumentException("승인 대기 상태가 아닌 회원입니다.");
}

String name = member.getName();
String email = member.getEmail();
memberRepository.delete(member);

if (email != null && !email.isBlank()) {
mailService.sendRejectionEmail(email, name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.kusitms.website.domain.admin.dto.response;

import com.kusitms.website.domain.user.Member;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
@Schema(description = "승인 대기 회원 응답")
public class PendingMemberResponse {
@Schema(description = "회원 PK")
private Long userId;

@Schema(description = "아이디")
private String id;

@Schema(description = "이름")
private String name;

@Schema(description = "휴대폰 번호")
private String phone;

@Schema(description = "활동 기수")
private Integer cardinal;

@Schema(description = "파트")
private String part;

@Schema(description = "프로필 이미지 URL")
private String profileImageUrl;

@Schema(description = "수료증 이미지 URL")
private String certificateImageUrl;

public static PendingMemberResponse from(Member member) {
return new PendingMemberResponse(
member.getUserId(),
member.getId(),
member.getName(),
member.getPhone(),
member.getCardinal(),
member.getPart().name(),
member.getProfileImageUrl(),
member.getCertificateImageUrl()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package com.kusitms.website.domain.email.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import javax.mail.internet.MimeMessage;

@Slf4j
@Service
@RequiredArgsConstructor
public class MailService {

private static final String BANNER_URL = "https://kusitms-bucket.s3.ap-northeast-2.amazonaws.com/meetup/OG/456ed518-7489-4106-9d33-94c491ef358732__OG_.png";
private static final String BRAND_COLOR = "#4A3AFF";

private final JavaMailSender mailSender;

@Async
public void sendApprovalEmail(String toEmail, String name) {
String subject = "[큐시즘] 회원가입이 승인되었습니다";
String html = buildEmail(name,
"회원가입 승인 완료",
"큐시즘 회원가입이 승인되었습니다.<br>지금 바로 로그인하여 서비스를 이용하실 수 있습니다.",
"로그인하기",
"https://kusitms.com");
sendHtmlEmail(toEmail, subject, html);
}

@Async
public void sendRejectionEmail(String toEmail, String name) {
String subject = "[큐시즘] 회원가입 신청이 반려되었습니다";
String html = buildEmail(name,
"회원가입 반려 안내",
"큐시즘 회원가입 신청이 반려되었습니다.<br>문의 사항이 있으시면 운영진에게 연락해 주세요.",
null, null);
sendHtmlEmail(toEmail, subject, html);
}

private void sendHtmlEmail(String toEmail, String subject, String html) {
try {
MimeMessage mime = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mime, "UTF-8");
helper.setTo(toEmail);
helper.setSubject(subject);
helper.setText(html, true);
mailSender.send(mime);
} catch (Exception e) {
log.error("이메일 발송 실패: {}", toEmail, e);
}
}

private String buildEmail(String name, String title, String body, String buttonText, String buttonUrl) {
StringBuilder sb = new StringBuilder();
sb.append("<!DOCTYPE html><html><head><meta charset='UTF-8'></head>");
sb.append("<body style='margin:0;padding:0;background-color:#f4f4f7;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;'>");
sb.append("<table width='100%' cellpadding='0' cellspacing='0' style='background-color:#f4f4f7;padding:40px 0;'><tr><td align='center'>");
sb.append("<table width='600' cellpadding='0' cellspacing='0' style='background-color:#ffffff;border-radius:12px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);'>");

// Banner
sb.append("<tr><td style='padding:0;'>");
sb.append("<img src='").append(BANNER_URL).append("' width='600' style='display:block;width:100%;height:auto;' alt='KUSITMS' />");
sb.append("</td></tr>");

// Title
sb.append("<tr><td style='padding:32px 40px 0 40px;'>");
sb.append("<h1 style='margin:0;font-size:22px;color:#1a1a1a;'>").append(title).append("</h1>");
sb.append("</td></tr>");

// Body
sb.append("<tr><td style='padding:16px 40px 0 40px;'>");
sb.append("<p style='margin:0;font-size:15px;line-height:1.7;color:#555555;'>");
sb.append(name).append("님, 안녕하세요.<br><br>");
sb.append(body);
sb.append("</p></td></tr>");

// Button
if (buttonText != null && buttonUrl != null) {
sb.append("<tr><td style='padding:28px 40px 0 40px;'>");
sb.append("<a href='").append(buttonUrl).append("' style='display:inline-block;padding:12px 32px;background-color:").append(BRAND_COLOR);
sb.append(";color:#ffffff;text-decoration:none;border-radius:8px;font-size:14px;font-weight:600;'>").append(buttonText).append("</a>");
sb.append("</td></tr>");
}

// Footer
sb.append("<tr><td style='padding:32px 40px;'>");
sb.append("<hr style='border:none;border-top:1px solid #eeeeee;margin:0 0 20px 0;' />");
sb.append("<p style='margin:0;font-size:12px;color:#999999;line-height:1.6;'>");
sb.append("본 메일은 큐시즘에서 발송한 자동 알림 메일입니다.<br>");
sb.append("© KUSITMS. All rights reserved.");
sb.append("</p></td></tr>");

sb.append("</table></td></tr></table></body></html>");
return sb.toString();
}
}
25 changes: 25 additions & 0 deletions src/main/java/com/kusitms/website/domain/user/CurrentCardinal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.kusitms.website.domain.user;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Entity
@Getter
@NoArgsConstructor
public class CurrentCardinal {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private Integer cardinal;

public CurrentCardinal(Integer cardinal) {
this.cardinal = cardinal;
}

public void updateCardinal(Integer cardinal) {
this.cardinal = cardinal;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.kusitms.website.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

public interface CurrentCardinalRepository extends JpaRepository<CurrentCardinal, Long> {
}
Loading
Loading