diff --git a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/controller/AdminController.java b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/controller/AdminController.java index 1b70e196..6a5bf1d2 100644 --- a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/controller/AdminController.java +++ b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/controller/AdminController.java @@ -12,8 +12,10 @@ import org.codeNbug.mainserver.domain.admin.dto.response.AdminLoginResponse; import org.codeNbug.mainserver.domain.admin.dto.response.AdminSignupResponse; import org.codeNbug.mainserver.domain.admin.dto.response.DashboardStatsResponse; +import org.codeNbug.mainserver.domain.admin.dto.response.EventAdminDto; import org.codeNbug.mainserver.domain.admin.dto.response.ModifyRoleResponse; import org.codeNbug.mainserver.domain.admin.service.AdminService; +import org.codeNbug.mainserver.domain.event.entity.EventStatusEnum; import org.codeNbug.mainserver.global.dto.RsData; import org.codenbug.user.domain.user.constant.UserRole; import org.codenbug.user.security.annotation.RoleRequired; @@ -26,7 +28,10 @@ import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.HashMap; /** * 관리자 컨트롤러 @@ -38,7 +43,7 @@ public class AdminController { private final AdminService adminService; - + /** * 관리자 대시보드 페이지 */ @@ -47,11 +52,11 @@ public class AdminController { public String dashboard(HttpServletRequest request, Model model) { // 인증 정보 확인 로직 (필요 시 구현) log.info(">> 관리자 대시보드 페이지 요청"); - + // 쿠키에서 토큰 확인 Cookie[] cookies = request.getCookies(); boolean hasToken = false; - + if (cookies != null) { for (Cookie cookie : cookies) { if ("accessToken".equals(cookie.getName())) { @@ -61,26 +66,26 @@ public String dashboard(HttpServletRequest request, Model model) { } } } - + if (!hasToken) { log.warn(">> 액세스 토큰이 없습니다. 인증 정보를 확인하세요."); } - + // 대시보드 통계 정보 조회 try { DashboardStatsResponse stats = adminService.getDashboardStats(); model.addAttribute("stats", stats); - log.info(">> 대시보드 통계 정보 조회 성공: 사용자={}, 이벤트={}", + log.info(">> 대시보드 통계 정보 조회 성공: 사용자={}, 이벤트={}", stats.getTotalUsers(), stats.getTotalEvents()); } catch (Exception e) { log.error(">> 대시보드 통계 정보 조회 실패: {}", e.getMessage(), e); model.addAttribute("errorMessage", "통계 정보를 불러오는 데 실패했습니다."); } - + model.addAttribute("welcomeMessage", "관리자 대시보드에 오신 것을 환영합니다!"); return "admin/dashboard"; } - + /** * 관리자 로그인 페이지 */ @@ -90,20 +95,20 @@ public String loginPage(Model model) { model.addAttribute("adminLoginRequest", new AdminLoginRequest()); return "admin/login"; } - + /** * 관리자 로그인 처리 */ @PostMapping("/login") public String login(@ModelAttribute AdminLoginRequest request, - BindingResult bindingResult, - HttpServletResponse response, - RedirectAttributes redirectAttributes, - @RequestParam(required = false) String email, - @RequestParam(required = false) String password) { - + BindingResult bindingResult, + HttpServletResponse response, + RedirectAttributes redirectAttributes, + @RequestParam(required = false) String email, + @RequestParam(required = false) String password) { + log.info(">> 관리자 로그인 요청: 이메일={}", email); - + // AdminLoginRequest가 null인 경우 수동으로 객체 생성 if (request == null || (request.getEmail() == null && email != null)) { request = AdminLoginRequest.builder() @@ -112,34 +117,34 @@ public String login(@ModelAttribute AdminLoginRequest request, .build(); log.info(">> 수동으로 AdminLoginRequest 생성: 이메일={}", email); } - + // 유효성 검사 실패 시 로그인 페이지로 리다이렉트 if (bindingResult.hasErrors()) { log.warn(">> 로그인 폼 유효성 검사 실패: {}", bindingResult.getAllErrors()); return "admin/login"; } - + try { // 로그인 처리 AdminLoginResponse loginResponse = adminService.login(request); - + // 토큰을 쿠키에 저장 Cookie accessTokenCookie = new Cookie("accessToken", loginResponse.getAccessToken()); accessTokenCookie.setHttpOnly(true); accessTokenCookie.setPath("/"); accessTokenCookie.setMaxAge(3600); // 1시간 - + Cookie refreshTokenCookie = new Cookie("refreshToken", loginResponse.getRefreshToken()); refreshTokenCookie.setHttpOnly(true); refreshTokenCookie.setPath("/"); refreshTokenCookie.setMaxAge(86400); // 24시간 - + response.addCookie(accessTokenCookie); response.addCookie(refreshTokenCookie); - + log.info(">> 관리자 로그인 성공: 이메일={}", email); return "redirect:/admin/dashboard"; - + } catch (AuthenticationFailedException e) { log.error(">> 관리자 로그인 실패: {}", e.getMessage()); redirectAttributes.addFlashAttribute("errorMessage", e.getMessage()); @@ -150,7 +155,7 @@ public String login(@ModelAttribute AdminLoginRequest request, return "admin/login"; } } - + /** * 관리자 회원가입 페이지 */ @@ -160,25 +165,25 @@ public String signupPage(Model model) { model.addAttribute("adminSignupRequest", new AdminSignupRequest()); return "admin/signup"; } - + /** * 관리자 회원가입 처리 */ @PostMapping("/signup") public String signup(@ModelAttribute AdminSignupRequest request, - BindingResult bindingResult, - RedirectAttributes redirectAttributes, - Model model, - @RequestParam(required = false) String email, - @RequestParam(required = false) String password, - @RequestParam(required = false) String name, - @RequestParam(required = false) Integer age, - @RequestParam(required = false) String sex, - @RequestParam(required = false) String phoneNum, - @RequestParam(required = false) String location) { - + BindingResult bindingResult, + RedirectAttributes redirectAttributes, + Model model, + @RequestParam(required = false) String email, + @RequestParam(required = false) String password, + @RequestParam(required = false) String name, + @RequestParam(required = false) Integer age, + @RequestParam(required = false) String sex, + @RequestParam(required = false) String phoneNum, + @RequestParam(required = false) String location) { + log.info(">> 관리자 회원가입 요청: 이메일={}, 이름={}", email, name); - + // AdminSignupRequest가 null인 경우 수동으로 객체 생성 if (request == null || (request.getEmail() == null && email != null)) { request = AdminSignupRequest.builder() @@ -192,22 +197,22 @@ public String signup(@ModelAttribute AdminSignupRequest request, .build(); log.info(">> 수동으로 AdminSignupRequest 생성: {}", request); } - + // 유효성 검사 실패 시 회원가입 페이지로 리다이렉트 if (bindingResult.hasErrors()) { log.warn(">> 회원가입 폼 유효성 검사 실패: {}", bindingResult.getAllErrors()); return "admin/signup"; } - + try { // 회원가입 처리 log.info(">> AdminService.signup 호출 전"); AdminSignupResponse signupResponse = adminService.signup(request); log.info(">> 관리자 회원가입 성공: id={}, 이메일={}", signupResponse.getId(), signupResponse.getEmail()); - + redirectAttributes.addFlashAttribute("successMessage", "관리자 계정이 성공적으로 생성되었습니다. 로그인해주세요."); return "redirect:/admin/login"; - + } catch (Exception e) { log.error(">> 관리자 회원가입 실패: {}", e.getMessage(), e); model.addAttribute("adminSignupRequest", request); // 입력값 유지 @@ -215,7 +220,7 @@ public String signup(@ModelAttribute AdminSignupRequest request, return "admin/signup"; } } - + /** * 관리자 로그아웃 처리 */ @@ -223,11 +228,11 @@ public String signup(@ModelAttribute AdminSignupRequest request, @RoleRequired(UserRole.ADMIN) public String logout(HttpServletRequest request, HttpServletResponse response) { log.info(">> 관리자 로그아웃 요청"); - + // 쿠키에서 토큰 추출 String accessToken = null; String refreshToken = null; - + Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { @@ -238,7 +243,7 @@ public String logout(HttpServletRequest request, HttpServletResponse response) { } } } - + // 토큰이 있으면 로그아웃 처리 if (accessToken != null && refreshToken != null) { try { @@ -250,24 +255,24 @@ public String logout(HttpServletRequest request, HttpServletResponse response) { } else { log.warn(">> 로그아웃 실패: 토큰 정보 없음"); } - + // 쿠키 삭제 Cookie accessTokenCookie = new Cookie("accessToken", null); accessTokenCookie.setHttpOnly(true); accessTokenCookie.setPath("/"); accessTokenCookie.setMaxAge(0); - + Cookie refreshTokenCookie = new Cookie("refreshToken", null); refreshTokenCookie.setHttpOnly(true); refreshTokenCookie.setPath("/"); refreshTokenCookie.setMaxAge(0); - + response.addCookie(accessTokenCookie); response.addCookie(refreshTokenCookie); - + return "redirect:/admin/login"; } - + /** * 사용자 관리 페이지 */ @@ -275,26 +280,26 @@ public String logout(HttpServletRequest request, HttpServletResponse response) { @GetMapping("/users") public String userManagement(Model model) { log.info(">> 사용자 관리 페이지 요청"); - + try { // 모든 사용자 목록 조회 Map usersData = adminService.getAllUsers(); model.addAttribute("regularUsers", usersData.get("regularUsers")); model.addAttribute("snsUsers", usersData.get("snsUsers")); model.addAttribute("roles", UserRole.values()); - + log.info(">> 사용자 목록 조회 성공: 일반 사용자={}, SNS 사용자={}", ((java.util.List) usersData.get("regularUsers")).size(), ((java.util.List) usersData.get("snsUsers")).size()); - + } catch (Exception e) { log.error(">> 사용자 목록 조회 실패: {}", e.getMessage(), e); model.addAttribute("errorMessage", "사용자 목록을 불러오는 데 실패했습니다."); } - + return "admin/users"; } - + /** * 사용자 역할 변경 API */ @@ -306,15 +311,15 @@ public ResponseEntity> modifyRole( @PathVariable Long userId, @Valid @RequestBody RoleUpdateRequest request, BindingResult bindingResult) { - + // 입력값 유효성 검사 if (bindingResult.hasErrors()) { log.warn(">> 역할 변경 요청 유효성 검증 실패: {}", bindingResult.getAllErrors()); return ResponseEntity.badRequest() .body(new RsData<>("400-BAD_REQUEST", "데이터 형식이 잘못되었습니다.")); } - - log.info(">> 관리자 권한 변경 요청: userType={}, userId={}, role={}", + + log.info(">> 관리자 권한 변경 요청: userType={}, userId={}, role={}", userType, userId, request.getRole()); // userType이 유효한지 검사 @@ -326,10 +331,10 @@ public ResponseEntity> modifyRole( try { ModifyRoleResponse response = adminService.modifyRole(userType, userId, request.getRole()); - + log.info(">> 권한 변경 성공: userType={}, userId={}, newRole={}", userType, userId, response.getRole()); - + return ResponseEntity.ok( new RsData<>("200-SUCCESS", "권한 변경 성공", response)); } catch (Exception e) { @@ -338,14 +343,26 @@ public ResponseEntity> modifyRole( .body(new RsData<>("400-BAD_REQUEST", e.getMessage())); } } - + /** * 티켓 관리 페이지 */ @RoleRequired(UserRole.ADMIN) @GetMapping("/tickets") public String ticketManagement(Model model) { - // TODO: 티켓 목록 조회 및 model.addAttribute("tickets", ...); + log.info(">> 티켓 관리 페이지 요청"); + + try { + // 모든 티켓 목록 조회 + List tickets = adminService.getAllTickets(); + model.addAttribute("tickets", tickets); + + log.info(">> 티켓 목록 조회 성공: {} 개의 티켓", tickets.size()); + } catch (Exception e) { + log.error(">> 티켓 목록 조회 실패: {}", e.getMessage(), e); + model.addAttribute("errorMessage", "티켓 목록을 불러오는 데 실패했습니다."); + } + return "admin/tickets"; } @@ -355,14 +372,148 @@ public String ticketManagement(Model model) { @RoleRequired(UserRole.ADMIN) @GetMapping("/events") public String eventManagement(Model model) { - // TODO: 이벤트 목록 조회 및 model.addAttribute("events", ...); + log.info(">> 이벤트 관리 페이지 요청"); + + try { + // 모든 이벤트 목록 조회 + List events = adminService.getAllEvents(); + model.addAttribute("events", events); + + log.info(">> 이벤트 목록 조회 성공: {} 개의 이벤트", events.size()); + } catch (Exception e) { + log.error(">> 이벤트 목록 조회 실패: {}", e.getMessage(), e); + model.addAttribute("errorMessage", "이벤트 목록을 불러오는 데 실패했습니다."); + } + return "admin/events"; } - + @ExceptionHandler(Exception.class) public String handleException(Exception e, RedirectAttributes redirectAttributes) { log.error(">> 예외 발생: {}", e.getMessage(), e); redirectAttributes.addFlashAttribute("errorMessage", "요청 처리 중 오류가 발생했습니다: " + e.getMessage()); return "redirect:/admin/login"; } -} \ No newline at end of file + /** + * 이벤트 상세 정보 페이지 + */ + @RoleRequired(UserRole.ADMIN) + @GetMapping("/events/{id}") + public String eventDetail(@PathVariable("id") Long eventId, Model model, RedirectAttributes redirectAttributes) { + log.info(">> 이벤트 상세 정보 페이지 요청: id={}", eventId); + + try { + // 특정 이벤트 조회 + org.codeNbug.mainserver.domain.admin.dto.response.EventAdminDto event = adminService.getEvent(eventId); + model.addAttribute("event", event); + + // 해당 이벤트의 티켓 목록 조회 (옵션) + List tickets = + adminService.getAllTickets().stream() + .filter(ticket -> ticket.getEventId().equals(eventId)) + .collect(Collectors.toList()); + model.addAttribute("tickets", tickets); + + log.info(">> 이벤트 상세 정보 조회 성공: id={}, 티켓 수={}", eventId, tickets.size()); + + return "admin/event-detail"; + } catch (Exception e) { + log.error(">> 이벤트 상세 정보 조회 실패: {}", e.getMessage(), e); + redirectAttributes.addFlashAttribute("errorMessage", "이벤트 상세 정보를 불러오는 데 실패했습니다: " + e.getMessage()); + return "redirect:/admin/events"; + } + } + + /** + * 이벤트를 삭제합니다. + * 관리자 권한으로 이벤트를 삭제하고, 관련된 구매자들에게 알림을 전송합니다. + * + * @param eventId 삭제할 이벤트 ID + * @return 성공 메시지 + * @throws IllegalAccessException 이미 삭제된 이벤트인 경우 + */ + @DeleteMapping("/events/{eventId}") + public ResponseEntity> deleteEvent(@PathVariable Long eventId) throws IllegalAccessException { + log.info(">> 이벤트 삭제 요청: eventId={}", eventId); + adminService.deleteEvent(eventId); + log.info(">> 이벤트 삭제 완료: eventId={}", eventId); + return ResponseEntity.ok(RsData.success("이벤트가 성공적으로 삭제되었습니다.")); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException(Exception e) { + log.error(">> 예외 발생: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(RsData.error("500-INTERNAL_SERVER_ERROR", "요청 처리 중 오류가 발생했습니다: " + e.getMessage())); + } + + /** + * 삭제 대기 중인 이벤트 목록을 조회합니다. + */ + @GetMapping("/api/events/deleted") + @RoleRequired(UserRole.ADMIN) + public ResponseEntity>> getDeletedEvents() { + log.info(">> 삭제 대기 중인 이벤트 목록 조회 요청"); + + try { + List events = adminService.getDeletedEvents(); + log.info(">> 삭제 대기 중인 이벤트 목록 조회 성공: {} 개의 이벤트", events.size()); + return ResponseEntity.ok(RsData.success("삭제 대기 중인 이벤트 목록 조회 성공", events)); + } catch (Exception e) { + log.error(">> 삭제 대기 중인 이벤트 목록 조회 실패: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(RsData.error("500-INTERNAL_SERVER_ERROR", "삭제 대기 중인 이벤트 목록을 불러오는 데 실패했습니다.")); + } + } + + /** + * 삭제된 이벤트를 복구합니다. + * + * @param eventId 복구할 이벤트 ID + * @param status 복구 후 설정할 이벤트 상태 + * @return 복구된 이벤트 정보 + */ + @PostMapping("/api/events/{eventId}/restore") + @RoleRequired(UserRole.ADMIN) + public ResponseEntity> restoreEvent( + @PathVariable Long eventId, + @RequestParam EventStatusEnum status) { + log.info(">> 이벤트 복구 요청: eventId={}, status={}", eventId, status); + + try { + EventAdminDto event = adminService.restoreEvent(eventId, status); + log.info(">> 이벤트 복구 성공: eventId={}, status={}", eventId, status); + return ResponseEntity.ok(RsData.success("이벤트가 성공적으로 복구되었습니다.", event)); + } catch (IllegalAccessException e) { + log.error(">> 이벤트 복구 실패: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(RsData.error("400-BAD_REQUEST", e.getMessage())); + } catch (Exception e) { + log.error(">> 이벤트 복구 중 오류 발생: {}", e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(RsData.error("500-INTERNAL_SERVER_ERROR", "이벤트 복구 중 오류가 발생했습니다.")); + } + } + + /** + * 이벤트 목록을 JSON으로 반환하는 API + */ + @GetMapping(value = "/api/events", produces = "application/json") + @ResponseBody + public Map getEventListJson() { + List events = adminService.getAllEvents(); + Map result = new HashMap<>(); + result.put("events", events); + return result; + } + + /** + * 모니터링(Grafana) 페이지 + */ + @GetMapping("/monitoring") + @RoleRequired(UserRole.ADMIN) + public String monitoringPage(Model model) { + model.addAttribute("currentPage", "monitoring"); + return "admin/monitoring"; + } +} \ No newline at end of file diff --git a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/dto/response/EventAdminDto.java b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/dto/response/EventAdminDto.java new file mode 100644 index 00000000..bf62beae --- /dev/null +++ b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/dto/response/EventAdminDto.java @@ -0,0 +1,42 @@ +package org.codeNbug.mainserver.domain.admin.dto.response; + +import java.time.LocalDateTime; + +import org.codeNbug.mainserver.domain.event.entity.Event; +import org.codeNbug.mainserver.domain.event.entity.EventCategoryEnum; +import org.codeNbug.mainserver.domain.event.entity.EventInformation; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class EventAdminDto { + private Long eventId; + private String title; + private String category; + private LocalDateTime eventStart; + private LocalDateTime eventEnd; + private Integer seatCount; // 총 티켓 수 + private Integer soldTickets; // 판매된 티켓 수 + private String status; + + public static EventAdminDto fromEntity(Event event, int soldTickets) { + EventInformation information = event.getInformation(); + + return EventAdminDto.builder() + .eventId(event.getEventId()) + .title(information.getTitle()) + .category(event.getCategory().name()) + .eventStart(information.getEventStart()) + .eventEnd(information.getEventEnd()) + .seatCount(information.getSeatCount()) + .soldTickets(soldTickets) + .status(event.getStatus().name()) + .build(); + } +} \ No newline at end of file diff --git a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/dto/response/TicketAdminDto.java b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/dto/response/TicketAdminDto.java new file mode 100644 index 00000000..26679982 --- /dev/null +++ b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/dto/response/TicketAdminDto.java @@ -0,0 +1,27 @@ +package org.codeNbug.mainserver.domain.admin.dto.response; + +import java.time.LocalDateTime; + +import org.codeNbug.mainserver.domain.purchase.entity.PaymentStatusEnum; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TicketAdminDto { + private Long ticketId; + private Long eventId; + private String eventTitle; + private String userName; + private String userEmail; + private String phoneNumber; + private String seatInfo; + private PaymentStatusEnum paymentStatus; + private Integer amount; + private LocalDateTime purchaseDate; +} \ No newline at end of file diff --git a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/service/AdminService.java b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/service/AdminService.java index da9b0e7f..f8447d85 100644 --- a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/service/AdminService.java +++ b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/admin/service/AdminService.java @@ -5,10 +5,18 @@ import org.codeNbug.mainserver.domain.admin.dto.response.AdminLoginResponse; import org.codeNbug.mainserver.domain.admin.dto.response.AdminSignupResponse; import org.codeNbug.mainserver.domain.admin.dto.response.DashboardStatsResponse; +import org.codeNbug.mainserver.domain.admin.dto.response.EventAdminDto; import org.codeNbug.mainserver.domain.admin.dto.response.ModifyRoleResponse; +import org.codeNbug.mainserver.domain.admin.dto.response.TicketAdminDto; import org.codeNbug.mainserver.domain.event.entity.Event; +import org.codeNbug.mainserver.domain.event.entity.EventStatusEnum; import org.codeNbug.mainserver.domain.manager.repository.EventRepository; +import org.codeNbug.mainserver.domain.notification.entity.NotificationEnum; +import org.codeNbug.mainserver.domain.notification.service.NotificationService; +import org.codeNbug.mainserver.domain.purchase.entity.Purchase; +import org.codeNbug.mainserver.domain.purchase.repository.PurchaseRepository; import org.codeNbug.mainserver.domain.ticket.repository.TicketRepository; +import org.codeNbug.mainserver.global.exception.globalException.BadRequestException; import org.codeNbug.mainserver.global.exception.globalException.DuplicateEmailException; import org.codenbug.user.domain.user.entity.User; import org.codenbug.user.domain.user.repository.UserRepository; @@ -23,9 +31,11 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * 관리자 관련 서비스 @@ -41,6 +51,8 @@ public class AdminService { private final TicketRepository ticketRepository; private final PasswordEncoder passwordEncoder; private final TokenService tokenService; + private final NotificationService notificationService; + private final PurchaseRepository purchaseRepository; /** * 관리자 회원가입 서비스 @@ -314,4 +326,261 @@ else if ("sns".equals(userType)) { log.error(">> 유효하지 않은 사용자 타입: {}", userType); throw new IllegalArgumentException("유효하지 않은 사용자 타입입니다: " + userType); } + + /** + * 모든 이벤트 목록을 조회하고 각 이벤트의 티켓 정보를 포함하여 반환합니다. + * + * @return 이벤트 관리자 DTO 목록 + */ + @Transactional(readOnly = true) + public List getAllEvents() { + log.info(">> 모든 이벤트 목록 조회"); + + try { + // 삭제되지 않은 이벤트만 조회 + List events = eventRepository.findAllByIsDeletedFalse(); + log.debug(">> 이벤트 목록 조회 완료: {} 개", events.size()); + + // 각 이벤트에 대한 정보와 티켓 정보를 포함한 DTO 생성 + List eventDtos = events.stream() + .map(event -> { + // 해당 이벤트의 판매된 티켓 수 조회 + int soldTickets = ticketRepository.countPaidTicketsByEventId(event.getEventId()); + log.debug(">> 이벤트 ID={}, 판매된 티켓 수={}", event.getEventId(), soldTickets); + + // DTO 생성 + return EventAdminDto.fromEntity(event, soldTickets); + }) + .collect(Collectors.toList()); + + log.info(">> 이벤트 목록 조회 완료: {} 개의 이벤트", eventDtos.size()); + + return eventDtos; + } catch (Exception e) { + log.error(">> 이벤트 목록 조회 중 오류: {}", e.getMessage(), e); + throw new RuntimeException("이벤트 목록 조회 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 모든 티켓 목록을 조회합니다. + * + * @return 티켓 관리자 DTO 목록 + */ + @Transactional(readOnly = true) + public List getAllTickets() { + log.info(">> 모든 티켓 목록 조회"); + + try { + // 모든 티켓 조회 + List tickets = ticketRepository.findAllTicketsForAdmin(); + log.debug(">> 티켓 목록 조회 완료: {} 개", tickets.size()); + + log.info(">> 티켓 목록 조회 완료: {} 개의 티켓", tickets.size()); + + return tickets; + } catch (Exception e) { + log.error(">> 티켓 목록 조회 중 오류: {}", e.getMessage(), e); + throw new RuntimeException("티켓 목록 조회 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 특정 이벤트 정보를 조회합니다. + * + * @param eventId 조회할 이벤트 ID + * @return 이벤트 관리자 DTO + * @throws RuntimeException 이벤트를 찾을 수 없거나 오류 발생 시 + */ + @Transactional(readOnly = true) + public EventAdminDto getEvent(Long eventId) { + log.info(">> 이벤트 상세 정보 조회: id={}", eventId); + + try { + // 이벤트 조회 + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> new RuntimeException("해당 이벤트를 찾을 수 없습니다: " + eventId)); + + // 해당 이벤트의 판매된 티켓 수 조회 + int soldTickets = ticketRepository.countPaidTicketsByEventId(eventId); + log.debug(">> 이벤트 ID={}, 판매된 티켓 수={}", event.getEventId(), soldTickets); + + // DTO 생성 + EventAdminDto eventDto = EventAdminDto.fromEntity(event, soldTickets); + + log.info(">> 이벤트 상세 정보 조회 완료: id={}, 제목={}", eventId, eventDto.getTitle()); + + return eventDto; + } catch (Exception e) { + log.error(">> 이벤트 상세 정보 조회 중 오류: {}", e.getMessage(), e); + throw new RuntimeException("이벤트 상세 정보 조회 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 삭제 대기 중인 이벤트 목록을 조회합니다. + * + * @return 삭제 대기 중인 이벤트 목록 + */ + @Transactional(readOnly = true) + public List getDeletedEvents() { + log.info(">> 삭제 대기 중인 이벤트 목록 조회"); + + try { + // 삭제된 이벤트 조회 + List events = eventRepository.findAllByIsDeletedTrue(); + log.debug(">> 삭제된 이벤트 목록 조회 완료: {} 개", events.size()); + + // 각 이벤트에 대한 정보와 티켓 정보를 포함한 DTO 생성 + List eventDtos = events.stream() + .map(event -> { + // 해당 이벤트의 판매된 티켓 수 조회 + int soldTickets = ticketRepository.countPaidTicketsByEventId(event.getEventId()); + log.debug(">> 이벤트 ID={}, 판매된 티켓 수={}", event.getEventId(), soldTickets); + + // DTO 생성 + return EventAdminDto.fromEntity(event, soldTickets); + }) + .collect(Collectors.toList()); + + log.info(">> 삭제된 이벤트 목록 조회 완료: {} 개의 이벤트", eventDtos.size()); + + return eventDtos; + } catch (Exception e) { + log.error(">> 삭제된 이벤트 목록 조회 중 오류: {}", e.getMessage(), e); + throw new RuntimeException("삭제된 이벤트 목록 조회 중 오류가 발생했습니다: " + e.getMessage()); + } + } + + /** + * 삭제된 이벤트를 복구합니다. + * + * @param eventId 복구할 이벤트 ID + * @param status 복구 후 설정할 이벤트 상태 + * @return 복구된 이벤트 정보 + * @throws IllegalAccessException 이벤트가 삭제되지 않은 경우 + */ + @Transactional + public EventAdminDto restoreEvent(Long eventId, EventStatusEnum status) throws IllegalAccessException { + log.info(">> 이벤트 복구 처리 시작: eventId={}, status={}", eventId, status); + + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> { + log.error(">> 이벤트 복구 실패: 존재하지 않는 이벤트 - eventId={}", eventId); + return new BadRequestException("존재하지 않는 이벤트입니다."); + }); + + // 이벤트가 삭제되지 않았다면 400에러 전송 + if (!event.getIsDeleted()) { + log.warn(">> 이벤트 복구 실패: 삭제되지 않은 이벤트 - eventId={}", eventId); + throw new IllegalAccessException("삭제되지 않은 이벤트입니다."); + } + + // 이벤트 상태 변경 + event.setIsDeleted(false); + event.setStatus(status); + log.info(">> 이벤트 상태 변경 완료: eventId={}, status={}", eventId, status); + + // 알림 처리는 메인 로직과 분리하여 예외 처리 + try { + // 해당 이벤트 구매자들 조회 + List purchases = purchaseRepository.findAllByEventId(eventId); + log.debug(">> 이벤트 구매자 조회 완료: eventId={}, 구매자 수={}", eventId, purchases.size()); + + // 모든 구매자에게 행사 복구 알림 전송 + String notificationContent = String.format( + "[%s] 행사가 복구되었습니다. 예매 내역을 확인해주세요.", + event.getInformation().getTitle() + ); + + for (Purchase purchase : purchases) { + try { + Long userId = purchase.getUser().getUserId(); + notificationService.createNotification( + userId, + NotificationEnum.EVENT, + notificationContent + ); + log.debug(">> 알림 전송 완료: userId={}, eventId={}", userId, eventId); + } catch (Exception e) { + log.error(">> 행사 복구 알림 전송 실패. 사용자ID: {}, 구매ID: {}, 오류: {}", + purchase.getUser().getUserId(), purchase.getId(), e.getMessage(), e); + // 개별 사용자 알림 실패는 다른 사용자 알림이나 이벤트 복구에 영향을 주지 않음 + } + } + } catch (Exception e) { + log.error(">> 행사 복구 알림 처리 실패. 이벤트ID: {}, 오류: {}", eventId, e.getMessage(), e); + // 알림 전체 실패는 이벤트 복구에 영향을 주지 않도록 예외를 무시함 + } + + // 복구된 이벤트 정보 반환 + int soldTickets = ticketRepository.countPaidTicketsByEventId(eventId); + EventAdminDto eventDto = EventAdminDto.fromEntity(event, soldTickets); + + log.info(">> 이벤트 복구 처리 완료: eventId={}", eventId); + return eventDto; + } + + /** + * 이벤트를 삭제 처리하는 메서드입니다. + * 관리자 권한으로 이벤트를 삭제하고, 관련된 구매자들에게 알림을 전송합니다. + * + * @param eventId 삭제할 이벤트 ID + * @throws IllegalAccessException 이미 삭제된 이벤트인 경우 + */ + @Transactional + public void deleteEvent(Long eventId) throws IllegalAccessException { + log.info(">> 이벤트 삭제 처리 시작: eventId={}", eventId); + + Event event = eventRepository.findById(eventId) + .orElseThrow(() -> { + log.error(">> 이벤트 삭제 실패: 존재하지 않는 이벤트 - eventId={}", eventId); + return new BadRequestException("존재하지 않는 이벤트입니다."); + }); + + // 이벤트가 이미 삭제되었다면 400에러 전송 + if (event.getIsDeleted()) { + log.warn(">> 이벤트 삭제 실패: 이미 삭제된 이벤트 - eventId={}", eventId); + throw new IllegalAccessException("이미 삭제된 이벤트입니다."); + } + + // 이벤트 상태 변경 + event.setIsDeleted(true); + event.setStatus(EventStatusEnum.CANCELLED); + log.info(">> 이벤트 상태 변경 완료: eventId={}, status=CANCELLED", eventId); + + // 알림 처리는 메인 로직과 분리하여 예외 처리 + try { + // 해당 이벤트 구매자들 조회 + List purchases = purchaseRepository.findAllByEventId(eventId); + log.debug(">> 이벤트 구매자 조회 완료: eventId={}, 구매자 수={}", eventId, purchases.size()); + + // 모든 구매자에게 행사 취소 알림 전송 + String notificationContent = String.format( + "[%s] 행사가 취소되었습니다. 예매 내역을 확인해주세요.", + event.getInformation().getTitle() + ); + + for (Purchase purchase : purchases) { + try { + Long userId = purchase.getUser().getUserId(); + notificationService.createNotification( + userId, + NotificationEnum.EVENT, + notificationContent + ); + log.debug(">> 알림 전송 완료: userId={}, eventId={}", userId, eventId); + } catch (Exception e) { + log.error(">> 행사 취소 알림 전송 실패. 사용자ID: {}, 구매ID: {}, 오류: {}", + purchase.getUser().getUserId(), purchase.getId(), e.getMessage(), e); + // 개별 사용자 알림 실패는 다른 사용자 알림이나 이벤트 취소에 영향을 주지 않음 + } + } + } catch (Exception e) { + log.error(">> 행사 취소 알림 처리 실패. 이벤트ID: {}, 오류: {}", eventId, e.getMessage(), e); + // 알림 전체 실패는 이벤트 취소에 영향을 주지 않도록 예외를 무시함 + } + + log.info(">> 이벤트 삭제 처리 완료: eventId={}", eventId); + } } \ No newline at end of file diff --git a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/manager/repository/EventRepository.java b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/manager/repository/EventRepository.java index 14ef2c33..3704ebaa 100644 --- a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/manager/repository/EventRepository.java +++ b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/manager/repository/EventRepository.java @@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface EventRepository extends JpaRepository { @@ -14,4 +15,17 @@ public interface EventRepository extends JpaRepository { "WHERE e.eventId = :eventId") Optional findByIdWithSeats(@Param("eventId") Long eventId); + /** + * 삭제된 모든 이벤트를 조회합니다. + * + * @return 삭제된 이벤트 목록 + */ + List findAllByIsDeletedTrue(); + + /** + * 삭제되지 않은 모든 이벤트를 조회합니다. + * + * @return 삭제되지 않은 이벤트 목록 + */ + List findAllByIsDeletedFalse(); } diff --git a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/ticket/repository/TicketRepository.java b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/ticket/repository/TicketRepository.java index 94d88fe7..0dcba10d 100644 --- a/service/main-server/src/main/java/org/codeNbug/mainserver/domain/ticket/repository/TicketRepository.java +++ b/service/main-server/src/main/java/org/codeNbug/mainserver/domain/ticket/repository/TicketRepository.java @@ -2,6 +2,7 @@ import java.util.List; +import org.codeNbug.mainserver.domain.admin.dto.response.TicketAdminDto; import org.codeNbug.mainserver.domain.manager.dto.TicketDto; import org.codeNbug.mainserver.domain.ticket.entity.Ticket; import org.springframework.data.jpa.repository.JpaRepository; @@ -23,4 +24,23 @@ public interface TicketRepository extends JpaRepository { List findAllByPurchaseId(Long purchaseId); + + @Query("SELECT COUNT(t) FROM Ticket t WHERE t.event.eventId = :eventId") + int countByEventId(@Param("eventId") Long eventId); + + @Query("SELECT COUNT(t) FROM Ticket t JOIN t.purchase p WHERE t.event.eventId = :eventId AND p.paymentStatus = org.codeNbug.mainserver.domain.purchase.entity.PaymentStatusEnum.DONE") + int countPaidTicketsByEventId(@Param("eventId") Long eventId); + + @Query(""" + SELECT new org.codeNbug.mainserver.domain.admin.dto.response.TicketAdminDto( + t.id, e.eventId, e.information.title, u.name, u.email, u.phoneNum, + t.seatInfo, p.paymentStatus, p.amount, p.purchaseDate + ) + FROM Ticket t + JOIN t.event e + JOIN t.purchase p + JOIN p.user u + ORDER BY p.purchaseDate DESC +""") + List findAllTicketsForAdmin(); } diff --git a/service/main-server/src/main/resources/templates/admin/event-detail.html b/service/main-server/src/main/resources/templates/admin/event-detail.html new file mode 100644 index 00000000..62b5d311 --- /dev/null +++ b/service/main-server/src/main/resources/templates/admin/event-detail.html @@ -0,0 +1,213 @@ + + + + + + +
+ + +
+
+

이벤트 상세 정보

+ +
+ + + + + +
+
+
기본 정보
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
이벤트 ID1
제목이벤트 제목
카테고리카테고리
이벤트 기간 + 2023-01-01 12:00 ~ + 2023-01-02 12:00 +
상태 + 상태 +
+
+
+
+
+
티켓 현황
+
+ 총 티켓 수: + 100 +
+
+ 판매된 티켓: + 50 +
+
+ 남은 티켓: + 50 +
+
+
+ 50% +
+
+
+
+
+
+
+
+ + +
+
+
티켓 목록
+ 0 개 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
티켓 ID구매자연락처좌석 정보결제 상태결제 금액구매 일시작업
+ 등록된 티켓이 없습니다. +
1 + 사용자 이름 +
+ 이메일 +
010-1234-5678A-1 + 결제 상태 + 10,000원2023-01-01 12:30 +
+ + + + +
+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/service/main-server/src/main/resources/templates/admin/events.html b/service/main-server/src/main/resources/templates/admin/events.html index 3278c018..fd8b4eb3 100644 --- a/service/main-server/src/main/resources/templates/admin/events.html +++ b/service/main-server/src/main/resources/templates/admin/events.html @@ -12,6 +12,16 @@

이벤트 관리

+ + + -
+
이벤트 목록
@@ -40,37 +50,40 @@
이벤트 목록
작업 - - - - 등록된 이벤트가 없습니다. - - - - 1 - 이벤트 제목 - 카테고리 - 시작일 - 종료일 - 총 티켓 - 판매된 티켓 - 남은 티켓 - 상태 - -
- - - - - - - -
- + + + + +
+
+ + + +
+
+
삭제된 이벤트
+ +
+
+
+ + + + + + + + + + + + + + + + +
ID제목카테고리시작일종료일총 티켓판매된 티켓남은 티켓상태작업
@@ -80,17 +93,103 @@
이벤트 목록
\ No newline at end of file diff --git a/service/main-server/src/main/resources/templates/admin/fragments/layout.html b/service/main-server/src/main/resources/templates/admin/fragments/layout.html index 178a5bfc..8d781caa 100644 --- a/service/main-server/src/main/resources/templates/admin/fragments/layout.html +++ b/service/main-server/src/main/resources/templates/admin/fragments/layout.html @@ -117,6 +117,11 @@ 설정 + 로그아웃 diff --git a/service/main-server/src/main/resources/templates/admin/monitoring.html b/service/main-server/src/main/resources/templates/admin/monitoring.html new file mode 100644 index 00000000..41234429 --- /dev/null +++ b/service/main-server/src/main/resources/templates/admin/monitoring.html @@ -0,0 +1,21 @@ + + + + +
+
+
+

모니터링 (Grafana)

+
+
+
+ +
+ ※ 위 화면이 보이지 않으면 Grafana 서버가 실행 중인지, 네트워크/권한 설정을 확인하세요. +
+
+
+
+
+ + \ No newline at end of file diff --git a/service/main-server/src/main/resources/templates/admin/tickets.html b/service/main-server/src/main/resources/templates/admin/tickets.html index 43fc6980..8a1015c0 100644 --- a/service/main-server/src/main/resources/templates/admin/tickets.html +++ b/service/main-server/src/main/resources/templates/admin/tickets.html @@ -18,6 +18,36 @@

티켓 관리

오류 메시지
+ +
+
+
필터링
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+
@@ -30,34 +60,48 @@
티켓 목록
티켓 ID 이벤트 - 구매자 + 사용자 + 연락처 좌석 정보 - 구매일 - 상태 + 결제 상태 + 결제 금액 + 구매 일시 작업 - + 등록된 티켓이 없습니다. - 1 - 이벤트 제목 - 구매자 - 좌석 정보 - 구매일 - 상태 + 1 + +
이벤트 제목 + + + 사용자 이름 +
+ 이메일 + + 010-1234-5678 + A-1 + + 결제 상태 + + 10,000원 + 2023-01-01 12:30
- +
@@ -71,24 +115,27 @@
티켓 목록