diff --git a/.github/workflows/cd-develop.yml b/.github/workflows/cd-develop.yml index e7867d16..e3ba4f71 100644 --- a/.github/workflows/cd-develop.yml +++ b/.github/workflows/cd-develop.yml @@ -2,7 +2,7 @@ name: CI/CD on: # push: -# branches: [ "develop" ] +# branches: [ "feature/seungmin" ] pull_request: branches: [ "main" ] diff --git a/build.gradle b/build.gradle index eb48ecc6..d42df073 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0' implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'io.jsonwebtoken:jjwt-api:0.11.5' diff --git a/src/main/java/com/wayble/server/admin/controller/AdminController.java b/src/main/java/com/wayble/server/admin/controller/AdminController.java new file mode 100644 index 00000000..0548850f --- /dev/null +++ b/src/main/java/com/wayble/server/admin/controller/AdminController.java @@ -0,0 +1,96 @@ +package com.wayble.server.admin.controller; + +import com.wayble.server.admin.dto.SystemStatusDto; +import com.wayble.server.admin.service.AdminSystemService; +import com.wayble.server.admin.service.AdminUserService; +import com.wayble.server.admin.service.AdminWaybleZoneService; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Slf4j +@Controller +@RequestMapping("/admin") +@RequiredArgsConstructor +public class AdminController { + + private final AdminSystemService adminSystemService; + private final AdminUserService adminUserService; + private final AdminWaybleZoneService adminWaybleZoneService; + + @Value("${spring.admin.username}") + private String adminUsername; + + @Value("${spring.admin.password}") + private String adminPassword; + + @GetMapping("") + public String adminLoginPage(HttpSession session, Model model) { + // 이미 로그인된 경우 대시보드로 리다이렉트 + if (session.getAttribute("adminLoggedIn") != null) { + return "redirect:/admin/dashboard"; + } + return "admin/login"; + } + + @PostMapping("/login") + public String adminLogin( + @RequestParam String username, + @RequestParam String password, + HttpSession session, + Model model + ) { + if (adminUsername.equals(username) && adminPassword.equals(password)) { + session.setAttribute("adminLoggedIn", true); + session.setAttribute("adminUsername", username); + log.info("관리자 로그인 성공: {}", username); + return "redirect:/admin/dashboard"; + } else { + model.addAttribute("error", "잘못된 사용자명 또는 비밀번호입니다."); + log.warn("관리자 로그인 실패 시도: {}", username); + return "admin/login"; + } + } + + @GetMapping("/dashboard") + public String adminDashboard(HttpSession session, Model model) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + // 시스템 상태 조회 + SystemStatusDto systemStatus = SystemStatusDto.of( + adminSystemService.isApiServerHealthy(), + adminSystemService.isDatabaseHealthy(), + adminSystemService.isElasticsearchHealthy(), + adminSystemService.isFileStorageHealthy() + ); + + // 통계 데이터 조회 + long totalUserCount = adminUserService.getTotalUserCount(); + long totalDeletedUserCount = adminUserService.getTotalDeletedUserCount(); + long totalWaybleZoneCount = adminWaybleZoneService.getTotalWaybleZoneCounts(); + + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + model.addAttribute("systemStatus", systemStatus); + model.addAttribute("totalUserCount", totalUserCount); + model.addAttribute("totalDeletedUserCount", totalDeletedUserCount); + model.addAttribute("totalWaybleZoneCount", totalWaybleZoneCount); + return "admin/dashboard"; + } + + @PostMapping("/logout") + public String adminLogout(HttpSession session) { + session.invalidate(); + log.info("관리자 로그아웃"); + return "redirect:/admin"; + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/controller/user/AdminUserController.java b/src/main/java/com/wayble/server/admin/controller/user/AdminUserController.java new file mode 100644 index 00000000..302efbf6 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/controller/user/AdminUserController.java @@ -0,0 +1,57 @@ +package com.wayble.server.admin.controller.user; + +import com.wayble.server.admin.dto.user.AdminUserDetailDto; +import com.wayble.server.admin.dto.user.AdminUserThumbnailDto; +import com.wayble.server.admin.service.AdminUserService; +import com.wayble.server.common.response.CommonResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +@Validated +@RequestMapping("/api/v1/admin/users") +public class AdminUserController { + + private final AdminUserService adminUserService; + + @GetMapping("/count") + public CommonResponse getTotalUserCount() { + return CommonResponse.success(adminUserService.getTotalUserCount()); + } + + @GetMapping() + public CommonResponse> findByCondition( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "100") int size) + { + return CommonResponse.success(adminUserService.findUsersByPage(page, size)); + } + + @GetMapping("/{userId}") + public CommonResponse> findUserById(@PathVariable("userId") long userId) { + return CommonResponse.success(adminUserService.findUserById(userId)); + } + + @GetMapping("/deleted/count") + public CommonResponse getTotalDeletedUserCount() { + return CommonResponse.success(adminUserService.getTotalDeletedUserCount()); + } + + @GetMapping("/deleted") + public CommonResponse> findDeletedUsers( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "100") int size) + { + return CommonResponse.success(adminUserService.findDeletedUsersByPage(page, size)); + } + + @GetMapping("/deleted/{userId}") + public CommonResponse> findDeletedUserById(@PathVariable("userId") long userId) { + return CommonResponse.success(adminUserService.findDeletedUserById(userId)); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/controller/user/AdminUserViewController.java b/src/main/java/com/wayble/server/admin/controller/user/AdminUserViewController.java new file mode 100644 index 00000000..687f5cd9 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/controller/user/AdminUserViewController.java @@ -0,0 +1,124 @@ +package com.wayble.server.admin.controller.user; + +import com.wayble.server.admin.dto.user.AdminUserDetailDto; +import com.wayble.server.admin.dto.user.AdminUserPageDto; +import com.wayble.server.admin.service.AdminUserService; +import jakarta.servlet.http.HttpSession; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.util.Optional; + +@Slf4j +@Controller +@RequestMapping("/admin/users") +@RequiredArgsConstructor +public class AdminUserViewController { + + private final AdminUserService adminUserService; + + @GetMapping + public String getUsers(HttpSession session, Model model, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "100") int size) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + // 페이징 데이터 조회 + AdminUserPageDto pageData = adminUserService.getUsersWithPaging(page, size); + + model.addAttribute("pageData", pageData); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + log.debug("사용자 목록 조회 - 페이지: {}, 전체: {}", page, pageData.totalElements()); + + return "admin/user/users"; + } + + @GetMapping("/{id}") + public String getUserDetail(HttpSession session, Model model, @PathVariable Long id) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + Optional userOpt = adminUserService.findUserById(id); + if (userOpt.isEmpty()) { + return "redirect:/admin/user/users?error=notfound"; + } + + model.addAttribute("user", userOpt.get()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + log.debug("사용자 상세 조회 - ID: {}", id); + + return "admin/user/user-detail"; + } + + @GetMapping("/deleted") + public String getDeletedUsers(HttpSession session, Model model, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "100") int size) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + // 페이징 데이터 조회 + AdminUserPageDto pageData = adminUserService.getDeletedUsersWithPaging(page, size); + + model.addAttribute("pageData", pageData); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + log.debug("삭제된 사용자 목록 조회 - 페이지: {}, 전체: {}", page, pageData.totalElements()); + + return "admin/user/deleted-users"; + } + + @GetMapping("/deleted/{id}") + public String getDeletedUserDetail(HttpSession session, Model model, @PathVariable Long id) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + Optional userOpt = adminUserService.findDeletedUserById(id); + if (userOpt.isEmpty()) { + return "redirect:/admin/users/deleted?error=notfound"; + } + + model.addAttribute("user", userOpt.get()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + log.debug("삭제된 사용자 상세 조회 - ID: {}", id); + + return "admin/user/deleted-user-detail"; + } + + @PostMapping("/deleted/{id}/restore") + public String restoreUser(HttpSession session, + @PathVariable Long id, + RedirectAttributes redirectAttributes) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + try { + adminUserService.restoreUser(id); + redirectAttributes.addFlashAttribute("successMessage", "사용자가 성공적으로 복원되었습니다."); + log.info("사용자 복원 완료 - ID: {}, 관리자: {}", id, session.getAttribute("adminUsername")); + return "redirect:/admin/users/" + id; + } catch (Exception e) { + log.error("사용자 복원 실패 - ID: {}", id, e); + redirectAttributes.addFlashAttribute("errorMessage", "사용자 복원에 실패했습니다: " + e.getMessage()); + return "redirect:/admin/users/deleted/" + id; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/controller/wayblezone/AdminWaybleZoneController.java b/src/main/java/com/wayble/server/admin/controller/wayblezone/AdminWaybleZoneController.java new file mode 100644 index 00000000..46ef540e --- /dev/null +++ b/src/main/java/com/wayble/server/admin/controller/wayblezone/AdminWaybleZoneController.java @@ -0,0 +1,73 @@ +package com.wayble.server.admin.controller.wayblezone; + +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneCreateDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneDetailDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneThumbnailDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneUpdateDto; +import com.wayble.server.admin.service.AdminWaybleZoneService; +import com.wayble.server.common.response.CommonResponse; + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; + +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Optional; + +@RestController +@RequiredArgsConstructor +@Validated +@RequestMapping("/api/v1/admin/wayble-zones") +public class AdminWaybleZoneController { + + private final AdminWaybleZoneService adminWaybleZoneService; + + @GetMapping() + public CommonResponse> findByCondition( + @RequestParam(name = "page", defaultValue = "0") int page, + @RequestParam(name = "size", defaultValue = "20") int size) + { + return CommonResponse.success(adminWaybleZoneService.findWaybleZonesByPage(page, size)); + } + + @GetMapping("/count") + public CommonResponse findAllWaybleZoneCount() { + return CommonResponse.success(adminWaybleZoneService.getTotalWaybleZoneCounts()); + } + + @GetMapping("/{waybleZoneId}") + public CommonResponse> findWaybleZoneById(@PathVariable("waybleZoneId") long waybleZoneId) { + return CommonResponse.success(adminWaybleZoneService.findWaybleZoneById(waybleZoneId)); + } + + @PostMapping + public CommonResponse createWaybleZone(@Valid @RequestBody AdminWaybleZoneCreateDto createDto) { + Long waybleZoneId = adminWaybleZoneService.createWaybleZone(createDto); + return CommonResponse.success(waybleZoneId); + } + + @PutMapping("/{waybleZoneId}") + public CommonResponse updateWaybleZone(@PathVariable("waybleZoneId") Long waybleZoneId, + @Valid @RequestBody AdminWaybleZoneUpdateDto updateDto) { + // DTO의 ID와 URL의 ID가 일치하는지 확인 + AdminWaybleZoneUpdateDto validatedDto = new AdminWaybleZoneUpdateDto( + waybleZoneId, + updateDto.zoneName(), + updateDto.contactNumber(), + updateDto.zoneType(), + updateDto.state(), + updateDto.city(), + updateDto.district(), + updateDto.streetAddress(), + updateDto.detailAddress(), + updateDto.latitude(), + updateDto.longitude(), + updateDto.mainImageUrl() + ); + + Long updatedWaybleZoneId = adminWaybleZoneService.updateWaybleZone(validatedDto); + return CommonResponse.success(updatedWaybleZoneId); + } +} diff --git a/src/main/java/com/wayble/server/admin/controller/wayblezone/AdminWaybleZoneViewController.java b/src/main/java/com/wayble/server/admin/controller/wayblezone/AdminWaybleZoneViewController.java new file mode 100644 index 00000000..ce88238b --- /dev/null +++ b/src/main/java/com/wayble/server/admin/controller/wayblezone/AdminWaybleZoneViewController.java @@ -0,0 +1,204 @@ +package com.wayble.server.admin.controller.wayblezone; + +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneCreateDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneDetailDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZonePageDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneUpdateDto; +import com.wayble.server.admin.service.AdminWaybleZoneService; +import com.wayble.server.wayblezone.entity.WaybleZoneType; +import jakarta.servlet.http.HttpSession; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.util.Optional; + +@Slf4j +@Controller +@RequestMapping("/admin/wayble-zones") +@RequiredArgsConstructor +public class AdminWaybleZoneViewController { + + private final AdminWaybleZoneService adminWaybleZoneService; + + @GetMapping + public String getWaybleZones(HttpSession session, Model model, + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "100") int size) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + // 페이징 데이터 조회 + AdminWaybleZonePageDto pageData = adminWaybleZoneService.getWaybleZonesWithPaging(page, size); + + model.addAttribute("pageData", pageData); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + log.debug("웨이블존 목록 조회 - 페이지: {}, 전체: {}", page, pageData.totalElements()); + + return "admin/wayblezone/wayble-zones"; + } + + @GetMapping("/{id}") + public String getWaybleZoneDetail(HttpSession session, Model model, @PathVariable Long id) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + Optional waybleZoneOpt = adminWaybleZoneService.findWaybleZoneById(id); + if (waybleZoneOpt.isEmpty()) { + return "redirect:/admin/wayblezone/wayble-zones?error=notfound"; + } + + model.addAttribute("waybleZone", waybleZoneOpt.get()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + log.debug("웨이블존 상세 조회 - ID: {}", id); + + return "admin/wayblezone/wayble-zone-detail"; + } + + @GetMapping("/create") + public String createWaybleZoneForm(HttpSession session, Model model) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + model.addAttribute("createDto", new AdminWaybleZoneCreateDto( + "", "", null, "", "", "", "", "", null, null, "" + )); + model.addAttribute("waybleZoneTypes", WaybleZoneType.values()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + return "admin/wayblezone/wayble-zone-create"; + } + + @PostMapping("/create") + public String createWaybleZone(HttpSession session, + @Valid @ModelAttribute("createDto") AdminWaybleZoneCreateDto createDto, + BindingResult bindingResult, + Model model, + RedirectAttributes redirectAttributes) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + if (bindingResult.hasErrors()) { + model.addAttribute("waybleZoneTypes", WaybleZoneType.values()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + return "admin/wayblezone/wayble-zone-create"; + } + + try { + Long waybleZoneId = adminWaybleZoneService.createWaybleZone(createDto); + redirectAttributes.addFlashAttribute("successMessage", "웨이블존이 성공적으로 생성되었습니다."); + return "redirect:/admin/wayble-zones/" + waybleZoneId; + } catch (Exception e) { + log.error("웨이블존 생성 실패", e); + model.addAttribute("errorMessage", "웨이블존 생성에 실패했습니다: " + e.getMessage()); + model.addAttribute("waybleZoneTypes", WaybleZoneType.values()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + return "admin/wayblezone/wayble-zone-create"; + } + } + + @GetMapping("/{id}/edit") + public String editWaybleZoneForm(HttpSession session, Model model, @PathVariable Long id) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + Optional waybleZoneOpt = adminWaybleZoneService.findWaybleZoneById(id); + if (waybleZoneOpt.isEmpty()) { + return "redirect:/admin/wayble-zones?error=notfound"; + } + + AdminWaybleZoneDetailDto waybleZone = waybleZoneOpt.get(); + AdminWaybleZoneUpdateDto updateDto = AdminWaybleZoneUpdateDto.fromDetailDto(waybleZone); + + model.addAttribute("updateDto", updateDto); + model.addAttribute("waybleZoneTypes", WaybleZoneType.values()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + + return "admin/wayblezone/wayble-zone-edit"; + } + + @PostMapping("/{id}/edit") + public String updateWaybleZone(HttpSession session, + @PathVariable Long id, + @Valid @ModelAttribute("updateDto") AdminWaybleZoneUpdateDto updateDto, + BindingResult bindingResult, + Model model, + RedirectAttributes redirectAttributes) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + if (bindingResult.hasErrors()) { + model.addAttribute("waybleZoneTypes", WaybleZoneType.values()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + return "admin/wayblezone/wayble-zone-edit"; + } + + // DTO의 ID와 URL의 ID 일치 확인 + AdminWaybleZoneUpdateDto validatedDto = new AdminWaybleZoneUpdateDto( + id, + updateDto.zoneName(), + updateDto.contactNumber(), + updateDto.zoneType(), + updateDto.state(), + updateDto.city(), + updateDto.district(), + updateDto.streetAddress(), + updateDto.detailAddress(), + updateDto.latitude(), + updateDto.longitude(), + updateDto.mainImageUrl() + ); + + try { + Long waybleZoneId = adminWaybleZoneService.updateWaybleZone(validatedDto); + redirectAttributes.addFlashAttribute("successMessage", "웨이블존이 성공적으로 수정되었습니다."); + return "redirect:/admin/wayble-zones/" + waybleZoneId; + } catch (Exception e) { + log.error("웨이블존 수정 실패", e); + model.addAttribute("errorMessage", "웨이블존 수정에 실패했습니다: " + e.getMessage()); + model.addAttribute("waybleZoneTypes", WaybleZoneType.values()); + model.addAttribute("adminUsername", session.getAttribute("adminUsername")); + return "admin/wayblezone/wayble-zone-edit"; + } + } + + @PostMapping("/{id}/delete") + public String deleteWaybleZone(HttpSession session, + @PathVariable Long id, + RedirectAttributes redirectAttributes) { + // 로그인 확인 + if (session.getAttribute("adminLoggedIn") == null) { + return "redirect:/admin"; + } + + try { + adminWaybleZoneService.deleteWaybleZone(id); + redirectAttributes.addFlashAttribute("successMessage", "웨이블존이 성공적으로 삭제되었습니다."); + log.info("웨이블존 삭제 완료 - ID: {}, 관리자: {}", id, session.getAttribute("adminUsername")); + return "redirect:/admin/wayble-zones"; + } catch (Exception e) { + log.error("웨이블존 삭제 실패 - ID: {}", id, e); + redirectAttributes.addFlashAttribute("errorMessage", "웨이블존 삭제에 실패했습니다: " + e.getMessage()); + return "redirect:/admin/wayble-zones/" + id; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/SystemStatusDto.java b/src/main/java/com/wayble/server/admin/dto/SystemStatusDto.java new file mode 100644 index 00000000..eb310243 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/SystemStatusDto.java @@ -0,0 +1,13 @@ +package com.wayble.server.admin.dto; + +public record SystemStatusDto( + boolean apiServerStatus, + boolean databaseStatus, + boolean elasticsearchStatus, + boolean fileStorageStatus +) { + + public static SystemStatusDto of(boolean apiServer, boolean database, boolean elasticsearch, boolean fileStorage) { + return new SystemStatusDto(apiServer, database, elasticsearch, fileStorage); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/user/AdminUserDetailDto.java b/src/main/java/com/wayble/server/admin/dto/user/AdminUserDetailDto.java new file mode 100644 index 00000000..d7034acc --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/user/AdminUserDetailDto.java @@ -0,0 +1,27 @@ +package com.wayble.server.admin.dto.user; + +import com.wayble.server.user.entity.Gender; +import com.wayble.server.user.entity.LoginType; +import com.wayble.server.user.entity.UserType; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +public record AdminUserDetailDto( + Long id, + String nickname, + String username, + String email, + LocalDate birthDate, + Gender gender, + LoginType loginType, + UserType userType, + String profileImageUrl, + String disabilityType, + String mobilityAid, + LocalDateTime createdAt, + LocalDateTime updatedAt, + Long reviewCount, + Long userPlaceCount +) { +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/user/AdminUserPageDto.java b/src/main/java/com/wayble/server/admin/dto/user/AdminUserPageDto.java new file mode 100644 index 00000000..0df43e74 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/user/AdminUserPageDto.java @@ -0,0 +1,29 @@ +package com.wayble.server.admin.dto.user; + +import java.util.List; + +public record AdminUserPageDto( + List content, + int page, + int size, + long totalElements, + int totalPages, + boolean first, + boolean last, + boolean hasNext, + boolean hasPrevious +) { + + public static AdminUserPageDto of(List content, int page, int size, long totalElements) { + int totalPages = (int) Math.ceil((double) totalElements / size); + boolean first = page == 0; + boolean last = page >= totalPages - 1; + boolean hasNext = page < totalPages - 1; + boolean hasPrevious = page > 0; + + return new AdminUserPageDto( + content, page, size, totalElements, totalPages, + first, last, hasNext, hasPrevious + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/user/AdminUserThumbnailDto.java b/src/main/java/com/wayble/server/admin/dto/user/AdminUserThumbnailDto.java new file mode 100644 index 00000000..c88e2c16 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/user/AdminUserThumbnailDto.java @@ -0,0 +1,20 @@ +package com.wayble.server.admin.dto.user; + +import com.wayble.server.user.entity.Gender; +import com.wayble.server.user.entity.LoginType; +import com.wayble.server.user.entity.UserType; + +import java.time.LocalDate; + +public record AdminUserThumbnailDto( + Long id, + String nickname, + String email, + LocalDate birthDate, + Gender gender, + LoginType loginType, + UserType userType, + String disabilityType, + String mobilityAid +) { +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneCreateDto.java b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneCreateDto.java new file mode 100644 index 00000000..c37fce9a --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneCreateDto.java @@ -0,0 +1,39 @@ +package com.wayble.server.admin.dto.wayblezone; + +import com.wayble.server.wayblezone.entity.WaybleZoneType; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record AdminWaybleZoneCreateDto( + @NotBlank(message = "웨이블존 이름은 필수입니다") + String zoneName, + + String contactNumber, + + @NotNull(message = "웨이블존 타입은 필수입니다") + WaybleZoneType zoneType, + + @NotBlank(message = "시/도는 필수입니다") + String state, + + @NotBlank(message = "시/군/구는 필수입니다") + String city, + + String district, + String streetAddress, + String detailAddress, + + @NotNull(message = "위도는 필수입니다") + @DecimalMin(value = "-90.0", message = "위도는 -90.0 이상이어야 합니다") + @DecimalMax(value = "90.0", message = "위도는 90.0 이하여야 합니다") + Double latitude, + + @NotNull(message = "경도는 필수입니다") + @DecimalMin(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다") + @DecimalMax(value = "180.0", message = "경도는 180.0 이하여야 합니다") + Double longitude, + + String mainImageUrl +) { } \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneDetailDto.java b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneDetailDto.java new file mode 100644 index 00000000..44b3e807 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneDetailDto.java @@ -0,0 +1,59 @@ +package com.wayble.server.admin.dto.wayblezone; + +import com.wayble.server.common.entity.Address; +import com.wayble.server.wayblezone.entity.WaybleZoneFacility; +import com.wayble.server.wayblezone.entity.WaybleZoneType; + +import java.time.LocalDateTime; +import java.util.List; + +public record AdminWaybleZoneDetailDto( + Long zoneId, + String zoneName, + String contactNumber, + WaybleZoneType zoneType, + Address address, + String fullAddress, + Double latitude, + Double longitude, + double rating, + long reviewCount, + long likes, + String mainImageUrl, + LocalDateTime createdAt, + LocalDateTime lastModifiedAt, + LocalDateTime syncedAt, + + // 시설 정보 + FacilityInfo facility, + + // 운영시간 정보 + List operatingHours, + + // 이미지 목록 + List imageUrls, + + // 최근 리뷰 (최대 5개) + List recentReviews +) { + + public record FacilityInfo( + WaybleZoneFacility facilityType, + String description + ) {} + + public record OperatingHourInfo( + String dayOfWeek, + String openTime, + String closeTime, + boolean isHoliday + ) {} + + public record RecentReviewInfo( + Long reviewId, + String content, + double rating, + String userName, + LocalDateTime createdAt + ) {} +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZonePageDto.java b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZonePageDto.java new file mode 100644 index 00000000..8904c822 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZonePageDto.java @@ -0,0 +1,34 @@ +package com.wayble.server.admin.dto.wayblezone; + +import java.util.List; + +public record AdminWaybleZonePageDto( + List content, + int currentPage, + int pageSize, + long totalElements, + int totalPages, + boolean hasNext, + boolean hasPrevious +) { + public static AdminWaybleZonePageDto of( + List content, + int currentPage, + int pageSize, + long totalElements + ) { + int totalPages = (int) Math.ceil((double) totalElements / pageSize); + boolean hasNext = currentPage < totalPages - 1; + boolean hasPrevious = currentPage > 0; + + return new AdminWaybleZonePageDto( + content, + currentPage, + pageSize, + totalElements, + totalPages, + hasNext, + hasPrevious + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneThumbnailDto.java b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneThumbnailDto.java new file mode 100644 index 00000000..a1e851d4 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneThumbnailDto.java @@ -0,0 +1,16 @@ +package com.wayble.server.admin.dto.wayblezone; + +import com.wayble.server.wayblezone.entity.WaybleZoneFacility; +import com.wayble.server.wayblezone.entity.WaybleZoneType; + +public record AdminWaybleZoneThumbnailDto( + Long zoneId, + String zoneName, + WaybleZoneType zoneType, + long reviewCount, + long likes, + double rating, + String address, + WaybleZoneFacility facilityInfo // 시설 정보를 문자열로 요약 +) { +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneUpdateDto.java b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneUpdateDto.java new file mode 100644 index 00000000..068a00af --- /dev/null +++ b/src/main/java/com/wayble/server/admin/dto/wayblezone/AdminWaybleZoneUpdateDto.java @@ -0,0 +1,63 @@ +package com.wayble.server.admin.dto.wayblezone; + +import com.wayble.server.wayblezone.entity.WaybleZoneType; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public record AdminWaybleZoneUpdateDto( + @NotNull(message = "웨이블존 ID는 필수입니다") + Long id, + + @NotBlank(message = "웨이블존 이름은 필수입니다") + String zoneName, + + String contactNumber, + + @NotNull(message = "웨이블존 타입은 필수입니다") + WaybleZoneType zoneType, + + @NotBlank(message = "시/도는 필수입니다") + String state, + + @NotBlank(message = "시/군/구는 필수입니다") + String city, + + String district, + String streetAddress, + String detailAddress, + + @NotNull(message = "위도는 필수입니다") + @DecimalMin(value = "-90.0", message = "위도는 -90.0 이상이어야 합니다") + @DecimalMax(value = "90.0", message = "위도는 90.0 이하여야 합니다") + Double latitude, + + @NotNull(message = "경도는 필수입니다") + @DecimalMin(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다") + @DecimalMax(value = "180.0", message = "경도는 180.0 이하여야 합니다") + Double longitude, + + String mainImageUrl +) { + + /** + * AdminWaybleZoneDetailDto로부터 수정용 DTO를 생성 + */ + public static AdminWaybleZoneUpdateDto fromDetailDto(AdminWaybleZoneDetailDto detailDto) { + return new AdminWaybleZoneUpdateDto( + detailDto.zoneId(), + detailDto.zoneName(), + detailDto.contactNumber(), + detailDto.zoneType(), + detailDto.address().getState(), + detailDto.address().getCity(), + detailDto.address().getDistrict(), + detailDto.address().getStreetAddress(), + detailDto.address().getDetailAddress(), + detailDto.latitude(), + detailDto.longitude(), + detailDto.mainImageUrl() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/exception/AdminErrorCase.java b/src/main/java/com/wayble/server/admin/exception/AdminErrorCase.java new file mode 100644 index 00000000..3bdb7387 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/exception/AdminErrorCase.java @@ -0,0 +1,17 @@ +package com.wayble.server.admin.exception; + +import com.wayble.server.common.exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AdminErrorCase implements ErrorCase { + + USER_NOT_FOUND(404, 9001, "사용자를 찾을 수 없습니다."), + WAYBLE_ZONE_NOT_FOUND(404, 9002, "해당 웨이블존을 찾을 수 없습니다."); + + private final Integer httpStatusCode; + private final Integer errorCode; + private final String message; +} diff --git a/src/main/java/com/wayble/server/admin/repository/AdminUserRepository.java b/src/main/java/com/wayble/server/admin/repository/AdminUserRepository.java new file mode 100644 index 00000000..2047e351 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/repository/AdminUserRepository.java @@ -0,0 +1,101 @@ +package com.wayble.server.admin.repository; + +import com.wayble.server.admin.dto.user.AdminUserDetailDto; +import com.wayble.server.admin.dto.user.AdminUserThumbnailDto; +import com.wayble.server.user.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; +import java.util.Optional; + +public interface AdminUserRepository extends JpaRepository { + + @Query(value = """ + SELECT u.id, u.nickname, u.email, u.birth_date, u.gender, + u.login_type, u.user_type, u.disability_type, u.mobility_aid + FROM user u + WHERE u.deleted_at IS NULL + ORDER BY u.created_at DESC + LIMIT :size OFFSET :offset + """, nativeQuery = true) + List findUsersWithPagingRaw(@Param("offset") int offset, @Param("size") int size); + + @Query(value = """ + SELECT u.id, u.nickname, u.username, u.email, u.birth_date, u.gender, + u.login_type, u.user_type, u.profile_image_url, u.disability_type, + u.mobility_aid, u.created_at, u.updated_at + FROM user u + WHERE u.id = :userId AND u.deleted_at IS NULL + """, nativeQuery = true) + List findAdminUserDetailByIdRaw(@Param("userId") Long userId); + + @Query(value = """ + SELECT u.id, u.nickname, u.email, u.birth_date, u.gender, + u.login_type, u.user_type, u.disability_type, u.mobility_aid + FROM user u + WHERE u.deleted_at IS NOT NULL + ORDER BY u.deleted_at DESC + LIMIT :size OFFSET :offset + """, nativeQuery = true) + List findDeletedUsersWithPaging(@Param("offset") int offset, @Param("size") int size); + + @Query(value = """ + SELECT u.id, u.nickname, u.username, u.email, u.birth_date, u.gender, + u.login_type, u.user_type, u.profile_image_url, u.disability_type, + u.mobility_aid, NOW(), NOW() + FROM user u + WHERE u.id = :userId AND u.deleted_at IS NOT NULL + """, nativeQuery = true) + List findDeletedUserDetailById(@Param("userId") Long userId); + + @Query(value = "SELECT COUNT(*) FROM user WHERE deleted_at IS NOT NULL", nativeQuery = true) + long countDeletedUsers(); + + @Modifying + @Query(value = """ + UPDATE user + SET deleted_at = NULL + WHERE id = :userId AND deleted_at IS NOT NULL + """, nativeQuery = true) + int restoreUserById(@Param("userId") Long userId); + + @Modifying + @Query(value = """ + UPDATE review + SET deleted_at = NULL + WHERE user_id = :userId AND deleted_at IS NOT NULL + """, nativeQuery = true) + int restoreUserReviews(@Param("userId") Long userId); + + @Modifying + @Query(value = """ + UPDATE review_image ri + INNER JOIN review r ON ri.review_id = r.id + SET ri.deleted_at = NULL + WHERE r.user_id = :userId AND ri.deleted_at IS NOT NULL + """, nativeQuery = true) + int restoreUserReviewImages(@Param("userId") Long userId); + + @Modifying + @Query(value = """ + UPDATE user_place + SET deleted_at = NULL + WHERE user_id = :userId AND deleted_at IS NOT NULL + """, nativeQuery = true) + int restoreUserPlaces(@Param("userId") Long userId); + + @Query(value = "SELECT COUNT(*) FROM review WHERE user_id = :userId AND deleted_at IS NULL", nativeQuery = true) + long countUserReviews(@Param("userId") Long userId); + + @Query(value = "SELECT COUNT(*) FROM user_place WHERE user_id = :userId AND deleted_at IS NULL", nativeQuery = true) + long countUserPlaces(@Param("userId") Long userId); + + @Query(value = "SELECT COUNT(*) FROM review WHERE user_id = :userId", nativeQuery = true) + long countUserReviewsIncludingDeleted(@Param("userId") Long userId); + + @Query(value = "SELECT COUNT(*) FROM user_place WHERE user_id = :userId", nativeQuery = true) + long countUserPlacesIncludingDeleted(@Param("userId") Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepository.java b/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepository.java new file mode 100644 index 00000000..81d48308 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepository.java @@ -0,0 +1,7 @@ +package com.wayble.server.admin.repository; + +import com.wayble.server.wayblezone.entity.WaybleZone; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AdminWaybleZoneRepository extends JpaRepository, AdminWaybleZoneRepositoryCustom { +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepositoryCustom.java b/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepositoryCustom.java new file mode 100644 index 00000000..55455959 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepositoryCustom.java @@ -0,0 +1,16 @@ +package com.wayble.server.admin.repository; + +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneDetailDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneThumbnailDto; + +import java.util.List; +import java.util.Optional; + +public interface AdminWaybleZoneRepositoryCustom { + + // 페이징 조회 메서드 (관리자용) + List findWaybleZonesWithPaging(int page, int size); + + // 상세 조회 메서드 (관리자용) + Optional findAdminWaybleZoneDetailById(Long zoneId); +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepositoryImpl.java b/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepositoryImpl.java new file mode 100644 index 00000000..e9c9a645 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/repository/AdminWaybleZoneRepositoryImpl.java @@ -0,0 +1,105 @@ +package com.wayble.server.admin.repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.impl.JPAQueryFactory; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneDetailDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneThumbnailDto; +import com.wayble.server.common.entity.Address; +import com.wayble.server.wayblezone.entity.WaybleZone; + +import java.util.List; +import java.util.Optional; + +import static com.wayble.server.wayblezone.entity.QWaybleZone.waybleZone; + +public class AdminWaybleZoneRepositoryImpl implements AdminWaybleZoneRepositoryCustom { + + private final JPAQueryFactory queryFactory; + + public AdminWaybleZoneRepositoryImpl(JPAQueryFactory queryFactory) { + this.queryFactory = queryFactory; + } + + @Override + public List findWaybleZonesWithPaging(int page, int size) { + return queryFactory + .select(Projections.constructor(AdminWaybleZoneThumbnailDto.class, + waybleZone.id, + waybleZone.zoneName, + waybleZone.zoneType, + waybleZone.reviewCount, + waybleZone.likes, + waybleZone.rating, + waybleZone.address.state + .concat(" ") + .concat(waybleZone.address.city.coalesce("")) + .concat(" ") + .concat(waybleZone.address.district.coalesce("")) + .concat(" ") + .concat(waybleZone.address.streetAddress.coalesce("")) + .concat(" ") + .concat(waybleZone.address.detailAddress.coalesce("")), + waybleZone.facility + )) + .from(waybleZone) + .leftJoin(waybleZone.facility) + .orderBy(waybleZone.id.asc()) + .offset((long) page * size) + .limit(size) + .fetch(); + } + + @Override + public Optional findAdminWaybleZoneDetailById(Long zoneId) { + // 1. 기본 WaybleZone 정보와 facility 조회 + WaybleZone zone = queryFactory + .selectFrom(waybleZone) + .leftJoin(waybleZone.facility).fetchJoin() + .where(waybleZone.id.eq(zoneId)) + .fetchOne(); + + if (zone == null) { + return Optional.empty(); + } + + // 2. DTO 변환 + AdminWaybleZoneDetailDto.FacilityInfo facilityInfo = null; + if (zone.getFacility() != null) { + facilityInfo = new AdminWaybleZoneDetailDto.FacilityInfo( + zone.getFacility(), + zone.getFacility() + " 이용 가능" + ); + } + + // 3. 빈 리스트로 초기화 (컬렉션은 나중에 필요시 별도 쿼리로 로드) + List operatingHours = List.of(); + List imageUrls = List.of(); + List recentReviews = List.of(); + Address address = zone.getAddress(); + + // 4. 최종 DTO 생성 + AdminWaybleZoneDetailDto detailDto = new AdminWaybleZoneDetailDto( + zone.getId(), + zone.getZoneName(), + zone.getContactNumber(), + zone.getZoneType(), + zone.getAddress(), + zone.getAddress().toFullAddress(), + zone.getAddress().getLatitude(), + zone.getAddress().getLongitude(), + zone.getRating(), + zone.getReviewCount(), + zone.getLikes(), + zone.getMainImageUrl(), + zone.getCreatedAt(), + zone.getLastModifiedAt(), + zone.getSyncedAt(), + facilityInfo, + operatingHours, + imageUrls, + recentReviews + ); + + return Optional.of(detailDto); + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/service/AdminSystemService.java b/src/main/java/com/wayble/server/admin/service/AdminSystemService.java new file mode 100644 index 00000000..5d7bee4b --- /dev/null +++ b/src/main/java/com/wayble/server/admin/service/AdminSystemService.java @@ -0,0 +1,49 @@ +package com.wayble.server.admin.service; + +import co.elastic.clients.elasticsearch.ElasticsearchClient; +import co.elastic.clients.elasticsearch.cluster.HealthResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; +import java.sql.Connection; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AdminSystemService { + + private final ElasticsearchClient elasticsearchClient; + private final DataSource dataSource; + + public boolean isElasticsearchHealthy() { + try { + HealthResponse response = elasticsearchClient.cluster().health(); + log.debug("Elasticsearch health status: {}", response.status()); + return response.status() != co.elastic.clients.elasticsearch._types.HealthStatus.Red; + } catch (Exception e) { + log.error("Elasticsearch health check failed", e); + return false; + } + } + + public boolean isDatabaseHealthy() { + try (Connection connection = dataSource.getConnection()) { + return connection.isValid(5); // 5초 타임아웃 + } catch (Exception e) { + log.error("Database health check failed", e); + return false; + } + } + + public boolean isApiServerHealthy() { + // API 서버가 실행 중이면 true (이 메서드가 호출되는 것 자체가 서버가 살아있다는 증거) + return true; + } + + public boolean isFileStorageHealthy() { + // 파일 스토리지 상태 체크 (예: S3 연결 확인 등) + return true; + } +} \ No newline at end of file diff --git a/src/main/java/com/wayble/server/admin/service/AdminUserService.java b/src/main/java/com/wayble/server/admin/service/AdminUserService.java new file mode 100644 index 00000000..1c79ee71 --- /dev/null +++ b/src/main/java/com/wayble/server/admin/service/AdminUserService.java @@ -0,0 +1,231 @@ +package com.wayble.server.admin.service; + +import com.wayble.server.admin.dto.user.AdminUserDetailDto; +import com.wayble.server.admin.dto.user.AdminUserPageDto; +import com.wayble.server.admin.dto.user.AdminUserThumbnailDto; +import com.wayble.server.admin.exception.AdminErrorCase; +import com.wayble.server.admin.repository.AdminUserRepository; +import com.wayble.server.common.exception.ApplicationException; +import com.wayble.server.user.entity.Gender; +import com.wayble.server.user.entity.LoginType; +import com.wayble.server.user.entity.UserType; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigInteger; +import java.sql.Date; +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AdminUserService { + + private final AdminUserRepository adminUserRepository; + + public long getTotalUserCount() { + try { + return adminUserRepository.count(); + } catch (Exception e) { + log.error("사용자 수 조회 실패", e); + return 0; + } + } + + public AdminUserPageDto getUsersWithPaging(int page, int size) { + int offset = page * size; + + List rawResults = adminUserRepository.findUsersWithPagingRaw(offset, size); + List content = rawResults.stream() + .map(this::convertToThumbnailDto) + .toList(); + long totalElements = adminUserRepository.count(); + + log.debug("사용자 페이징 조회 - 페이지: {}, 크기: {}, 전체: {}", page, size, totalElements); + + return AdminUserPageDto.of(content, page, size, totalElements); + } + + public List findUsersByPage(int page, int size) { + int offset = page * size; + List rawResults = adminUserRepository.findUsersWithPagingRaw(offset, size); + return rawResults.stream() + .map(this::convertToThumbnailDto) + .toList(); + } + + public Optional findUserById(Long userId) { + List rawResults = adminUserRepository.findAdminUserDetailByIdRaw(userId); + if (rawResults.isEmpty()) { + return Optional.empty(); + } + + Object[] row = rawResults.get(0); + long reviewCount = adminUserRepository.countUserReviews(userId); + long userPlaceCount = adminUserRepository.countUserPlaces(userId); + + return Optional.of(convertToDetailDtoWithStats(row, reviewCount, userPlaceCount)); + } + + public long getTotalDeletedUserCount() { + try { + return adminUserRepository.countDeletedUsers(); + } catch (Exception e) { + log.error("삭제된 사용자 수 조회 실패", e); + return 0; + } + } + + public AdminUserPageDto getDeletedUsersWithPaging(int page, int size) { + int offset = page * size; + + List rawResults = adminUserRepository.findDeletedUsersWithPaging(offset, size); + List content = rawResults.stream() + .map(this::convertToThumbnailDto) + .toList(); + long totalElements = adminUserRepository.countDeletedUsers(); + + log.debug("삭제된 사용자 페이징 조회 - 페이지: {}, 크기: {}, 전체: {}", page, size, totalElements); + + return AdminUserPageDto.of(content, page, size, totalElements); + } + + public List findDeletedUsersByPage(int page, int size) { + int offset = page * size; + List rawResults = adminUserRepository.findDeletedUsersWithPaging(offset, size); + return rawResults.stream() + .map(this::convertToThumbnailDto) + .toList(); + } + + public Optional findDeletedUserById(Long userId) { + List rawResults = adminUserRepository.findDeletedUserDetailById(userId); + if (rawResults.isEmpty()) { + return Optional.empty(); + } + + Object[] row = rawResults.get(0); + log.debug("삭제된 사용자 데이터 조회 - ID: {}, 데이터: {}", userId, java.util.Arrays.toString(row)); + + // 삭제된 사용자의 경우 삭제된 것들도 포함하여 통계 조회 + long reviewCount = adminUserRepository.countUserReviewsIncludingDeleted(userId); + long userPlaceCount = adminUserRepository.countUserPlacesIncludingDeleted(userId); + + return Optional.of(convertToDetailDto(row, reviewCount, userPlaceCount)); + } + + @Transactional + public void restoreUser(Long userId) { + try { + // 삭제된 사용자가 존재하는지 먼저 확인 + Optional deletedUserOpt = findDeletedUserById(userId); + if (deletedUserOpt.isEmpty()) { + throw new ApplicationException(AdminErrorCase.USER_NOT_FOUND); + } + + log.info("사용자 복원 시작 - ID: {}, 이메일: {}", userId, deletedUserOpt.get().email()); + + // 사용자 계정 복원 + int restoredUser = adminUserRepository.restoreUserById(userId); + log.debug("사용자 계정 복원 완료 - ID: {}, 처리된 행: {}", userId, restoredUser); + + // 연관된 리뷰들 복원 + int restoredReviews = adminUserRepository.restoreUserReviews(userId); + log.debug("사용자 리뷰 복원 완료 - ID: {}, 복원된 리뷰: {}", userId, restoredReviews); + + // 리뷰 이미지들 복원 + int restoredReviewImages = adminUserRepository.restoreUserReviewImages(userId); + log.debug("사용자 리뷰 이미지 복원 완료 - ID: {}, 복원된 이미지: {}", userId, restoredReviewImages); + + // 사용자 즐겨찾기 복원 + int restoredUserPlaces = adminUserRepository.restoreUserPlaces(userId); + log.debug("사용자 즐겨찾기 복원 완료 - ID: {}, 복원된 즐겨찾기: {}", userId, restoredUserPlaces); + + log.info("사용자 복원 완료 - ID: {}, 계정: {}, 리뷰: {}, 리뷰이미지: {}, 즐겨찾기: {}", + userId, restoredUser, restoredReviews, restoredReviewImages, restoredUserPlaces); + + } catch (ApplicationException e) { + throw e; + } catch (Exception e) { + log.error("사용자 복원 실패 - ID: {}", userId, e); + throw new RuntimeException("사용자 복원에 실패했습니다", e); + } + } + + private AdminUserThumbnailDto convertToThumbnailDto(Object[] row) { + Long id = convertToLong(row[0]); + String nickname = (String) row[1]; + String email = (String) row[2]; + LocalDate birthDate = row[3] != null ? ((Date) row[3]).toLocalDate() : null; + Gender gender = row[4] != null ? Gender.valueOf((String) row[4]) : null; + LoginType loginType = LoginType.valueOf((String) row[5]); + UserType userType = UserType.valueOf((String) row[6]); + String disabilityType = (String) row[7]; + String mobilityAid = (String) row[8]; + + return new AdminUserThumbnailDto(id, nickname, email, birthDate, gender, + loginType, userType, disabilityType, mobilityAid); + } + + private AdminUserDetailDto convertToDetailDto(Object[] row, long reviewCount, long userPlaceCount) { + Long id = convertToLong(row[0]); + String nickname = (String) row[1]; + String username = (String) row[2]; + String email = (String) row[3]; + LocalDate birthDate = row[4] != null ? ((Date) row[4]).toLocalDate() : null; + Gender gender = row[5] != null ? Gender.valueOf((String) row[5]) : null; + LoginType loginType = LoginType.valueOf((String) row[6]); + UserType userType = UserType.valueOf((String) row[7]); + String profileImageUrl = (String) row[8]; + String disabilityType = (String) row[9]; + String mobilityAid = (String) row[10]; + Timestamp createdAtStamp = (Timestamp) row[11]; + LocalDateTime createdAt = createdAtStamp != null ? createdAtStamp.toLocalDateTime() : null; + Timestamp updatedAtStamp = (Timestamp) row[12]; + LocalDateTime updatedAt = updatedAtStamp != null ? updatedAtStamp.toLocalDateTime() : null; + + return new AdminUserDetailDto(id, nickname, username, email, birthDate, gender, + loginType, userType, profileImageUrl, disabilityType, + mobilityAid, createdAt, updatedAt, reviewCount, userPlaceCount); + } + + private AdminUserDetailDto convertToDetailDtoWithStats(Object[] row, long reviewCount, long userPlaceCount) { + Long id = convertToLong(row[0]); + String nickname = (String) row[1]; + String username = (String) row[2]; + String email = (String) row[3]; + LocalDate birthDate = row[4] != null ? ((Date) row[4]).toLocalDate() : null; + Gender gender = row[5] != null ? Gender.valueOf((String) row[5]) : null; + LoginType loginType = LoginType.valueOf((String) row[6]); + UserType userType = UserType.valueOf((String) row[7]); + String profileImageUrl = (String) row[8]; + String disabilityType = (String) row[9]; + String mobilityAid = (String) row[10]; + Timestamp createdAtStamp = (Timestamp) row[11]; + LocalDateTime createdAt = createdAtStamp != null ? createdAtStamp.toLocalDateTime() : null; + Timestamp updatedAtStamp = (Timestamp) row[12]; + LocalDateTime updatedAt = updatedAtStamp != null ? updatedAtStamp.toLocalDateTime() : null; + + return new AdminUserDetailDto(id, nickname, username, email, birthDate, gender, + loginType, userType, profileImageUrl, disabilityType, + mobilityAid, createdAt, updatedAt, reviewCount, userPlaceCount); + } + + private Long convertToLong(Object value) { + if (value instanceof Long) { + return (Long) value; + } else if (value instanceof BigInteger) { + return ((BigInteger) value).longValue(); + } else if (value instanceof Integer) { + return ((Integer) value).longValue(); + } + throw new IllegalArgumentException("Cannot convert " + value.getClass() + " to Long"); + } +} diff --git a/src/main/java/com/wayble/server/admin/service/AdminWaybleZoneService.java b/src/main/java/com/wayble/server/admin/service/AdminWaybleZoneService.java new file mode 100644 index 00000000..2a8090de --- /dev/null +++ b/src/main/java/com/wayble/server/admin/service/AdminWaybleZoneService.java @@ -0,0 +1,195 @@ +package com.wayble.server.admin.service; + +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneCreateDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneDetailDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZonePageDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneThumbnailDto; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneUpdateDto; +import com.wayble.server.admin.exception.AdminErrorCase; +import com.wayble.server.admin.repository.AdminWaybleZoneRepository; +import com.wayble.server.common.exception.ApplicationException; +import com.wayble.server.explore.service.WaybleZoneDocumentService; +import com.wayble.server.user.repository.UserPlaceWaybleZoneMappingRepository; +import com.wayble.server.wayblezone.entity.WaybleZone; +import com.wayble.server.wayblezone.repository.WaybleZoneRepository; +import com.wayble.server.wayblezone.repository.WaybleZoneVisitLogRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class AdminWaybleZoneService { + private final AdminWaybleZoneRepository adminWaybleZoneRepository; + private final WaybleZoneDocumentService waybleZoneDocumentService; + private final WaybleZoneRepository waybleZoneRepository; + private final UserPlaceWaybleZoneMappingRepository userPlaceWaybleZoneMappingRepository; + private final WaybleZoneVisitLogRepository waybleZoneVisitLogRepository; + + public long getTotalWaybleZoneCounts() { + return adminWaybleZoneRepository.count(); + } + + public AdminWaybleZonePageDto getWaybleZonesWithPaging(int page, int size) { + // 페이징 데이터 조회 + List content = adminWaybleZoneRepository.findWaybleZonesWithPaging(page, size); + + // 전체 개수 조회 + long totalElements = adminWaybleZoneRepository.count(); + + log.debug("웨이블존 페이징 조회 - 페이지: {}, 크기: {}, 전체: {}", page, size, totalElements); + + // 페이징 응답 DTO 생성 + return AdminWaybleZonePageDto.of(content, page, size, totalElements); + } + + public List findWaybleZonesByPage(int page, int size) { + return adminWaybleZoneRepository.findWaybleZonesWithPaging(page, size); + } + + public Optional findWaybleZoneById(Long waybleZoneId) { + return adminWaybleZoneRepository.findAdminWaybleZoneDetailById(waybleZoneId); + } + + @Transactional + public Long createWaybleZone(AdminWaybleZoneCreateDto adminWaybleZoneCreateDto) { + try { + WaybleZone waybleZone = WaybleZone.fromAdminDto(adminWaybleZoneCreateDto); + WaybleZone savedZone = adminWaybleZoneRepository.save(waybleZone); + + log.info("새 웨이블존 생성 완료 - ID: {}, 이름: {}", savedZone.getId(), savedZone.getZoneName()); + + try { + waybleZoneDocumentService.saveDocumentFromEntity(savedZone); + log.info("WaybleZoneDocument 동기화 완료 - ID: {}", savedZone.getId()); + } catch (Exception esException) { + log.warn("WaybleZoneDocument 동기화 실패, 데이터 불일치 발생 가능 - ID: {}, 오류: {}", + savedZone.getId(), esException.getMessage()); + } + + return savedZone.getId(); + } catch (Exception e) { + log.error("웨이블존 생성 실패", e); + throw new RuntimeException("웨이블존 생성에 실패했습니다", e); + } + } + + @Transactional + public Long updateWaybleZone(AdminWaybleZoneUpdateDto updateDto) { + try { + // 기존 웨이블존 조회 + WaybleZone waybleZone = waybleZoneRepository.findById(updateDto.id()) + .orElseThrow(() -> new ApplicationException(AdminErrorCase.WAYBLE_ZONE_NOT_FOUND)); + + // 주소 정보 업데이트 + com.wayble.server.common.entity.Address updatedAddress = com.wayble.server.common.entity.Address + .builder() + .state(updateDto.state()) + .city(updateDto.city()) + .district(updateDto.district()) + .streetAddress(updateDto.streetAddress()) + .detailAddress(updateDto.detailAddress()) + .latitude(updateDto.latitude()) + .longitude(updateDto.longitude()) + .build(); + + // 웨이블존 정보 업데이트 + waybleZone.updateZoneName(updateDto.zoneName()); + waybleZone.updateContactNumber(updateDto.contactNumber()); + waybleZone.updateZoneType(updateDto.zoneType()); + waybleZone.updateAddress(updatedAddress); + waybleZone.updateMainImageUrl(updateDto.mainImageUrl()); + + WaybleZone savedZone = waybleZoneRepository.save(waybleZone); + + log.info("웨이블존 수정 완료 - ID: {}, 이름: {}", savedZone.getId(), savedZone.getZoneName()); + + try { + waybleZoneDocumentService.saveDocumentFromEntity(savedZone); + log.info("WaybleZoneDocument 동기화 완료 - ID: {}", savedZone.getId()); + } catch (Exception esException) { + log.warn("WaybleZoneDocument 동기화 실패, 데이터 불일치 발생 가능 - ID: {}, 오류: {}", + savedZone.getId(), esException.getMessage()); + } + + return savedZone.getId(); + } catch (ApplicationException e) { + throw e; + } catch (Exception e) { + log.error("웨이블존 수정 실패 - ID: {}", updateDto.id(), e); + throw new RuntimeException("웨이블존 수정에 실패했습니다", e); + } + } + + @Transactional + public void deleteWaybleZone(Long waybleZoneId) { + try { + WaybleZone waybleZone = waybleZoneRepository.findById(waybleZoneId) + .orElseThrow(() -> new ApplicationException(AdminErrorCase.WAYBLE_ZONE_NOT_FOUND)); + + log.info("웨이블존 삭제 시작 - ID: {}, 이름: {}", waybleZoneId, waybleZone.getZoneName()); + + // 연관된 리뷰들 soft delete + waybleZone.getReviewList().forEach(review -> { + review.softDelete(); + // 리뷰 이미지들도 함께 soft delete + review.getReviewImageList().forEach(reviewImage -> reviewImage.softDelete()); + }); + log.debug("웨이블존 리뷰 삭제 완료 - ID: {}, 리뷰 개수: {}", waybleZoneId, waybleZone.getReviewList().size()); + + // 운영시간 정보 soft delete + waybleZone.getOperatingHours().forEach(operatingHour -> { + operatingHour.softDelete(); + }); + log.debug("웨이블존 운영시간 삭제 완료 - ID: {}, 운영시간 개수: {}", waybleZoneId, waybleZone.getOperatingHours().size()); + + // 시설 정보 soft delete (null 체크) + if (waybleZone.getFacility() != null) { + waybleZone.getFacility().softDelete(); + log.debug("웨이블존 시설 정보 삭제 완료 - ID: {}", waybleZoneId); + } + + // 웨이블존 이미지들 soft delete + waybleZone.getWaybleZoneImageList().forEach(image -> image.softDelete()); + log.debug("웨이블존 이미지 삭제 완료 - ID: {}, 이미지 개수: {}", waybleZoneId, waybleZone.getWaybleZoneImageList().size()); + + // 사용자 즐겨찾기 매핑 hard delete (참조 관계 제거) + userPlaceWaybleZoneMappingRepository.deleteAll(waybleZone.getUserPlaceMappings()); + log.debug("웨이블존 사용자 매핑 삭제 완료 - ID: {}, 매핑 개수: {}", waybleZoneId, waybleZone.getUserPlaceMappings().size()); + + // 방문 로그 삭제 (해당 웨이블존의 모든 방문 로그 삭제) + try { + waybleZoneVisitLogRepository.deleteByZoneId(waybleZoneId); + log.debug("웨이블존 방문 로그 삭제 완료 - ID: {}", waybleZoneId); + } catch (Exception e) { + log.warn("웨이블존 방문 로그 삭제 실패 - ID: {}, 오류: {}", waybleZoneId, e.getMessage()); + } + + // 웨이블존 자체 soft delete + waybleZone.softDelete(); + log.info("웨이블존 soft delete 완료 - ID: {}", waybleZoneId); + + // Elasticsearch 문서 삭제 + try { + waybleZoneDocumentService.deleteDocumentById(waybleZoneId); + log.info("WaybleZoneDocument 삭제 완료 - ID: {}", waybleZoneId); + } catch (Exception esException) { + log.warn("WaybleZoneDocument 삭제 실패, 수동 정리 필요 - ID: {}, 오류: {}", + waybleZoneId, esException.getMessage()); + } + + log.info("웨이블존 삭제 완료 - ID: {}", waybleZoneId); + } catch (ApplicationException e) { + throw e; + } catch (Exception e) { + log.error("웨이블존 삭제 실패 - ID: {}", waybleZoneId, e); + throw new RuntimeException("웨이블존 삭제에 실패했습니다", e); + } + } +} diff --git a/src/main/java/com/wayble/server/common/config/SecurityConfig.java b/src/main/java/com/wayble/server/common/config/SecurityConfig.java index 61990404..2c96a974 100644 --- a/src/main/java/com/wayble/server/common/config/SecurityConfig.java +++ b/src/main/java/com/wayble/server/common/config/SecurityConfig.java @@ -66,7 +66,7 @@ public CorsConfigurationSource corsConfigurationSource() { configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 허용할 HTTP 메서드 configuration.setAllowedHeaders(Arrays.asList("*")); // 모든 헤더 허용 configuration.setAllowCredentials(true); // 쿠키, 인증 정보 허용 - + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); // 모든 경로에 적용 return source; diff --git a/src/main/java/com/wayble/server/common/entity/BaseEntity.java b/src/main/java/com/wayble/server/common/entity/BaseEntity.java index 9044360d..9247cef8 100644 --- a/src/main/java/com/wayble/server/common/entity/BaseEntity.java +++ b/src/main/java/com/wayble/server/common/entity/BaseEntity.java @@ -16,9 +16,11 @@ public class BaseEntity { @CreatedDate + @Column(name = "created_at") private LocalDateTime createdAt; @LastModifiedDate + @Column(name = "updated_at") private LocalDateTime updatedAt; @Column(name = "deleted_at") @@ -27,4 +29,8 @@ public class BaseEntity { public void restore() { this.deletedAt = null; } + + public void softDelete() { + this.deletedAt = LocalDateTime.now(); + } } diff --git a/src/main/java/com/wayble/server/explore/service/WaybleZoneDocumentService.java b/src/main/java/com/wayble/server/explore/service/WaybleZoneDocumentService.java index c694ed20..2647ea05 100644 --- a/src/main/java/com/wayble/server/explore/service/WaybleZoneDocumentService.java +++ b/src/main/java/com/wayble/server/explore/service/WaybleZoneDocumentService.java @@ -29,4 +29,8 @@ public void saveDocumentFromEntity(WaybleZone waybleZone) { public void saveDocumentFromDto(WaybleZoneRegisterDto dto) { waybleZoneDocumentRepository.save(WaybleZoneDocument.fromDto(dto)); } + + public void deleteDocumentById(Long id) { + waybleZoneDocumentRepository.deleteById(id); + } } diff --git a/src/main/java/com/wayble/server/user/repository/UserRepository.java b/src/main/java/com/wayble/server/user/repository/UserRepository.java index 01289862..b375ea87 100644 --- a/src/main/java/com/wayble/server/user/repository/UserRepository.java +++ b/src/main/java/com/wayble/server/user/repository/UserRepository.java @@ -3,8 +3,6 @@ import com.wayble.server.user.entity.LoginType; import com.wayble.server.user.entity.User; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import java.util.Optional; diff --git a/src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java b/src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java index d3a3362c..cd59e586 100644 --- a/src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java +++ b/src/main/java/com/wayble/server/wayblezone/entity/WaybleZone.java @@ -1,5 +1,6 @@ package com.wayble.server.wayblezone.entity; +import com.wayble.server.admin.dto.wayblezone.AdminWaybleZoneCreateDto; import com.wayble.server.common.entity.Address; import com.wayble.server.common.entity.BaseEntity; import com.wayble.server.review.entity.Review; @@ -102,6 +103,32 @@ public void addLikes(long count) { this.markAsModified(); // 변경 시 자동으로 수정 시간 갱신 } + // 관리자 업데이트 메서드들 + public void updateZoneName(String zoneName) { + this.zoneName = zoneName; + this.markAsModified(); + } + + public void updateContactNumber(String contactNumber) { + this.contactNumber = contactNumber; + this.markAsModified(); + } + + public void updateZoneType(WaybleZoneType zoneType) { + this.zoneType = zoneType; + this.markAsModified(); + } + + public void updateAddress(Address address) { + this.address = address; + this.markAsModified(); + } + + public void updateMainImageUrl(String mainImageUrl) { + this.mainImageUrl = mainImageUrl; + this.markAsModified(); + } + // ES 동기화 관련 메서드들 public void markAsModified() { this.lastModifiedAt = LocalDateTime.now(); @@ -130,4 +157,30 @@ public static WaybleZone from(WaybleZoneRegisterDto dto) { .syncedAt(null) .build(); } + + public static WaybleZone fromAdminDto(AdminWaybleZoneCreateDto dto) { + Address dtoAddress = Address + .builder() + .state(dto.state()) + .city(dto.city()) + .district(dto.district()) + .streetAddress(dto.streetAddress()) + .detailAddress(dto.detailAddress()) + .latitude(dto.latitude()) + .longitude(dto.longitude()) + .build(); + + return WaybleZone.builder() + .zoneName(dto.zoneName()) + .contactNumber(dto.contactNumber()) + .zoneType(dto.zoneType()) + .address(dtoAddress) + .mainImageUrl(null) + .rating(0.0) + .reviewCount(0) + .likes(0) + .lastModifiedAt(LocalDateTime.now()) + .syncedAt(null) + .build(); + } } diff --git a/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java b/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java index c34cd3e9..175b4329 100644 --- a/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java +++ b/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepository.java @@ -7,4 +7,6 @@ public interface WaybleZoneRepository extends JpaRepository, WaybleZoneRepositoryCustom { List findByAddress_CityContainingAndZoneType(String city, com.wayble.server.wayblezone.entity.WaybleZoneType category); + + long count(); } diff --git a/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepositoryImpl.java b/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepositoryImpl.java index 6838c042..d476c56b 100644 --- a/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepositoryImpl.java +++ b/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneRepositoryImpl.java @@ -10,6 +10,7 @@ import java.time.LocalDateTime; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; import static com.wayble.server.wayblezone.entity.QWaybleZone.waybleZone; @@ -125,6 +126,7 @@ public List findZonesModifiedAfter(LocalDateTime since, int limit) { .fetch(); } + // 내부 클래스로 visit count 담을 DTO public static class VisitCount { public final Long zoneId; diff --git a/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneVisitLogRepository.java b/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneVisitLogRepository.java index cee2219b..6bb074d0 100644 --- a/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneVisitLogRepository.java +++ b/src/main/java/com/wayble/server/wayblezone/repository/WaybleZoneVisitLogRepository.java @@ -2,6 +2,9 @@ import com.wayble.server.wayblezone.entity.WaybleZoneVisitLog; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; @@ -9,4 +12,8 @@ public interface WaybleZoneVisitLogRepository extends JpaRepository findByUserIdAndZoneId(Long userId, Long zoneId); + + @Modifying + @Query("DELETE FROM WaybleZoneVisitLog v WHERE v.zoneId = :zoneId") + void deleteByZoneId(@Param("zoneId") Long zoneId); } diff --git a/src/main/resources/templates/admin/dashboard.html b/src/main/resources/templates/admin/dashboard.html new file mode 100644 index 00000000..4acb70af --- /dev/null +++ b/src/main/resources/templates/admin/dashboard.html @@ -0,0 +1,246 @@ + + + + + + Wayble 관리자 대시보드 + + + + + + + + +
+ +
+

대시보드

+

Wayble 서비스 현황

+
+ + +
+ +
+
+
+
+
+ + + +
+
+
+
+
전체 사용자
+
0명
+
+
+
+
+
+ + +
+
+
+
+
+ + + +
+
+
+
+
전체 웨이블존
+
0개
+
+
+
+
+
+ + +
+
+
+
+
+ + + +
+
+
+
+
오늘 방문자
+
892명
+
+
+
+
+
+
+ + + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/admin/login.html b/src/main/resources/templates/admin/login.html new file mode 100644 index 00000000..fb835969 --- /dev/null +++ b/src/main/resources/templates/admin/login.html @@ -0,0 +1,106 @@ + + + + + + Wayble 관리자 로그인 + + + + +
+
+
+ + + +
+

+ Wayble 관리자 +

+

+ 관리자 계정으로 로그인하세요 +

+
+ +
+ +
+
+
+ + + +
+
+

+
+
+
+ +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+

+ © 2025 Wayble. 관리자 전용 페이지입니다. +

+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/user/deleted-user-detail.html b/src/main/resources/templates/admin/user/deleted-user-detail.html new file mode 100644 index 00000000..bbcbc058 --- /dev/null +++ b/src/main/resources/templates/admin/user/deleted-user-detail.html @@ -0,0 +1,325 @@ + + + + + + 삭제된 사용자 상세정보 - Wayble 관리자 + + + + + + + +
+ +
+
+
+

+

삭제된 사용자 상세 정보

+
+
+ + + 삭제됨 + + + + + + +
+
+
+ +
+ +
+
+
+

기본 정보

+
+
+
+
+
사용자 ID
+
+
+
+
닉네임
+
+
+
+
사용자명
+
+
+
+
이메일
+
+
+
+
생년월일
+
+
+
+
성별
+
+
+
+
로그인 타입
+
+
+
+
사용자 타입
+
+
+
+
프로필 이미지
+
+
+ 프로필 이미지 + +
+ 설정되지 않음 +
+
+
+
+
+ + +
+
+

시간 정보

+
+
+
+
+
가입일
+
+
+
+
마지막 수정일
+
+
+
+
+
+
+ + +
+
+
+

장애 관련 정보

+
+
+
+
+
장애 유형
+
+ + + 설정되지 않음 +
+
+
+
이동 보조 수단
+
+ + + 설정되지 않음 +
+
+
+
+
+ + +
+
+
+
+ + + +
+
+

삭제된 사용자

+
+

이 사용자는 삭제된 상태입니다. 모든 관련 데이터가 비활성화되어 있습니다.

+
+
+
+
+
+ + +
+
+

활동 통계

+
+
+
+
0
+
작성한 리뷰 수 (삭제된 것 포함)
+
+
+
0
+
좋아요한 장소 수 (삭제된 것 포함)
+
+
+
+ 복원 시 활성 상태인 데이터만 다시 표시됩니다 +
+
+
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/user/deleted-users.html b/src/main/resources/templates/admin/user/deleted-users.html new file mode 100644 index 00000000..9f7c4288 --- /dev/null +++ b/src/main/resources/templates/admin/user/deleted-users.html @@ -0,0 +1,235 @@ + + + + + + 삭제된 사용자 관리 - Wayble 관리자 + + + + + + + +
+ +
+
+
+

삭제된 사용자 목록

+

+ 총 명의 삭제된 사용자가 있습니다 +

+
+
+
+ / 페이지 +
+
+
+
+ + +
+
+
+ + + +
+
+

삭제된 사용자를 찾을 수 없습니다.

+
+
+
+ + +
+
+
+
+ +
+ +
+
+

+ + 삭제됨 + + + + + +
+ +
+
+ + + + + + +
+
+ + + + + + + + + + + + +
+
+
+ + +
+
+
+
장애 유형
+
+
+
+
이동 보조 수단
+
+
+
+ 장애 관련 정보 없음 +
+
+
+
+
+
+ + +
+ + + +

삭제된 사용자가 없습니다

+

삭제된 사용자가 아직 없습니다.

+
+
+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/admin/user/user-detail.html b/src/main/resources/templates/admin/user/user-detail.html new file mode 100644 index 00000000..cc03e71a --- /dev/null +++ b/src/main/resources/templates/admin/user/user-detail.html @@ -0,0 +1,208 @@ + + + + + + 사용자 상세정보 - Wayble 관리자 + + + + + + + +
+ +
+
+
+

+

사용자 상세 정보

+
+
+ + + + +
+
+
+ +
+ +
+
+
+

기본 정보

+
+
+
+
+
사용자 ID
+
+
+
+
닉네임
+
+
+
+
사용자명
+
+
+
+
이메일
+
+
+
+
생년월일
+
+
+
+
성별
+
+
+
+
로그인 타입
+
+
+
+
사용자 타입
+
+
+
+
프로필 이미지
+
+
+ 프로필 이미지 + +
+ 설정되지 않음 +
+
+
+
+
+ + +
+
+

시간 정보

+
+
+
+
+
가입일
+
+
+
+
마지막 수정일
+
+
+
+
+
+
+ + +
+
+
+

장애 관련 정보

+
+
+
+
+
장애 유형
+
+ + + 설정되지 않음 +
+
+
+
이동 보조 수단
+
+ + + 설정되지 않음 +
+
+
+
+
+ + +
+
+

활동 통계

+
+
+
+
0
+
작성한 리뷰 수
+
+
+
0
+
좋아요한 장소 수
+
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/admin/user/users.html b/src/main/resources/templates/admin/user/users.html new file mode 100644 index 00000000..ab1a6e9c --- /dev/null +++ b/src/main/resources/templates/admin/user/users.html @@ -0,0 +1,232 @@ + + + + + + 사용자 관리 - Wayble 관리자 + + + + + + + +
+ +
+
+
+

사용자 목록

+

+ 총 명의 사용자가 등록되어 있습니다 +

+
+
+
+ / 페이지 +
+
+
+
+ + +
+
+
+ + + +
+
+

사용자를 찾을 수 없습니다.

+
+
+
+ + +
+
+
+
+ +
+ +
+
+

+ + + + +
+ +
+
+ + + + + + +
+
+ + + + + + + + + + + + +
+
+
+ + +
+
+
+
장애 유형
+
+
+
+
이동 보조 수단
+
+
+
+ 장애 관련 정보 없음 +
+
+
+
+
+
+ + +
+ + + +

사용자가 없습니다

+

등록된 사용자가 아직 없습니다.

+
+
+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/admin/wayblezone/wayble-zone-create.html b/src/main/resources/templates/admin/wayblezone/wayble-zone-create.html new file mode 100644 index 00000000..f3396c70 --- /dev/null +++ b/src/main/resources/templates/admin/wayblezone/wayble-zone-create.html @@ -0,0 +1,388 @@ + + + + + + 웨이블존 생성 - Wayble 관리자 + + + + + + + +
+ +
+

새 웨이블존 생성

+

새로운 웨이블존을 등록하세요

+
+ + +
+
+
+ + + +
+
+

+
+
+
+ + +
+
+
+
+
+ + + +
+
+
+

웨이블존 정보 입력

+

새로운 웨이블존의 상세 정보를 입력해주세요

+
+
+
+ +
+
+ + +
+
+

기본 정보

+

웨이블존의 기본 정보를 입력하세요

+
+ +
+ +
+ +
+ +
+ + + +
+
+

+ + + +

+
+ + +
+ +
+ +
+ + + +
+
+

+ + + +

+
+ + +
+ +
+ +
+ + + +
+
+

+ + + +

+
+
+
+ + +
+
+

주소 정보

+

웨이블존의 정확한 위치 정보를 입력하세요

+
+ +
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ +
+ +
+ + + + +
+
+

+ + + +

+
+ + +
+ +
+ +
+ + + + +
+
+

+ + + +

+
+
+
+ + +
+
+

이미지 정보

+

웨이블존의 대표 이미지를 설정하세요

+
+ + +
+ +
+ +
+ + + +
+
+

+ + + +

+
+
+
+ + +
+
+ * 표시는 필수 입력 항목입니다 +
+
+ + + + + 취소 + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html b/src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html new file mode 100644 index 00000000..ebff87b3 --- /dev/null +++ b/src/main/resources/templates/admin/wayblezone/wayble-zone-detail.html @@ -0,0 +1,385 @@ + + + + + + 웨이블존 상세정보 - Wayble 관리자 + + + + + + + +
+ +
+
+
+

+
+ + + ID: +
+
+
+ + + + + 편집 + + + +
+
+
+ +
+ +
+
+

기본 정보

+
+
+
웨이블존명
+
+
+
+
연락처
+
+
+
+
주소
+
+
+
+
위도
+
+
+
+
경도
+
+
+
+
등록일
+
+
+
+
최종 수정일
+
+
+
+
+
+ + +
+
+

통계 정보

+
+
+
+
+ + + + +
+
+
평점
+
+ +
+
+
+ + + + +
+
+
리뷰 수
+
+ +
+
+
+ + + + +
+
+
좋아요
+
+
+
+
+ + +
+
+

시설 정보

+
+
+
시설 타입
+
+
+
+
설명
+
+
+
+
+
+ + +
+
+

운영시간

+
+
+ + +
+
+
+
+ + +
+
+

이미지 갤러리

+
+
+ 웨이블존 이미지 +
+
+
+
+ + +
+
+

최근 리뷰 (최대 5개)

+
+
+
+
+
+ +
+ +
+ +
+

+
+
+
+
+
+
+ + +
+
+

동기화 정보

+
+
+
최종 동기화 시간
+
+
+
+
동기화 상태
+
+ + 동기화 완료 + + + 동기화 필요 + +
+
+
+
+
+
+
+ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/wayblezone/wayble-zone-edit.html b/src/main/resources/templates/admin/wayblezone/wayble-zone-edit.html new file mode 100644 index 00000000..e7739d30 --- /dev/null +++ b/src/main/resources/templates/admin/wayblezone/wayble-zone-edit.html @@ -0,0 +1,407 @@ + + + + + + 웨이블존 수정 - Wayble 관리자 + + + + + + + +
+ +
+

웨이블존 정보 수정

+

웨이블존 정보를 수정하세요

+
+ + +
+
+
+ + + +
+
+

+
+
+
+ + +
+
+
+ + + +
+
+

+
+
+
+ + +
+
+
+
+
+ + + +
+
+
+

웨이블존 정보 수정

+

웨이블존의 정보를 수정해주세요

+
+
+
+ +
+ + + +
+ + +
+
+

기본 정보

+

웨이블존의 기본 정보를 수정하세요

+
+ +
+ +
+ +
+ +
+ + + +
+
+

+ + + +

+
+ + +
+ +
+ +
+ + + +
+
+

+ + + +

+
+ + +
+ +
+ +
+ + + +
+
+

+ + + +

+
+
+
+ + +
+
+

주소 정보

+

웨이블존의 정확한 위치 정보를 수정하세요

+
+ +
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ + +

+ + + +

+
+ + +
+ +
+ +
+ + + + +
+
+

+ + + +

+
+ + +
+ +
+ +
+ + + + +
+
+

+ + + +

+
+
+
+ + +
+
+

이미지 정보

+

웨이블존의 대표 이미지를 수정하세요

+
+ + +
+ +
+ +
+ + + +
+
+

+ + + +

+
+
+
+ + +
+
+ * 표시는 필수 입력 항목입니다 +
+
+ + + + + 취소 + + +
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/admin/wayblezone/wayble-zones.html b/src/main/resources/templates/admin/wayblezone/wayble-zones.html new file mode 100644 index 00000000..ce5fffb6 --- /dev/null +++ b/src/main/resources/templates/admin/wayblezone/wayble-zones.html @@ -0,0 +1,243 @@ + + + + + + 웨이블존 관리 - Wayble 관리자 + + + + + + + +
+ +
+
+
+

웨이블존 목록

+

+ 총 개의 웨이블존이 등록되어 있습니다 +

+
+ +
+
+ + +
+
+
+ + + +
+
+

웨이블존을 찾을 수 없습니다.

+
+
+
+ + +
+
+
+
+ +
+ +
+
+

+ + +
+ +
+
+ + + + + + + +
+
+ +
+
+
+ + +
+
+
+
+ + + + +
+
평점
+
+ +
+
+ + + + +
+
리뷰
+
+ +
+
+ + + + +
+
좋아요
+
+
+
+
+
+
+ + +
+ + + +

웨이블존이 없습니다

+

등록된 웨이블존이 아직 없습니다.

+
+
+
+ + +
+ +
+
+ + \ No newline at end of file